Curve StableSwap — Implementation Reference¶
Operational details for Curve StableSwap pools that don’t belong in the domain glossary.
Variant Enum Values¶
Mainnet Curve pools use different calculation formulas depending on the pool contract version. The variant is determined by the pool address at construction time and stored as an enum on the pool instance.
DVariant¶
Identifies which D-calculation formula pair (calc_d / calc_dp) a pool uses in _get_d().
Value |
|
|
|---|---|---|
|
Standard formula |
Standard formula |
|
Variant alpha formula |
Standard formula |
|
Variant alpha formula |
Variant alpha formula |
|
Standard formula |
Variant alpha formula |
|
Standard formula |
Variant beta formula |
|
Standard formula |
Variant gamma formula |
Resolution: resolve_d_variant() in _variant_groups.py, called by CurvePoolBuilder.build().
YVariant¶
Identifies which Y-calculation formula a pool uses in _get_y(). Controls amp divisor and c/b formula selection.
Value |
Amp divisor |
c/b formula |
|---|---|---|
|
With |
With |
|
Without |
Without |
|
With |
Without |
Resolution: resolve_y_variant() in _variant_groups.py.
YDVariant¶
Identifies which Y_D-calculation formula a pool uses in _get_y_d(). Controls whether A_PRECISION appears in b/c formulas.
Value |
b/c formula |
|---|---|
|
Without |
|
With |
Resolution: resolve_yd_variant() in _variant_groups.py.
Strategy Enum Values¶
Mainnet Curve pools differ in their swap computation path, rate source, and fee application. Each pool receives a PoolStrategies frozen dataclass at construction time.
SwapStyle¶
Identifies which computation path get_dy() uses for dy calculation, fee application, and rate conversion order.
Value |
dy formula |
Fee |
Rate |
|---|---|---|---|
|
|
After dy |
After fee |
|
Converted before fee |
After conversion |
Before fee |
|
Converted before fee (no |
After conversion |
Before fee |
|
No rate conversion |
— |
— |
|
Newton’s method |
Dynamic fee |
— |
|
Live balances minus admin |
— |
— |
|
Live balances, dynamic offpeg fee |
Dynamic |
— |
|
Live balances, precision multipliers for xp |
Dynamic |
— |
|
Live balances, oracle rates |
— |
Oracle |
|
|
After dy |
After fee |
|
|
Inside rate conversion |
— |
MetapoolRateStyle¶
Identifies how a metapool constructs its rate tuple in get_dy().
Value |
Rate tuple |
|---|---|
|
|
|
|
|
|
MetapoolUnderlyingStyle¶
Identifies how a metapool computes get_dy_underlying().
Value |
Underlying path |
|---|---|
|
Default underlying path |
|
|
|
Redemption price for first coin |
LendingRateStyle¶
Identifies which _stored_rates_from_*() method provides lending rates.
Value |
Rate source |
|---|---|
|
No lending rates; uses |
|
cToken accrual rates |
|
yToken price-per-share rates |
|
Yearn vault cToken rates |
|
ankrETH ratio rates |
|
rETH oracle rates |
|
Oracle-based rates |
Strategy resolution is done by resolve_pool_strategies() in _pool_strategies.py, which combines the address→strategy mapping with variant group resolution. The PoolStrategies dataclass is frozen (immutable after creation) and picklable. Calculators are auto-constructed from enum values in __post_init__ via each enum’s make_calculator() factory method; explicitly-passed calculator arguments are preserved.
Crypto Pool Internals¶
Crypto pools (e.g., Tricrypto USDT-WBTC-WETH) use a fundamentally different calculation path from stableswap pools.
Components¶
Component |
Purpose |
I/O Required |
|---|---|---|
|
Current invariant value |
Fetched on-chain via |
|
Curve shape parameter |
Fetched on-chain via |
|
Current prices of volatile assets |
Fetched on-chain via |
|
Newton’s method solver for y |
Pure math (no I/O) |
|
Fee reduction based on imbalance |
Pure math (no I/O) |
Dynamic fee |
Interpolation between mid_fee and out_fee using fee_gamma |
Uses pool state (no I/O) |
Dynamic fee formula¶
fee_calc = (mid_fee * f + out_fee * (10^18 - f)) / 10^18
where f = _reduction_coefficient(xp, fee_gamma)
Crypto pool detection¶
A pool with fee_gamma > 0 is identified as a crypto pool during the Curve Pool Builder (invoked via Bot.build_pool()). This triggers creation of the D, gamma, and price_scale data provider methods.
Contract parameters¶
Parameter |
Purpose |
Source |
|---|---|---|
|
Fee curve parameter adjusting fees based on imbalance |
|
|
Mid-range fee percentage |
|
|
Outlier fee percentage at extreme imbalance |
|
|
Curve shape parameter for CryptoSwap invariant |
|
|
Current price of volatile assets |
|
|
Fee multiplier for off-peg swaps in some lending pools |
|
Detection Heuristics¶
These detection heuristics currently live in CurvePoolBuilder.build(). Plan 018 proposes decomposing them into standalone detector modules (CoinDiscovery, LendingDetector, MetapoolDetector, CryptoDetector, ARampingDetector), each returnable as a frozen dataclass and independently testable with a fake provider.
Metapool Detection¶
Check Registry
is_meta(pool_address)if availableCheck Factory
is_meta(pool_address)as fallbackIf neither works, check if second coin is 3Crv LP token (
0x6c3F90f043a72FA612CbAC8115EEe7f52CdE6E490)Fall back to
base_pool()/get_base_pool()contract methodsIf all fail, mark as plain pool (is_meta = False)
Lending Token Detection¶
Token Type |
Detection Method |
Why This Method |
|---|---|---|
cToken |
|
Avoids false positives from |
yToken |
|
More reliable than |
cyToken |
|
Yearn vault tokens that are also Compound tokens |
aETH |
Detected by |
Lido-style staking wrappers |
rETH |
Detected by specific oracle method check |
Rocket Pool token |
Plain Token |
None of the above checks succeed |
No rate conversion needed |
Coin Indexing¶
Try
coins(uint256)first (modern pools)Fall back to
coins(int128)(legacy pools using older Solidity int types)Map coins[] indices to underlying_coins[] if underlying differs
Precision Multipliers for cTokens¶
Critical: cTokens have different decimals than their underlying (e.g., cDAI = 8 decimals, DAI = 18 decimals). The precision multiplier must use underlying token decimals:
cToken_decimals = 8
underlying_decimals = 18
multiplier = 10^(18 - underlying_decimals) = 1 (not 10^10)
This is fetched via underlying() contract call + decimals() on the underlying token.
Error Types¶
Exception |
When Raised |
|---|---|
|
Base class for all Curve-specific errors |
|
Required on-chain data unavailable via data_provider (e.g., data_provider is None for a crypto pool) |
|
Pool has < 2 tokens or returns invalid data |
|
Swap amount exceeds available liquidity |
|
Pool has zero reserves for the requested direction |
Debugging Swap Mismatches¶
When get_dy() disagrees with the on-chain contract call for a specific pool, the mismatch is almost always a wrong strategy enum — not a floating-point or arithmetic error.
Step 1: Fetch the verified contract source¶
cast source <pool_address> > /tmp/pool_source.vy
Curve V1 pools are written in Vyper. The source is the ground truth for all calculation paths.
Step 2: Verify each strategy enum against the source¶
Python enum |
Contract reference |
What to check |
|---|---|---|
|
|
Does the contract have any |
|
|
Compare the dy formula: presence/absence of |
|
|
Check whether |
|
|
Check whether |
|
|
Same A_PRECISION check as YVariant, but for the y_d calculation. |
|
|
What rate tuple is constructed? |
|
|
Same analysis for the underlying swap path. |
Step 3: Check the address mapping¶
The address→strategy mapping in _pool_strategies.py was derived from old class-level frozensets, not verified against contract source. Two common errors:
Wrong
LendingRateStyle. A pool was grouped into a cToken/yToken frozenset because the old Python code routed it through_stored_rates_from_ctokens(), but the contract hasUSE_LENDING = [False, ...]. The old code worked because the_stored_rates_from_*()method returnsPRECISION * LENDING_PRECISIONwhenuse_lendingis all-False — same asrate_multipliers. The new code creates a data provider that makes spurious on-chain calls, potentially returning different rates.Missing address. A pool not in the mapping falls through to
PoolStrategies()defaults (SwapStyle.STANDARD,LendingRateStyle.NONE). This is correct for plain 2-token stablecoin pools but wrong for unlisted lending, metapool, or crypto pools.
Step 4: Verify with on-chain call¶
cast call <pool_address> "get_dy(int128,int128,uint256)" <i> <j> <dx> --block <block>
Compare against the Python pool’s get_dy() result. If they match for several block heights and token pairs, the strategy is correct.
Common pitfalls¶
sUSD pool pattern. Pool
0xA5407eAEholds DAI, USDC, USDT, sUSD (no cTokens) but the old code had it in theCTOKEN_ADDRESSESfrozenset. The contract’sUSE_LENDING = [False, False, False, False]means_stored_rates()just returnsPRECISION_MUL * LENDING_PRECISION.Y pool sub-variants. Some Y pools use
(xp[j] - y - 1)in the dy formula (RATE_ADJUSTED) while others use(xp[j] - y)without the-1(RATE_ADJUSTED_NO_ONE). The Vyper source is the only way to distinguish.A_PRECISION in get_y. The contract stores
Aas the raw value (not scaled byA_PRECISION). But degenbot storesself.a_coefficientand scales it in_a(). WhenYVariant.VARIANT_0dividesampbyA_PRECISIONbefore passing to_get_d(), the intermediate values change — this is correct, matching the contract’s direct use ofself.A. But the D calculation must also use a matchingd_variantthat omitsA_PRECISIONfrom the formula.