---
title: "Basic Operations"
description: "Solidity signatures, a copy-pasteable interface, units, reverts and events for an AIMM pool"
audience: tech
type: reference
status: live
lang: en
updated: "2026-09-02"
publish: true
---
# Basic Operations

Signatures and call order for the four pool entry points: `swap`, `deposit`, `withdraw`, `swapLiability`. Everything here targets one `Pool` on one chain; choosing between pools is an off-chain decision covered in [Quotes & Routing](/docs/5-2-2-quotes-routing).

---

## 1. Interfaces

> **`IPool.sol` does not declare the trading functions.** The shipped `IPool` carries the shared types (`Asset`, `SwapQuote`, `OracleConfig`, `RiskConfig`, `FeeParams`, `OpType`, `HookSlot`, `DepositResult`, `WithdrawResult`, `RoutePath`), the `admin*` and `hook*` gated entry points, and a set of views. `swap`, `deposit`, `withdraw`, `withdrawTo`, `donate`, `swapLiability`, `getSwapQuote`, `previewWithdraw` and `getCoverageRatio` are declared on **`Pool.sol` only**. `IPool(pool).swap(...)` does not compile.

Declare the surface you need. This block is complete and correct for everything on this page:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IBtrPool {
    struct DepositResult {
        uint256 lpAmount;      // shares minted, net of deadLp
        uint256 actualDeposit; // amount actually received (measured)
        uint256 deadLp;        // shares sunk to address(0) if this deposit opened the leg
    }

    struct WithdrawResult {
        uint256 amountOut;
        uint256 lpBurned;
    }

    struct SwapQuote {
        uint256 amountOut;
        uint256 amountIn;
        uint16  spreadPbps;
        uint256 protoFee;
        uint256 lpFee;
        int8    skewIn;
        int8    skewOut;
        uint256 markPrice;   // oracle fair value, WAD, tokenOut per tokenIn
        uint256 midPrice;    // inventory-skewed centre the book quotes around
        uint256 covToll;     // coverage toll withheld from gross output, tokenOut units
        address[] routeHops;
        uint256[] hopAmounts;
        uint256[] hopPrices; // populated by the view, empty on the exec path
    }

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

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

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

    function withdrawTo(
        address tokenFrom,
        address tokenTo,
        uint256 lpAmount,
        uint256 minAmountOut,
        uint256 deadline
    ) external returns (WithdrawResult memory);

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

    function donate(address token, uint256 amount) external payable;

    function getSwapQuote(address tokenIn, address tokenOut, uint256 amountIn)
        external view returns (SwapQuote memory);

    function previewWithdraw(address token, uint256 lpAmount)
        external view returns (uint256, uint256);

    function getCoverageRatio(address token) external view returns (uint256);
    function getLPBalance(address user, address token) external view returns (uint256);
    function lpToken(address token) external view returns (address);
    function getRiskFlags(address token) external view returns (uint16);
    function getProtocolFees(address token) external view returns (uint256);
    function treasury() external view returns (address);
    function flowCooldownSecs() external view returns (uint16);
}
```

The generated ABIs are also served live at `GET /v1/abis/{Pool,Admin,Flash,PoolFactory,ExternalOracleV4,ExternalOracle,AccessControl,LPToken}`; see the [API & SDK Reference](/docs/5-2-1-api-sdk-reference#21-get).

On each supported chain, one shared `Pool` implementation plus one `Admin` and one `Flash` serve every pool on that chain. The standards each piece sits on:

- **Pools** are ERC-1967 beacon proxies.
- **LP receipts** are ERC-20.
- **Flash** is ERC-3156-style with a `postFlashLoan` callback.
- **Guards** use ERC-1153 transient storage.

Per-chain addresses come from `GET /v1/venues`.

---

## 2. Check the flags first

Every entry point gates on the leg's risk flags before anything else. Read them with `getRiskFlags(token)` and test the bit for the operation you are about to attempt: a swap-enabled leg is not necessarily liability-swap-enabled.

| Bit | Value | Meaning |
|---|---|---|
| `HALT_RISK_BIT` | `1 << 0` = 1 | Risk halt is set on this leg |
| `SWAP_ENABLED_BIT` | `1 << 1` = 2 | `swap` permitted |
| `LIABILITY_SWAP_ENABLED_BIT` | `1 << 2` = 4 | `swapLiability` permitted |
| `FLASH_ENABLED_BIT` | `1 << 4` = 16 | flash loans permitted |
| `HALT_GUARDIAN_BIT` | `1 << 6` = 64 | Guardian halt is set on this leg |

Halt bits refcount by source: a guardian halt and an owner risk halt are separate bits, and clearing one does not relist a leg the other still holds down.

---

## 3. Swap

Runnable, tested: [`foundry/test/03_PoolSwap.t.sol`](https://github.com/btr-protocol/examples/blob/main/foundry/test/03_PoolSwap.t.sol) (Solidity, forked) · [`sdk/03-pool-swap.ts`](https://github.com/btr-protocol/examples/blob/main/typescript/sdk/03-pool-swap.ts) · [`rpc/03-pool-swap.ts`](https://github.com/btr-protocol/examples/blob/main/typescript/rpc/03-pool-swap.ts) (no SDK).

```solidity
function swap(
    address tokenIn,
    address tokenOut,
    uint256 amountIn,
    uint256 minAmountOut,
    address recipient,
    uint256 deadline          // inclusive: reverts only when block.timestamp > deadline
) external payable returns (uint256 amountOut);
```

1. **Quote.** `getSwapQuote(tokenIn, tokenOut, amountIn)` is a view returning the full `SwapQuote`: output, spread, both fee slices, coverage toll, mark and mid. Derive `minAmountOut` from `amountOut` and your own tolerance.
2. **Approve.** Non-native: `approve(pool, amountIn)`. Native: send it in `msg.value`.
3. **Execute.** `Pool.swap` prices against the external mark, applies the coverage toll and fee, then updates reserves. No oracle write happens on a swap.
4. **Protect.** The call reverts if `amountOut < minAmountOut`.

`Pool.swap` pulls `tokenIn` from `msg.sender`, so the call must originate from the account holding the balance and allowance. It cannot be wrapped in Multicall3; see [Quotes & Routing §2](/docs/5-2-2-quotes-routing#2-pipeline).

**SDK** (`@btr-protocol/sdk/pool`, first argument is an EIP-1193 provider):

```typescript
import { getSwapQuote, swap, defaultDeadline } from '@btr-protocol/sdk/pool';

const quote = await getSwapQuote(provider, pool, tokenIn, tokenOut, amountIn);
const minOut = (quote.amountOut * 9950n) / 10000n; // 50 bps - see §7
const txHash = await swap(provider, pool, {
  tokenIn, tokenOut, amountIn,
  minAmountOut: minOut,
  recipient: user,
  deadline: defaultDeadline(),
});
```

---

## 4. Deposit & withdraw

Runnable, tested: deposit [`04_Deposit.t.sol`](https://github.com/btr-protocol/examples/blob/main/foundry/test/04_Deposit.t.sol) · [`sdk/04-deposit.ts`](https://github.com/btr-protocol/examples/blob/main/typescript/sdk/04-deposit.ts) · [`rpc/04-deposit.ts`](https://github.com/btr-protocol/examples/blob/main/typescript/rpc/04-deposit.ts). Withdraw [`05_Withdraw.t.sol`](https://github.com/btr-protocol/examples/blob/main/foundry/test/05_Withdraw.t.sol) · [`sdk/05-withdraw.ts`](https://github.com/btr-protocol/examples/blob/main/typescript/sdk/05-withdraw.ts) · [`rpc/05-withdraw.ts`](https://github.com/btr-protocol/examples/blob/main/typescript/rpc/05-withdraw.ts).

```solidity
function deposit(address token, uint256 amount)
    external payable returns (DepositResult memory);

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

function withdrawTo(
    address tokenFrom,
    address tokenTo,
    uint256 lpAmount,
    uint256 minAmountOut,
    uint256 deadline
) external returns (WithdrawResult memory);
```

**LP accounting.** Each leg has its own ERC-20 share receipt: one EIP-1167 clone per `(pool, leg)`, address from `pool.lpToken(token)`. Read a balance with `pool.getLPBalance(user, token)` or the receipt's own `balanceOf`. Shares become transferable once past the anti-JIT cooldown stamped at mint (`flowCooldownSecs()`, default 15 s). Price an exit off `pool.previewWithdraw`, never off `decimals()`.

**Deposit.** `token.approve(pool, amount)`, then `pool.deposit(token, amount)`. Shares mint to the caller on the leg receipt. If the deposit opens the leg, `DepositResult.deadLp` shares are sunk to `address(0)` and are not yours.

**Withdraw.** `pool.withdraw(...)` for a same-asset exit, `pool.withdrawTo(...)` to burn on one leg and receive another (priced along the anchor path).

**Haircut.** Fires only at coverage below 100%, and protects the LPs who stay. Formula and worked examples: [Inventory Management §5](/docs/1-1-1-inventory-management#5-withdrawal-haircut).

```typescript
import { deposit, withdraw, getLPBalance, defaultDeadline } from '@btr-protocol/sdk/pool';

await deposit(provider, pool, { token, amount });
const lpAmount = await getLPBalance(provider, pool, user, token);
await withdraw(provider, pool, { token, lpAmount, minAmountOut, deadline: defaultDeadline() });
```

`deposit` is payable: pass the EIP-7528 native sentinel `NATIVE_TOKEN` (`0xEeee…eEEeE`) as `token` and the SDK sends `amount` as `msg.value`.

---

## 5. Swap liability

```solidity
function swapLiability(
    address tokenIn,          // leg to leave
    address tokenOut,         // leg to enter
    uint256 lpAmountIn,
    uint256 minLpAmountOut,
    uint256 deadline
) external returns (uint256 lpAmountOut);
```

Moves an LP claim from one leg to another. **Reserves do not move**: this re-denominates a liability, it does not trade. A haircut may apply on an under-covered input leg.

An LP holding a balanced ETH/USDC position who expects ETH to fall can sell ETH liabilities for USDC liabilities and end up USDC-heavy without leaving the pool. No first-class SDK sender exists; encode against `POOL_ABI`.

Four things separate a debt swap from a swap, and getting any of them wrong produces a call that either reverts or, worse, books the wrong number:

1. **Reserves do not move.** Only the liability side of two legs changes. Nothing enters or leaves the pool, and nothing reaches the caller's wallet. An indexer that treats it as a trade will book phantom volume.
2. **Both amounts are LP SHARES, not token units.** `minLpAmountOut` is a SHARE floor. Passing a token amount there is a units bug that a passing test will not catch, because the call still succeeds.
3. **Shares are not comparable across legs.** Each leg carries its own liquidity index, so the output share count legitimately differs from the input one at identical value. Compare VALUE, through `previewWithdraw` on each side, or you will conclude liquidity vanished when it did not.
4. **An under-covered input leg takes a haircut.** Leaving a leg that cannot pay everyone in full, at face value, would hand the shortfall to whoever stays. The haircut is applied before the mark conversion, and a round trip therefore strictly loses: a strategy that flips legs on every signal pays for it each time.

Both legs need `LIABILITY_SWAP_ENABLED_BIT` (bit 2) in `getRiskFlags` (§2). Check it BEFORE building the call: the revert does not say which of the two legs was the problem.

There is no `previewSwapLiability`. To learn the output share count off chain, `eth_call` the real function: it is a state-changing signature, so the call is simulated and never mined.

Runnable, tested: [`06_DebtSwap.t.sol`](https://github.com/btr-protocol/examples/blob/main/foundry/test/06_DebtSwap.t.sol) · [`sdk/06-debt-swap.ts`](https://github.com/btr-protocol/examples/blob/main/typescript/sdk/06-debt-swap.ts) · [`rpc/06-debt-swap.ts`](https://github.com/btr-protocol/examples/blob/main/typescript/rpc/06-debt-swap.ts).

---

## 6. Units

Four scales appear across the protocol and they are not interchangeable.

| Unit | Scale | Used by |
|---|---|---|
| **BPS** | 10 000 = 100% | `kappaCovBps`, `vegaBps`, `refBandBps`, `confidenceBps`, `maxDeviationBps` |
| **PBPS** | 1 000 000 = 100% | `spreadPbps`, `minFeePbps`, `minDispersionPbps`, `flashFeePbps`, `sigmaPbps`, `dispRefPbps` |
| **WAD** | 1e18 = 1.0 | `markPrice`, `midPrice`, `mark1e18`, `liquidityIndexWad` |
| **Percent** | 100 = 100% | `protoSharePct` only |
| **Half-scale** | **20 000 = 100%** | `haircutSuppressorBps`; see below |

`vegaBps = 10000` means a 1.0× multiplier, not 100%. `minFeePbps = 100` is 1 bp. `flashFeePbps = 5` is 0.0005%.

> **`haircutSuppressorBps` does not use the BPS denominator.** The suppressed fraction is `haircutSuppressorBps / HAIRCUT_SUPPRESSOR_FULL_BPS`, and that constant is `2 × BPS = 20 000`. So the natural-looking `10000` is **half** suppression, not full, and it is the value `initAsset` seeds. The 2× is deliberate: config caps strictly below `20000`, so full suppression is unreachable and some deficit is always socialized to the leg's LPs. Writes must satisfy `haircutSuppressorBps < 20000`, and `== 0` whenever `kappaCovBps > 0`.

**`spreadPbps` saturates; it does not revert.** `SwapQuote.spreadPbps` is a `uint16`, and a composed path spread above 65 535 PBPS is clamped to 65 535 rather than rejected. What saturates away is the σ, confidence and staleness *premium* (a risk surcharge, not a security floor), because reverting would halt a legitimately wide quote (both endpoints at a 5% `minFeePbps` with confidence at the halt cap is a live shape). The interior fence is what fails closed, and it is bounded by construction: the widest path the tree admits carries 6 interior legs, so the composed fence is at most 60 306, below the `uint16` ceiling.

The consequence for a caller: **at an extreme spread the quote under-reports the premium and the swap still executes.** Do not treat `spreadPbps` as a sufficient risk gate near its ceiling. Bound execution with `minAmountOut` (§7), which is derived from `amountOut` and is unaffected by the clamp.

---

## 7. Slippage protection

Always pass a real `minAmountOut`. `0` accepts any output.

```solidity
// Safe
swap(tokenIn, tokenOut, amountIn, minAmountOut, recipient, deadline);

// Unsafe: accepts any output
swap(tokenIn, tokenOut, amountIn, 0, recipient, deadline);
```

No *configuration* field bounds the quoted spread from above: the σ, confidence and staleness terms widen it freely, and there is no ceiling parameter an operator can set. The only hard limit is the `uint16` saturation at 65 535 PBPS described in §6, which clamps the reported number without blocking the trade. `minAmountOut` is therefore the only real bound on your execution: derive it from a fresh `getSwapQuote` rather than from any config field or from `spreadPbps`.

Working defaults, applied to `quote.amountOut`:

| Trade size | Tolerance |
|---|---|
| < $10k | 10–50 bps |
| $10k – $100k | 50–100 bps |
| > $100k | 100 bps and up, sized against the depth ladder |

Stable-to-stable legs sit at the bottom of each band; volatile legs at the top. Splits and multi-hop plans apply the tolerance **per leg**; see [Quotes & Routing §5](/docs/5-2-2-quotes-routing#5-depth-and-slippage).

---

## 8. Reverts

Errors are shared custom errors from the `Err` library, so they decode identically across `Pool`, `Admin`, `Flash` and `PoolFactory`. Merge the library errors into `POOL_ABI` (the shipped SDK ABI already does) so revert data decodes.

| Error | Signature | Common cause on a trading path |
|---|---|---|
| `InsufficientAmount` | `(uint256 available, uint256 required)` | Output below `minAmountOut`; or a withdrawal below `minLiquidity` |
| `Expired` | `()` | `block.timestamp > deadline`. Also a matured timelock left past its 7-day grace |
| `FeatureDisabled` | `(Resource)` | The operation's flag bit is clear: `SWAP`, `LIABILITY_SWAP` or `FLASH` |
| `NotFound` | `(Resource, address)` | Token is not a listed leg on this pool |
| `StaleData` | `(uint32 age, uint32 maxAge)` | The mark feed is past its TTL |
| `PriceOutsideRefBand` | `(uint256 markWad, uint256 refWad)` | Depeg breaker tripped: the leg's mark left `refBandBps` of its reference feed |
| `BaseDepegged` | `(uint256 basePriceWad, uint256 deviationBps)` | The base token itself moved past `BASE_DEPEG_HALT_BPS` (500 bps) |
| `CooldownActive` | `()` | LP receipt still inside the anti-JIT window from mint |
| `ThresholdViolation` | `(uint256 value, uint256 threshold)` | A bounded parameter or capacity limit was exceeded |
| `NotAuth` | `()` | Caller is not the authority for a gated call |
| `ZeroValue` / `ZeroAddr` | `()` | Zero amount, or a zero address where one is required |
| `InvalidState` | `()` | Pool not initialized, or bootstrap already sealed |
| `Overflow` | `()` | Arithmetic bound exceeded |

`Resource` is the enum `{ASSET, ORACLE, FEED, TREASURY, GOVERNANCE, SWAP, LIABILITY_SWAP, FLASH, TRANSFER}`.

A swap that reverts `FeatureDisabled(SWAP)` or `StaleData` is a live protocol state, not a bug in your call. Re-read the flags and the feed before retrying.

---

## 9. Events

Indexers should key off these. `Swapped`, `Deposited`, `Withdrawn`, `LiabilitySwapped` and `Donated` are emitted by the pool.

```solidity
event Swapped(
    address indexed sender,
    address indexed recipient,
    address indexed tokenIn,
    address tokenOut,
    uint256 amountIn,
    uint256 amountOut,
    uint16  spreadPbps,
    uint256 protoFee,
    uint256 lpFee,
    uint256 markPrice,
    uint256 midPrice,
    uint256 covToll
);

event Deposited(address indexed sender, address indexed token, uint256 amount, uint256 lpAmount);
event Withdrawn(address indexed sender, address indexed token, uint256 amount, uint256 lpAmount);
event Donated(address indexed sender, address indexed token, uint256 amount);
event DeadSharesSeeded(address indexed token, uint256 value, uint256 lpAmount);

// The one an indexer needs for NAV: every movement of a leg's liquidity index,
// including hook yield credits and write-downs. `reason` discriminates the source.
event IndexUpdated(
    address indexed token,
    uint256 index,
    uint128 reserves,
    uint128 liabilities,
    uint8   reason
);

event LiabilitySwapped(
    address indexed sender,
    address indexed tokenIn,
    address indexed tokenOut,
    uint256 lpAmountIn,
    uint256 lpAmountOut,
    uint256 haircut
);
```

Only three parameters are indexed on `Swapped`; `tokenOut` is not, so a filter on the outbound side has to be done off-log. `protoFee`, `lpFee` and `covToll` are denominated in `tokenOut`.

---

## 10. Arc testnet quickstart

See [deployed instances](/docs/2-1-contract-addresses) for the current fleet. Every instance is a real deployment, not a sandbox.

**Gas is paid in native USDC.** USDC is the chain's native currency at the predeploy address `0x3600000000000000000000000000000000000000`, so the gas token and the pools' base token are the same asset. Fund an address once and it can both transact and trade.

1. **Get addresses.** Everything comes from one call; never hardcode:

   ```bash
   curl -s https://api.btr.markets/v1/venues | jq '."5042002"'
   ```

   Three maps in that payload:

   - **`contracts`** carries `poolFactory`, `admin`, `flash`, `oracle`, `refOracle`, `guardian`, `treasury` and a **`faucet`**.
   - **`tokens`** maps every listed symbol to its address.
   - **`feeds`** maps every `<SYM>-USDC` pair to its feed id.

2. **Fund the account** from the `faucet` contract in that payload.

3. **Pick a pool and a live pair.**

   ```bash
   curl -s https://api.btr.markets/v1/pools | jq '.pools[] | {address, tag, symbols}'
   ```

   Four pools are deployed: `btr-stable`, `btr-fx`, `btr-crypto` and `btr-stocks`. Each lists the symbols tradeable on it, and every one is USDC-based. `btr-stocks` currently ships with no fitted equity legs, so start on one of the other three.

4. **Check the feed is alive before quoting.** `/v1/md/tickers` reports `status` and `age_ms` per ticker; a `dead` feed keeps its last `mid` and will revert `StaleData` on chain.

5. **First swap.** Approve the pool for `tokenIn`, call `getSwapQuote`, then `swap` with a `minAmountOut` derived from it (§7) and a `deadline`.

RPC endpoint, explorer and the full address tables: [Deployments](/docs/2-overview) and [Contract Addresses](/docs/2-1-contract-addresses).

---

## 11. Related

- [Integration examples](https://github.com/btr-protocol/examples): every call on this page, runnable and tested against a live deployment
- [Quotes & Routing](/docs/5-2-2-quotes-routing): choosing between pools, splits, EIP-5792 batching
- [Cookbook](/docs/5-2-3-cookbook): ready-made call stacks in four languages
- [Composability](/docs/5-1-4-composability): flash loans and protocol-owned liquidity
- [Pool Deployment & Curation](/docs/5-1-2-pool-deployment-curation): listing assets and configuring a pool
- [Spread & Fees](/docs/1-1-4-spread-fees) · [AIMM Overview](/docs/1-overview)
