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

SymbolNameBaseMeaning
BPBPS1061 unit = 0.0001% = 0.01 bp
BPSBPS1041 unit = 0.01% = 1 bp
WADWAD1018prices, coverage, Q

Conversions used on the spread path: bps → PBPS is ×B/BPS=×100. Volatility σ is PBPS-based (104=1%), capped at MAX_SIGMA_PBPS =108 (PoolConstantsLib.sol).

Notation. κ is dispersion throughout this chapter (as in Liquidity Shaping and Parametrization §4.2). The coverage wall is κcov (kappaCovBps). The feed’s confidence interval is u (bps): it never shares a symbol with dispersion. On the interior leg, Fi is the manipulation fence, wi the leg’s mid swing, Σi its curve’s span, ρi its dispRefPbps and w¯ 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 S 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 S/2, once, on the output (§5).

S=∑ifmin,i+σpνp100·BPS⏟Svol+∑izσiτiBPS⏟Ustale+(∑iui)·BBPS⏟Uconf,σp=∑iσi2

PricingLib._pathSpread and _staleTerm. Integer arithmetic, floor division at each /. On an interior leg fmin,i is already raised to max(fmin,i,Fi) 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: ∑ifmin,i is the additive base of Svol, so S≥∑ifmin,i. 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 S=min(Sraw,fmax), 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:

  1. A cap is not trader protection. minAmountOut is: 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.
  2. A cap creates its own pathologies. Composed per asset, ∑ifmax,i, 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.
  3. 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 raw over the cap, by waiting out a keeper or by choosing a high-CI moment, chooses when it binds.

Scale. Uconf=100∑iui, 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: ∑ifmin,i with fmin,i≥2θi 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.

InputSourceAggregation (shipped)Site
fmin,pAsset.minFeePbps, raised to max(fmin,i,Fi) on an interior leg∑i over legs_walkLegs, floor at _priceInteriorLeg
σpfeed word sigmaquadrature ∑iσi2, one sqrt at the end_walkLegs, rooted _pathSpread
νpAsset.vegaBpsmax(νin,νout) over the endpoints only, a path constantgetAnchorPathQuote
upfeed word conf∑i over legs_walkLegs
Ustalestaleness excess (s)∑i of per-leg surcharges zσiτi/BPS_walkLegs

Confidence and staleness share one accumulator (PathAccumulator.riskPath) because _pathSpread only ever consumed their sum, and the fusion is exact: ∑istalei+(∑iui)·(B/BPS)≡∑i(stalei+ui·B/BPS), 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.

σi is the volatility stored on chain with the mark, raised to at least the realized |Δmark|/mark on each push (MarkStoreP8). Which feed supplies it depends on the leg’s oracle mode; see Mark Relayer.

3.3. Volatility band

Svol=fmin,p+σp·νp100·BPS

Vega has base 10,000 = 1.0x. At ν=104 and σ=104 PBPS (1%), the volatility contribution is 104·104/106=100 PBPS = 1 bp above the floor. There is no hardcoded base: the base is the configured floor.

3.4. Staleness surcharge

Ustale=∑iz·σi·τiBPS,τi=max(0,tnow-tobs,i-gi),gi=min(ttli2,30)

with z=472 (STALE_Z) and the grace cap 30 s (MAX_STALE_GRACE_SECS), both file-private constants in PricingLib.sol. z 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: 4721800/(2·BPS)=1.00. τi is computed in _staleExcessOf off the feed the leg already read, the per-leg term in _staleTerm, which short-circuits to 0 when τi=0 or σi=0 (skips the sqrt), and the legs are summed at _walkLegs.

Per leg, not coupled. A coupled form, zσpmaxiτi/BPS, 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 2/2=70.7% of what they each owe (6675 against 9440 PBPS at σ=1%, τ=100 s: a 27.65 bp leak), while over-charging a fresh leg 1.41× 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 /BPS is a unit normalization, not a tuning knob. σ is PBPS-scaled, so without it the term is 104× too large and pins the spread at its uint16 field ceiling the instant age crosses the grace, turning a ramp into a step. Worked: σ=104 PBPS (1%), τp=100 s →472·104·10/104=4720 PBPS = 47.2 bp of spread, 23.6 bp paid per swap. At τp=1800 s: 472·104·42/104=19,824 PBPS = 198 bp (⌊1800⌋=42).

The grace cap binds independently of ttl: 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.

Ustale is a graceful-degradation layer below a hard revert, not instead of it. FeedMathLib.gate reverts:

  • StaleData at age > ttl
  • ZeroValue at mark == 0
  • ThresholdViolation at confidence > MAX_CONFIDENCE_HALT_BPS
  • FeatureDisabled(Err.Resource.FEED) on the guardian fast-freeze bit FEED_HALT_BIT

3.5. Confidence surcharge

Uconf=(∑iui)·BBPS=100∑iui

ui is leg i’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 (X,Y), S(X→Y)=S(Y→X) at the same block and the same pool state, for any trade size.

Proof. S is a function of exactly five quantities: ∑ifmin,i, σp, νp, ∑iui, Ustale (_pathSpread). Each is built by one of two constructions:

  1. A fold over the leg multiset with a commutative, associative operator: ∑ over {fmin,i}, {ui} and the per-leg staleness surcharges, and ∑ of squares for σp (_walkLegs).
  2. A max over the unordered pair {cIn, cOut}: νp alone (getAnchorPathQuote).

Reversing the swap reverses the hop order. The route between X and Y 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 S is identical. □

The theorem holds for any symmetric aggregate. ∑, ∑(·)2 and max 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 - feeOut

So, with yg the gross output from the leg walk, after the cap:

ϕtoll=T(yg),ϕ=⌈(yg-ϕtoll)·S2B⌉,y′=yg-ϕtoll-ϕ

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 S=1 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 yg 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 S/2 per side, not S. 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

ϕp=⌊ϕ·r100⌋,ϕl=ϕ-ϕp

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
QuantityReserve effect (out leg)Liability effect (out leg)Coverage effect
ϕp (protocol)stays in reserves+ϕp as pending treasury shares (PoolLiquidityLib.accrueTreasury)c unchanged, claimed with Admin.claimTreasury
ϕl (LP)stays in reserves+ϕl (PoolLiquidityLib._credit)c unchanged, LP claim rises via liquidityIndexWad
ϕtollstays in reservesnonec 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 c unchanged. The protocol slice is booked the same way, as treasury shares instead of an index raise, so it does not move c either. Both credits are booked at the cached pool rate g (lastGoodC): face is the fee divided by g when g>1, 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: Q telescopes exactly only at constant L, and the fee credits move L mid-loop. The residual is pool-favorable (higher L ⇒ lower c ⇒ more toll), documented at PricingLib.sol.

5.4. Worked example

1 ETH into a WETH/USDC pair, mark 2000 USDC/ETH, S=5000 PBPS (0.5%), USDC coverage at or above 1 after the fill (so ϕtoll=0), r=10.

yg≈2000,ϕtoll=0,ϕ=2000·50002·106=5,y′=1995

ϕp=0.5 USDC stays in reserves and is booked as pending treasury shares; ϕl=4.5 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 S, which is what makes S 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 bid(0)=ask(0)=mid; with centered curves mid=mark exactly (Anchor Path Pricing §3.1). The spline contributes price impact only.
  • protoSharePct splits that one pot, it does not add to it (§5.3).
  • There is no static fee. Every term of S 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; _settleQuote touches amountIn nowhere.

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 R and L be the output asset’s reserves and liabilities, c=R/L in WAD, and yg the gross output.

Q(c)=lnc-c+1,c0=min(RL,1),c1=min(R-ygL,1)

ΔQ=Q(c0)-Q(c1),T(yg)=ygyg≥R0ΔQ≤0min(⌈ΔQ·κcov·LBPS·WAD⌉,yg)else

PricingLib._covToll, _covQ (FixedPointMathLib.lnWad). Returns 0 immediately when L=0 or yg=0; there is no κcov=0 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.

Q is concave with a unique maximum Q(1)=0, and Q(c)→-∞ as c→0: a convex no-drain wall.

6.2. The three structural properties

Charge-only, clamped to the increasing branch of Q, 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 q→0+. With c=R/L and dc=-q/L:

ΔQ≈-Q′(c)dc=(1c-1)qL

limq→0+T(q)q=κcovBPS(1c-1)forc<1,=0exactlyforc≥1

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 c=0.9575 and κcov=100 bps the marginal toll is 0.01·(1/0.9575-1)=4.44 bp, and it scales linearly in κcov. 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 50≤κcov≤BPS on every listed asset, hub included (requireNeverDepletable). On X→base→Y the terminal is Y, 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 XrightarrowY at one block. The spread is identical in both directions. The toll is TY one way and TX the other. By §6.3 at small size that is

κcovBPS[(1cY-1)+-(1cX-1)+]

A trade whose output is the hub pays Thub.

Interior vs hub. A non-base interior can carry κcov>0 and still be tolled at zero, because it is interior (ΔR=0), 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 <= BPS on every listed asset including the hub, enforced by requireNeverDepletable at 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 -4.0 PBPS to the LP at c=0.60). 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 m but on the skew-loaded mid r=m(1+δ(c)), where ψ is the int8 inventory-skew index and δ the price displacement it produces. The spread is symmetric about r:

bid=r(1-S2B),ask=r(1+S2B)

The skew is piecewise-linear in the coverage ratio, clamped at ±100 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:

  1. Mid skew δ(c): 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.
  2. Spline traverse direction: the buy and sell legs integrate the same monotone curve over different intervals from the anchor. Size-dependent, endpoint legs only.
  3. Coverage toll T: charge-only, terminal asset only, never rebated. Theorem 2.
  4. 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.

P=m(1+δ(c))±S2B,δ(c)≈ψ(c)·κ100·B

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 m is a frozen external mark between pushes, P drifts only through realized inventory c.

  • Inventory-moving flow walks up the skew and pays δ on top of fmin. 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 fmin.

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 XrightarrowY at quoted spread S. 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 fmin,X+fmin,Y; the base’s own minFee never enters, because minFee is per leg’s profile asset and the base supplies no leg profile.

TermBasis
fmin,pconfigured: fmin,X+fmin,Y
Svol-fmin,pcomputed: σpνp/(100·BPS) at the quoted σ
Uconf+Ustaleresidual: S less the two rows above, not an independent reading

The last row is solved for, not measured. S 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 ∑i(stalei+ui·B/BPS), 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 fmin,p.

Track the surcharge share as an operational metric, not revenue.


9. Why the path fee floor sums over legs

The invariant. Each leg ships fmin,i≥2θi, where θi 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 max. 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 max funds one leg and leaves the rest unfunded.

maxifmin,idoesnotmeetfmin,p≥2∑iθi

Under the shipped ∑ifmin,i the floor clears 2∑iθi for every cross whose legs each carry fmin,i≥2θi; 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 6×10,922=65,532 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 max(fmin,i,Fi), not bare fmin,i. Fi is a ratio over its live swing, computed per quote, and the swing cap w¯ 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, b=⌊65,535/6⌋=10,922, gives w¯=⌊2bB/(2B+b)⌋=10,862. 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: ΔRinterior=0 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 X must be attested in units of assets[X].anchor, and mark = MarkWordLib.mark(word) with no correction. At depth ≥2 a base-denominated rule is dimensionally wrong, not merely imprecise: stETH -> ETH -> USDC would compose stETH·ETH/USDC2.

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 2(θchild+θparent). 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

BehaviorStateReference
Fee once, output-side, S/2, post-tollshippedPricingLib._settleQuote
Toll charged before fee, terminal asset onlyshipped_covToll
50≤κcov≤BPS on every listed asset, the base/hub includedshipped, enforcedPoolConfigLib.requireNeverDepletable rejects κ<50 and κ>BPS at every writer; requireHubKappaDominant keeps every spoke at or under its hub
Direction-invariant spread (Theorem 1)shipped_pathSpread
min(c,1) clamp on the tollshipped_covToll
Sum / quadrature path aggregationshipped_walkLegs, _pathSpread, §9
No cap of any kind on the composed spreadshipped§3.1; trader protection is minAmountOut
Per-leg staleness surcharge, summedshipped_walkLegs
Anchor-unit marks consumed as attested, quoteUnitshipped§10.3
MAX_DEPTH = 4 multi-anchor tree (8 legs, 9 nodes)shipped, activation gatedAnchorTreeLib.sol, Anchor Path Pricing §10
Interior leg priced by the same _legMid as the terminal legshippedPricingLib.sol
Interior-leg manipulation fence in the path floorshipped_interiorMidAndFence, _priceInteriorLeg, §10.2
Dispersion band bound to the curve’s fence cap at the writeshippedPricingLib.dispersionCap, PoolConfigLib.sanitizeDispersion
Re-anchor timelocked at the base-migration tier, atomic with the oracle configshippedrequestOp(..., UPDATE_ANCHOR, ...) / Admin.executeUpdateAnchor
Parent-depeg breaker for an anchor-attested childnot designed into any fieldAnchor 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. _legScaleOut caps 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:

SiteWhich vegaBpsScope
_pathSpread volatility band, σpνp/(100·BPS)νp=max(νin,νout)the two endpoints only, applied to the whole path’s σp
_calculateDispersion, per legthe profile asset’s own Asset.vegaBpsthat 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 σp=∑iσi2, 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 νmax/νleg 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 ν≠0 (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 νmax/νleg 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

ϕf=⌈x·ffB⌉

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

ProtocolFee taken inChargedDistribution
Uniswap V2/V3100% inputper hopone-sided LP
Trader Joe V2100% inputper hopone-sided LP
Curve V1100% outputper hopone-sided LP
AIMM100% outputonce per swap, S/2LP claim via index; toll to coverage
AspectUniswap V3AIMM
Fee structurefixed tiers 0.01-1%multi-factor, σ + CI + staleness
Direction dependence of the widthnonenone (Theorem 1)
Inventory responsenonemid skew + terminal coverage toll
Quote centerpool reservesexternal keeper mark, no write-on-swap
LVR mitigationhooks onlyfresh mark + 2θ 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 R against unchanged L.

Surplus is redeemable, pool-wide. It raises R against unchanged L, so it raises the pool rate C, and every LP exit settles at C 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 Ustale. 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

FunctionFileRole
_pathSpreadPricingLib.solSvol+Ustale+Uconf, saturating narrow into uint16
_staleTermPricingLib.solzσiτi/BPS, per leg
_settleQuotePricingLib.soltoll, then fee, then split
_covToll / _covQPricingLib.solconvex coverage toll
_legMidPricingLib.solthe one offset law, interior and terminal alike
_interiorMidAndFencePricingLib.solinterior mid + its manipulation fence
dispersionCapPricingLib.solwrite-path half of the fence: the band ceiling
_priceInteriorLegPricingLib.solraises the leg floor to max(fmin,i,Fi)
splitFeePricingLib.solprotocol / LP division
_walkLegsPricingLib.solper-leg fold: sums, quadrature
swap_qePool.solthe single state-mutating swap entry (calls PricingLib.swapExactIn)
FeedMathLib.gateFeedMathLib.solstale / dead / uncertain / paused halts
PoolIOLib.settlePoolIOLib.solendpoint-only settlement (there is no separate exec)
PoolLiquidityLib._credit, _raiseIndexPoolLiquidityLib.solLP fee to liabilities + index
PoolLiquidityLib.accrueTreasuryPoolLiquidityLib.solprotocol fee to pending treasury shares

17. References