Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.raydium.io/llms.txt

Use this file to discover all available pages before exploring further.

This page is operational: it gives the formulas, fixed-point conventions, and step-through used by the CLMM program. For the reasoning behind the concentrated-liquidity curve itself — why L = sqrt(x · y) matters — see algorithms/clmm-math. This page assumes you have read that.

Sqrt-price representation

CLMM stores price as sqrt_price_x64 — the square root of the token1-per-token0 price, as a Q64.64 fixed-point number: sqrt_price_x64=p264\text{sqrt\_price\_x64} = \lfloor \sqrt{p} \cdot 2^{64} \rfloor where p = token1_amount / token0_amount. Working in sqrt instead of p linearizes the swap math (token-amount deltas become linear in Δsqrt_price), and the x64 fixed-point keeps precision through many-tick swaps. Tick ↔ sqrt-price conversion is precomputed via a bit-by-bit log-approximation: sqrt_price_x64(t)264(1.0001)t/2\text{sqrt\_price\_x64}(t) \approx 2^{64} \cdot (1.0001)^{t/2} implemented as a lookup-based exponentiation in tick_math::get_sqrt_price_at_tick.

Liquidity as a canonical unit

Inside a range [sqrt_a, sqrt_b] (with sqrt_a < sqrt_b) a position of liquidity L maps to token amounts as follows. Let sqrt_c = sqrt_price_x64 be the pool’s current price.
Caseamount0amount1
sqrt_c <= sqrt_a (pool price below range)L · (sqrt_b - sqrt_a) / (sqrt_a · sqrt_b)0
sqrt_a < sqrt_c < sqrt_b (in range)L · (sqrt_b - sqrt_c) / (sqrt_c · sqrt_b)L · (sqrt_c - sqrt_a)
sqrt_c >= sqrt_b (pool price above range)0L · (sqrt_b - sqrt_a)
All three identities come from the invariant x = L / sqrt_p, y = L · sqrt_p that concentrated liquidity satisfies within a range. Integrators typically want the inverse: given a deposit of amount0 / amount1, compute the maximum L that fits in the range. The SDK’s LiquidityMath.getLiquidityFromTokenAmounts does this. The formula for the in-range case: L0=amount0sqrt_csqrt_bsqrt_bsqrt_c,L1=amount1sqrt_csqrt_a,L=min(L0,L1)L_0 = \text{amount0} \cdot \frac{\text{sqrt\_c} \cdot \text{sqrt\_b}}{\text{sqrt\_b} - \text{sqrt\_c}}, \qquad L_1 = \frac{\text{amount1}}{\text{sqrt\_c} - \text{sqrt\_a}}, \qquad L = \min(L_0, L_1) Whichever side binds determines the ratio actually consumed; the other side may have leftover.

Single-tick swap step

A swap proceeds in steps. Each step either (a) consumes all available input within the current tick range without crossing a tick, or (b) moves the price exactly to the next initialized tick. Given current state (sqrt_c, L) and a swap up (token0 in, token1 out, sqrt_price increases), the distance to the next initialized tick is sqrt_t. Inside this micro-interval the relationship between input and price is: Δamount0=L(1sqrt_c1sqrt_t)=L(sqrt_tsqrt_c)sqrt_csqrt_t\Delta\text{amount0} = L \cdot \left( \frac{1}{\text{sqrt\_c}} - \frac{1}{\text{sqrt\_t}} \right) = \frac{L \cdot (\text{sqrt\_t} - \text{sqrt\_c})}{\text{sqrt\_c} \cdot \text{sqrt\_t}} and Δamount1=L(sqrt_tsqrt_c)\Delta\text{amount1} = L \cdot (\text{sqrt\_t} - \text{sqrt\_c}) The program does one of two things:
  • Does the entire input fit? If the input remaining (after fee) is less than Δamount0 to reach sqrt_t, solve for the new sqrt_c' exactly: sqrt_c=Lsqrt_cL+Δinputsqrt_c\text{sqrt\_c}' = \frac{L \cdot \text{sqrt\_c}}{L + \Delta\text{input} \cdot \text{sqrt\_c}} (for an exact-input token0 → token1 swap). The swap completes in this step without crossing a tick.
  • Input exceeds Δamount0? Set sqrt_c' = sqrt_t, cross the tick (apply liquidity_net), decrement remaining input by Δamount0, increment output by Δamount1, and repeat.
For the opposite direction (token1 → token0, price going down), the formulas have sqrt_c and sqrt_t swapped and the inversion in the other slot. The full Rust implementation lives in raydium-clmm/programs/amm/src/libraries/swap_math.rs. The logic there matches Uniswap v3’s SwapMath.computeSwapStep one-for-one.

Fees on each step

Trade fees are taken off the input amount in each step, same convention as CPMM:
step_fee_amount  = ceil(step_input * trade_fee_rate / 1_000_000)
step_net_input   = step_input - step_fee_amount
protocol_portion = floor(step_fee_amount * protocol_fee_rate / 1_000_000)
fund_portion     = floor(step_fee_amount * fund_fee_rate     / 1_000_000)
lp_portion       = step_fee_amount - protocol_portion - fund_portion
The LP portion is split across the currently-in-range liquidity by updating the global fee-growth accumulator: fee_growth_globalin+=lp_portion264L\text{fee\_growth\_global}_{\text{in}} \mathrel{+}= \text{lp\_portion} \cdot \frac{2^{64}}{L} — i.e., it is denominated in fees per unit of liquidity, Q64.64, so that a position of size L_i that stayed in range across this swap will later read back L_i · Δfee_growth_global / 2^{64} owed tokens. The protocol and fund portions accrue to PoolState.protocol_fees_token_{0,1} and PoolState.fund_fees_token_{0,1} respectively, identical to CPMM. They are swept by CollectProtocolFee / CollectFundFee.

Fee growth outside and inside

The tricky part of CLMM fee accounting: a position earns fees only while the pool’s price is inside its range. The pool tracks cumulative fees globally; the position needs to know the cumulative fees while inside its specific range. The solution is a tick-based accumulator. Each tick stores:
fee_growth_outside_0_x64
fee_growth_outside_1_x64
At the moment of tick initialization:
  • If the pool’s price is above this tick (tick_current >= this_tick), fee_growth_outside = fee_growth_global. (Everything earned so far is “outside” — i.e., below — this tick, relative to the current price.)
  • Else fee_growth_outside = 0.
When the price crosses a tick, the program flips that tick’s fee_growth_outside: fee_growth_outsidefee_growth_globalfee_growth_outside\text{fee\_growth\_outside} \gets \text{fee\_growth\_global} - \text{fee\_growth\_outside} The invariant this preserves: for any tick t, fee_growth_outside(t) equals the fees that accrued while tick_current was on the opposite side of t. Fee growth inside a range [tick_lower, tick_upper] is then derived:
if tick_current >= tick_upper:
    fee_growth_below = fee_growth_outside(tick_lower)
    fee_growth_above = fee_growth_global - fee_growth_outside(tick_upper)
elif tick_current >= tick_lower:
    fee_growth_below = fee_growth_outside(tick_lower)
    fee_growth_above = fee_growth_outside(tick_upper)
else:
    fee_growth_below = fee_growth_global - fee_growth_outside(tick_lower)
    fee_growth_above = fee_growth_outside(tick_upper)

fee_growth_inside = fee_growth_global - fee_growth_below - fee_growth_above
This is the Uniswap-v3 fee-growth formula, unchanged.

What a position stores and what it reads

A PersonalPositionState stores fee_growth_inside_0_last_x64 and fee_growth_inside_1_last_x64: the fee_growth_inside values at the last time the position was touched. At any subsequent touch (increase, decrease, collect), the program:
  1. Computes the current fee_growth_inside_{0,1}_x64 using the formula above.
  2. Computes Δ = fee_growth_inside_now − fee_growth_inside_last (modular-subtraction on u128).
  3. Adds Δ × position.liquidity / 2^{64} to tokens_fees_owed_{0,1}.
  4. Updates fee_growth_inside_last to the new value.
Tokens actually move out of the vaults only on CollectFees / DecreaseLiquidity, against tokens_fees_owed.

Rewards

Each of the pool’s up to 3 reward streams uses the same growth-inside machinery, in its own reward_growth_global_x64 accumulator. At emission time: reward_growth_global+=emission_per_secondΔt264L\text{reward\_growth\_global} \mathrel{+}= \text{emission\_per\_second} \cdot \Delta t \cdot \frac{2^{64}}{L} — emissions scale inversely with active liquidity, so a denser pool pays each position proportionally less per second, but over more positions total. The per-position reward owed is reward_owed=(reward_growth_insidenowreward_growth_insidelast)L/264\text{reward\_owed} = (\text{reward\_growth\_inside}_{\text{now}} - \text{reward\_growth\_inside}_{\text{last}}) \cdot L / 2^{64} and is claimed via CollectReward. See products/clmm/fees.

Worked example: exact-input swap

Suppose:
  • tick_spacing = 60
  • sqrt_price_x64 = 1 × 2^{64} — price = 1.0, so tick_current = 0.
  • Active liquidity L = 1_000_000 × 2^{64}.
  • Next initialized tick above: t = 60 (sqrt_price_b ≈ 1.003004 × 2^{64}).
  • Trade fee rate: 500 (0.05%).
User: SwapBaseInput exact-input 1,000 token0. Step 1 — fees:
trade_fee       = ceil(1000 * 500 / 1_000_000)  = 1
step_net_input  = 999
Step 2 — does 999 fit within the current tick range?
Δ to next tick (amount0):
  L · (sqrt_t - sqrt_c) / (sqrt_c * sqrt_t)
  ≈ 1_000_000 · (1.003004 − 1) / (1 · 1.003004)
  ≈ 2995.5 token0
999 < 2995.5, so the entire input fits without crossing the tick. Step 3 — new price:
sqrt_c' = L · sqrt_c / (L + Δin · sqrt_c)
        = 1_000_000 · 1 / (1_000_000 + 999 · 1)
        ≈ 0.999001
i.e., sqrt_c' slightly below sqrt_c. Note that the formula above is for a token1 → token0 swap. The example here is token0 → token1, which drives the price up, not down — so we use the corresponding form for token0 in:
sqrt_c' = sqrt_c + Δin / L
        = 1 + 999 / 1_000_000
        = 1.000999
(this matches the expected swap direction for token0 → token1: sqrt_c rises along with the price.) Step 4 — amount out:
Δout token1 = L · (sqrt_c' − sqrt_c)
            = 1_000_000 · 0.000999
            = 999.00
After accounting for rounding, the user receives ≈ 999 token1. The fee (1 token0) is split between LP, protocol, and fund by trade_fee_rate × protocol_fee_rate / 1e6 (and similar for fund); the LP portion flows into fee_growth_global_0_x64.

Limit-order matching during swap

When a swap step crosses a tick that holds open limit orders, those orders consume swap input before the LP curve does, at the tick’s exact price. The matching is FIFO within the tick by order_phase cohort.

Per-cohort state on TickState

order_phase                  : u64    monotonic cohort id
orders_amount                : u64    input-token total in the current (newest) cohort
part_filled_orders_remaining : u64    remaining input of the cohort that swap is currently filling
unfilled_ratio_x64           : u128   Q64.64 fill ratio for the partially-filled cohort
The two-cohort layout exists because new orders may be opened on a tick while an older cohort is still being filled. Newly-opened orders join orders_amount and inherit the next order_phase; they cannot fill until the previous cohort is fully consumed.

Matching step

Pseudo-code for the matching that happens at each tick crossing during a swap:
fn match_limit_orders_at_tick(tick, swap_input_remaining, sqrt_p):
    # 1. Try to fill the partially-filled cohort first.
    if tick.part_filled_orders_remaining > 0:
        consume = min(tick.part_filled_orders_remaining, swap_input_remaining)
        # Update the unfilled-ratio for that cohort.
        tick.unfilled_ratio_x64 *= (1 - consume / tick.part_filled_orders_remaining)
        tick.part_filled_orders_remaining -= consume
        swap_input_remaining -= consume
        if tick.part_filled_orders_remaining == 0:
            tick.unfilled_ratio_x64 = 0
        if swap_input_remaining == 0: return

    # 2. Promote the active cohort.
    if tick.orders_amount > 0:
        tick.part_filled_orders_remaining = tick.orders_amount
        tick.orders_amount = 0
        tick.order_phase += 1
        tick.unfilled_ratio_x64 = ONE_X64
        # Recurse with the freshly-promoted cohort.
        return match_limit_orders_at_tick(tick, swap_input_remaining, sqrt_p)

    return  # tick has no more limit orders
Output tokens going to the limit-order owners are not transferred per swap. They sit virtually in the pool’s output vault until the order owner calls SettleLimitOrder (or DecreaseLimitOrder). The pool simply tracks how much of the cohort is now filled via unfilled_ratio_x64. Each LimitOrderState stores its own (order_phase, unfilled_ratio_x64) snapshot at open time, so settlement reduces to:
filled_amount  = total_amount × (1 − tick_now.unfilled_ratio_x64 / order.unfilled_ratio_x64)
                if tick_now.order_phase > order.order_phase
                else 0
output_amount  = price_at(tick_index) × filled_amount   # adjusted for direction
This O(1) settlement is the whole point of the cohort design — a tick can fill arbitrarily many orders without per-order gas.

Interaction with the LP curve

In a swap step, limit-order matching happens at the tick (zero Δsqrt_price); LP curve consumption happens between ticks. The order is therefore:
  1. Cross tick t_cross (apply LP liquidity_net change first, since this is how Uniswap-V3 does it).
  2. Fill any limit orders sitting at t_cross.
  3. Continue along the LP curve to the next initialized tick or to swap_input exhaustion.
Limit orders thus give traders more effective liquidity at exactly the order’s tick price (a price-improvement effect), at the cost of LPs not earning fees on that portion of the swap volume — the limit-order portion of the trade is fee-free for the swapper, since the limit-order placer is acting as a maker. The dynamic-fee surcharge (if enabled) still applies to the LP portion of the same swap.

Dynamic fee derivation

PoolState.dynamic_fee_info carries the volatility state. Each swap step computes the per-step fee rate as: fee_ratetotal=trade_fee_rateconfig+dynamic_fee_control(vol_acctick_spacing)2DctrlSvol2dynamic surcharge\text{fee\_rate}_{\text{total}} = \text{trade\_fee\_rate}_{\text{config}} + \underbrace{\frac{\text{dynamic\_fee\_control} \cdot (\text{vol\_acc} \cdot \text{tick\_spacing})^2} {D_{\text{ctrl}} \cdot S_{\text{vol}}^2}}_{\text{dynamic surcharge}} where:
  • Dctrl=100,000D_{\text{ctrl}} = 100{,}000DYNAMIC_FEE_CONTROL_DENOMINATOR
  • Svol=10,000S_{\text{vol}} = 10{,}000VOLATILITY_ACCUMULATOR_SCALE
  • vol_acc is the per-swap accumulator after the update rule below
  • tick_spacing is from PoolState.tick_spacing
The result is clamped at 100,000/106=10%100{,}000 / 10^6 = 10\%.

Accumulator update

Two rules are applied each swap, in order: Decay. The reference floor decays based on time since last update: vol_ref={0if Δt>decay_periodvol_accprevreduction_factor10,000if filter_period<Δtdecay_periodvol_refprevif Δtfilter_period\text{vol\_ref} = \begin{cases} 0 & \text{if } \Delta t > \text{decay\_period} \\ \text{vol\_acc}_{\text{prev}} \cdot \dfrac{\text{reduction\_factor}}{10{,}000} & \text{if } \text{filter\_period} < \Delta t \le \text{decay\_period} \\ \text{vol\_ref}_{\text{prev}} & \text{if } \Delta t \le \text{filter\_period} \end{cases} Accumulate. The new accumulator is the reference plus tick-distance traversed since the previous reference index: vol_acc=min(vol_ref+treftnowSvol,max_vol_acc)\text{vol\_acc} = \min\left( \text{vol\_ref} + \left| t_{\text{ref}} - t_{\text{now}} \right| \cdot S_{\text{vol}}, \text{max\_vol\_acc} \right) tick_spacing_index_reference (treft_{\text{ref}}) is in tick-spacing-units, not raw ticks: tref=tick_current/tick_spacingt_{\text{ref}} = \lfloor \text{tick\_current} / \text{tick\_spacing} \rfloor.

Why parabolic in tick distance

Squaring the accumulator means the fee rises as the square of how far price has walked away from its reference point. Empirically this matches the variance scaling of price under random-walk pressure: a 2× tick excursion implies 4× the implied volatility, so charges 4× the surcharge. The dynamic_fee_control parameter calibrates the absolute level. The filter_period window prevents tiny sub-second oscillations (e.g., MEV bots sandwiching) from inflating the accumulator. The decay_period window prevents a single past spike from charging fees indefinitely after the market has calmed.

Numerical robustness

  • All intermediate products go through u128 or u256-shaped arithmetic. CLMM uses U128Sqrt helpers and FullMath::mulDiv patterns directly ported from Uniswap v3.
  • Division rounding is chosen per-step to enforce the invariant k' ≥ k locally. SwapBaseInput rounds output down; SwapBaseOutput rounds input up.
  • Tick crossings that drop PoolState.liquidity to zero are allowed (the price can traverse a “liquidity hole”) but the swap simply advances to the next initialized tick without consuming input, charging no fee.
  • Overflow guard: sqrt_price_x64 is kept in the inclusive range [MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64] corresponding to [MIN_TICK, MAX_TICK]. A swap that would push past either bound reverts with SqrtPriceLimitOverflow.

Where to go next

Sources: