Pool Maths Reference

Every pool factory approved for Bush lives in the bush-pool-factoriesopen in new window repository, and every factory there ships with exact off-chain reference maths in maths/open in new window, in three languages:

PackageLanguageInstall
@bush.fi/mathsopen in new windowTypeScript (bigint)npm install @bush.fi/maths
bush-mathsopen in new windowPython (int)from source
bush-mathsopen in new windowRust (U256)from source

The maths is exact integer fixed-point: the primitives are ports of the Solidity FixedPoint / LogExpMath libraries and round the same way the contracts do. Each package is verified bit-for-bit against pools deployed through the real factories on a local chain (see maths/checkopen in new window), so aggregators should consume the package for their language rather than re-deriving swap/join/exit math from Solidity.

The Vault class

Each package exposes a Vault that reproduces the whole on-chain flow - token scaling and rates, swap fees, hook calls, add/remove liquidity maths - on top of per-pool-type classes. Its results are the raw amounts the on-chain Router returns, so poolState is the only input:

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

const vault = new Vault();

const amountOut = vault.swap(
  { amountRaw: 1_000_000n, tokenIn, tokenOut, swapKind: SwapKind.GivenIn },
  poolState,
);

const { bptAmountOutRaw, amountsInRaw } = vault.addLiquidity(
  { pool, maxAmountsInRaw, minBptAmountOutRaw: 0n, kind: AddKind.UNBALANCED },
  poolState,
);

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

Operations the pool would reject (over the max swap ratio, LBP sale restrictions, ...) throw. Pools with a hook take a hookState as the third argument.

Building the pool state

poolState is the pool's state as read from the chain. Every pool exposes it in two calls - get<Pool>ImmutableData() and get<Pool>DynamicData() - and the Vault's getPoolConfig fills in the rest (see Fetching Pools And Data). The base fields are the same for every pool type:

const poolState: WeightedState = {
  poolType: 'WEIGHTED',
  poolAddress: POOL_ADDRESS,
  tokens: [...immutable.tokens],
  scalingFactors: [...immutable.decimalScalingFactors],
  balancesLiveScaled18: [...dynamic.balancesLiveScaled18],
  tokenRates: [...dynamic.tokenRates],
  swapFee: dynamic.staticSwapFeePercentage,
  totalSupply: dynamic.totalSupply,
  aggregateSwapFee: config.aggregateSwapFeePercentage,
  supportsUnbalancedLiquidity: !config.liquidityManagement.disableUnbalancedLiquidity,
  // pool-specific fields below, e.g. for WEIGHTED:
  weights: [...immutable.normalizedWeights],
  minTokenBalances: [...immutable.minTokenBalances],
};

The pool-specific fields per type:

poolTypeState typePool-specific fieldsFrom
WEIGHTED, WEIGHTED_8020, COWWeightedStateweights, minTokenBalancesgetWeightedPoolImmutableData()
STABLEStableStateampgetAmplificationParameter()[0] (raw, x1000)
LIQUIDITY_BOOTSTRAPPINGLiquidityBootstrappingStateweights schedule, projectTokenIndex, isSwapEnabled, currentTimestampgetLBPoolImmutableData() / getLBPoolDynamicData()
FIXED_PRICE_LBPFixedPriceLBPStateprojectTokenRate, indices, startTime/endTime, currentTimestampgetFixedPriceLBPoolImmutableData() / getFixedPriceLBPoolDynamicData()
RECLAMMReClammStatelastVirtualBalances, dailyPriceShiftBase, lastTimestamp, centerednessMargin, the price-ratio update fields, currentTimestampgetReClammPoolDynamicData()

Time-dependent pools (LBPs, ReClamm) take a currentTimestamp: use the timestamp of the block the transaction will land in, not the block the state was read at.

The testData/open in new window directory holds one JSON file per pool type, generated from a real pool: pool is the exact state shape, and swaps / adds / removes are the results the Vault returned for it. It is the quickest way to see what your integration should produce.

A full ethers v6 walkthrough of fetching the three calls and assembling the state is in the @bush.fi/maths READMEopen in new window.

Supported Pool Types

The table below mirrors the approved factoriesopen in new window. Each factory has its own docs folder with a README.md, PARAMETERS.md (every creation parameter and its range) and GAS_CHARACTERISTICS.md. Not every factory is deployed on every network; check the Deployment Addresses page for what is live.

FactorypoolTypeHookContractsDocs
WeightedPoolFactoryWEIGHTED-contracts/weightedopen in new windowweighted-pool-factoryopen in new window
WeightedPool8020FactoryWEIGHTED_8020-contracts/weightedopen in new windowweighted-8020-pool-factoryopen in new window
StablePoolFactorySTABLE-contracts/stableopen in new windowstable-pool-factoryopen in new window
StableSurgePoolFactorySTABLEStableSurgecontracts/stable-surgeopen in new windowstable-surge-pool-factoryopen in new window
LBPoolFactoryLIQUIDITY_BOOTSTRAPPING-contracts/lbpopen in new windowlbpool-factoryopen in new window
FixedPriceLBPoolFactoryFIXED_PRICE_LBP-contracts/lbpopen in new windowfixed-price-lbpool-factoryopen in new window
ReClammPoolFactoryRECLAMM-contracts/reclammopen in new windowReClamm Pool
CowPoolFactoryCOWself (CowPool)contracts/cowopen in new windowcow-pool-factoryopen in new window

Weighted Pool

Pools that swap tokens by enforcing a Constant Weighted Product invariant. 2-8 tokens, weights fixed at deployment (each >= 1%, summing to 100%). See the Weighted Pool concept docs and Weighted Math.

struct WeightedPoolImmutableData {
    IERC20[] tokens;
    uint256[] decimalScalingFactors;
    uint256[] normalizedWeights;
    uint256[] minTokenBalances;
}

struct WeightedPoolDynamicData {
    uint256[] balancesLiveScaled18;
    uint256[] tokenRates;
    uint256 staticSwapFeePercentage;
    uint256 totalSupply;
    bool isPoolInitialized;
    bool isPoolPaused;
    bool isPoolInRecoveryMode;
}

Weighted 80/20 Pool

The same WeightedPool contract, deployed by WeightedPool8020Factory with fixed 80/20 weights (see 80/20 Pools). Maths and data structs are identical to Weighted; poolType is WEIGHTED_8020.

Stable Pool

Pools that swap tokens by enforcing a Stable Math invariant, based on Curve. See the Stable Pool concept docs and Stable Math.

struct StablePoolImmutableData {
    IERC20[] tokens;
    uint256[] decimalScalingFactors;
    uint256 amplificationParameterPrecision;
}

struct StablePoolDynamicData {
    uint256[] balancesLiveScaled18;
    uint256[] tokenRates;
    uint256 staticSwapFeePercentage;
    uint256 totalSupply;
    uint256 bptRate;
    uint256 amplificationParameter;
    uint256 startValue;
    uint256 endValue;
    uint32 startTime;
    uint32 endTime;
    bool isAmpUpdating;
    bool isPoolInitialized;
    bool isPoolPaused;
    bool isPoolInRecoveryMode;
}

Stable Surge Pool

Stable Pools that use the Stable Surge Hook, a dynamic fee implementation that increases fees on transactions that unbalance the pool. The pool itself is exactly the same - a standard Stable Pool. The only difference is the hook, which is attached to the pool by the factory. The maths takes hookType: 'StableSurge' and a hookState; see the Hooks Reference.

Liquidity Bootstrapping Pool

Liquidity Bootstrapping Pools have linearly changing weights but use weighted math to determine prices. They are two-token pools (a project token and a reserve token) with a sale window between startTime and endTime; swaps are disabled outside it, and buying the project token back in can be blocked.

struct LBPoolImmutableData {
    IERC20[] tokens;
    uint256[] decimalScalingFactors;
    uint256 startTime;
    uint256 endTime;
    uint256 projectTokenIndex;
    uint256 reserveTokenIndex;
    bool isProjectTokenSwapInBlocked;
    uint256[] startWeights;
    uint256[] endWeights;
    uint256 reserveTokenVirtualBalance;
}

struct LBPoolDynamicData {
    uint256[] balancesLiveScaled18;
    uint256 staticSwapFeePercentage;
    uint256 totalSupply;
    bool isPoolInitialized;
    bool isPoolPaused;
    bool isPoolInRecoveryMode;
    bool isSwapEnabled;
    uint256[] normalizedWeights;
}

Fixed Price LBP

A Liquidity Bootstrapping Pool variant that sells the project token at a fixed projectTokenRate (the number of reserve tokens per project token, 18 decimals) during the sale window instead of along a weight curve.

struct FixedPriceLBPoolImmutableData {
    IERC20[] tokens;
    uint256[] decimalScalingFactors;
    uint256 startTime;
    uint256 endTime;
    uint256 projectTokenIndex;
    uint256 reserveTokenIndex;
    uint256 projectTokenRate;
}

struct FixedPriceLBPoolDynamicData {
    uint256[] balancesLiveScaled18;
    uint256 staticSwapFeePercentage;
    uint256 totalSupply;
    bool isPoolInitialized;
    bool isPoolPaused;
    bool isPoolInRecoveryMode;
    bool isSwapEnabled;
}

ReClamm Pool

Readjusting Concentrated Liquidity AMM. A two-token pool that concentrates liquidity in a price range, like a fungible concentrated-liquidity position, but with a range that is not fixed: the pool tracks virtual balances (offsets added to the real balances) that define the range, and moves them itself when the price leaves the range, shifting the range back toward the current price at a bounded daily rate. The range's width (the price ratio) can also be widened or narrowed by governance, slowly over time. See the ReClamm Pool concept docs.

What the maths needs. Everything comes from getReClammPoolDynamicData(), plus a currentTimestamp. The state is time-dependent in two ways, and the maths reproduces both from the stored fields:

  • Out of range: while the pool's centeredness is below centerednessMargin, the overvalued virtual balance decays every second by dailyPriceShiftBase (1 - tau, derived from dailyPriceShiftExponent), moving the range toward the price. lastVirtualBalances and lastTimestamp are the values as of the last interaction; the maths advances them to currentTimestamp.
  • Price ratio update: governance can schedule a change of the range width; startFourthRootPriceRatio / endFourthRootPriceRatio are interpolated between priceRatioUpdateStartTime and priceRatioUpdateEndTime.

Use the timestamp of the block the swap will land in. A quote made with a stale timestamp will be wrong whenever the pool is out of range or an update is in progress; in range with no update, the virtual balances do not move.

function getReClammPoolDynamicData() external view returns (ReClammPoolDynamicData memory data);

struct ReClammPoolDynamicData {
    // Base Pool
    uint256[] balancesLiveScaled18;
    uint256[] tokenRates;
    uint256 staticSwapFeePercentage;
    uint256 totalSupply;
    // ReClamm
    uint256 lastTimestamp;
    uint256[] lastVirtualBalances;
    uint256 dailyPriceShiftExponent;
    uint256 dailyPriceShiftBase;
    uint256 centerednessMargin;
    uint256 currentPriceRatio;
    uint256 currentFourthRootPriceRatio;
    uint256 startFourthRootPriceRatio;
    uint256 endFourthRootPriceRatio;
    uint32 priceRatioUpdateStartTime;
    uint32 priceRatioUpdateEndTime;
    // Pool State
    bool isPoolInitialized;
    bool isPoolPaused;
    bool isPoolInRecoveryMode;
}

function getReClammPoolImmutableData() external view returns (ReClammPoolImmutableData memory data);

struct ReClammPoolImmutableData {
    // Base Pool
    IERC20[] tokens;
    uint256[] decimalScalingFactors;
    bool tokenAPriceIncludesRate;
    bool tokenBPriceIncludesRate;
    uint256 minSwapFeePercentage;
    uint256 maxSwapFeePercentage;
    // Initialization (only used to seed the pool with correctly priced amounts)
    uint256 initialMinPrice;
    uint256 initialMaxPrice;
    uint256 initialTargetPrice;
    uint256 initialDailyPriceShiftExponent;
    uint256 initialCenterednessMargin;
    // Operating Limits
    uint256 minPriceRatio;
    uint256 maxPriceRatio;
    uint256 maxCenterednessMargin;
    uint256 maxDailyPriceShiftExponent;
    uint256 maxDailyPriceRatioUpdateRate;
    uint256 minPriceRatioUpdateDuration;
    uint256 minPriceRatioDelta;
    uint256 balanceRatioAndPriceTolerance;
}

Tracking state with events. Because the range moves on its own, an event-based indexer needs more than the Vault's Swap event. The pool emits:

  • VirtualBalancesUpdated(uint256 virtualBalanceA, uint256 virtualBalanceB) - on every swap or liquidity operation, and when the margin or shift exponent changes
  • LastTimestampUpdated(uint32 lastTimestamp) - on every swap or liquidity operation
  • PriceRatioStateUpdated(uint256 startFourthRootPriceRatio, uint256 endFourthRootPriceRatio, uint256 priceRatioUpdateStartTime, uint256 priceRatioUpdateEndTime) - on initialization and when a price ratio update is scheduled
  • DailyPriceShiftExponentUpdated(uint256 dailyPriceShiftExponent, uint256 dailyPriceShiftBase) and CenterednessMarginUpdated(uint256 centerednessMargin) - when governance or the swap manager changes the pool's parameters

Individual getters (getLastVirtualBalances, getLastTimestamp, getCenterednessMargin, getDailyPriceShiftBase, getPriceRatioState) are also available if you only need to refresh one field.

Rates. ReClamm pools often hold rate-bearing tokens. tokenAPriceIncludesRate / tokenBPriceIncludesRate record whether the pool's price range was specified in wrapped or underlying terms; the maths handles this through the base state's tokenRates, so no extra work is needed beyond passing the dynamic data through.