---
title: "Providing liquidity"
description: "Single-sided deposit into one pool leg, the per-leg bLP receipt and its liquidity index, the anti-JIT cooldown, and what a same-asset or cross-asset exit costs"
audience: both
type: guide
status: live
lang: en
publish: true
---
# Providing liquidity

This page follows a liquidity position from deposit to exit: what you send, what receipt you get back, how its value accrues, and what a withdrawal is charged. Liquidity on an AIMM pool is **single-sided and per leg**: you deposit one listed token into one pool and receive an ERC-20 receipt for that leg only, never a proportional basket share. It covers the app surface (`/pools` → Manage) and the contract calls behind it, so it serves both LPs and integrators. Pricing of the swaps your liquidity backs lives in [Spread & Fees](/docs/1-1-4-spread-fees); the coverage ratio the exit haircut reads is defined in [Inventory Management](/docs/1-1-1-inventory-management#2-coverage-ratio).

## 1. What a position is

Each `(pool, listed asset)` pair has exactly one `LPToken`, an EIP-1167 clone-with-immutable-args deployed at `initAsset` (`PoolConfig._deployLpToken`). Its on-chain symbol is `bLP-<SYM>` and its name `BTR LP: <SYM>`, both read off the underlying token; the app displays the same receipt as `<SYM>.<core acronym>` (e.g. `USDT.sc`). The pool address is pinned in the clone's code and is the sole mint and burn authority.

A receipt balance is not 1:1 with the underlying. The claim is

$$\text{underlying} = \frac{\text{balance} \cdot \text{liquidityIndexWad}}{10^{18}}$$

What you are exposed to is that one leg, but its coverage is moved by the whole core: a swap that delivers your asset raises it, a swap that takes your asset lowers it. Cores are grouped by correlated asset class so that the two tend to offset, which is what keeps a leg near coverage 1 rather than drifting one way and staying there. That matters to you because the exit haircut (§5.1) reads coverage. It is a tendency, not a guarantee: correlated legs can move together and leave every leg in the core on the same side at once. Why a core holds what it holds: [Pool Composition](/docs/2-3-pool-composition#why-several-cores-rather-than-one).

`liquidityIndexWad` starts at `LIQUIDITY_INDEX_INIT_WAD` = $10^{18}$ (`PoolConstantsLib.sol`) and rises when LP fees accrue to the leg (`PoolLiquidity.accrueLpFee`, `raiseIndex`). It is a `uint96`: `raiseIndex` clamps at `type(uint96).max` rather than reverting, and value above the clamp stays in `liabilities` unclaimable. An index of **0** means the leg was written down to a total loss: every share is worth zero, the receipt cannot be burned, and the leg cannot be re-listed.

## 2. Deposit

`Pool.deposit(address token, uint256 amount)` is `payable` and forwards to `PoolLiquidity.deposit`. Shares always mint to `msg.sender`: the entrypoint takes no recipient, no `minLpOut` and no deadline.

1. **Approve.** The app reads `allowance(user, pool)` and, if short, prepends `approve(pool, amount)`, the exact amount by default, `MAX_UINT256` only if you opt into infinite approval. Both calls go out as one EIP-5792 batch where the wallet supports `sendCalls`, otherwise sequentially.
2. **Validate.** `amount == 0` reverts `ZeroValue`. `PoolIOLib.asset` resolves the leg, mapping the NATIVE sentinel to wnative, and reverts `NotFound` on an unlisted token. `checkRiskFlags` reverts `FeatureDisabled` if `HALT_RISK_BIT` or `HALT_GUARDIAN_BIT` is set on the leg. A leg with `reserves == 0 && liabilities != 0` reverts `InvalidState`.
3. **Pull.** `PoolIOLib.pull` calls `requireNoFlash` (deposits are blocked inside a flash callback, closing the repay-by-depositing double credit), wraps `msg.value` via `IWETH9.deposit` for the NATIVE sentinel and refunds the excess, otherwise rejects nonzero `msg.value` and does a balance-delta `safeTransferFrom`. A fee-on-transfer token therefore credits only what actually arrived.
4. **Mint.** `idx = asset.liquidityIndexWad` (`mintIndex` reverts `InvalidState` at 0); `lpAmt = amt * 1e18 / idx`, less the dead-share seed on a first credit ([§3](#3-the-dead-share-seed)). `lpAmt == 0` reverts `ZeroValue`, so a dust deposit that would mint nothing cannot be gifted to existing LPs.
5. **Book.** `reserves += amt` **and** `liabilities += amt`, then `ILPToken.mint`. `postInflow` fires if the asset carries `HOOK_POST_INFLOW` ([Hooks](/docs/5-1-3-hooks)). Emits `Deposited(sender, token, amt, lpAmt)` and returns `DepositResult{lpAmount, actualDeposit, deadLp}`.

Both ledger fields move by the same amount, so a deposit is a mediant on coverage: $c' = (R+a)/(L+a)$ approaches 1 asymptotically and never reaches it. A deposit cannot restore an under-covered leg to par; a UI reading exactly 1.0 after one is a rounding artifact. `reserves` and `liabilities` are `uint128`, and a deposit past that ceiling reverts `Overflow`.

A leg wiped to `reserves == 0` with liabilities outstanding is terminal for deposits. `Pool.donate(token, amount)` credits reserves and liabilities and raises the index but mints nothing; donating to a leg that was never credited (`liabilities == 0`) strands the gift, because the index raise no-ops.

Deposit is charged nothing: no protocol fee, no spread, no coverage toll. Every charge on this page falls on the exit.

## 3. The dead-share seed

The first depositor into a leg (receipt `totalSupply() == 0`) funds a permanent floor. `PoolLiquidity.seedDeadShares` does three things:

- computes `seed = 10**deadSeedPow10`, or `10**decimals / DEAD_SHARE_SEED_DIV` (`DEAD_SHARE_SEED_DIV` = 1000, i.e. 0.001 token) when `deadSeedPow10` is 0;
- mints `deadLp = ceil(seed * 1e18 / idx)` to `address(0)`;
- subtracts it from that depositor's own mint (`lpAmt -= deadLp`).

It emits `DeadSharesSeeded` and is paid once per leg, forever unburnable. Per-asset `deadSeedPow10` is capped at the leg's decimals plus `DEAD_SEED_POW10_HEADROOM` = 3.

Users cannot transfer a receipt to `address(0)` themselves: `LPToken._beforeTokenTransfer` reverts `ZeroAddr` unless the caller is the pool, since a user burn there would raise the dead floor with LP money.

## 4. The anti-JIT cooldown

| Parameter | Value | Site |
|---|---|---|
| `DEFAULT_FLOW_COOLDOWN` | 15 s, set at `Pool.initialize` | `PoolConstantsLib.sol` |
| `MAX_FLOW_COOLDOWN` | 300 s, ceiling on `PoolConfig.setFlowCooldown` | `PoolConstantsLib.sol` |
| Read at runtime | `Pool.flowCooldownSecs()` | `Pool.sol` |

The lock is armed **at mint**, on the receipt: `LPToken.mint` writes `locks[to] = {stamp: block.timestamp, frozen: previousFrozen + amount}`, resetting `frozen` to `amount` if the previous lock had already expired. It freezes a **quantity**, not the account; an older, unfrozen balance stays movable. Topping up inside an unexpired window restarts the clock over the whole recent parcel, and `swapLiability` arms a fresh lock on the destination shares.

Enforcement lives entirely in `LPToken._beforeTokenTransfer`, which reverts `CooldownActive` when `balanceOf(from) < amount + frozen` and `block.timestamp < stamp + flowCooldownSecs`. It gates the withdraw burn, the `swapLiability` burn and plain ERC-20 transfers of the receipt alike; `PoolLiquidity` performs no cooldown check of its own. Any lock older than `MAX_FLOW_COOLDOWN` short-circuits without reading the pool. Related block-level protections: [Flow Guards](/docs/3-3-flow-guards).

## 5. Withdraw

`Pool.withdraw(token, lpAmount, minAmountOut, deadline)` forwards to `PoolLiquidity.withdrawTo($, token, token, …)`; the same-asset case is literally `withdrawTo` with `tokenFrom == tokenTo`. Both entrypoints are `nonReentrant`, `whenInitialized` and `beforeDeadline`, and both endpoint legs are checked against `HALT_MASK`. No ERC-20 approval is needed: the pool burns the caller's own receipt.

Sequence, after `requireNoFlash` and the `lpAmount == 0` check:

1. **Face value.** `withdrawValue = lpAmount * assetFrom.liquidityIndexWad / 1e18`. Above `assetFrom.liabilities` reverts `InsufficientAmount`; zero reverts `ZeroValue`.
2. **Haircut** on the from-leg ([§5.1](#51-the-coverage-haircut)).
3. **Cross-asset only:** anchor-path conversion, mark cap and a second haircut ([§5.2](#52-cross-asset-exit-withdrawto)).
4. **Guards.** `amt == 0` reverts `ZeroValue` before any burn. `preOutflow` recalls from the yield hook for `amt + protoFee + minLiquidity`; after the ledger move, `liquidReserves < minLiquidity` reverts `ThresholdViolation`, as does `amt < minAmountOut`.
5. **Burn**, where the cooldown bites ([§4](#4-the-anti-jit-cooldown)), then settle and `PoolIOLib.push` the output, unwrapping to ETH for the NATIVE sentinel.

### 5.1. The coverage haircut

`PoolLiquidity.applyHaircut(amount, reserves, liabilities, haircutSuppressorBps)` is the identity when `liabilities == 0` or `reserves >= liabilities`. Below par, with $c = R/L$:

$$\text{deficit} = \frac{L - R}{L}, \qquad \text{factor} = 1 - \frac{\text{supp}}{20{,}000}, \qquad h = \min\big(\text{deficit}\cdot\text{factor},\ 10^{18}\big)$$

The relation is **linear** in the deficit, and the haircut rounds up so the payout rounds down. `reserves < amt` after it reverts `InsufficientAmount`.

The denominator is `HAIRCUT_SUPPRESSOR_FULL_BPS` = $2\cdot\text{BPS}$ = 20,000, not 10,000. `initAsset` seeds `haircutSuppressorBps` = 10,000, which is **half** suppression: a leg at 80% coverage charges a 10% exit haircut, not 20%. `setAssetParams` rejects any value $\ge$ 20,000, so the haircut can never be switched off entirely, and a coverage-walled leg (`kappaCovBps > 0`) is forced to `haircutSuppressorBps = 0` (full haircut) at `initAsset` and on every subsequent param write ([Spread & Fees §6.5](/docs/1-1-4-spread-fees#65-coupled-configuration)).

Settlement is asymmetric on purpose: `reserves -= amt` (post-haircut) but `liabilities -= withdrawValue` (full face). The difference is socialised: exiting an under-covered leg raises coverage for the LPs who stay.

### 5.2. Cross-asset exit (`withdrawTo`)

`Pool.withdrawTo(tokenFrom, tokenTo, lpAmount, minAmountOut, deadline)` burns a receipt on one leg and pays out in another. It costs strictly more than a same-asset exit:

- The from-leg haircut applies **before** conversion, so an under-covered LP cannot route around its deficit.
- `Pricing.anchorPathQuoteLp` converts the fair value, carrying the path's `protoFee` and `lpFee` ([Anchor Path Pricing](/docs/1-1-3-anchor-path-pricing)).
- The output is capped at the **oracle mark path** (`_markCap`, corrected by $10^{d_{to}-d_{from}}$), so the inventory skew cannot be monetised one-way by an LP conversion.
- `PoolIOLib.priceBandGuardAll` depeg-checks both legs ([Depeg Halt](/docs/3-5-depeg-halt)).
- `applyHaircut` runs a **second** time, against the to-leg's coverage.

Settlement: `assetFrom.liabilities -= withdrawValue`; `protocolFees[toTk] += protoFee`; `assetTo.reserves -= (amt + protoFee)`; `accrueLpFee(assetTo, lpFee)`. The spread is charged on the output leg, as with a swap.

`Pool.swapLiability(tokenIn, tokenOut, lpAmountIn, minLpAmountOut, deadline)` is the receipt-to-receipt variant:

- it moves no reserves;
- it is protocol-fee exempt by design;
- its LP fee is not routed through `accrueLpFee` (there is no retained token to back an index raise, so it lands as a coverage gain);
- it additionally requires `LIABILITY_SWAP_ENABLED_BIT` on both legs, which deposit and withdraw do not.

### 5.3. Events

A same-asset withdraw emits one `Withdrawn(sender, fromTk, amt, lpAmount)`. A cross-asset `withdrawTo` emits **two**: `LiabilitySwapped(sender, fromTk, toTk, lpAmount, 0, haircut)` for the burn and `Withdrawn(sender, toTk, amt, 0)` for the payout; a single `Withdrawn` cannot carry both legs. An indexer that treats `Withdrawn` as the whole story corrupts both legs' reconstructed balances.

## 6. What can stop you exiting

Three different things gate an exit, and they do not gate the same paths. Verified against
`PoolLiquidity.withdrawTo` and `Pricing.anchorPathQuoteLp` at HEAD.

| Condition | Same-asset `withdraw` | Cross-asset `withdrawTo` | `swapLiability` |
|---|---|---|---|
| The leg's oracle feed is stale or dead | **not blocked** | blocked | blocked |
| A halt bit is set on the leg | blocked | blocked | blocked |
| Executable reserves below `minLiquidity` | blocked | blocked | not applicable |

The first row is the one worth remembering. A same-asset withdraw reads no mark at all:
`_quoteWithdrawSame` is a `view` over the leg's own reserves, liabilities and haircut suppressor,
so an oracle that has gone stale cannot trap a position in the leg it was deposited into. Both
other exits price off marks and revert while the feed is down.

A halt bit is a different matter and does stop all three, including the same-asset path: the
`HALT_MASK` check runs on both endpoints of every withdraw. Halting is a guardian-or-owner action;
un-halting is owner-only, so a halt can be applied faster than it can be lifted. See
[Access Control](/docs/3-1-access-control-roles-emergency-powers).

The reserve floor is the third case: `maxRedeem` is a floor, not a promise. It is computed from
executable liquidity at the time you read it, and a withdraw re-checks it after any hook recall, so
a large exit can revert even though the app quoted it a moment earlier.

---

## 7. What the position actually earns

Your claim grows through `liquidityIndexWad`, not through a token balance that goes up. Four things
move it, and one of them moves it down:

- the LP share of swap fees on that leg, net of `protoSharePct` (currently 20%, so LPs keep 80%);
- the coverage toll paid by anyone draining the leg;
- donations;
- **`hookWriteDown`, which lowers the index.** If a leg's assets are lent to a venue through a hook
  and that venue loses money, the loss is written down onto the index. There is no protocol
  backstop for it; see [Hooks](/docs/5-1-3-hooks).

What the index does *not* include is any compensation for
[LVR](/docs/glossary#lvr-loss-versus-rebalancing). AIMM prices adverse selection into the spread so
that flow pays for it rather than LPs subsidising it, but there is no mechanism that pays an LP
back for it after the fact. A position's return is fees and tolls earned, minus what the spread
failed to charge.

---

## 8. In the app

The LP surface is the swap form: `/pools` → **Manage** writes `?pool=&asset=&action=deposit|withdraw|swap` into the URL and renders `<SwapForm mode="lp">` with `fromSym == toSym`, so the deposit/withdraw toggle is the same flip gesture as buy/sell.

- The **withdraw amount field is in underlying units**, converted to shares by `amt * WAD / index`, that is, by face. The haircut applies on top, so on an under-covered leg you receive less than the number you typed.
- **Max / 75%** read `Pool.maxRedeem(owner, token)`, which folds `HALT_MASK`, a zero index, frozen shares from `LPToken.locks`, and `liquidReserves − minLiquidity` into a share capacity. It deliberately ignores hook recall, oracle staleness and depeg bands, and `requireNoFlash`, so a call can still revert at a size `maxRedeem` allowed.
- The front currently sends `minAmountOut = 0` on both `withdraw` and `withdrawTo`, and a hardcoded `deadline = now + 600` (10 minutes). The recap's slippage row is display-only; the on-chain floor is zero. Marked testnet in the code; integrators must set their own `minAmountOut`.

## 9. Contract reference

| Call | Purpose |
|---|---|
| `Pool.deposit(token, amount)` | single-sided mint, returns `DepositResult` |
| `Pool.withdraw(token, lpAmount, minAmountOut, deadline)` | same-asset exit |
| `Pool.withdrawTo(tokenFrom, tokenTo, lpAmount, minAmountOut, deadline)` | cross-asset exit |
| `Pool.swapLiability(tokenIn, tokenOut, lpAmountIn, minLpAmountOut, deadline)` | receipt-to-receipt move |
| `Pool.donate(token, amount)` | credit a leg without minting |
| `Pool.previewWithdraw(tk, lp)` | `(amountOut, haircut)` |
| `Pool.maxRedeem(owner, tk)` | redeemable share capacity |
| `Pool.getLPBalance(u, tk)` / `Pool.lpToken(tk)` | receipt balance / receipt address |
| `Pool.getAsset(tk)` / `Pool.getCoverageRatio(tk)` | leg ledger and $c$ |
| `Pool.flowCooldownSecs()` | live anti-JIT window |

SDK entry points are `deposit` / `withdraw` in `@btr-protocol/sdk` (`src/pool`), with EIP-5792 batch builders in `src/router`; see [Cookbook §3](/docs/5-2-3-cookbook#3-liquidity) and [Basic Operations](/docs/5-1-1-basic-operations).

## 10. LP checklist

The decisions this page leaves to you, in the order you make them. Everything here is stated in
full above; this is the short form to run before you commit capital.

**Before depositing**

- [ ] Read the coverage ratio $c = R/L$ on the leg. It does two things: it shifts the quoted mid through the inventory skew (saturating at $c \le \tfrac12$ and $c \ge 2$, fixed in code), and below 1 it haircuts your exit. Price that haircut rather than acknowledging it ([§5.1](#51-the-coverage-haircut)).
- [ ] Deposit the asset you are willing to leave in. Same-asset `withdraw` is not feed-gated; cross-asset `withdrawTo` and `swapLiability` are, so those are the exits a dead or depegged feed can shut ([§6](#6-what-can-stop-you-exiting)).
- [ ] Check the fee split you are actually on. $\texttt{protoSharePct} \in [0, 100]$ is the share of every swap and flash spread routed to `pool.treasury()`; the remainder raises your index. It is set in `initdata` at `createPool` and moved afterwards by a `LOW`-tier `UPDATE_FEES`, one day of notice under `PROD_DELAYS`, so watch the queue if it matters to you ([Incentivization §2](/docs/5-3-3-incentivization#2-fee-split)).
- [ ] If the leg runs a hook, read which venue before depositing ([Hooks](/docs/5-1-3-hooks)). You carry its credit risk with no insurance layer, and part of the leg's reserves sit off-pool: pricing uses full $R$ but your withdrawal executes against $R_{liq}$, so check $R_{liq}$ against `minLiquidity`, not just $R$. A leg can be solvent on paper and unwithdrawable in size in the same block.

**While the position is open**

- [ ] Do not read fee APR as return. Fee APR is gross; your realised return is the change in `liquidityIndexWad` net of the LVR you paid to arbitrageurs against the external mark, and nothing rebates that ([§7](#7-what-the-position-actually-earns)). Judge a leg on realised index growth over a full cycle.
- [ ] Expect other LPs to move your coverage without touching reserves. `swapLiability` re-denominates a liability from one leg to another at the oracle mark, moving no cash: it changes your leg's coverage ratio, and therefore your haircut, with no reserve flow you could have watched for.
- [ ] Know you cannot pin a version. An `UPGRADE`-tier beacon swap at `PoolFactory` re-points every live pool at once. Your notice is the timelock and your only response is to exit; there is no opt-out ([Deployment & Upgrades §4](/docs/3-2-deployment-upgrades#4-upgrade-mechanisms)).

**When you exit**

- [ ] Size the exit from `Pool.maxRedeem`, not from your balance. It folds the halt bits, a wiped index, your frozen shares and $R_{liq} - \texttt{minLiquidity}$ into one number and returns `0` rather than a partial answer when any of them binds. It deliberately does not assume a hook recall will succeed, so it is a floor, not a promise ([§8](#8-in-the-app)).
- [ ] Read `LPToken.locks(holder)` and size from `balance − frozen`. The cooldown freezes the freshly minted quantity against `withdraw`, `withdrawTo`, `swapLiability` **and** plain `transfer`: the receipt itself cannot leave, and a violation reverts `CooldownActive` ([§4](#4-the-anti-jit-cooldown)).
- [ ] Treat a halt as a locked door in both directions. `checkRiskFlags` gates `deposit`, `donate`, `withdrawTo` and `swapLiability` on `HALT_MASK`, so a halted leg cannot be exited by anyone at any price until the **owner**, never a guardian, clears the specific bit. This is the single largest discretionary risk an LP carries.

## 11. Related documentation

- [Inventory Management](/docs/1-1-1-inventory-management#5-withdrawal-haircut): coverage ratio and the haircut in the pricing context
- [Spread & Fees](/docs/1-1-4-spread-fees#5-the-fee): how the LP fee reaches `liquidityIndexWad`
- [Pool](/docs/1-2-1-pool): the module, its storage and its full ABI
- [Flow Guards](/docs/3-3-flow-guards): cooldowns and block-level protections
- [Invariants](/docs/1-1-8-invariants#i-14-executable-liquidity-floor-not-raw-reserves): the liquidity floor a withdrawal must respect
