Skip to main content

The lookup-table curve

Stable AMM replaces the formula x·y=k with a sparse lookup table of (x, y, price) tuples. When pricing a swap, the program:
  1. Computes the pool’s current ratio from reserves.
  2. Binary searches the table to find the two entries that bracket that ratio.
  3. Linearly interpolates between them to get an intermediate price.
  4. Applies fees and returns the quote.
This approach trades the determinism of a formula for admin flexibility in price shaping, and it’s efficient enough to fit in Solana’s compute budget. The ModelDataInfo holds up to 50,000 DataElement entries, indexed by the admin. Only the first valid_data_count are live. Each entry:
DataElement {
  x: u64,      // X coordinate (coin-side amount, scaled)
  y: u64,      // Y coordinate (pc-side amount, scaled)
  price: u64,  // price = x/y, scaled by multiplier
}
To find a price at the current pool reserves (x_real, y_real):
  1. Compute the ratio: target_ratio = (x_real * multiplier) / y_real.
  2. Binary search for entries where (element.x * multiplier) / element.y brackets target_ratio.
  3. When a bracket [min_idx, max_idx] is found, interpolate.
The program’s binary search code spans ~150 lines in state.rs::ModelDataInfo::get_mininum_range_by_xy_real. The key invariant: entries must be sorted (x ascending, y descending, price ascending) for the search to work.

Linear interpolation

Once two table points bracket the ratio, interpolation computes an intermediate price and reserve pair:
target = (x_real * multiplier) / y_real

[x1, y1, p1] = table[min_idx]
[x2, y2, p2] = table[max_idx]

// Interpolate price
p = p1 + (p2 - p1) * (target - ratio1) / (ratio2 - ratio1)

// Interpolate reserve
x = x1 + (x2 - x1) * (target - ratio1) / (ratio2 - ratio1)
y = y1 + (y2 - y1) * (target - ratio1) / (ratio2 - ratio1)
The result is a piecewise-linear curve that smoothly connects the table points.

Scaling: the multiplier

Pool reserves and prices are stored at different scales. The multiplier field on ModelDataInfo accounts for this. A common pattern:
  • Coin has 6 decimals, PC has 18 decimals.
  • Multiplier = 10^6 (or similar).
  • Table entries are stored at a reduced scale to fit u64 bounds.
The program rescales on read/write via:
real_value = table_value * ratio / multiplier
table_value = real_value * multiplier / ratio

Swap pricing: SwapBaseIn and SwapBaseOut

SwapBaseIn (exact input)

Given input amount amount_in:
  1. Get current ratio from (coin_vault, pc_vault).
  2. Find bracketing table entries and interpolate to get the table-space ratio.
  3. Convert input to table space: dx_table = amount_in * multiplier / ratio.
  4. Query the table at the new X coordinate to find the new Y.
  5. dy_table = y_old - y_new.
  6. Convert back: dy_real = dy_table * ratio / multiplier.
  7. Apply trade fee: dy_output = dy_real - (dy_real * trade_fee_numerator / trade_fee_denominator).
  8. Return dy_output.

SwapBaseOut (exact output)

Symmetric: given desired amount_out, solve for the required amount_in. Both paths read effective reserves directly from the pool vaults. The pool has held no OpenBook open orders for years, so there is nothing to settle first — the vault balances are the whole story. (The 2026-06-22 upgrade removed the leftover market code.)

Fee application

Identical to AMM v4: see products/amm-v4/math for the full derivation.
gross_fee = amount_in * (swap_fee_numerator / swap_fee_denominator)    // e.g., 0.25%
lp_portion = gross_fee - (gross_fee * pnl_numerator / pnl_denominator) // e.g., 0.22%
pnl_portion = gross_fee * (pnl_numerator / pnl_denominator)            // e.g., 0.03%
The pnl_portion goes to need_take_pnl_* and is swept by the admin via WithdrawPnl. The lp_portion stays in the vault, inflating k and benefiting LP token holders.

Pool-asset accounting

The formula historically added the funds the pool held as open orders in its OpenBook OpenOrders account. That term has been zero in practice since the pool stopped posting orders, and the 2026-06-22 upgrade dropped it from the formula entirely, leaving the vault-only calculation:
Old: total assets = vault balances + open-order funds (native_coin_total / native_pc_total) − pending PnL (need_take_pnl)
New: total assets = vault balances − pending PnL (need_take_pnl)
This is the value the curve math treats as effective reserves (the accrued-but-unswept need_take_pnl portion sits physically in the vault but is excluded from pricing). Quoting code and indexers that previously read OpenOrders balances must drop that term.

MonitorStep (removed)

MonitorStep was the crank instruction that settled pending OpenBook fills, recomputed AmmInfo.target_orders, and reposted the limit-order grid derived from the lookup table. The pool stopped posting orders to OpenBook years ago, so the crank had nothing left to do; it was removed in the 2026-06-22 upgrade. Integrators do not need to crank Stable pools.

Summary: why this works

The lookup table + interpolation design is efficient and flexible:
  • Efficiency: Binary search is O(log 50,000) ≈ 16 iterations, each ~ 300–500 CU. Interpolation is a few multiplies/divides. Total quoting cost is ~5k–15k CU, much cheaper than recomputing a formula on every swap.
  • Flexibility: The admin can encode any piecewise-linear curve. Stablecoin pairs get high density around 1:1; collateralized pairs get custom curves.
  • Self-contained liquidity: All funds live in the pool vaults and pricing reads them directly — no crank, no external order book, fewer accounts per transaction.
For deep dives into the interpolation logic, see raydium-stable/program/src/state.rs, methods get_data_by_x, get_data_by_y, get_dy_by_dx_base_in, etc.

Where to go next

  • AccountsModelDataInfo and DataElement field reference.
  • Instructions — the callable set (swap, deposit, withdraw, WithdrawPnl) and the removed instructions.
  • Fees — fee application and WithdrawPnl.
  • products/amm-v4/math — for the OpenBook fee-inclusive order pricing logic.
Sources:
  • raydium-stable/program/src/state.rs (interpolation and binary search implementations)
  • raydium-stable/program/src/math.rs (calculator utilities)