AIMM spread and fee model
One swap pays exactly two charges, both on the output token and in this order: the convex coverage toll, then the fee at half the spread.
1. Scope and status
Formulas are stated in contract units and cited to the implementing function. The surrounding function name is the stable reference.
Path risk aggregation is shipped. minFee and confidence sum over legs, sigma composes in quadrature, and the staleness surcharge sums per leg (§3.2). Nothing caps the composed spread from above; the only bound is the saturating narrowing into the uint16 quote field, reasoning in §3.1.
The multi-anchor tree is shipped too. The interior leg prices under the same _legMid law as the terminal one and carries a manipulation fence in its floor (§10.2). Topology, depth bounds and what the anchor column configures: Anchor Path Pricing §1.
2. Units
| Symbol | Name | Base | Meaning |
|---|---|---|---|
PBPS | 1 unit = 0.0001% = 0.01 bp | ||
BPS | 1 unit = 0.01% = 1 bp | ||
| WAD | WAD | prices, coverage, |
Conversions used on the spread path: bps PBPS is . Volatility is PBPS-based (), capped at MAX_SIGMA_PBPS (PoolConstantsLib.sol).
Notation. is dispersion throughout this chapter (as in Liquidity Shaping and Parametrization §4.2). The coverage wall is (kappaCovBps). The feed’s confidence interval is (bps): it never shares a symbol with dispersion. On the interior leg, is the manipulation fence, the leg’s mid swing, its curve’s span, its dispRefPbps and the protocol swing cap (§10.2).
minFeePbps is uint16 in PBPS, and it is the only fee rate getAsset returns. It is bounded two-sidedly at every write path (initAsset and requestAssetParams): floor MIN_FEE_PBPS = 1 (0.01 bp), ceiling ONE_PCT_PBPS = 10,000 PBPS (1%). minFeePbps is charged unconditionally on every swap, so ONE_PCT_PBPS is the only guard against an owner setting an excessive floor. SwapQuote.spreadPbps is separately uint16, ceiling 65,535 6.55%: that is a field width, not a fee policy (§3.1).
3. The spread
3.1. Definition
Vocabulary, canonical: mark, mid, skew, price impact, spread, fee and coverage toll each name exactly one object, and only impact and the toll are direction-asymmetric. Slippage & Price Impact §1.1.
The spread is a round-trip width in PBPS. It is computed once per swap over the whole path and returned as SwapQuote.spreadPbps (uint16). A single swap is charged , once, on the output (§5).
PricingLib._pathSpread and _staleTerm. Integer arithmetic, floor division at each /. On an interior leg is already raised to before it enters the sum (§10.2); on an endpoint leg it is the stored Asset.minFeePbps.
No clamp on either side. The floor needs none: is the additive base of , so . The only bound above is the saturating narrowing into SwapQuote.spreadPbps at 65,535 PBPS, a uint16 field width and not a policy.
Any formula of the form , and any per-leg clamp composed into a path bound, is therefore wrong.
Why no cap. Written out in full in the _pathSpread doc comment; three claims:
- A cap is not trader protection.
minAmountOutis: exact, caller-set, per trade, and it bounds delivered output rather than one input to it. A protocol-wide fee cap is a worse instrument for the same job. - A cap creates its own pathologies. Composed per asset, , it makes a 2-leg quote cost more than the same two legs traded separately, paying a splitter to route around the hub, which breaks the routing invariant path composition exists to hold. Repaired per leg, it silently under-quotes an unpinned path by about 47 bp unless the staleness term is also summed per leg.
- A cap binds on the tape where the defense is needed. A stale or high-CI leg is when the premium must be paid. Clamping there discounts the quote on the leg being picked off, and every such clamp is attacker-timeable: an adversary who can push
rawover the cap, by waiting out a keeper or by choosing a high-CI moment, chooses when it binds.
Scale. , so a single leg reporting a 500 bps CI adds exactly 50,000 PBPS (5%) on its own, on top of the floor. Integrators must derive worst-case cost from getSwapQuote rather than from any config field, and enforce it with minAmountOut.
The floor is a security quantity: with per leg (§9), plus the interior fence folded into it. Saturation at uint16 cannot remove that floor; the bound and its arithmetic are in §9.
3.2. The five inputs, and how they aggregate
All five compose over legs, never over endpoints, and none of them is a max.
| Input | Source | Aggregation (shipped) | Site |
|---|---|---|---|
Asset.minFeePbps, raised to on an interior leg | over legs | _walkLegs, floor at _priceInteriorLeg | |
feed word sigma | quadrature , one sqrt at the end | _walkLegs, rooted _pathSpread | |
Asset.vegaBps | over the endpoints only, a path constant | getAnchorPathQuote | |
feed word conf | over legs | _walkLegs | |
| staleness excess (s) | of per-leg surcharges | _walkLegs |
Confidence and staleness share one accumulator (PathAccumulator.riskPath) because _pathSpread only ever consumed their sum, and the fusion is exact: , the scale being an integer constant.
Every quantity comes off the feed the leg already read (_legMarkAndFees): staleness and confidence are carried on the leg walk, not cached per endpoint, so a quote performs no second oracle read. is the one endpoint-scoped input: vega is the pool’s per-asset sigma-sensitivity dial, not a per-leg risk quantity, and the endpoints are the assets paid in and out. That scoping has a configuration consequence: §11.2.
On a flat roster - every leg base-anchored (Anchor Path Pricing §1.1) - the only interior node is the base, which supplies no leg profile, so a path has at most two legs. At MAX_DEPTH = 4 it has up to eight (§10.1), and every one pays into all five aggregates.
is the volatility stored on chain with the mark, raised to at least the realized on each push (MarkStoreP8). Which feed supplies it depends on the leg’s oracle mode; see Mark Relayer.
3.3. Volatility band
Vega has base 10,000 = 1.0x. At and PBPS (1%), the volatility contribution is PBPS = 1 bp above the floor. There is no hardcoded base: the base is the configured floor.
3.4. Staleness surcharge
with (STALE_Z) and the grace cap s (MAX_STALE_GRACE_SECS), both file-private constants in PricingLib.sol. is sized so the half-spread a trader pays at 1800 s past grace is one full of the mark’s diffusion over that window: . is computed in _staleExcessOf off the feed the leg already read, the per-leg term in _staleTerm, which short-circuits to 0 when or (skips the sqrt), and the legs are summed at _walkLegs.
Per leg, not coupled. A coupled form, , charges one aggregate age against the path sigma and is wrong. One keeper feeds several spokes, so an outage staleses them together, the case the term exists for, and the coupled form charges two equally stale legs of what they each owe (6675 against 9440 PBPS at , s: a 27.65 bp leak), while over-charging a fresh leg for its neighbor’s lag. Pinned by the staleness regression tests (per-leg summation; a fresh leg pays no staleness premium).
The clock is the attested source second of the push: MarkStoreP8 refuses a push whose source second is more than 120 s old or more than 5 s ahead of the landing block, so a relay cannot stamp an old signed quote fresh.
The is a unit normalization, not a tuning knob. is PBPS-scaled, so without it the term is too large and pins the spread at its uint16 field ceiling the instant age crosses the grace, turning a ramp into a step. Worked: PBPS (1%), s PBPS = 47.2 bp of spread, 23.6 bp paid per swap. At s: PBPS = 198 bp ().
The grace cap binds independently of : at ttl = 600 the grace is 30 s, not 300 s. Without the cap a long ttl buys a proportionally long unpriced window, which is the window a delinquent keeper occupies.
Why the grace exists: the keeper snaps the mark whenever the deviation crosses an edge-relative push boundary, or on a heartbeat. That boundary is a fraction of the leg’s own live edge rather than a fixed number of basis points, with the static retained only as the fallback when the pool’s minFeePbps is unknown to the keeper. The fraction is an operational parameter and is not published, but it is held well inside one half-spread, so the mark is snapped before the drift reaches the half-spread the swap is charged. Inside that contract an old but accurate mark (flat tape, no push owed) must not be penalized, or the pool quotes wide against no risk. Past the grace the guarantee is void and the surcharge ramps with the unobserved drift up to the hard TTL revert. The confidence term is excluded from the keeper’s basis while confidenceBps on the wire is a freshness proxy rather than a dispersion measure: folding it in would invert the trigger, making a staling feed push less.
is a graceful-degradation layer below a hard revert, not instead of it. FeedMathLib.gate reverts:
StaleDataatage > ttlZeroValueatmark == 0ThresholdViolationatconfidence > MAX_CONFIDENCE_HALT_BPSFeatureDisabled(Err.Resource.FEED)on the guardian fast-freeze bitFEED_HALT_BIT
3.5. Confidence surcharge
is leg ’s feed-reported 1σ confidence interval in bps. There is no per-asset knob: 1 bp of reported CI adds 100 PBPS of spread, unconditionally. A path inherits every leg’s uncertainty, not the worst one: a quiet leg beside a noisy one is still charged its own CI (_walkLegs; pinned by regression tests). Past MAX_CONFIDENCE_HALT_BPS = 1000 bps (10%) the swap reverts rather than quoting (FeedMathLib.gate, PoolConstantsLib.sol). Fail-closed, like the depeg band.
See §8.
4. Theorem 1: the spread is direction-invariant
Claim. For any listed pair , at the same block and the same pool state, for any trade size.
Proof. is a function of exactly five quantities: , , , , (_pathSpread). Each is built by one of two constructions:
- A fold over the leg multiset with a commutative, associative operator: over , and the per-leg staleness surcharges, and of squares for (
_walkLegs). - A
maxover the unordered paircIn,cOut: alone (getAnchorPathQuote).
Reversing the swap reverses the hop order. The route between and is the unique tree path, so the reversed route is the same edge set traversed backwards, and the per-leg profile asset is a property of the edge (_executeLeg: profileAsset = isUpward ? from : to, which is the child endpoint of the edge either way). The leg multiset is therefore identical. A commutative fold over an identical multiset returns an identical value; a max over an unordered pair likewise. Hence all five inputs are identical and is identical.
The theorem holds for any symmetric aggregate. , and are all symmetric functions of the leg multiset, so any fold drawn from that set preserves direction invariance exactly. Any new term must be a symmetric fold.
Consequence. No part of the bid/ask asymmetry can come from the spread. Everything asymmetric is in the mid (skew, §7) or in the toll (Theorem 2, §6.4).
5. The fee
5.1. Order of operations
PricingLib._settleQuote (PricingLib.sol) settles in exactly this order:
require(Σ maxDevBps over the route <= MAX_ROUTE_MAX_DEV_BPS) // 400, else ThresholdViolation
spreadPbps = _pathSpread(acc)
gross = min(gross, G*) // G* = _grossCap(cOut); refused past G* + 2%·L, or at G* == 0
covToll = _covToll(cOut, gross) // terminal asset only
gross -= covToll
feeOut = ceil(gross * spreadPbps / (2 * PBPS))
(protoFee, lpFee) = splitFee(feeOut, protoSharePct)
amountOut = gross - feeOutSo, with the gross output from the leg walk, after the cap:
Both charges are in output token units. The fee base is post-toll, not gross. The fee is charged once per swap, not once per leg: the spread is already a path aggregate.
The multiplication precedes the halving (* spreadPbps / (2 * PBPS), one division) so PBPS does not truncate to zero on any economically relevant size, and the division rounds up (+ 2*PBPS - 1 in PricingLib). A floored fee is a wei per order kept by the trader, and a split order keeps one per slice; the dust is the pool’s.
5.2. Why there is no input-side skim
The gross output is priced off the full amountIn: the spline contributes price impact only, no fee. An input-side half-spread would therefore be phantom revenue, crediting the trader the full input in the gross while withholding part of that input. That is an LP-reserve drain into the treasury, not a fee, so the charge is one-sided.
An integrator deriving a two-sided quote from SwapQuote.spreadPbps must apply per side, not . The ABI comment at IPool.sol states it normatively: protoFee + lpFee is the whole fee, denominated in tokenOut, and an indexer must credit 100% of it to tokenOut and never to both legs.
5.3. Protocol split and settlement
PricingLib.splitFee, protoSharePct a uint8 with base 100. Per-pool values: pool composition.
Settlement is endpoint-only (PoolIOLib.settle, PoolIOLib.sol):
aIn.reserves += amtIn
aOut.reserves -= amountOut // both fee slices stay in reserves
aOut.liabilities += lpFee, index raised // PoolLiquidityLib._credit, _raiseIndex
aOut.liabilities += protoFee // PoolLiquidityLib.accrueTreasury, pending treasury shares| Quantity | Reserve effect (out leg) | Liability effect (out leg) | Coverage effect |
|---|---|---|---|
| (protocol) | stays in reserves | as pending treasury shares (PoolLiquidityLib.accrueTreasury) | unchanged, claimed with Admin.claimTreasury |
| (LP) | stays in reserves | (PoolLiquidityLib._credit) | unchanged, LP claim rises via liquidityIndexWad |
| stays in reserves | none | rises: this is the surplus |
The LP fee is booked as an LP claim, not pool surplus: the settlement credits liabilities by lpFee and raises liquidityIndexWad in the same step (PoolLiquidityLib._raiseIndex, INDEX_REASON_FEE), leaving unchanged. The protocol slice is booked the same way, as treasury shares instead of an index raise, so it does not move either. Both credits are booked at the cached pool rate (lastGoodC): face is the fee divided by when , the fee itself otherwise (PoolLiquidityLib._credit). The only charge that improves coverage is the coverage toll, withheld with no matching liability credit.
Consequence for the toll’s round-trip identity: telescopes exactly only at constant , and the fee credits move mid-loop. The residual is pool-favorable (higher lower more toll), documented at PricingLib.sol.
5.4. Worked example
1 ETH into a WETH/USDC pair, mark 2000 USDC/ETH, PBPS (0.5%), USDC coverage at or above 1 after the fill (so ), .
USDC stays in reserves and is booked as pending treasury shares; USDC stays in reserves and is credited to LP liabilities. The trader pays 25 bp, which is half the 50 bp round-trip width.
5.5. There is exactly one charge
The complete list of what one swap is charged:
- One charge, the half-spread, once per swap. A round trip therefore pays the full spread , which is what makes a round-trip width and not a per-swap fee.
- No half-spread is embedded in the executed price. At zero size the buy and the sell branch return the identical expression, so ; with centered curves exactly (Anchor Path Pricing §3.1). The spline contributes price impact only.
protoSharePctsplits that one pot, it does not add to it (§5.3).- There is no static fee. Every term of is live.
- The charge is output-side only. A swap that also skimmed the input would pay the half-spread twice, which is the failure mode §5.2 exists to rule out;
_settleQuotetouchesamountInnowhere.
5.6. What that costs a trader
Quotes carry no curve LVR: the quote tracks the external mark instead of being arbed onto it (Toxic Flow Mitigation).
6. The coverage toll
The coverage toll is its own object, distinct from the coverage ratio it reads: a convex, charge-only levy on the output leg, computed by PricingLib._covToll and reported as SwapQuote.covToll. Reference it under that name.
6.1. Definition
Let and be the output asset’s reserves and liabilities, in WAD, and the gross output.
PricingLib._covToll, _covQ (FixedPointMathLib.lnWad). Returns 0 immediately when or ; there is no branch, because every writer floors κ at 50 bps. The toll rounds up, in the same direction as the fee: the charge is the pool’s, so the dust is never the trader’s.
is concave with a unique maximum , and as : a convex no-drain wall.
6.2. The three structural properties
Charge-only, clamped to the increasing branch of , terminal-only and capped-fill are stated with their sites and tests at Invariants §I-9; the clamps are argued at Inventory Management §6.2. Charge-only is what makes the toll safe to retain in reserves rather than express as a mark shift: a mark shift would be round-trip extractable.
6.3. The marginal toll: a level shift, not a large-trade wall
Differentiate at . With and :
The marginal rate is strictly positive at zero size whenever the output leg is under-covered. Dust pays it at the same rate a block trade pays on its first wei. It is a level shift keyed on the state of the terminal asset, plus convexity on top.
Example: at and bps the marginal toll is bp, and it scales linearly in . Any description of the toll as a “large trade only” convex wall is wrong: convexity is the second-order behavior, the level shift is the first-order one.
6.4. Theorem 2: the toll is the only asymmetric charge
Claim. The coverage toll is charged once, on the terminal output, and is zero only when the drain does not worsen below-peg coverage. Same-pool spoke→spoke never outputs the hub; a hop that extracts the hub does.
Proof. _covToll is evaluated only on cOut (_settleQuote), and every writer holds on every listed asset, hub included (requireNeverDepletable). On the terminal is , so hub κ does not fire on that same-pool cross; a swap whose output is the hub pays hub κ.
Consequence, combined with Theorem 1. Take a same-pool cross at one block. The spread is identical in both directions. The toll is one way and the other. By §6.3 at small size that is
A trade whose output is the hub pays .
Interior vs hub. A non-base interior can carry and still be tolled at zero, because it is interior (), not because it is unwalled. The hub is not interior on a hop that extracts it.
6.5. Coupled configuration
One coupling is enforced on chain, at every write:
- Lemma B of the published coverage proofs (a walled leg carries no exit-haircut suppressor) holds: LP exits settle at the pool rate and the suppressor field is gone. Three arms bind the wall:
MIN_KAPPA_COV_BPS = 50 <= kappaCovBps <= BPSon every listed asset including the hub, enforced byrequireNeverDepletableat every writer; a spoke’s κ never exceeds its hub’s (requireHubKappaDominant); and κ can never be stripped, the floor holding on every writer rather than on any curve flag. Canonical statement: Invariants §I-9.
Nothing on the depth axis pushes back against the wall. The traverse denominator is the leg’s raw reserves at every coverage (Inventory Management §4). A denominator that grew as coverage fell would shorten the traverse on the leg the wall defends, which is how a two-block round trip becomes trader-positive (worth PBPS to the LP at ). Coverage reaches the quote through the skew anchor and this toll, and through nothing else.
7. Bid-ask asymmetry
7.1. Where it comes from
The book is centered not on the mark but on the skew-loaded mid , where is the int8 inventory-skew index and the price displacement it produces. The spread is symmetric about :
The skew is piecewise-linear in the coverage ratio, clamped at and returned as an int8 (PricingLib.computeInventorySkew); it maps onto the spline’s depth axis in _skewToDepth, displacing off the curve’s stored density median at BPS/200 per skew unit, so the book is quoted from the skew anchor outward and zero skew quotes the mark. It is a fixed protocol law with no per-asset dial, and its two arms are asymmetric, the filling slope being the round-trip conservation bound. Formula, slopes and saturation points: Inventory Management §3.
Four asymmetry sources, ranked:
- Mid skew : shifts the center; coverage-worsening flow gets a worse price at the same spread. Above parity it is a level and cancels on a round trip. Below parity the pool never buys above the mark (Liquidity Shaping §8.5), so a round trip pays once.
- Spline traverse direction: the buy and sell legs integrate the same monotone curve over different intervals from the anchor. Size-dependent, endpoint legs only.
- Coverage toll : charge-only, terminal asset only, never rebated. Theorem 2.
- Spread: contributes nothing. Theorem 1.
7.2. Inventory skew as a Glosten-Milgrom soft fee
The skew doubles as a toxicity-targeting soft fee in the sense of Glosten & Milgrom (1985): flow that persistently moves the maker’s inventory one way is on average the informed flow, and should pay more than flow that round-trips. AIMM realizes this through the skew slope, not a flat spread.
with the dispersion. The linear form is exact only for a flat density curve: the displacement is the curve’s value at the skew anchor (_skewToDepth, _legMid). Because is a frozen external mark between pushes, drifts only through realized inventory .
- Inventory-moving flow walks up the skew and pays on top of . That slippage is retained in reserves, so the pool captures the adverse-selection cost it would otherwise leak.
- Round-trip flow restores coverage and pays the floor; below parity it also pays once, because the sell side is capped at the mark.
This is why a competitive base fee can coexist with charging toxic flow: tune the defensiveness with (how fast ramps), not with .
Guardrail. The construction is safe only because the drift is via realized inventory around a frozen mark. If the center drifted via a mutable internal accumulator that the swaps themselves push, the identical soft fee would be a flash-loan-manipulable surface. AIMM has no such surface: the quote center is MarkWordLib.mark, never a reserve- or trade-derived invariant, and the toll is charge-only so a round trip strictly loses.
8. What the number users call “spread” actually is
Decomposition of a two-leg cross at quoted spread . Both assets anchor to the base, so the path is X -> base -> Y: two legs, and the profile asset of each is the spoke. The floor is ; the base’s own minFee never enters, because minFee is per leg’s profile asset and the base supplies no leg profile.
| Term | Basis |
|---|---|
| configured: | |
| computed: at the quoted | |
| residual: less the two rows above, not an independent reading |
The last row is solved for, not measured. is read from the quote and the floor is configuration, so the surcharge total is what the identity leaves. It is one figure for a second reason as well: the contract carries confidence and staleness in a single accumulator (PathAccumulator.riskPath holds , and _pathSpread only ever consumes the sum, §3.2), so no quote splits them either. Splitting it requires per-feed confidenceBps and mark age off chain, at the same block.
On a stressed feed most of the quoted spread is an oracle-health readout, not a fee. The surcharges measure the pool’s uncertainty about its own mark; neither is LP compensation. A pool whose keeper is healthy and whose feed reports a tight CI quotes near .
Track the surcharge share as an operational metric, not revenue.
9. Why the path fee floor sums over legs
The invariant. Each leg ships , where is the keeper’s configured per-feed deviation threshold in bps. The keeper checks it at startup and hard-fails; there is no override: the on-chain invariant is convention only, so the gate lives in the pusher. This is the static config value the boot gate reads; the runtime push boundary is edge-relative and derived from the same floor (§3.4). Derivation: Parametrization §11.
Why not . The pick-off edge an adversary can realize is the sum of the per-leg thetas: the adversary picks the joint worst realization, and every mark on the path is independently allowed to sit away from truth. A funds one leg and leaves the rest unfunded.
Under the shipped the floor clears for every cross whose legs each carry ; per-pool floors and thetas: pool composition. The full aggregation is at §3.2.
composes in quadrature rather than as a sum because independent leg innovations add in variance, not in level. The off-chain mark pipeline already composes bridged pairs this way. minFee and confidence add in level because both bound a worst-case displacement, not a variance.
The floor, the quadrature and the per-leg staleness sum are one mechanism. A path whose floor sums but whose staleness couples charges a mixed-freshness path about 47 bp too little; the reverse pairing over-charges a fresh leg for its neighbor’s lag. The sum/quadrature aggregation stands alone, with no clamp above it (§3.1).
Overflow discipline. The accumulators are uint256 in memory (PricingLib.PathAccumulator), so the leg sums cannot wrap. There is exactly one narrowing on the path, in _pathSpread, into SwapQuote.spreadPbps (uint16, 6.55% ceiling), and it saturates explicitly. At eight legs a summed floor exceeds uint16; an unchecked cast would turn a maximally fenced path into a near-zero fee. Pinned by a spread-composition regression test.
Saturation cannot remove the security floor. The raw spread is bounded below by the composed interior fence, which the swing cap bounds at PBPS, so what saturates away is the /CI/staleness surcharge. That bound needs no runtime check to carry it; it is pinned by value in test_the_interior_fence_ceiling_fits_the_uint16_spread, which is where a future MAX_DEPTH or swing-cap change fails.
10. Multi-hop
10.1. Routing
Shipped: the multi-anchor tree, bounded at 9 nodes and 8 legs. The route is the unique tree path, so a quote cannot depend on a routing choice. Topology, the depth arithmetic and what the anchor column configures are stated once at Anchor Path Pricing §1 and §1.1. Every summed budget on this page is sized against 8 legs.
Cycle safety at any depth follows from the tree structure: a closed trading cycle is a closed walk in a tree and crosses every edge equally often up and down, so per-edge safety gives cycle safety at any length. The three per-edge conditions are stated at Anchor Path Pricing §4.
10.2. Leg pricing
Endpoint legs carry full price impact: spline traversal plus reserve accounting (_priceEdgeHop, PricingLib.sol).
Interior legs price at mid with inventory skew and carry no impact.
Skew belongs on an interior leg because it is pricing, not a charge for inventory change: an under-covered pivot is worth less to the pool, and pricing the hop at the unskewed mark gives the trader the pivot’s depeg risk at no charge.
It cancels on a round trip only against unchanged interior coverage; the residue and its measurement are at Anchor Path Pricing §3.1.
Shipped fence. An interior leg’s floor is , not bare . is a ratio over its live swing, computed per quote, and the swing cap is 10,862 PBPS (MAX_INTERIOR_SWING_PBPS, peak-to-peak): formula, units and the one-sided reading are at Anchor Path Pricing §3.2. The cap is solved for rather than chosen: the uint16 spread budget split over 6 interior legs, , gives . The fence folds into the leg’s floor and sums into minFeePath like any other leg floor, with nothing above it that could clamp it away; its ceilings are in §9.
Impact must not be charged on an interior leg: under endpoint-only settlement, so an impact charge invents a cost for a displacement that did not occur and replaces an exact identity with a size coincidence. The revenue an interior leg owes is collected by the fee sum in §3.2, which is why the sum rule is required.
Every node on the path, interior included, is checked against the halt bits, the reference band and (at the base) the parity halt; the ref band, not the parity halt, is what scales with depth. See Anchor Path Pricing §8.
10.3. Denomination
Shipped: the feed for asset must be attested in units of assets[X].anchor, and mark = MarkWordLib.mark(word) with no correction. At depth a base-denominated rule is dimensionally wrong, not merely imprecise: stETH -> ETH -> USDC would compose .
OracleConfig.quoteUnit is a uint8. QUOTE_UNIT_UOA = 1 (PoolConstantsLib.sol) is the instruction “divide by the base mark”, not a unit, so PoolConfigLib rejects it on any asset that does not anchor directly to the base (PoolConfigLib.sol), and a bridged leg’s minFee must cover . There is no re-denomination helper on the path: the division is inline in _legMarkAndFees (PricingLib.sol), reusing the gated, depeg-banded base read the path already performs.
11. Shipped vs decided
| Behavior | State | Reference |
|---|---|---|
| Fee once, output-side, , post-toll | shipped | PricingLib._settleQuote |
| Toll charged before fee, terminal asset only | shipped | _covToll |
| on every listed asset, the base/hub included | shipped, enforced | PoolConfigLib.requireNeverDepletable rejects and at every writer; requireHubKappaDominant keeps every spoke at or under its hub |
| Direction-invariant spread (Theorem 1) | shipped | _pathSpread |
| clamp on the toll | shipped | _covToll |
| Sum / quadrature path aggregation | shipped | _walkLegs, _pathSpread, §9 |
| No cap of any kind on the composed spread | shipped | §3.1; trader protection is minAmountOut |
| Per-leg staleness surcharge, summed | shipped | _walkLegs |
Anchor-unit marks consumed as attested, quoteUnit | shipped | §10.3 |
MAX_DEPTH = 4 multi-anchor tree (8 legs, 9 nodes) | shipped, activation gated | AnchorTreeLib.sol, Anchor Path Pricing §10 |
Interior leg priced by the same _legMid as the terminal leg | shipped | PricingLib.sol |
| Interior-leg manipulation fence in the path floor | shipped | _interiorMidAndFence, _priceInteriorLeg, §10.2 |
| Dispersion band bound to the curve’s fence cap at the write | shipped | PricingLib.dispersionCap, PoolConfigLib.sanitizeDispersion |
| Re-anchor timelocked at the base-migration tier, atomic with the oracle config | shipped | requestOp(..., UPDATE_ANCHOR, ...) / Admin.executeUpdateAnchor |
| Parent-depeg breaker for an anchor-attested child | not designed into any field | Anchor Path Pricing §7.1 |
11.1. Open items
- Re-anchoring is timelocked and atomic with the oracle config. Gating and payload: Anchor Path Pricing §10.1.
- The reserve clamp binds on the delivering leg only.
_legScaleOutcaps output at the destination’s reserves only when the leg is the one delivering (PricingLib.sol). Applied to every leg it would clamp an interior hop against a hub balance the swap never spends, and a large cross would silently quote short. The decimal rescale runs per leg regardless.
11.2. Deploy-config hazard: heterogeneous vegaBps
Canonical statement of this hazard. Parametrization and Invariants I-11 carry the shipped default and a pointer, not a second derivation.
Hazard, derived from the code. vegaBps enters the quote at exactly two places, and the two have different scopes:
| Site | Which vegaBps | Scope |
|---|---|---|
_pathSpread volatility band, | the two endpoints only, applied to the whole path’s | |
_calculateDispersion, per leg | the profile asset’s own Asset.vegaBps | that leg alone |
The per-leg dispersion is correctly scoped: each leg shapes its own curve off its own asset’s dial. The volatility band is not. It multiplies the endpoint maximum against , which includes the of legs whose own vega is lower. On a composite path, an interior or far-side leg’s volatility is priced at another asset’s sensitivity dial.
With uniform vega the composite is never worse than the split, because quadrature sum. With heterogeneous vega that ordering can invert, and the inversion scales with across the assets on the path. The vegaHardMinBps / vegaHardMaxBps fences in RiskFences do not bound it in general: they run only in Admin.setAssetParamsBounded, the risk-steward lane, which itself refuses to execute on an unseeded fence (maxDeltaBps == 0 reverts NotConfigured), and the owner lane Admin.requestAssetParams checks only the minFeeHardMinPbps floor and the coop fences. A per-leg fence window is not a relation across assets, so the write-path bounds on are (PoolConfigLib.validateAssetParams) and, on a raise, the dispersion cap at live σ on an interior-capable leg (PoolConfigLib.setAssetParams).
Nothing relates two assets’ vegaBps. Both write lanes move one asset at a time: the owner’s requestAssetParams writes a vega raise at once and queues a cut at TUNING (AdminParamsLib.set); the steward’s setAssetParamsBounded moves either direction instantly inside vegaHardMinBps / vegaHardMaxBps.
The hazard’s size is the ratio across the assets on a path. It moves the premium term only, the fee floor sums per leg and dispersion reads each leg’s own , so what it can do is make a composite path quote wider than the same two legs traded separately, which a splitter can route around. It is a competitiveness cost taken to keep every leg quotable, not a safety hole, and it must be re-derived before any depth-2 listing, where the far-side leg stops being an endpoint.
12. Flash loan fee
Flash.flashLoan, flashFeePbps a per-pool FeeParams field. Bounded [MIN_FLASH_FEE_PBPS, MAX_FLASH_FEE_PBPS] = 10 to 10,000 PBPS, 0.001% to 1% (PoolConstantsLib.sol), at initialize and at every admin write. There is no zero default: a pool handed a zero-initialised struct reverts at creation rather than launching with flash free. Per-deployment values: pool composition.
Flash loans are not tolled and not spread-charged: they do not change coverage over the transaction.
13. Comparison with other AMMs
| Protocol | Fee taken in | Charged | Distribution |
|---|---|---|---|
| Uniswap V2/V3 | 100% input | per hop | one-sided LP |
| Trader Joe V2 | 100% input | per hop | one-sided LP |
| Curve V1 | 100% output | per hop | one-sided LP |
| AIMM | 100% output | once per swap, | LP claim via index; toll to coverage |
| Aspect | Uniswap V3 | AIMM |
|---|---|---|
| Fee structure | fixed tiers 0.01-1% | multi-factor, + CI + staleness |
| Direction dependence of the width | none | none (Theorem 1) |
| Inventory response | none | mid skew + terminal coverage toll |
| Quote center | pool reserves | external keeper mark, no write-on-swap |
| LVR mitigation | hooks only | fresh mark + floor + staleness surcharge |
14. Economics
LPs. The LP share of the fee is credited as a liability and paid through liquidityIndexWad, so it is a claim, not surplus. Genuine surplus comes from the coverage toll and from retained skew slippage, both of which raise against unchanged .
Surplus is redeemable, pool-wide. It raises against unchanged , so it raises the pool rate , and every LP exit settles at uncapped: a same-asset exit pays face · min(c_leg, C), a cross exit converts face · C (Inventory Management §5). The index is not raised: surplus reaches LPs through the rate, not through accretion on the leg that earned it.
Traders. Quotes track a fresh external mark, so there is no hidden stale-mark tax beyond the priced . Coverage-improving trades get a better mid and pay zero toll. Cost is bounded by the caller’s own minAmountOut (§3.1); the quote is not capped protocol-side. The one cost not visible in spreadPbps is the toll’s level shift on an under-covered destination (§6.3): it is in SwapQuote.covToll, and integrators should surface it separately from spreadPbps.
15. Implementation reference
| Function | File | Role |
|---|---|---|
_pathSpread | PricingLib.sol | , saturating narrow into uint16 |
_staleTerm | PricingLib.sol | , per leg |
_settleQuote | PricingLib.sol | toll, then fee, then split |
_covToll / _covQ | PricingLib.sol | convex coverage toll |
_legMid | PricingLib.sol | the one offset law, interior and terminal alike |
_interiorMidAndFence | PricingLib.sol | interior mid + its manipulation fence |
dispersionCap | PricingLib.sol | write-path half of the fence: the band ceiling |
_priceInteriorLeg | PricingLib.sol | raises the leg floor to |
splitFee | PricingLib.sol | protocol / LP division |
_walkLegs | PricingLib.sol | per-leg fold: sums, quadrature |
swap_qe | Pool.sol | the single state-mutating swap entry (calls PricingLib.swapExactIn) |
FeedMathLib.gate | FeedMathLib.sol | stale / dead / uncertain / paused halts |
PoolIOLib.settle | PoolIOLib.sol | endpoint-only settlement (there is no separate exec) |
PoolLiquidityLib._credit, _raiseIndex | PoolLiquidityLib.sol | LP fee to liabilities + index |
PoolLiquidityLib.accrueTreasury | PoolLiquidityLib.sol | protocol fee to pending treasury shares |
16. Related documentation
- Inventory Management: coverage ratio, pool rate
- Liquidity Shaping: dispersion law and the spline
- Price feed reads: mark, , confidence, push API
- Toxic Flow Mitigation: LVR/OEV budget, the fence
- Parametrization: every parameter, its fence, its live value
- Invariants: settlement and liquidity-floor invariants
17. References
- Glosten, Milgrom, Bid, Ask and Transaction Prices in a Specialist Market with Heterogeneously Informed Traders, JFE 14(1), 1985. doi:10.1016/0304-405X(85)90044-3
- Avellaneda, Stoikov, High-frequency trading in a limit order book, Quantitative Finance 8(3), 2008. (Reservation price and the inventory term.)
- Relation between Bid-Ask Spread, Impact and Volatility in Double Auction Markets
- Volatility, Information Feedback and Market Microstructure Noise
- Inventory Risk and Market Making