Inventory management
AIMM books every asset separately: each leg carries its own reserves, its own LP liabilities and its own risk parameters, with no pool-wide invariant tying them together. The ratio of the two, coverage, is the single state variable the rest of the pricing stack reads. This page derives it and the three mechanisms it drives, each stated exactly as the contracts compute it.
0. Units and notation
Every quantity below is an on-chain integer. Fixed-point bases (Constants.sol):
| Symbol | Base | Value | Used for |
|---|---|---|---|
| WAD | 1e18 | coverage, haircut ratio, prices | |
| BPS | 1e4 | 0.01% units: kappa, spline x-domain | |
| PBPS | 1e6 | 0.0001% units: dispersion, spread |
All division is Solidity integer division, truncating toward zero. Truncation is called out where it
changes an economic outcome. Symbol collision: denotes dispersion in
Liquidity Shaping and Spread & Fees; this
document writes the convex coverage-wall strength as (kappaCovBps) to keep the
two apart.
Live figures come from a shipped stable pool (addresses: 2. Deployments) and from the shipped reference-roster parameters (per-parameter tables in Parametrization). Reserves move: treat reserve-derived numbers as a dated snapshot. The parameters are stable.
1. Overview
AIMM uses Asset-Liability Management (ALM) in the lineage of Wombat and Platypus:
- Reserves (
Asset.reserves, uint128, token units): tokens the pool holds. - Liabilities (
Asset.liabilities, uint128, token units): LP claims on that asset. - Coverage .
There is no pool-wide invariant. Each asset carries its own and its own risk parameters, and the price is anchored to an external oracle mark rather than to a reserve ratio. Coverage enters pricing at exactly three points:
| Mechanism | Function | Effect |
|---|---|---|
| Inventory skew | Pricing.computeInventorySkew | Shifts the quote center along the spline |
| Coverage toll | Pricing._covToll | Convex charge on draining an under-covered leg |
| Withdrawal haircut | PoolLiquidity.applyHaircut | Prices an LP exit out of a deficit |
It does not enter the depth axis: the denominator converting trade size into spline domain is the leg’s raw reserves (§4). Coverage moves the quote center and charges the drain; it never manufactures depth the leg does not hold.
1.1. Beyond pool-wise invariants
Traditional AMMs enforce a pool-wise invariant: ; Curve StableSwap’s ; Balancer’s ; Orbital/CCMM’s spherical . Each of those is simultaneously the pricing rule and the solvency rule, so N-asset generalization requires new geometry. AIMM separates them:
- Pricing comes from the oracle mark, the inventory skew, and a chosen density profile.
- Solvency is per-asset bookkeeping.
Adding an asset therefore adds a row, not a dimension to an invariant.
2. Coverage ratio
2.1. Definition
Pricing.calculateCoverage returns exactly this, in WAD, so is 100% coverage. Two edge
behaviors:
- returns
type(uint256).max, not a revert. Every consumer treats that as “infinitely covered”. - The division truncates, so the stored is a floor. A leg is never reported as better covered than it is.
is the economic reserve, the full dual-ledger book
when a yield hook is invested. Pricing and coverage always use full ; executable cash for
swap/flash/push is only, checked separately at settlement
(PoolIOLib.settle, PoolIOLib.sol). See Pool Hooks.
2.2. Key thresholds
The saturation points are protocol constants, not configuration: the skew law hard-codes and (§3), identically on every leg of every pool.
| Coverage | State | Skew | Behavior |
|---|---|---|---|
| Overcollateralized | Max discount, pool wants to sell the asset | ||
| Equilibrium | Quote centered on the mark | ||
| Undercollateralized | Max premium, pool wants to buy the asset |
Both are constants, so no write can move the slope or reprice an open book by shifting where the arms saturate. Why those two points are the only admissible ones: §3.2.1.
3. Inventory skew (coverage → skew)
maps to a position on the spline in Liquidity Shaping §5.
3.1. Exact formula
Pricing.computeInventorySkew, in order:
with from §2.1. Four properties of that law:
- No inputs beyond coverage: no per-asset parameter, no oracle read.
- Return type is
int8, so by both clamp and type. - The magnitude truncates toward zero: a computed 14.62 becomes 14, never 15.
- Positive means undercovered, and the pool quotes the asset at a premium so it is bought back.
The empty-liability sentinel is not a rounding case. returns , the maximum discount, not : a leg with reserves and no LP claims is pure surplus and the pool should give it away.
What makes the skew earn rather than merely protect is the roster, not the formula. The law only prices imbalance: a premium on the side that drains, a discount on the side that refills. If the discount is never taken, the leg sits at its clamp holding whatever the market wanted to sell. Inside a core of correlated legs the two sides usually meet, because the flow that refills one leg is flow that drains another: a single trade pays skew on the leg it takes while restoring coverage on the leg it delivers, so inventory mean-reverts instead of accumulating. Correlation makes that likely, not certain: legs that move together can leave the whole core displaced the same way at once. Why cores are cut by asset class: Pool Composition.
3.2. The two arms are not symmetric
The draining arm has slope 200, the filling arm slope 100. That is a conservation bound, not an oversight: the two must not be tidied into one .
Both slopes come from the same requirement: a closed round trip must not refund more impact than the
outbound leg charged. The skew step and the volume traverse advance the same spline x-coordinate, so
the skew slope is pinned against the traverse’s step. On the draining side the traverse already
out-steps and 200 is admissible. On the filling side it under-steps, and a 200 slope there would put
the return leg of a ping-pong ahead of the outbound one by exactly the gap, which a trader extracts
from LPs. Pinned by ImpactConservation.t.sol, not by a comment.
| Coverage | Note | |
|---|---|---|
| saturated | ||
| 75% | draining arm | |
| 90% | draining arm | |
| 100% | centered on the mark | |
| 120% | filling arm | |
| 150% | filling arm | |
| saturated |
The arms meet their clamps exactly, so the whole range carries information and neither arm has a dead zone.
3.2.1. Why saturation sits at 1/2 and 2
Write the saturation points as . Two independent studies asked whether should be a per-asset dial; both closed the question the same way.
cannot reach the draining arm at all. The round-trip conservation bound of §3.2 pins the draining slope at 200 for every . All could ever do is soften the filling arm, whose slope would be .
is the unique admissible value, pinned from both sides:
- Continuity. The draining arm reaches at . Saturating at (that is, ) clamps the arm early, at , so jumps at the saturation point. Measured leak across such a discontinuity: up to pbps.
- Conservation. A filling slope above 100 lets a ping-pong round trip refund more impact than it charged (§3.2), which requires .
is discontinuous and only weakens inventory control. is the single point satisfying both, which is why 200, 100 and the clamps at and are constants rather than writable fields.
The empirics agree. is also the LVR minimum on all three real tapes (ETH, BTC, SOL). More importantly, the coverage floor is fee-determined: a drain stops where the skew’s mid displacement equals the spread, which puts on a volatile leg and on a stable one independently of shock size. The clamp region is therefore unreachable in practice. Real-tape dwell below : 0.14% to 0.70% of the time for an unwalled volatile leg, and 0.000% of 3.5 years for a walled stable. A per-asset saturation point would be a dial on a region the pool does not visit.
3.3. Measured skew, stable pool
and are a dated on-chain snapshot of a pre-Arc reference roster; is recomputed from
§3.1 against that snapshot rather than read back from chain, so the rows are worked examples of the two
arms, not verification. DAI, RLUSD and USDG are not listed on the Arc fleet - its stable core is USDC,
USDT, USDS, USD1 and PYUSD (deployments/arc-risk-params.json). Other
pages quote coverages captured at other times. The toll example in
Spread & Fees §6.3
reads RLUSD at , from an earlier snapshot than this table. Reserves move; only the
parameters are stable.
| Asset | Arm | ||||
|---|---|---|---|---|---|
| USDC (base) | 66,369.84 | 50,154.23 | 1.32332 | filling | |
| USDT | 60,035.65 | 50,130.65 | 1.19758 | filling | |
| DAI | 52,714.57 | 50,104.15 | 1.05210 | filling | |
| RLUSD | 48,212.31 | 50,041.02 | 0.96346 | draining | |
| USDG | 47,805.89 | 50,081.42 | 0.95456 | draining |
The book is asymmetric in normal operation. No leg sits at , so the tradeable spline domain is split unevenly about the center before any other mechanism acts (see Liquidity Shaping §5). Each filling-arm row displaces half as far as the same distance from par displaces on the draining arm: the conservation bound of §3.2 doing its work.
3.4. Relation to Avellaneda-Stoikov
Avellaneda-Stoikov displace the quote by , linear in inventory and scaled by their risk-aversion coefficient and variance . AIMM keeps the linearity, replaces with the coverage ratio (the Platypus/Wombat imbalance metric), and moves both the risk-aversion multiplier and out of the skew entirely. What remains is an inventory-driven mid shift, first instantiated on-chain by DODO PMM’s oracle-anchored proactive maker, driven by a coverage metric rather than by a slippage-on-invariant.
Risk aversion is not a dial on the skew: the round-trip conservation bound of §3.2 determines both
slopes uniquely. AIMM expresses it instead through vegaBps, which widens the quoting band against
, and through minFeePbps, which floors the spread.
enters two other places: the dispersion that scales the density curve
(Liquidity Shaping §6) and the volatility term
of the spread (Spread & Fees). The separation lets the level shift and the
width be tuned independently, and keeps computeInventorySkew free of any oracle read.
See Foundations §2.4 for the AS reference.
4. The depth denominator
Canonical. Liquidity Shaping §7, Spread & Fees §6.5 and Slippage & Price Impact §2.1 restate the result only.
4.1. What it is
The denominator converting a trade size into a fraction of the spline domain is the leg’s raw reserves, with a zero-guard and nothing else:
uint256 depth = reserves == 0 ? 1 : uint256(reserves); // Pricing.quoteSwap, Pricing._priceEdgeHop
volumeFraction = amountIn * BPS / depth;It is expressed in the profile asset’s own token units, so the x-axis is anchor-free: it depends
neither on which asset the leg is anchored to nor on any price. The guard exists because
_traverseCurve divides by it; 1 wei is not a liquidity claim, it is a division floor.
4.2. Properties
| Point | Value |
|---|---|
| wei (division guard) | |
| , any coverage |
Monotone in reserves, independent of , independent of , and constant per swap. The real depth lever is the reserve balance itself.
Coverage does not reach this axis: an under-covered leg gets no virtual depth. Subsidizing the denominator on a leg an informed flow is draining would shrink the traversed spline interval and so reduce the impact charged on the drain, paying a trader to cycle exactly the leg that needs reserves. Coverage is priced by the two mechanisms that charge instead: the skew shifts the mid against the deficit (§3) and the convex wall tolls the drain (§6).
5. Withdrawal haircut
The haircut stops an LP exiting an under-covered leg at face value and leaving the deficit to whoever stays.
5.1. Flow
5.2. Exact formula
PoolLiquidity.applyHaircut:
with the coverage deficit, the suppressor factor, the haircut ratio, the haircut
and the payout; is the face amount, = Asset.haircutSuppressorBps (uint16) and
is PoolConstantsLib.HAIRCUT_SUPPRESSOR_FULL_BPS.
The haircut is linear in the coverage deficit: no power-law exponent, no separate severity curve.
Four implementation facts:
- Early exit at or returns : full face, no haircut, no reads.
- There is no branch.
setAssetParamsrejects that range outright andinitAssetseeds BPS, so no writer can reach it. Out-of-range stored state underflows here rather than silently zeroing the haircut: fail closed, not open. - The haircut rounds up (ceil-div) so the payout rounds down. Without it a withdrawer could over-draw an under-covered reserve by up to 1 wei per call.
- Liabilities burn at full face , not at . The retained is exactly the exiting LP’s pro-rata share of the deficit at , so a same-asset exit leaves where it found it rather than raising it; §5.5 does that arithmetic.
5.3. Suppressor
| factor | Effect | |
|---|---|---|
| 0 | 1.00 | Full linear haircut |
| 10000 | 0.50 | Half haircut |
| 15000 | 0.25 | Quarter haircut |
| 20000 | - | No haircut is possible: the writer reverts - setAssetParams rejects this range outright (PoolConfig), so no zeroing branch exists |
Worked, at : . With , . With , . A 1000-unit exit therefore returns 800 or 900 units respectively, and burns LP for 1000 in both cases.
Only the row is reachable on a listed asset. Two predicates compose to pin it:
PoolConfig.requireNeverDepletable rejects at every writer, and
PoolConfig.requireWallOk rejects paired with at the same
writers. initAsset seeds and setupOracleAndConfig zeroes it the moment a walled
asset is listed. Every leg of the Arc fleet carries
(deployments/arc-risk-params.json); is a dead field, and the non-zero rows above are the
arithmetic of a value the write path will not admit.
5.4. Cross-asset exit
PoolLiquidity._quoteWithdrawCross applies the haircut twice; the order is a security property:
- Haircut on the source asset first, before the mark conversion. Skipping this would let an LP in an under-covered leg exit at full face out of a healthy leg, dumping the deficit on that leg’s LPs.
- Route the haircut face through
Pricing.anchorPathQuoteLp(the normal swap path, including spread and coverage toll). - Mark cap: the payout is capped at the haircut face converted through the path’s oracle
mark,
min(amountOut, fair · markPrice · 10^(d_to - d_from))(PoolLiquidity._markCap). The inventory skew is a level, and it is one-sided on an under-covered child (draining slope 200 against filling 100), so a one-way LP conversion would monetize it out of the healthy destination leg. Trader swaps are immune - the level cancels on paired crossings - so only this path needs the cap. The decimal factor is carried explicitly becausemarkPriceis a whole-unit WAD ratio while the amount walk is rescaled per leg; without it the cap misses by on a mixed-decimal pool, binding to dust one way and never binding the other. - Haircut again on the destination asset, because that leg’s own coverage now applies.
Both depeg breakers run after the quote, which is state-identical to running them before in an all-or-nothing transaction, and lets them hit the primed transient feed cache.
5.5. How coverage restores
Restoration is priced, never scheduled. The haircut acts on the event that matters, an LP leaving an under-covered leg; what it does to coverage depends on which exit is taken.
A same-asset withdrawal at leaves exactly unchanged. With the
haircut ratio is the deficit itself, , so the payout is the withdrawer’s own
pro-rata share of the reserve, . Settlement burns and
(PoolLiquidity._applyWithdraw, same-asset branch), giving
The exiting LP takes its share of the deficit with it and leaves the ratio where it found it. Only the
ceil-div dust the haircut keeps with the pool moves , by at most 1 wei of reserve per call. Pinned
by test_withdraw_coverage_neutral_when_suppressor_zero. A suppressed haircut () would make
this exit coverage-raising, at the LP’s expense; is the only value a listed asset can
hold (§5.3), so the coverage-neutral case is the only one on the fleet.
Two exits do raise the source leg’s , and both are cross-asset: withdrawTo into a different
token (§5.4) and swapLiability. Each burns the full face from the source leg’s liabilities
and leaves the source leg’s reserves untouched - the payout is debited from the destination leg
instead (PoolLiquidity._applyWithdraw cross branch; PoolLiquidity.swapLiability). The source leg
therefore moves to . The source-side haircut on those paths is not what restores
coverage: it exists so the exiting LP converts only of face and cannot dump its deficit on
the destination leg’s LPs.
The convex wall (§6) is the other priced restorer, tolling any swap that drains a walled leg further: the toll is withheld from the gross output with no matching liability credit, so it raises against unchanged . Deposits move toward 1 from below (§8.1). Neither the haircut nor the wall reduces liabilities on its own, and no background process rewrites coverage.
The one path that can lower liquidityIndexWad is Pool.hookWriteDown, which realizes an actual
loss on invested reserves.
6. Convex coverage wall
Canonical derivation. Spread & Fees §6 owns the settlement order and the charge semantics; this section owns the potential and the clamps.
Pricing._covToll is the second coverage mechanism. Unlike the skew it is a charge, not a level
shift. It mirrors the reference simulator’s cov_q.
6.1. Potential and toll
where is the gross output before fees, in output-token units, and =
RiskConfig.kappaCovBps. Charged only when ; the toll is withheld from the gross
output and retained in the output reserve, so it accrues to LPs and is never a mark shift.
6.2. Why the clamps are there
- on both endpoints. is non-monotonic: it decreases on both sides of . A raw endpoint difference lets a drain that starts over-covered cross the peg and land below it with , paying zero toll. Clamping restricts to its increasing branch so the toll prices exactly the below-peg deficit and the over-peg portion stays free.
- short-circuits to : a fill that fully drains the leg is tolled to zero output.
Pricing.swapthen reverts rather than settling a zero-delivery swap. - Charge-only, no rebate ledger. A coverage-restoring trade drains the healthy leg, where and , so it pays approximately nothing. A round trip therefore strictly loses, which is the LP-safety property.
6.3. Round-trip behavior
Finite differences of telescope to zero over any closed reserve loop at constant . Since LP
fees accrue into liabilities mid-loop (§7), the telescoping is only approximate. The residual is
pool-favorable: higher gives lower gives more toll.
The wall is output-only: a same-pool spoke→spoke swap never outputs the hub, while a cross-pool hop that takes the hub out pays hub .
The protocol rule is that every listed asset, the hub included, carries ;
PoolConfig.requireNeverDepletable rejects a zero at every writer. The Arc fleet carries
= 300 on stables, crypto and metals, 400 on FX and 600 on equities, sized so the
resting coverage holds against each class’s on-chain
maxDeviationBps as the worst-case single-push mark error . Per-chain values live in
2. Deployments; they differ by deployment and nothing on this page should be read
as a fleet-wide statement. Read the live value with getAsset, never off a params file.
7. Settlement: what actually moves
PoolIOLib.settle is the only place reserves and liabilities change on a swap (there is no separate
exec: the split was considered and deliberately not made, since it would only thread need across
the boundary as an argument):
aIn.reserves += amountIn
aOut.reserves -= amountOut + protoFee
protocolFees[tokenOut] += protoFee
accrueLpFee(aOut, lpFee) // aOut.liabilities += lpFee, index raisedTwo consequences that are commonly stated wrong:
- Settlement is endpoint-only. Interior hops of a multi-leg path do not move any reserve. Only the two endpoint legs settle. This is what makes per-leg impact charging on interior hops incorrect (see Anchor Path Pricing).
- Liabilities are NOT unchanged on a swap.
PoolLiquidity.accrueLpFeeraisesaOut.liabilitiesby the LP fee and raises the liquidity index to match. The LP fee is the output the reserve debit deliberately did not pay out, so and both rise bylpFeerelative to a fee-free settlement, and moves accordingly.
The executable-liquidity check reads fresh at settlement
and reverts InsufficientAmount if it cannot cover
amountOut + protoFee + minLiquidity.
8. ALM flows
8.1. Deposit
- Pull , compute
lpAmt = x * WAD / mintIndex(asset). - Reject if
lpAmtrounds to fewer shares than the dead-share seed requires: a deposit too small to mint a share would otherwise donate reserves to existing LPs for free. - , . Coverage unchanged when the index is at parity.
Deposits are single-sided by construction: each asset’s is independent, so there is no pairing requirement and no forced ratio.
8.2. Withdrawal
- Compute face from LP shares and the liquidity index.
- per §5, .
- , (full face). At the gap is the withdrawer’s own share of the deficit and is unchanged (§5.5). A cross exit debits from the destination leg instead, so the source leg’s rises.
8.3. Swap
- Skews per §3, depth denominators per §4.
- Spline traversal per Liquidity Shaping §8.
- Coverage toll on the output leg per §6, then the spread fee.
- Settlement per §7.
- , (net of the LP-fee accrual on the output leg).
9. Coverage classification
9.1. Per asset
| Coverage | State |
|---|---|
| 200% | OVER |
| 200% 100% | ADEQUATE |
| 100% 50% | UNDER |
| 50% | CRITICAL |
Only deposits, withdrawals and swaps move a leg between these states; nothing runs on a clock (§5.5).
9.2. Pool level
with the mark of asset in the pool’s unit of account. This is an off-chain reporting aggregate. No on-chain code path consumes : every gate in the contract is per-asset.
10. Parameter reference
10.1. Per-asset RiskConfig (IPool.sol)
Two fields, and that is the whole struct. It is an ABI and memory type only: both fields are
stored in Asset slot 2, not in a mapping of their own, which is why a risk write and an asset
write touch the same word.
| Field | Type | Unit | Deployed (Arc) | Purpose |
|---|---|---|---|---|
flags | uint16 | bitfield | 0x06 (SWAP_ENABLED_BIT | LIABILITY_SWAP_ENABLED_BIT; FLASH_ENABLED_BIT is set on no leg) | SWAP_ENABLED_BIT, LIABILITY_SWAP_ENABLED_BIT, FLASH_ENABLED_BIT, the two halt bits |
kappaCovBps | uint16 | bps | 600 stables, 1,500 FX, 2,000 crypto majors, 1,800 metals, 2,500 equities; each pool’s hub leg at the maximum of its own spokes (κ=0 is rejected by requireNeverDepletable at every writer) | Convex coverage wall (§6). Ladder and its derivation: Parametrization §6.2 |
10.2. Per-asset coverage sensitivity (IPool.Asset)
| Field | Type | Unit | Deployed (Arc) | Purpose |
|---|---|---|---|---|
haircutSuppressorBps | uint16 | basis 20000 | 0 on every leg | Withdrawal haircut gentleness (§5.3); forced to 0 on a -walled leg, and every listed leg is walled |
The inventory skew carries no per-asset field: it is the fixed law of §3, identical on every leg. Neither does the depth denominator, which is the leg’s own reserves (§4).
11. Traditional AMM vs AIMM ALM
| Aspect | Traditional AMM | AIMM ALM |
|---|---|---|
| Price source | Reserve ratio via invariant | Oracle mark, shifted by inventory skew |
| LP accounting | Share of the whole pool | Per-asset liability + liquidity index |
| Deposit | Both sides required | Single-sided |
| Withdrawal | Pro-rata across assets | Named asset, haircut on deficit |
| Rebalancing incentive | Arbitrage on the invariant | Skew (level) plus coverage toll (charge) |
| Undercollateralization | Implicit as impermanent loss | Explicit as , priced by skew, toll and exit haircut |
12. Security considerations
12.1. Bank-run resistance
- Withdrawal haircuts make an exit from a deficit cost the exiting LP the deficit share (§5), removing the first-mover advantage that drives a run.
- Mid skew gives a coverage-restoring trade the favorable side of the same shift a coverage-worsening trade pays (§3), so flow is priced toward equilibrium continuously.
- The convex wall (§6) makes the marginal cost of draining an under-covered leg diverge, so the last of the reserve is never cheap.
minLiquidityfloors the executable reserve, so a leg cannot be drained to zero even where leaves no wall (§6). It is the last outflow backstop, but it is inert unless a deploy config raises it:initAssetwrites 0.
12.2. Manipulation resistance
- External-mark pricing. Quotes center on the keeper-pushed mark (
FeedMathLib.mark). No quote input is derived from the pool’s own reserves or its own trade history, so a flash-loan-and-swap cannot move the quote center within a block. Only realized inventory skew moves, and skew is a level: it cancels exactly on a round trip. - The skew law takes no configuration. A sign-inverted or over-steep skew is not a rejected write, it is an unrepresentable state: both slopes and both saturation points are constants (§3.2.1).
- Skew bounds hard-cap the inventory-driven price displacement at one wall of the spline.
- Flow Guard locks freshly minted LP receipt shares for
flowCooldownSecs, against transfer and against burn alike (LPToken._beforeTokenTransfer), so JIT liquidity around a single block is impractical. There is no staking surface. See Flow Guards.
12.3. Circuit breakers
HALT_MASK(HALT_RISK_BIT | HALT_GUARDIAN_BIT=0x0041) halts the asset, checked on every hop of every path.FeedMathLib.gatefails closed on stale, dead, or over-confident feeds.BASE_DEPEG_HALT_BPS = 500reverts the whole pool if the base mark leaves of parity.- The per-asset depeg band is feed-relative:
OracleConfig.refFeedId+refBandBps, armed at settlement byPoolIOLib.priceBandGuardPath, halting the leg when its mark leavesrefBandBpsof an independent reference feed’s price. A discretionary “stop quoting this leg” call is the guardian halt, not a stored bound.
13. Related documentation
- Liquidity Shaping: the depth curve skew and depth feed into
- Anchor Path Pricing: multi-leg composition and marks
- Spread & Fees: the fee charged on top of the coverage mechanics
- Slippage & Price Impact: the full mark-to-execution decomposition
- Toxic Flow Mitigation
- Parametrization: full parameter reference
- Invariants