Skip to main content

Integrating Stableswap-NG

Stableswap-NG is Curve's AMM for trading pegged and correlated assets — stablecoins (USDC/USDT), liquid staking tokens (wstETH/ETH), yield-bearing tokens (sDAI), and other assets expected to trade near a fixed ratio. Pools support 2–8 coins.

There are two pool types:

  • Plain pools — all coins are top-level ERC-20 tokens (e.g., USDC/USDT/DAI)
  • Metapools — one coin paired against an existing Curve base pool's LP token (e.g., newStable/3CRV), enabling swaps with all underlying base pool tokens

Both share the same swap interface. Metapools add exchange_underlying() for direct swaps into base pool tokens.

info

All Stableswap-NG pools are deployed via the Factory (0x6A8cbed756804B16E05E741eDaBd5cB544AE21bf). The Factory uses blueprint patterns — pool contracts are deployed as minimal proxies of stored implementations.


Pool Discovery

Via the Factory

The Factory maintains a registry of all deployed Stableswap-NG pools.

// Get total number of pools
factory.pool_count() → uint256

// Get pool address by index
factory.pool_list(i: uint256) → address

// Find a pool for a specific token pair
// Use i=0 for first match, i=1 for second, etc.
factory.find_pool_for_coins(_from: address, _to: address, i: uint256) → address

// Get pool metadata
factory.get_coins(pool: address) → address[]
factory.get_n_coins(pool: address) → uint256
factory.get_balances(pool: address) → uint256[]
factory.get_decimals(pool: address) → uint256[]
factory.is_meta(pool: address) → bool
factory.get_pool_asset_types(pool: address) → uint8[]
factory.get_implementation_address(pool: address) → address

Via the MetaRegistry

The MetaRegistry aggregates pools across all Curve Factory types (Stableswap-NG, Twocrypto-NG, Tricrypto-NG, and legacy). If you want to find the best pool for a pair regardless of AMM type, use the MetaRegistry:

metaRegistry.find_pool_for_coins(_from: address, _to: address, i: uint256) → address

Quoting Swap Amounts

On the Pool

Every pool exposes quote functions directly:

// How much `j` will I get for `dx` of `i`?
pool.get_dy(i: int128, j: int128, dx: uint256) → uint256

// How much `i` do I need to get `dy` of `j`?
pool.get_dx(i: int128, j: int128, dy: uint256) → uint256

For metapools, you can also quote swaps through to the underlying base pool tokens:

pool.get_dy_underlying(i: int128, j: int128, dx: uint256) → uint256
pool.get_dx_underlying(i: int128, j: int128, dy: uint256) → uint256
Coin Indices

Stableswap-NG uses int128 for coin indices in swap and quote functions (not uint256). This is a legacy convention. Indices start at 0.

For metapools, coins(0) is the metapool's own coin, and coins(1) is the base pool LP token. When using exchange_underlying(), the indices span all underlying tokens: index 0 is the metapool coin, and indices 1..N map to the base pool's coins.

Via the Views Contract

The Views contract (0xFF53042865dF617de4bB871bD0988E7B93439cCF) provides the same quoting functions but parameterized by pool address. This is useful for off-chain integrations where you want a single contract to query any pool:

views.get_dy(i: int128, j: int128, dx: uint256, pool: address) → uint256
views.get_dx(i: int128, j: int128, dy: uint256, pool: address) → uint256
views.get_dy_underlying(i: int128, j: int128, dx: uint256, pool: address) → uint256
views.get_dx_underlying(i: int128, j: int128, dy: uint256, pool: address) → uint256
views.dynamic_fee(i: int128, j: int128, pool: address) → uint256
views.calc_token_amount(amounts: uint256[], is_deposit: bool, pool: address) → uint256
views.calc_withdraw_one_coin(burn_amount: uint256, i: int128, pool: address) → uint256

The Views contract address is stored in the Factory and can be queried via factory.views_implementation().


Executing Swaps

exchange — Standard Swap

The primary swap function. Requires the caller to have approved the pool to spend the input token.

pool.exchange(
i: int128, // index of input coin
j: int128, // index of output coin
_dx: uint256, // amount of input coin to swap
_min_dy: uint256, // minimum output (slippage protection)
_receiver: address // recipient of output (defaults to msg.sender)
) → uint256 // actual output amount

Flow:

  1. Caller approves pool for _dx of coins(i)
  2. Call exchange()
  3. Pool transfers _dx from caller, sends output to _receiver

exchange_received — Approval-Free Swap

Designed for aggregators and smart contract integrators. Instead of the pool pulling tokens via transferFrom, the caller sends tokens to the pool first, and the pool calculates the swap based on its balance change.

pool.exchange_received(
i: int128, // index of input coin
j: int128, // index of output coin
_dx: uint256, // expected amount of input coin (already sent)
_min_dy: uint256, // minimum output (slippage protection)
_receiver: address // recipient of output (defaults to msg.sender)
) → uint256 // actual output amount

Flow:

  1. Transfer _dx of coins(i) directly to the pool address
  2. Call exchange_received()
  3. Pool detects the balance increase, executes the swap, sends output to _receiver

This saves one ERC-20 approval and is gas-efficient when chaining swaps across multiple protocols (e.g., Uniswap → Curve in a single aggregator route).

warning

exchange_received does not work with rebasing tokens that have fee-on-transfer, as the actual amount received by the pool may differ from _dx. The pool checks balanceOf(self) - stored_balance >= _dx — if the received amount is less due to a transfer fee, the call reverts.

Article

For a deeper dive into exchange_received, including efficiency benefits, security considerations, and practical integration examples, see: How to Do Cheaper, Approval-Free Swaps.

exchange_underlying — Metapool Underlying Swap

Only available on metapools. Swaps directly between the metapool coin and any coin in the underlying base pool, without the caller needing to interact with the base pool separately.

pool.exchange_underlying(
i: int128, // index of input coin (0 = meta coin, 1..N = base pool coins)
j: int128, // index of output coin
_dx: uint256, // amount of input coin
_min_dy: uint256, // minimum output
_receiver: address // recipient (defaults to msg.sender)
) → uint256 // actual output amount

Fees

Stableswap-NG uses dynamic fees that increase when the pool is imbalanced (off-peg). This incentivizes arbitrage to restore the peg.

// Base fee (in 1e10 precision, e.g., 4000000 = 0.04% = 4bps)
pool.fee() → uint256

// Off-peg fee multiplier
pool.offpeg_fee_multiplier() → uint256

// Actual fee for a specific swap pair (accounts for current pool state)
pool.dynamic_fee(i: int128, j: int128) → uint256

The dynamic fee formula scales between the base fee (when balanced) up to fee * offpeg_fee_multiplier / 1e10 (when heavily imbalanced). The fee precision is 1e10, so to get the fee as a percentage: fee / 1e10 * 100.

Admin fees are hardcoded at 50% of trading fees — these go to the protocol (Curve DAO / fee receiver). The remaining 50% accrues to LPs via the pool's virtual price.


Token Handling & Asset Types

Stableswap-NG supports four asset types. This matters for integrators because it affects how token balances and rates are calculated internally:

Asset TypeIDDescriptionRate SourceExamples
Standard0Regular ERC-201e18 (no rate adjustment)USDC, USDT, DAI
Oracle1Token with an exchange rate oracleExternal oracle contractwstETH, cbETH, rETH
Rebasing2Token whose balance changes automaticallybalanceOf() tracked directlystETH
ERC46263Tokenized vault with convertToAssetsconvertToAssets(1e(decimals))sDAI

Query a pool's asset types:

factory.get_pool_asset_types(pool: address) → uint8[]

Query the internal rates used for balancing:

pool.stored_rates() → uint256[]

For oracle-type tokens (type 1), the rate is fetched from an external oracle contract using a method ID specified at pool deployment. This rate is used to normalize token values so the invariant can treat them as equivalent.


Oracles

Each Stableswap-NG pool provides built-in exponential moving average (EMA) oracles. For a full technical deep-dive, see the Stableswap-NG Oracle documentation.

// EMA price oracle for coin i relative to coin 0
pool.price_oracle(i: uint256) → uint256 // 1e18 precision

// Last raw price (spot price from most recent trade)
pool.last_price(i: uint256) → uint256

// Current spot price (calculated from current balances)
pool.get_p(i: uint256) → uint256

// EMA of the D invariant (useful for LP token pricing)
pool.D_oracle() → uint256

The EMA is calculated as: EMA = last_spot × (1 - α) + prev_EMA × α, where α = e^(-Δt / ma_exp_time). The smoothing window ma_exp_time is set at pool deployment (default ~866 seconds for prices). A separate D_ma_time (default ~62,324 seconds) controls the D oracle.

Update behavior:

  • EMA values update at most once per block — multiple swaps in the same block only update the spot price, not the EMA
  • Spot prices (last_price) are capped at 2 × 1e18 before entering the EMA to limit manipulation impact
  • price_oracle() and D_oracle() are triggered by swaps, add_liquidity, remove_liquidity_one_coin, and remove_liquidity_imbalance — but not by balanced remove_liquidity (which doesn't change prices, though D oracle is still updated)
info

The index i for oracle functions uses uint256 (not int128), and represents the coin index relative to coins(0). For a 2-coin pool, price_oracle(0) gives the price of coins(1) in terms of coins(0).


Useful Pool Getters

// Token addresses
pool.coins(i: uint256) → address

// Pool balances
pool.balances(i: uint256) → uint256
pool.get_balances() → uint256[]

// Amplification parameter (controls concentration around peg)
pool.A() → uint256

// Virtual price of LP token (increases monotonically as fees accrue)
pool.get_virtual_price() → uint256

// Total LP token supply (pool itself is the LP token, ERC-20 compliant)
pool.totalSupply() → uint256

// Internal rates (for asset type normalization)
pool.stored_rates() → uint256[]

Deployments & Pool Implementations

Stableswap-NG is deployed across many chains. Select a chain below to view contract addresses and pool implementations.

Implementations have been upgraded over time, but pools are immutable once deployed — a pool keeps the implementation it was deployed with forever. The Factory's set_pool_implementations() only affects newly deployed pools. All implementations share the same ABI interface, so integrators can use a single interface regardless of which implementation a pool uses.

The Factory stores separate implementations for plain pools and metapools (pool_implementations(idx) and metapool_implementations(idx)). Use factory.is_meta(pool) to determine the pool type.

Live Contract & Implementation Explorer

This tool fetches contract addresses and pool implementations directly from the on-chain Factory contract. It queries math_implementation(), views_implementation(), and gauge_implementation() for infrastructure contracts, then scans all deployed pools via pool_list() and get_implementation_address() to discover which implementation each pool uses. All calls are batched via Multicall3 for efficiency. Results are cached in your browser — click Refresh to fetch newly deployed pools. You can optionally provide a custom RPC URL if the default public endpoint is unreliable.