Pool Contract

Pool is the contract an integrator calls. Swap, deposit, withdraw, donate, liability swap, flash and hook entry points are all functions on one pool proxy. This page is the reference for that surface: signature, parameters, return struct, events and revert conditions per operation, plus the anchor-tree routing and the state rules every operation shares. Pricing maths behind the returned numbers: Inventory Management and Spread & Fees.


1. Overview

Pool (Pool.sol) implements AIMM’s primary liquidity operations as a PoolProxy (ERC-1967) deployed via PoolFactory.createPool, an owner-gated call that mints the proxy through the CREATE3 factory at a pre-mined salt. Each proxy holds its implementation in its own slot, which only PoolFactory can write; a timelocked fleet upgrade (executeReferenceUpgrade) calls upgradeTo on every proxy the factory minted (Admin). It is the whole pool-side entry surface and dispatches to linked libraries: deposit/withdraw/withdrawTo/donate/swapLiability to PoolLiquidityLib.sol, swap_qe to PricingLib.sol, config writes to PoolConfigLib.sol. Architecture: AIMM Overview §2.1.

Marks come from the lane each asset’s OracleConfig names in the chain’s one mark store (MarkStoreP8, run by the Pool implementation over its own storage and pushed by the keeper), and never from the pool’s own reserves or trade history (Mark Relayer).

FunctionDoes
swap_qe()Exchange tokens at oracle-aware pricing. Single entry; there is no on-chain batch or multi-swap call. The suffix is mined: selector 0x01dc30b8 sorts 2nd in the dispatcher, after publishMarks
deposit()Add single-sided liquidity
withdraw() / withdrawTo()Remove liquidity at the pool-level solvency rate C, same or different asset
donate()Gift tokens to LPs: raises the liquidity index, mints no LP
swapLiability()Move an LP position between assets
swapCoop_cd()Coop fill for CoopArb (lane 11): the swap’s quote, the fee re-split per leg (§2.6)

2. Swap

2.1. Function Signature

function swap_qe( address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, address recipient, uint256 deadline ) external payable returns (uint256 amountOut)

(The full SwapQuote struct is only returned by the view getSwapQuote().)

2.2. Parameters

ParameterTypeDescription
tokenInaddressToken being sold (or NATIVE sentinel)
tokenOutaddressToken being bought
amountInuint256Exact amount to sell
minAmountOutuint256Slippage protection
recipientaddressReceives output tokens
deadlineuint256Unix timestamp, inclusive. Reverts Err.Expired if block.timestamp > deadline. Pass type(uint256).max to opt out; there is no 0 sentinel

2.3. Execution Flow

Validate inputs and feature flags

Pull input

Resolve anchor-tree path

Per-leg mark skew traverse

Settle spread toll fee

Update reserves and fees

Validate minAmountOut

Transfer output

(No oracle write happens on swap - the keeper is the only feed writer.)

2.4. Return Value

swap_qe() returns uint256 amountOut. The view getSwapQuote() returns:

struct SwapQuote { uint256 amountOut; // Tokens received uint256 amountIn; // Tokens paid uint16 spreadPbps; // Path ROUND-TRIP spread width (0.0001% units); this swap pays half uint256 protoFee; // Protocol share of that one half-spread fee uint256 lpFee; // LP share of the same fee int8 skewIn; // Input asset inventory skew int8 skewOut; // Output asset inventory skew uint256 markPrice; // Path oracle mark, exact WAD, tokenOut per tokenIn, chained across legs uint256 midPrice; // Inventory-skewed center the book quotes around, exact WAD uint256 covToll; // Coverage toll withheld from gross output BEFORE the fee (tokenOut units) address[] routeHops; // Full routing path uint256[] hopAmounts; // Amount at each hop uint256[] hopPrices; // Per-leg realized exec price, WAD; analytics views only }

OEV decomposes exactly off the two price fields: (exec - mid)/mid is the extractable part, (mid - mark)/mark is inventory skew.

markPrice and midPrice are exact WAD, not the packed B64 the V1 oracle stores. Both are computed and handed straight out as a struct field and one 128-bit field of the Swapped prices word (saturated at 2^128 - 1), never into a storage slot, so a packed float would save nothing while costing 917 to 1,557 gas per swap and truncating the mantissa to 52 bits. The oracle stores its mark packed because there the encoding saves a storage slot: Mark Relayer.

2.5. Events

event Swapped( address indexed sender, address indexed recipient, address indexed tokenIn, address tokenOut, uint256 amounts, // amountIn | amountOut << 128 uint256 fees, // protoFee | lpFee << 128 uint256 prices, // markPrice | midPrice << 128, each saturated at 2^128 - 1 uint256 outBook, // tokenOut reserves | liabilities << 128, after settlement uint256 outState // covToll | liquidityIndexWad << 128 (uint72) | spreadPbps << 224 (uint16) );

Five packed words, low 128 bits first; the decode table and a TypeScript snippet are in Basic Operations §9. protoFee + lpFee is the whole fee and is denominated in tokenOut: there is no input-leg fee, so an indexer credits 100% of a swap’s fee to tokenOut. A swap emits no IndexUpdated: the out leg’s new book and liquidityIndexWad ride in outBook and outState.

2.6. Coop fill

function swapCoop_cd(address tokenIn, address tokenOut, uint256 amountIn, uint256 basis) external returns (uint256 amountOut);

Same quote, settlement, marks and reference band as swap_qe; paid to the caller. Selector 0x02d629af; swap_qe is 0x01dc30b8, and the _qe / _cd suffixes are mined for dispatch order. Both legs need COOP_ENABLED_BIT (bit 11) and SWAP_ENABLED_BIT, no halt; the caller needs AccessControl lane 11, held only by CoopArb. SWAP_GATED_BIT is not read. With the out leg’s terms disc (BPS of the LP fee, at most 3000) and rebate (BPS of the caller’s gain), and gross = amountOut + lpFee + protoFee of the public quote:

  • d = lpFee − ⌊lpFee·disc·(S − R)/S⌋: S = the quote’s total fee spread, R = its risk path (stale/CI premium + interior fence, never discounted); R ≥ S ⇒ no discount;
  • lpFee' = d + ⌈rebate·G⌉, G = max(gross − d − basis, 0) = the caller’s gain over basis (the tokenOut it paid for amountIn);
  • amountOut' = gross − lpFee', protoFee' = 0: the protocol share stays in CoopArb until the treasury sweeps it.

Swapped is emitted with sender = recipient = CoopArb and a zero protoFee half in fees. CoopArb.arb runs the external leg (a PancakeSwap v3 pool paid in its callback, or TychoRouter), then swapCoop_cd, and reverts unless it ends with at least minProfit more of the token it started from; only lane-12 operator keys call it. Terms are set per leg with coopDiscBps and coopRebateBps (requestAssetParams, setAssetParamsBounded) inside the leg’s coop fences. Kill: clear COOP_ENABLED_BIT on a leg (guardian, steward, owner, instant) or revoke lane 12.


3. Deposit

3.1. Function Signature

function deposit( address token, uint256 amount ) external payable returns (DepositResult memory result)

LP shares are credited to msg.sender; there is no minLPAmount / recipient parameter (share pricing is deterministic off the liquidity index).

3.2. Parameters

ParameterTypeDescription
tokenaddressAsset to deposit (NATIVE sentinel for native + msg.value)
amountuint256Amount to deposit

3.3. Mechanics

Validate inputs

Pull tokens

Compute lpAmount

Update reserves and mint

A deposit never calls the asset hook: only the keeper invests (see §10).

3.4. Coverage Impact

Deposits always improve coverage when the leg starts under-covered:

c0=R/L,c1=R+xL+x

If c0<1 then c1>c0. Proof: Invariants I-6.

3.5. Return Value

struct DepositResult { uint256 lpAmount; // LP tokens minted (net of deadLp) uint256 actualDeposit; // Tokens accepted uint256 deadLp; // Shares carved out and sunk to address(0) if this opened the leg }

4. Withdraw

4.1. Function Signature

function withdraw( address token, uint256 lpAmount, uint256 minAmountOut, uint256 deadline ) external returns (WithdrawResult memory result)

Output goes to msg.sender (no recipient parameter). withdrawTo(tokenFrom, tokenTo, lpAmount, minAmountOut, deadline) withdraws into a different asset, priced through the anchor-path quote. deadline is inclusive, same convention as swap_qe. Deposit and donate carry no deadline: they set no minOut and mint at the current index, so there is nothing stale to protect.

4.2. Parameters

ParameterTypeDescription
tokenaddressAsset to withdraw
lpAmountuint256LP shares to burn
minAmountOutuint256Slippage protection
deadlineuint256Unix timestamp, inclusive

4.3. Settlement Rate

Every LP entrypoint settles at the pool-level rate C = Σ R_k·m_k / Σ L_k·m_k over the leg roster (PoolSolvencyLib.solvency), uncapped. A same-asset withdraw pays face · min(c_leg, C), less the exit toll (Inventory Management §5.2), where c_leg = R·1e18/L is the in-kind delivery bound, and with any roster mark unusable it still settles at face · min(c_leg, min(1, lastGoodCWad)). A cross-asset withdrawTo converts face · C along the anchor path and reverts FeedUnavailable while any roster mark is unusable. Derivation, edges and tests: Inventory Management §5.

function exitMu(uint256 reserves, uint256 liabilities, uint256 capWad) internal pure returns (uint256) { if (liabilities == 0) return capWad; uint256 cLeg = (reserves * SC.WAD) / liabilities; return cLeg < capWad ? cLeg : capWad; // capWad = PoolSolvencyLib.exitCap($) }

4.4. Settlement Examples

At face = 1000, before the conversion’s spread and mark cap:

c_legCSame-asset payoutCross-asset fair value
0.801.038001030
1.101.0310301030
1.100.97970970
0.800.97800970

4.5. Return Value

struct WithdrawResult { uint256 amountOut; // Tokens received (after the exit rate) }

5. Donate

5.1. Function Signature

function donate( address token, uint256 amount ) external payable

5.2. Purpose

Gifts value to existing LPs (protocol fee redistribution, third-party yield injection, recovery top-ups). The donation raises the liquidity index, so every existing LP share is worth more underlying.

donate() accrues no LP credit to the sender: it mints no shares and takes no position. The sender is logged, event Donated(address indexed sender, address indexed token, uint256 amount) (IPool.sol), so the call is traceable but not creditable.

Donating to a leg that has never been credited strands the gift. raiseIndex no-ops when liabBefore == 0, so everything above the dead-share seed lands in liabilities backing no share. Deposit first to open a leg; donate is the non-dilutive top-up for a leg that already has LPs.

5.3. Mechanics

Validate token

Pull tokens

Add to reserves

Add to liabilities

Raise liquidity index

5.4. Coverage Impact

A donation leaves coverage unchanged: reserves and liabilities rise equally, and the value transfer to LPs runs through the liquidity index instead.

c_1 = (R + a) / (L + a) ~ c_0

It still helps an under-covered pool in absolute terms: the deficit L - R stays constant while L grows, so the relative deficit shrinks. The coverage ratio itself does not improve.


6. Liability Swap

6.1. Function Signature

function swapLiability( address tokenIn, address tokenOut, uint256 lpAmountIn, uint256 minLpAmountOut, uint256 deadline ) external returns (uint256 lpAmountOut)

(The function is named swapLiability, not liabilitySwap; shares move for msg.sender - no recipient parameter.)

6.2. Purpose

Moves an LP position from one asset to another without a withdraw + deposit cycle.

6.3. Mechanics

Enable bits and halt checks

Compute liabIn

Fair value liabIn times C

Anchor-path quote

Mark cap on the payout

Depeg band guard

Adjust liabilities burn mint

Check minLpAmountOut

Reading C rather than the input leg’s own coverage stops a liability swap escaping the pool rate, and PoolHooksLib.requireBacked on the input leg stops it converting units the pool does not hold. The mark cap (PoolLiquidityLib._markCap, shared with withdrawTo’s cross path) bounds the payout: fair value is credited through the path’s oracle mark, min(amountOut, fair · markPrice · 10^(d_to - d_from)), never through the inventory-skewed mid. Skew cancels on a trader’s paired crossings but not on a one-way LP conversion, and on an under-covered leg it is one-sided. The explicit decimal factor is required because markPrice is a whole-unit WAD ratio while the amount walk is rescaled per leg. Liability re-denomination is protocol-fee exempt: it moves no reserves, so there is no outflow to skim. Both fee legs land as reduced net liability instead.

6.4. Requirements

  • LIABILITY_SWAP_ENABLED_BIT must be set on both assets

7. Swap Paths (Anchor Tree)

7.1. Topology

Every asset anchors to one parent, which need not be the base. Every chain terminates at the base, the root.

WBTC base

WETH

USDC

stETH

weETH

USDT

DAI

Examples:

  • stETH → DAI: path [stETH, WETH, WBTC, USDC, DAI] (4 legs; endpoints settle, interiors do not)
  • USDT → DAI: path [USDT, USDC, DAI] (2 legs; LCA is USDC; BTC mark never read)

7.2. Path Computation

The path is the unique tree path between the endpoints: walk up from each endpoint to the lowest common ancestor, then concatenate. There is nothing to search and no alternative decomposition. A malformed tree (an anchor chain that does not terminate at the root) reverts.

7.3. Path Constraints

ConstraintValuePurpose
Max depthMAX_DEPTH = 4Bounds gas and the summed path fee
Max nodesMAX_PATH_LENGTH = 9 (8 legs)Two walks meeting at the LCA; depth bounds one walk, a path is two of them (Anchor Path Pricing §1)
No cyclesenforcedvalidateAnchor walks to the root with an explicit current == asset check and an unconditional step cap. A disconnected 2-cycle never reaches the root, so a depth counter alone is insufficient

Why a tree rather than a star: correlated assets get their own edge, feed, sigma and fee floor. A BTC-based pool quotes USDT -> DAI off two stable feeds instead of round-tripping through the BTC mark.

Topology is configuration, not contract state. The tree is general: any asset may anchor to a non-base parent up to MAX_DEPTH = 4, which is how curators price correlated pairs against their own reference and run distinct fee profiles inside one pool. Each pool’s anchor cells decide its actual shape. An empty cell anchors the leg directly to the base and makes it an endpoint leg; a filled cell deepens the tree and activates the interior fence for the routes through it. Depth 4 is the contract capability: the schema, the deploy scripts and the fence budgets are all general to it, nothing special-cases a shallower tree. Activating a deep edge needs the mark feed serving the cross pair rather than any contract change; the sequence is one UPDATE_ANCHOR op per leg, requested through Admin.requestOp and applied by Admin.executeUpdateAnchor after the GOVERNANCE tier delay, carrying the leg’s anchor and its oracle config in one inseparable payload. Every deep leg must carry quoteUnit = 0 and its own reference feed.

The depeg guards do not extend to a parent automatically. The reference band scales with depth: PoolIOLib.priceBandGuardPath runs it on every interior node except the base, priceBandGuard runs it on both endpoints. The base parity halt does not scale: it is gated on the hop being the base token, so it tests only the base mark against 1e18. Neither is a peg test for a parent. OracleConfig.refBandBps is a same-unit agreement check between the lane’s two tiers; no field carries a parent-depeg breaker for an anchor-attested child. See Anchor Path Pricing §7.1.


8. State Management

8.1. Transient Storage (EIP-1153)

Transient storage holds the reentrancy guard and the oracle cache (one price lookup per asset per transaction).

8.2. Reentrancy Protection

Pool inherits solady’s ReentrancyGuardTransient via @btr-shared/base/TransientGuard.sol, which overrides _useTransientReentrancyGuardOnlyOnMainnet() to return false. Solady defaults that to true and so TSTOREs only on chainid 1, falling back to SSTORE everywhere else at a measured +4,874 gas cold / +2,874 warm per guarded call. Every target chain runs Cancun-or-later, so the fallback only adds cost. nonReentrant is not hand-rolled here.

A second, independent transient slot, FLASH_INFLIGHT_SLOT (PoolIOLib.sol), is set for the duration of a flash callback and read by PoolIOLib.requireNoFlash at four sites: PoolIOLib.pull (the reserve-crediting chokepoint behind deposit, donate and swap_qe), PoolLiquidityLib.withdrawTo, PoolLiquidityLib.swapLiability, and the hook writers on Pool. The reentrancy slot is not held across the ERC-3156 callback, which is why the flash flag exists.

8.3. Liquidity Index Writers

Coverage is never rewritten by a background process: it moves only when a swap, deposit, withdrawal or donation moves it. Two live pricing mechanisms restore it, neither on a clock and neither reducing liabilities on its own.

  • LP exits (§4.3) settle at the pool rate, not at a per-leg haircut. A same-asset exit at μ = c_leg leaves c unchanged, and at μ = C < c_leg raises it. What raises the source leg’s c in every case is a cross exit: withdrawTo into a different token, or swapLiability, which burns the full face from this leg’s liabilities while debiting the payout from the destination leg.
  • The convex coverage toll (kappaCovBps) prices any swap that drains a walled leg further, rising superlinearly as c falls.

Every listed asset including the hub must carry kappaCovBps of at least MIN_KAPPA_COV_BPS (50) (Invariants §I-9). The toll is output-only: hub κ prices taking the hub out.

liquidityIndexWad has a small, explicit set of writers, each emitting IndexUpdated with a reason code (PoolConstantsLib.sol):

ReasonConstantDirectionSite
0INDEX_REASON_DONATEupPoolLiquidityLib.donate
1INDEX_REASON_YIELDupPool.hookCreditYield
3INDEX_REASON_WRITEDOWNdownPool.hookWriteDown
4INDEX_REASON_FEEupPoolLiquidityLib._raiseIndex

hookWriteDown is the only writer that lowers the index, and it is reachable only from a configured asset hook booking a realized loss on rehypothecated reserves.

RiskConfig.flags bit layout, masks and gating: Guards Overview §1. DEPOSIT_GATED_BIT requires AccessControl.perms(msg.sender) lane 9 (0x200); SWAP_GATED_BIT lane 10 (0x400).


9. Native Token Support

9.1. Sentinel Address

address constant NATIVE = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

9.2. Auto-Wrapping

// PoolIOLib.wrap if (token != NATIVE) return token; if ($.wnative == address(0)) revert InvalidInput(); // fail closed on a no-wrapper chain return $.wnative;

9.3. Transfer Handling

// PoolIOLib.pull requireNoFlash(); if (token == NATIVE) { if ($.wnative == address(0)) revert InvalidInput(); // no wrapper on this chain if (msg.value < amount) revert InsufficientAmount(msg.value, amount); IWETH9($.wnative).deposit{value: amount}(); uint256 excess = msg.value - amount; // overpay is refunded, if (excess > 0) safeTransferETH(msg.sender, excess); // not rejected return amount; } // Payable entrypoints exist solely for the native sentinel. if (msg.value != 0) revert InvalidInput(); uint256 balBefore = _balanceOf(token); safeTransferFrom(token, msg.sender, address(this), amount); return _balanceOf(token) - balBefore; // FoT captured by delta
// PoolIOLib.push if (token == NATIVE) { if ($.wnative == address(0)) revert InvalidInput(); IWETH9($.wnative).withdraw(amount); safeTransferETH(to, amount); } else { safeTransfer(token, to, amount); }

Three details: the native branch compares with < and refunds the excess rather than reverting on it; the ERC-20 branch rejects any attached value outright, so value cannot land outside the reserve ledger; the amount credited to reserves is the measured balance delta, so a fee-on-transfer token books what actually arrived. On a chain shipping no WETH9-style wrapper, initialize takes wnative_ == address(0) (Pool.sol) and every NATIVE call reverts InvalidInput at the wrapper check; see deployed instances.


10. Hook Integration

Per-asset IPoolHooks: one void callback, preOutflow. Full reference: Hooks.

AspectShape
StorageHookSlot {target, flags} + invested (Rinv)
Dual ledgerR=Rliq+Rinv; pricing and coverage use R, executable capacity is Rliq
DispatchLiquid check first (0 CALL if the buffer covers it); shortfall → liquidate via preOutflow, fail-closed. Invest on the keeper’s rebalance only
YieldSync → hookCreditYield / hookWriteDown
AdminTimelocked queue via generic Admin requestOp(UPDATE_HOOK), execute executeUpdateHook, cancel cancelOp; direct clearAssetHook (steward or owner), which recalls the invested tranche first

11. Error Handling

ErrorCondition
ZeroValue()Amount is zero
InsufficientAmount(available, required)Not enough reserves/balance
ThresholdViolation(value, threshold)Slippage exceeded (minAmountOut / minLpAmountOut); also a minLiquidity breach on withdraw (PoolLiquidityLib.withdrawTo) and the confidence-interval halt at MAX_CONFIDENCE_HALT_BPS (FeedMathLib.gate)
FeatureDisabled(resource)Swap/flash disabled
Expired()block.timestamp > deadline
FeedUnavailable()A mark the pool-level solvency rate needs is unavailable; a mint or cross conversion refuses
WeightCapExceeded(weightBps, capBps)A claim credit would push a leg past its maxLiabWeightBps share of the claim book
Reentrancy()Recursive call attempt
InvalidState()Pool not initialized (Pool.whenInitialized); also a wiped leg, liquidityIndexWad == 0, at PoolLiquidityLib.mintIndex, and a deposit into a leg holding reserves == 0 against non-zero liabilities

12. Gas Costs

GasProbe.t.sol (re-measured on the live pool build): one transaction per probe, the proxy a factory deploys, the quartic curve, three stables at mark 1.0 with 10M seeded each, 10k per call (0.1% of depth). Marks come from a mock store, so the figures are execution gas: they exclude the 21k intrinsic cost and calldata, and the store read of a real push.

OperationGas (cold)
Swap base -> spoke130.2k
Swap spoke -> base132.5k
Cross-spoke swap (spoke -> base -> spoke)145.9k
Swap refused at minAmountOut (stops before settlement)97.9k
Quote base -> spoke / spoke -> base / spoke -> spoke (getSwapQuote)59.4k / 59.1k / 75.5k
Deposit, holder’s first into a seeded leg219.1k
Donate172.2k
Withdraw same-asset / cross-asset (partly warm)185.0k / 232.4k

Curve components: eval 3.7k; the full sell-leg quote (PricingLib.quoteSwap) 5.8k. The range integral is O(1): at most 5 SLOADs, flat in trade size and segments crossed.

A production-shaped swap (real mark store, both legs’ lane words read in one STATICCALL, ProdSwapGas.t.sol) costs about 130k to 137k all-in, intrinsic gas and calldata included.


PageCovers
AIMM OverviewArchitecture map, invariants, gas table
Inventory ManagementCoverage, skew, pool rate
Liquidity ShapingCurve depth curves
Anchor Path PricingPath composition, reference band
Spread & FeesFee calculation
InvariantsI-6, I-9 and the rest
Price feed readsKeeper-pushed mark store (MarkStoreP8)
HooksDual ledger, rehypothecation buffer
Admin · FlashPeripheral singletons calling Pool externally