Basic Operations

Signatures and call order for the four pool entry points: swap_qe, deposit, withdraw, swapLiability. One Pool, one chain. Choosing between pools is off chain: Quotes & Routing.


1. Interfaces

IPool.sol does not declare the trading functions. It 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_qe, deposit, withdraw, withdrawTo, donate, swapLiability, getSwapQuote, previewWithdraw and getCoverageRatio are declared on Pool.sol only, so IPool(pool).swap_qe(...) does not compile.

Declare the surface you need. This block covers everything on this page:

// 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; } 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_qe( 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 treasuryShares(address token) external view returns (uint256); function treasury() external view returns (address); function flowCooldownSecs() external view returns (uint16); }

ABIs: GET /v1/abis/{name} or @btr-protocol/sdk/abis (list).

One shared Pool implementation plus one Admin and one Flash serve every pool on a chain. Pools are ERC-1967 PoolProxy contracts, upgraded as a fleet by PoolFactory; LP receipts are ERC-20 (LP token); Flash is ERC-3156-style with a onFlashLoan 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 getRiskFlags(token) and test the bit for the operation. A swap-enabled leg is not necessarily liability-swap-enabled. Full bit layout: Guards Overview §1.

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 (Access Control).


3. Swap

Runnable, tested: foundry/test/03_PoolSwap.t.sol (Solidity, forked) · sdk/03-pool-swap.ts · rpc/03-pool-swap.ts (no SDK).

function swap_qe( 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) returns the full SwapQuote. Derive minAmountOut from amountOut.
  2. Approve: non-native approve(pool, amountIn); native goes in msg.value.
  3. Execute: prices against the external mark, applies the coverage toll and fee, updates reserves. No oracle write happens on a swap.
  4. The call reverts if amountOut < minAmountOut. This slippage floor is checked before settlement, so a swap that would also fail on settlement reverts with ThresholdViolation first.

What the call charges: Spread & Fees.

Pool.swap_qe pulls tokenIn from msg.sender, so the call must originate from the account holding the balance and allowance. It cannot be wrapped in Multicall3 (Quotes & Routing §2).

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

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 · sdk/04-deposit.ts · rpc/04-deposit.ts. Withdraw 05_Withdraw.t.sol · sdk/05-withdraw.ts · rpc/05-withdraw.ts.

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);

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.

Every exit settles at the pool-level rate C (pool.poolSolvencyWad()): a same-asset exit pays face · min(c_leg, C), a cross exit converts face · C. Deposits mint face at C and revert FeedUnavailable while any roster mark is unusable. Formula and edges: Inventory Management §5. LP-side detail: Providing Liquidity.

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

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. The claim converts at the pool rate C, like a cross exit. An LP holding a balanced ETH/USDC position who expects ETH to fall sells ETH liabilities for USDC liabilities and ends up USDC-heavy without leaving the pool. No first-class SDK sender exists; encode against POOL_ABI.

Four differences from a swap:

  1. Reserves do not move. Only the liability side of two legs changes. Nothing reaches the caller’s wallet, and an indexer treating it as a trade books phantom volume.
  2. Both amounts are LP shares, not token units. minLpAmountOut is a share floor; a token amount there is a units bug the call will not reject.
  3. Shares are not comparable across legs. Each leg carries its own liquidity index, so an identical value yields a different share count. Compare value, through previewWithdraw on each side.
  4. The claim converts at C, then pays the spread. fairIn = liabIn · C goes through the anchor path and the mark cap, so a round trip strictly loses. Reverts FeedUnavailable while any roster leg’s mark is unusable.

Both legs need LIABILITY_SWAP_ENABLED_BIT (bit 5) in getRiskFlags (§2). Check it before building the call: the revert does not say which leg was the problem.

There is no previewSwapLiability. For 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. Full sequence: Liability Swaps.

Runnable, tested: 06_DebtSwap.t.sol · sdk/06-debt-swap.ts · rpc/06-debt-swap.ts.


6. Units

Four scales, not interchangeable.

UnitScaleUsed by
BPS10 000 = 100%kappaCovBps, vegaBps, refBandBps, confidenceBps, maxDevBps
PBPS1 000 000 = 100%spreadPbps, minFeePbps, minDispersionPbps, flashFeePbps, sigmaPbps, dispRefPbps
WAD1e18 = 1.0markPrice, midPrice, mark1e18, liquidityIndexWad
Percent100 = 100%protoSharePct only
Decimal floatm<<4|e = m·10^e whole base tokensdepositCapCode

vegaBps = 10000 is a 1.0× multiplier, not 100%. minFeePbps = 100 is 1 bp. flashFeePbps = 100 is 0.01%, and PoolConfigLib.setFeeParams holds it in [MIN_FLASH_FEE_PBPS, MAX_FLASH_FEE_PBPS] = [10, 10000].

depositCapCode is not a rate. It decodes as (code >> 4) · 10^(code & 0xF) whole base tokens of leg notional; (5 << 4) | 5 is 500,000.

spreadPbps saturates rather than reverting. SwapQuote.spreadPbps is a uint16, and a composed path spread above 65 535 PBPS clamps to 65 535. What saturates away is the σ, confidence and staleness premium, a risk surcharge rather than a security floor: reverting would halt a legitimately wide quote, and both endpoints at a 5% minFeePbps with confidence at the halt cap is a live shape. The interior fence fails closed instead, and is bounded: the widest path the tree admits carries 6 interior legs, so the composed fence is at most 60 306, below the uint16 ceiling.

At an extreme spread the quote under-reports the premium and the swap still executes. spreadPbps is not a sufficient risk gate near its ceiling. Bound execution with minAmountOut (§7), which derives from amountOut and is unaffected by the clamp.


7. Slippage protection

Always pass a real minAmountOut. 0 accepts any output.

// Safe swap_qe(tokenIn, tokenOut, amountIn, minAmountOut, recipient, deadline); // Unsafe: accepts any output swap_qe(tokenIn, tokenOut, amountIn, 0, recipient, deadline);

No configuration field bounds the quoted spread from above: the σ, confidence and staleness terms widen it without limit, and no operator-settable ceiling exists. The only hard limit is the uint16 saturation at 65 535 PBPS (§6), which clamps the reported number without blocking the trade. Derive minAmountOut from a fresh getSwapQuote, never from a config field or from spreadPbps.

Working defaults, applied to quote.amountOut:

Trade sizeTolerance
< $10k10–50 bps
$10k – $100k50–100 bps
> $100k100 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 (Quotes & Routing §5). Definitions of slippage and price impact: Slippage & Price Impact.


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.

ErrorSignatureCommon cause on a trading path
InsufficientAmount(uint256 available, uint256 required)The pool cannot cover the transfer: inventory, liquidity or msg.value short of what the call needs
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 the lane’s reference tier, or left 1.0 by more than its depeg code allows
BaseDepegged(uint256 basePriceWad, uint256 deviationBps)The base token itself moved past its depeg band (code x 250 bps, 2,500 bps at code 0)
CooldownActive()LP receipt still inside the anti-JIT window from mint
ThresholdViolation(uint256 value, uint256 threshold)Output below minAmountOut on a swap or withdrawal; a withdrawal below minLiquidity; a bounded parameter or capacity limit exceeded
FeedUnavailable()A roster leg’s mark is unusable; deposits and liability swaps refuse until it recovers
ExcessiveAmount / WeightCapExceeded(uint256, uint256)deposit, donate or a swapLiability output past the leg’s depositCapCode / maxLiabWeightBps (Pool Deployment & Curation §3.3)
NotAuthorized()Caller is not the authority for a gated call
ZeroValue / ZeroAddress()Zero amount, or a zero address where one is required
InvalidState()Pool not initialized, or bootstrap already sealed
Overflow()Arithmetic bound exceeded
Reentrancy()A guarded entry point was re-entered

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

FeatureDisabled(SWAP) and StaleData are live protocol states, not bugs in your call. Re-read the flags and the feed before retrying. Halt semantics: Flow Guards. Feed gating: Mark Relayer.


9. Events

Indexers key off these. Swapped, Deposited, Withdrawn, LiabilitySwapped and Donated are emitted by the pool, as are SolvencyUpdated(cWad, navBase, claimBase) after each LP entry point (the pool rate C and its numerator and denominator in base units), Swept and UntrackedSynced.

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) ); 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: 0 donate, 1 hook yield, 3 hook write-down, 4 fee. 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 );

Swapped packs its payload into five words, so a decoder that expects one field per word reads garbage. Low half first: each uint256 holds a 128-bit value in bits 0..127 and the second value above it.

WordBits 0..127Bits 128..255
amountsamountInamountOut
feesprotoFeelpFee
pricesmarkPricemidPrice
outBooktokenOut reserves after settlementtokenOut liabilities after settlement
outStatecovTollliquidityIndexWad in bits 128..199 (uint72), spreadPbps in bits 224..239 (uint16)
const LO = (1n << 128n) - 1n; const amountIn = amounts & LO, amountOut = amounts >> 128n; const protoFee = fees & LO, lpFee = fees >> 128n; const markPrice = prices & LO, midPrice = prices >> 128n; const covToll = outState & LO; const liquidityIndexWad = (outState >> 128n) & ((1n << 72n) - 1n); const spreadPbps = Number(outState >> 224n);

Only three parameters are indexed on Swapped: tokenOut is not, so a filter on the outbound side has to run off-log. amountOut, protoFee, lpFee and covToll are denominated in tokenOut; markPrice and midPrice are WAD, tokenOut per tokenIn, whole-unit (decimals-normalised). The event also carries the out leg’s book and index, so a swap emits no separate IndexUpdated: take the fee raise from liquidityIndexWad here.


10. Testnet quickstart

See deployed instances for the current fleet and its chain id. Every instance is a real deployment, not a sandbox.

Gas token and pool base can differ, and both vary per chain: read them from deployed instances before assuming either.

  1. Addresses, from one call, never hardcoded:

    curl -s https://api.btr.markets/v1/venues | jq '.["<chainId>"]'

    Take <chainId> from deployed instances.

    contracts carries poolFactory, admin, flash, oracle, refOracle, guardian, treasury and faucet. tokens maps every listed symbol to its address. feeds maps every pair to its feed id.

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

  3. Pick a pool and a live pair:

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

    Each pool lists its tradeable symbols (Pool Composition).

  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 reverts StaleData on chain.

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

RPC endpoint, explorer and the full address tables: Deployments and Contract Addresses.