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).
| Function | Does |
|---|---|
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
| Parameter | Type | Description |
|---|---|---|
tokenIn | address | Token being sold (or NATIVE sentinel) |
tokenOut | address | Token being bought |
amountIn | uint256 | Exact amount to sell |
minAmountOut | uint256 | Slippage protection |
recipient | address | Receives output tokens |
deadline | uint256 | Unix 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
(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 overbasis(thetokenOutit paid foramountIn);amountOut' = gross − lpFee',protoFee' = 0: the protocol share stays inCoopArbuntil 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
| Parameter | Type | Description |
|---|---|---|
token | address | Asset to deposit (NATIVE sentinel for native + msg.value) |
amount | uint256 | Amount to deposit |
3.3. Mechanics
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:
If then . 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
| Parameter | Type | Description |
|---|---|---|
token | address | Asset to withdraw |
lpAmount | uint256 | LP shares to burn |
minAmountOut | uint256 | Slippage protection |
deadline | uint256 | Unix 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_leg | C | Same-asset payout | Cross-asset fair value |
|---|---|---|---|
| 0.80 | 1.03 | 800 | 1030 |
| 1.10 | 1.03 | 1030 | 1030 |
| 1.10 | 0.97 | 970 | 970 |
| 0.80 | 0.97 | 800 | 970 |
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 payable5.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
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
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_BITmust 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.
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
| Constraint | Value | Purpose |
|---|---|---|
| Max depth | MAX_DEPTH = 4 | Bounds gas and the summed path fee |
| Max nodes | MAX_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 cycles | enforced | validateAnchor 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_legleavescunchanged, and atμ = C < c_legraises it. What raises the source leg’scin every case is a cross exit:withdrawTointo a different token, orswapLiability, 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 ascfalls.
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):
| Reason | Constant | Direction | Site |
|---|---|---|---|
| 0 | INDEX_REASON_DONATE | up | PoolLiquidityLib.donate |
| 1 | INDEX_REASON_YIELD | up | Pool.hookCreditYield |
| 3 | INDEX_REASON_WRITEDOWN | down | Pool.hookWriteDown |
| 4 | INDEX_REASON_FEE | up | PoolLiquidityLib._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.
| Aspect | Shape |
|---|---|
| Storage | HookSlot {target, flags} + invested () |
| Dual ledger | ; pricing and coverage use , executable capacity is |
| Dispatch | Liquid check first (0 CALL if the buffer covers it); shortfall → liquidate via preOutflow, fail-closed. Invest on the keeper’s rebalance only |
| Yield | Sync → hookCreditYield / hookWriteDown |
| Admin | Timelocked 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
| Error | Condition |
|---|---|
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.
| Operation | Gas (cold) |
|---|---|
| Swap base -> spoke | 130.2k |
| Swap spoke -> base | 132.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 leg | 219.1k |
| Donate | 172.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.
13. Related Documentation
| Page | Covers |
|---|---|
| AIMM Overview | Architecture map, invariants, gas table |
| Inventory Management | Coverage, skew, pool rate |
| Liquidity Shaping | Curve depth curves |
| Anchor Path Pricing | Path composition, reference band |
| Spread & Fees | Fee calculation |
| Invariants | I-6, I-9 and the rest |
| Price feed reads | Keeper-pushed mark store (MarkStoreP8) |
| Hooks | Dual ledger, rehypothecation buffer |
| Admin · Flash | Peripheral singletons calling Pool externally |