---
title: "Liability swaps"
description: "Moving an LP claim between two legs of one pool: swapLiability, its two haircuts, the Lemma B mark cap, and the routes that embed it"
audience: both
type: guide
status: live
lang: en
publish: true
---

# Liability swaps

A liability swap moves an LP's claim from one leg of a pool to another leg of the *same* pool, in one transaction. It burns the source leg's receipt, mints the destination leg's receipt, and moves only the two `liabilities` counters; the pool's token reserves are untouched. The app labels this **"Debt swap"** (third tab of the LP form, and the step label in the routing recap); the contract function is `swapLiability`. This page is for LPs deciding whether to use it and for integrators encoding it.

AIMM receipts are per leg: `IPool` holds `mapping(address leg => address) lpTokens`, one ERC-20 clone per listed asset, and every LP position is single-sided. An LP holding USDC-leg shares who wants ETH-leg exposure would otherwise withdraw and redeposit. `swapLiability` is the single-call substitute, and it is also a leg of two of the four ranked LP routes ([§7](#7-where-it-appears-inside-lp-routes)).

---

## 1. Entry point

```solidity
function swapLiability(
    address tokenIn,
    address tokenOut,
    uint256 lpAmountIn,
    uint256 minLpAmountOut,
    uint256 deadline
) external nonReentrant whenInitialized beforeDeadline(deadline) returns (uint256 lpAmountOut);
```

`Pool.sol`, delegating to `PoolLiquidity.swapLiability`. `tokenIn` and `tokenOut` are the two **assets**, but the amounts are **LP shares** of the corresponding receipts. There is no `recipient` parameter: shares burn from and mint to `msg.sender` only. No ERC-20 approval is required: nothing leaves the caller's account except their own receipt, which the pool burns.

---

## 2. What one call does, in order

1. **Guards.** `requireNoFlash()`; revert on zero amount; revert `InvalidInput` if `tokenIn == tokenOut`; `checkRiskFlags(..., LIABILITY_SWAP_ENABLED_BIT)` on **both** legs, which also enforces `HALT_MASK` on both.
2. **Face.** `liabIn = lpAmountIn * liquidityIndexWad / WAD`. Reverts `InsufficientAmount` if `liabIn` exceeds the leg's live liabilities: shares never claim more face than the leg owes.
3. **Input haircut.** `applyHaircut(liabIn, R_in, L_in, suppressor_in)` → `fairIn` ([Haircut](/docs/glossary#haircut)).
4. **Quote.** `Pricing.anchorPathQuoteLp($, tokenIn, tokenOut, fairIn)`, the LP-path entry to the anchor-tree quote, the same curve a market swap crosses, so the mover pays the full embedded spread, skew and toll ([Spread & Fees](/docs/1-1-4-spread-fees)).
5. **Mark cap.** The curve output is clamped at `fairIn · markPrice`, decimal-corrected by `10^(d_to − d_from)` (`PoolLiquidity._markCap`, Lemma B).
6. **Depeg breaker.** `priceBandGuardAll($, tokenOut, tokenIn, q.routeHops)` over both legs and every interior hop ([Depeg Halt](/docs/3-5-depeg-halt)).
7. **Output haircut and shares.** `applyHaircut(q.amountOut, R_out, L_out, suppressor_out)` → `liabOut`; `lpAmountOut = liabOut · WAD / mintIndex(tokenOut)`, less any dead-share seed on a first-credit leg. Reverts `ZeroValue` if that lands at zero.
8. **Ledger.** `assetIn.liabilities -= liabIn; assetOut.liabilities += liabOut;` then the `minLpAmountOut` check. No line of the function touches `.reserves`.
9. **Receipts.** Burn the source receipt, mint the destination receipt, both for `msg.sender`.
10. **Event.** `LiabilitySwapped(sender, tokenIn, tokenOut, lpAmountIn, lpAmountOut, haircut)`, where `haircut` is the **output**-leg haircut only.

---

## 3. Two haircuts, and why the full face burns

`liabIn` is burned in full, but only `fairIn`, the post-haircut face, is re-denominated. Swapping out of an under-covered leg therefore costs the LP the coverage haircut exactly as an exit would: it is not a toll-free escape from a deficit, and it cannot dump that deficit onto the destination leg's LPs.

The mark cap in step 5 closes the other side of the same hole. Re-denomination credits at the oracle mark, never at the skewed mid; without it, an under-covered input leg could mint destination claims above its fair haircut value whenever adaptive dispersion widened the skew past the spread.

---

## 4. Coverage and fee consequences

| Quantity | Input leg | Output leg |
|---|---|---|
| `reserves` | unchanged | unchanged |
| `liabilities` | $-\,$`liabIn` | $+\,$`liabOut` |
| [Coverage ratio](/docs/glossary#coverage-ratio) $c = R/L$ | **rises** | **falls** |

This makes the call a coverage instrument as much as an exposure change, and the LP form leads with that. It shows:

- pre/post coverage on both legs with a healing / hurting / neutral signal;
- the APR delta between the legs;
- "Best Coverage" / "Best APR" destination lists.

No protocol fee is charged, and the call is deliberately not routed through `accrueLpFee`: there is no physical outflow to skim and no retained token to back an index raise. The mover still pays the full embedded spread; both fee components land as reduced net liability, i.e. a global coverage gain, realisable only while some leg is under-covered.

---

## 5. Quoting

There is **no on-chain preview**. `Pool` exposes `previewWithdraw` and `getSwapQuote`; neither covers this path, and no `eth_call` returns a liability-swap quote. The only quote is the SDK mirror `quoteSwapLiability(state, inLeg, outLeg, lpAmountIn)` (`@btr-protocol/sdk`, `src/pool/liability.ts`), which replays the contract pipeline in `f64`: input haircut → anchor-path quote → mark cap → output haircut → shares. It returns `null` in exactly the cases the contract would revert (over-burn, zero output), so a reverting transaction is never quoted.

`minLpAmountOut(quotedShares, slippageFrac)` floors the quote. **Slippage here guards LP shares, not tokens**: an LP reusing the market-swap slippage percentage is guarding a different unit.

The mark cap can bind silently: when it does, `LiabilitySwapped` carries no trace of it. The SDK surfaces it as `markCapBinding` / `markClampBps`; a chain observer cannot reconstruct it. This is the observability gap tracked in [Observability](/docs/3-8-observability).

---

## 6. Pre-flight conditions

Three conditions block the call, each checked client-side before the button enables and enforced again on chain:

| Condition | Cause | Where |
|---|---|---|
| Feature flag | `LIABILITY_SWAP_ENABLED_BIT` unset on either leg (or either leg halted) | `checkRiskFlags`; reverts `Err.Resource.LIABILITY_SWAP` |
| Cooldown | anti-JIT lock still armed on the source receipt (`ILPToken.locks`) | `LPToken._beforeTokenTransfer` |
| Balance | `lpAmountIn` above the held receipt balance | client-side, then the burn |

**The cooldown bites twice.** The source burn is gated by the source receipt's lock, and the mint arms a **fresh** lock over the destination shares. A user who just deposited cannot immediately debt-swap, and a user who just debt-swapped cannot immediately withdraw the destination; the rebalanced position exits no earlier than the original deposit could have. Cooldown bounds are in [Flow Guards](/docs/3-3-flow-guards) (`DEFAULT_FLOW_COOLDOWN` 15 s, `MAX_FLOW_COOLDOWN` 300 s). This is why the SDK returns the affected routes as `feasible: false, reason: 'cooldown'` instead of quoting a batch that would revert, and why those routes run sequentially rather than atomically.

---

## 7. Where it appears inside LP routes

The SDK ranks four LP routes; two contain a `swapLiability` leg:

| Route | Calls |
|---|---|
| Mint **B**: deposit-first | `[approve?, deposit(X), swapLiability(X → target)]` |
| Redeem **B'**: transfer-exit | `[swapLiability(held → X), withdraw(X)]` |

`buildDepositCalls` and `buildRedeemCalls` (`sdk/src/router/index.ts`) emit these on their `'transfer'` branch. When one wins the ranking, a user performing what looks like a plain cross-asset deposit or withdrawal executes a liability swap inside the batch; the routing recap labels that step "Debt swap". Route ranking itself is covered in [Quotes & Routing](/docs/5-2-2-quotes-routing).

---

## 8. Reading the event

`LiabilitySwapped` is **not** unique to this flow. A cross-asset `withdrawTo`, an ordinary withdrawal, also emits it, as `LiabilitySwapped(..., lpAmountIn, 0, haircut)` alongside a `Withdrawn` carrying `lpAmount = 0`.

**Discriminator: `lpAmountOut == 0` means it was not a liability swap.**

The indexer registers the topic, stores the raw log, and flags the block `economic` to force a fresh coverage/parameter snapshot. It does not decode the event into a typed table the way swaps are decoded, so consumers wanting fields must decode the log themselves.

---

## 9. Constants and configuration

| Name | Value | Source |
|---|---|---|
| `LIABILITY_SWAP_ENABLED_BIT` | `1 << 2` (0x04) | `PoolConstantsLib.sol` |
| Deployed asset flags | `SWAP_ENABLED_BIT \| LIABILITY_SWAP_ENABLED_BIT` = 0x06 | Arc pool deploy scripts |
| `HALT_MASK` | `0x0041` (risk halt bit 0, guardian halt bit 6) | checked on both legs, see [Pool](/docs/1-2-1-pool) |
| `HAIRCUT_SUPPRESSOR_FULL_BPS` | `20_000` | `PoolConstantsLib.sol` |
| Mark-cap decimal correction | `10^(d_to − d_from)` | `PoolLiquidity._markCap` |
| SDK default route slippage | `DEFAULT_SLIP = 0.005` (0.5%) | `sdk/src/router/lpRoutes.ts` |
| App deadline, standalone debt swap | `now + 600` s | `front/src/hooks/usePoolData.ts` |

The Arc testnet fleet ships 0x06, so the path is enabled on every listed asset there. On any other roster, check the flag on **both** legs before assuming the call is available.

---

## 10. Failure modes

| Revert | Trigger |
|---|---|
| `InvalidInput` | `tokenIn == tokenOut` |
| `InsufficientAmount` | `liabIn` exceeds the input leg's live liabilities |
| `FeatureDisabled(Err.Resource.LIABILITY_SWAP)` | flag unset on either leg |
| `ZeroValue` | a fully hair-cut destination leg re-denominates to nothing |
| slippage revert | delivered shares below `minLpAmountOut` |
| depeg / staleness halts | `priceBandGuardAll`, feed gate on any leg or interior hop |

The SDK mirror returns `null` for the over-burn and zero-output cases, so the app never quotes them.

---

## 11. Related documentation

- [Inventory Management](/docs/1-1-1-inventory-management): coverage ratio and the withdrawal haircut
- [Spread & Fees](/docs/1-1-4-spread-fees): the spread, the coverage toll, and what the LP path pays
- [Pool](/docs/1-2-1-pool): risk flags, halt mask, module surface
- [Flow Guards](/docs/3-3-flow-guards): the anti-JIT cooldown
- [Quotes & Routing](/docs/5-2-2-quotes-routing): route enumeration and ranking
