Skip to main content

Core Contract Overview

The main job of the core contract is to take in a Å-Node supplied bundle, decode, validate and execute it. In an effort to save gas the contract relaxes its trust assumptions in certain areas, relying on the economic security guarantees provided by the off-chain set of Angstrom nodes.

The below paragraphs give an overview of different important parts & concepts in Angstrom:

Node vs. Users

Trusted, staked nodes as described above are the ones responsible for calling execute and providing some of the parameters for orders. Assets, prices, amountFilled for partial orders and gasUsedAsset0 / extraFeeAsset0 are all computed and supplied by the trusted node.

Users supply their orders/intents in the form of src/_reference/SignedTypes.sol. The node performs order matching determining which ToB & user orders to include as well as what amount to fill for partial orders. The node is also responsible for computing and fairly splitting gas & referral fees among users.

The execute(bytes) Entry Point

The Angstrom::execute method is the entry point for bundle execution, it calls into the Uniswap V4 position manager to establish a lock so that it can execute pool swaps & settle swap deltas.

The execute takes a PADE-encoded payload.

Bundle processing proceeds in the following stages:

  1. Load & validate list of assets
  2. Load & validate list of pairs for that bundle
  3. Take tokens from Uniswap (either to settle swap deltas or as flashloan for settling orders).
  4. For each pool: update
    1. Execute a swap against the underlying Uniswap pool
    2. Update pool reward accumulators to account for new rewards & tick crossings
  5. Validate & execute ToB (top of block) orders
  6. Validate & execute user orders
  7. Settle remaining deltas with Uniswap
  8. Emit collected fee summary event

Intra Bundle Accounting

To ensure the contract remains solvent the DeltaTracker keeps track of net balance changes for every asset and is checked at the end.

The solvency invariant it maintains for every asset α\alpha is:

TotalTokenBalanceα=Deltaα+iNUserBalancei,α+iNLPRewardi,α\text{TotalTokenBalance}_{\alpha}=\text{Delta}_{\alpha} + \sum^N_i \text{UserBalance}_{i,\alpha} + \sum^N_i \text{LPReward}_{i,\alpha}

Here a visualization of how each action in a bundle is accounted for:

A component of general solvency that the core contract does not directly guarantee is amounts related to limit order fees (fees accrued via feeInE6 & maxExtraF).

Pool Rewards

Angstrom internally uses "growth outside" accumulators to track and account for LP rewards very similar to the way Uniswap V3+ fee accumulators function. The goal of this system is to be able to efficiently update & compute rewards to be distributed to ranged positions without needing to iterate across entire ranges.

The reward tracking consists of two accumulators:

  • a global reward growth accumulator
  • a per-tick "growth outside" accumulator

Global Reward Accumulator

Any accrued rewards get added to the global accumulator and represent the rewards accrued by active positions, position's whos tick range contains the current tick:

As the current tick moves around and leaves one range into another we want to keep track of the fact that the accrued rewards should not count towards the rewards for ranges that were previously active. This is where the "growth outside" accumulators come in:

Reward Growth Outside Accumulators

To ensure we don't double count rewards we update a growth outside accumulator whenever we cross a tick such that it represents the sum of rewards accrued in the direction away from the current tick:

Now we continue to update the global reward growth as usual whenever we distribute new rewards:

Generalizing the update of the growth outside value at tick boundaries we simply do growth_outside' := global_growth - growth_outside:

Notice that as current tick moves and positions become inactive/active the amount of active liquidity will change. To ensure that the rewards tracking remains consistent despite this the accumulators do not track the absolute growth in rewards but instead the growth per unit of liquidity.

As rewards accumulate and the current tick moves back and forth you end up with growth outside accumulators looking like this:

The brackets representing the range for which the growth outside value is the sum relative to the current tick. Notice the discrepency between the meaning of the growth outside accumulators for ticks below or at the current tick and above the current tick. Below or at the curren tick the growth outside range is exclusive, above it is inclusive.

When rewarding multiple ranges in one go as in Angstrom one must consider this asymmetry when handling the above and below cases.

Gas Payment & Extra Fee

To ensure users trading on Angstrom don't have to think about ETH-denominated gas payments gas is charged in either the input/output asset of the respective trade.

On top of this users may be charged a referral fee if they opted into one via a referral tag (ref_id field). Validating the ref id, retrieving the fee rate and applying it correctly is the responsibility of the nodes. They will use a registry in the periphery contracts as their source of truth.

The total gas fee + referral fee is called the "extra fee" and is charged in the respective asset0 of the pair.