Remove Liquidity Guide

Bush supports several different types of remove liquidity operations. All of them are executed through a Router; the Router functions and their parameters are documented in the Router API.

Core Concepts

The core concepts of removing liquidity are the same for any programming language or framework:

  • When removing liquidity the user sends Bush Pool Tokens (BPTs), and will receive pool tokens
  • BPT is an ERC20 with permit; use a permit signature (or a regular approval) to allow the Router to spend it. Permit2 is not required for BPT. See Token Approvals.
  • Token amount inputs/outputs are always in the raw token scale, e.g. 1 USDC should be sent as 1000000 because it has 6 decimals
  • If a pool's tokens include an ERC4626 with an initialized buffer, you have the option to receive the asset() of the ERC4626 when removing liquidity.
  • Transactions are always sent to a Router

Which function to call

You knowFunctionKind
The exact amount of BPT to burn, receiving every token proportionallyremoveLiquidityProportionalPROPORTIONAL
The exact amount of BPT to burn, receiving a single tokenremoveLiquiditySingleTokenExactInSINGLE_TOKEN_EXACT_IN
The exact amount of a single token to receiveremoveLiquiditySingleTokenExactOutSINGLE_TOKEN_EXACT_OUT
The pool is in Recovery ModeremoveLiquidityRecovery-

Proportional removes pay no swap fee and always work, including when the pool is paused (via Recovery Mode). Single-token removes pay the pool's swap fee on the non-proportional portion and can be disabled per pool.

Quoting

Every remove function has a matching query on the Router, e.g. queryRemoveLiquidityProportional. For off-chain quoting at scale, the @bush.fi/mathsopen in new window Vault exposes the same operation:

import { Vault, RemoveKind } from '@bush.fi/maths';

const { amountsOutRaw, bptAmountInRaw } = new Vault().removeLiquidity(
  { pool, minAmountsOutRaw, maxBptAmountInRaw, kind: RemoveKind.PROPORTIONAL },
  poolState,
);

See Pool Maths And Details for how to build poolState.

Queries should not be used onchain to set `minAmountsOut` due to possible manipulation via frontrunning.

Example

Removing liquidity proportionally with viem, after approving the Router to spend BPT:

// Query first to learn the expected amounts out
const { result: expectedAmountsOut } = await client.simulateContract({
    address: routerAddress,
    abi: routerAbi,
    functionName: "queryRemoveLiquidityProportional",
    args: [
        poolAddress,
        1000000000000000000n, // exactBptAmountIn
        sender,               // sender (for query context)
        "0x",                 // userData
    ],
});

// Then send, applying your slippage tolerance to each minAmountOut
const hash = await walletClient.writeContract({
    address: routerAddress,
    abi: routerAbi,
    functionName: "removeLiquidityProportional",
    args: [
        poolAddress,
        1000000000000000000n,                              // exactBptAmountIn
        expectedAmountsOut.map((a) => (a * 99n) / 100n),  // minAmountsOut (1% slippage)
        false,                                             // wethIsEth
        "0x",                                              // userData
    ],
    account,
});

minAmountsOut must be in the pool's token order; fetch it with getPoolTokens(pool) on the Vault or from the pool's get<Pool>ImmutableData().tokens.

From Solidity, import IRouter from @bush.fi/v3-interfaces/contracts/vault/IRouter.sol and call the same function; see the Swapping guide for the contract pattern.