Pool Maths Reference
Every pool factory approved for Bush lives in the bush-pool-factories repository, and every factory there ships with exact off-chain reference maths in maths/, in three languages:
| Package | Language | Install |
|---|---|---|
@bush.fi/maths | TypeScript (bigint) | npm install @bush.fi/maths |
bush-maths | Python (int) | from source |
bush-maths | Rust (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/check), 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:
poolType | State type | Pool-specific fields | From |
|---|---|---|---|
WEIGHTED, WEIGHTED_8020, COW | WeightedState | weights, minTokenBalances | getWeightedPoolImmutableData() |
STABLE | StableState | amp | getAmplificationParameter()[0] (raw, x1000) |
LIQUIDITY_BOOTSTRAPPING | LiquidityBootstrappingState | weights schedule, projectTokenIndex, isSwapEnabled, currentTimestamp | getLBPoolImmutableData() / getLBPoolDynamicData() |
FIXED_PRICE_LBP | FixedPriceLBPState | projectTokenRate, indices, startTime/endTime, currentTimestamp | getFixedPriceLBPoolImmutableData() / getFixedPriceLBPoolDynamicData() |
RECLAMM | ReClammState | lastVirtualBalances, dailyPriceShiftBase, lastTimestamp, centerednessMargin, the price-ratio update fields, currentTimestamp | getReClammPoolDynamicData() |
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/ 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 README.
Supported Pool Types
The table below mirrors the approved factories. 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.
| Factory | poolType | Hook | Contracts | Docs |
|---|---|---|---|---|
| WeightedPoolFactory | WEIGHTED | - | contracts/weighted | weighted-pool-factory |
| WeightedPool8020Factory | WEIGHTED_8020 | - | contracts/weighted | weighted-8020-pool-factory |
| StablePoolFactory | STABLE | - | contracts/stable | stable-pool-factory |
| StableSurgePoolFactory | STABLE | StableSurge | contracts/stable-surge | stable-surge-pool-factory |
| LBPoolFactory | LIQUIDITY_BOOTSTRAPPING | - | contracts/lbp | lbpool-factory |
| FixedPriceLBPoolFactory | FIXED_PRICE_LBP | - | contracts/lbp | fixed-price-lbpool-factory |
| ReClammPoolFactory | RECLAMM | - | contracts/reclamm | ReClamm Pool |
| CowPoolFactory | COW | self (CowPool) | contracts/cow | cow-pool-factory |
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.
- Contracts:
WeightedPool.sol,WeightedPoolFactory.sol - Maths: TypeScript, Python, Rust
- Factory address: Deployment Addresses - see
WeightedPoolFactory - Every pool enforces per-token minimum balances (
minTokenBalancesin the immutable data, set by the factory asmax(1e6, 10^(18 - decimals))in 18-decimal units): swaps, adds and removes that would leave a token below its floor revert withTokenBalanceBelowMin. The pool also rounds the token-in balance up by 1 before computing a swap. Both are modelled by the Bush maths; older Balancer-derived maths packages do not model them, so use the Bush maths rather than a port. - The minimum static swap fee is 0.05% (Balancer's upstream
WeightedPoolallows 0.001%); the maximum is 10%.
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.
- Contracts:
contracts/stable - Maths: TypeScript, Python, Rust
- Factory address: Deployment Addresses - see
StablePoolFactory - Amplification factor can be dynamic; see:
getAmplificationParameter()view function (returns(value, isUpdating, precision); the maths takes the rawvalue)AmpUpdateStarted&AmpUpdateStoppedevents
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.
- Contracts:
contracts/stable-surge - Maths: Stable maths above plus the StableSurge hook maths
- Factory address: Deployment Addresses - see
StableSurgePoolFactory
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.
- Contracts:
LBPool.sol,LBPoolFactory.sol - Maths: TypeScript, Python, Rust
- Factory address: Deployment Addresses - see
LBPoolFactory - Weight calculation is time-dependent: pass
currentTimestampfor the block the swap will land in. - Pool tokens are always sorted alphanumerically; use
projectTokenIndex/reserveTokenIndexrather than assuming an order.
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.
- Contracts:
FixedPriceLBPool.sol,FixedPriceLBPoolFactory.sol - Maths: TypeScript, Python, Rust
- Factory address: Deployment Addresses - see
FixedPriceLBPoolFactory
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.
- Contracts:
ReClammPool.sol,ReClammPoolFactory.sol,IReClammPool.sol - Maths: TypeScript, Python, Rust
- Test data:
reclamm-pool-factory-*.json- in-range, out-of-range and mid-price-ratio-update pools - Factory address: Deployment Addresses - see
ReClammPoolFactory
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 bydailyPriceShiftBase(1 - tau, derived fromdailyPriceShiftExponent), moving the range toward the price.lastVirtualBalancesandlastTimestampare the values as of the last interaction; the maths advances them tocurrentTimestamp. - Price ratio update: governance can schedule a change of the range width;
startFourthRootPriceRatio/endFourthRootPriceRatioare interpolated betweenpriceRatioUpdateStartTimeandpriceRatioUpdateEndTime.
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 changesLastTimestampUpdated(uint32 lastTimestamp)- on every swap or liquidity operationPriceRatioStateUpdated(uint256 startFourthRootPriceRatio, uint256 endFourthRootPriceRatio, uint256 priceRatioUpdateStartTime, uint256 priceRatioUpdateEndTime)- on initialization and when a price ratio update is scheduledDailyPriceShiftExponentUpdated(uint256 dailyPriceShiftExponent, uint256 dailyPriceShiftBase)andCenterednessMarginUpdated(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.