Перейти к основному содержанию

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.

Эта страница переведена с помощью ИИ. За эталон принимается английская версия.Открыть английскую версию →

Почему sqrt-price, а не price

CLMM семейства Uniswap-v3 представляют цену как её квадратный корень, хранящийся в виде фиксированной точки Q64.64:
sqrt_price_x64 = floor(sqrt(price) · 2^64)
Три причины:
  1. Линейная математика ликвидности. Объём token0 или token1 в диапазоне цен оказывается линейной функцией sqrt_price, а не price. Хранение sqrt_price позволяет шагу свопа вычислять эти линейные формулы без вычисления квадратного корня.
  2. Контроль переполнения. Произведение sqrt_price · L умещается в u256 при всех разумных параметрах; price · L может переполниться намного раньше.
  3. Равномерная математика тиков. Поскольку тики определены как 1.0001^i, то sqrt(price) = 1.00005^i также является точной степенью лестницы 1.00005. Каждое пересечение тика переводится в небольшое умножение в пространстве sqrt_price_x64.
Price и sqrt-price находятся в взаимно однозначном соответствии; преобразование: price = (sqrt_price_x64 / 2^64)^2.

Решётка тиков

Цены дискретизируются на сетку:
price(tick_i) = 1.0001^i
tick_i — это i32. Активный диапазон — [MIN_TICK, MAX_TICK] = [−443636, 443636], что даёт диапазон цен примерно [2^−128, 2^128]. Значение tick_spacing каждого пула устанавливается его уровнем комиссии: меньшие значения для узких пар (например, уровень 0.01% для стейблкойнов использует spacing 1), большие значения для волатильных пар (уровень 0.25% использует 60, уровень 1% использует 120). Позиции должны иметь tick_lower и tick_upper, выравненные по tick_spacing. Активные тики пула (те, у которых начинается или заканчивается ликвидность) — единственные тики, которые интересуют шаг свопа.

Преобразование ликвидности в объёмы

Для позиции с ликвидностью L и диапазоном цен [sqrt_lo, sqrt_hi] (все значения в sqrt_price):
Состояние пулаОбъём token0Объём token1
Цена выше диапазона (sqrt_p ≥ sqrt_hi)0L · (sqrt_hi − sqrt_lo)
Цена внутри диапазонаL · (sqrt_hi − sqrt_p) / (sqrt_p · sqrt_hi)L · (sqrt_p − sqrt_lo)
Цена ниже диапазона (sqrt_p ≤ sqrt_lo)L · (sqrt_hi − sqrt_lo) / (sqrt_lo · sqrt_hi)0
Выведение: дифференцируем инвариант CPMM локально. Внутри любого одного диапазона тика позиция ведёт себя как CPMM с виртуальными резервами (x_v, y_v), выбранными так, чтобы текущие (sqrt_p, L) пула были согласованы с L = sqrt(x_v · y_v). Интегрирование от sqrt_p до границы диапазона даёт указанные выше объёмы. Обратные формулы (используются при создании позиции с заданным amount0 или amount1):
L_from_amount0(amount0, sqrt_lo, sqrt_hi, sqrt_p) =
    amount0 · sqrt_p · sqrt_hi / (sqrt_hi − sqrt_p)

L_from_amount1(amount1, sqrt_lo, sqrt_hi, sqrt_p) =
    amount1 / (sqrt_p − sqrt_lo)

// Для симметричного взноса в позицию внутри диапазона берём минимум.
L = min(L_from_amount0, L_from_amount1)

Шаг свопа внутри одного тика

Внутри одного диапазона тика пул ведёт себя как CPMM. При текущем sqrt_p и целевом sqrt_target:
Δamount0_step = L · (sqrt_target − sqrt_p) / (sqrt_p · sqrt_target)     // при свопе на token0
Δamount1_step = L · (sqrt_target − sqrt_p)                              // при свопе на token1

Шаг с точным входом

При заданном Δin_remaining:
// Кандидат на новый sqrt_p если мы заполним до границы тика:
sqrt_after_full = sqrt_target
amount_to_full  = Δamount_in_to_reach(sqrt_p → sqrt_target)

if Δin_remaining ≥ amount_to_full:
    // потребляем остаток бакета
    sqrt_p'         = sqrt_target
    Δin_consumed    = amount_to_full
    Δout            = amount_out_at_boundary
else:
    // не пересекаем; решаем для финального sqrt_p
    sqrt_p'         = L · sqrt_p / (L + Δin_remaining · sqrt_p)      // для свопов 0→1
    Δin_consumed    = Δin_remaining
    Δout            = L · (sqrt_p − sqrt_p')                          // пропорционально Δsqrt
Своп 0→1 снижает sqrt_p (цена падает по мере продажи token0). Своп 1→0 повышает её. Формулы симметричны с перестановкой sqrt_p и sqrt_target.

Шаг с точным выходом

Та же структура, но решаем для Δin вместо Δout.

Цикл свопа между несколькими тиками

Своп итерирует по тикам до истощения входа или достижения лимита цены:
while Δin_remaining > 0 and sqrt_p != sqrt_price_limit:
    next_tick = find_next_initialized_tick(pool.tick_current, direction)
    sqrt_target = min(next_tick.sqrt_price, sqrt_price_limit)       // в направлении

    (Δin, Δout, sqrt_p') = single_step(sqrt_p, sqrt_target, L, Δin_remaining)

    Δin_remaining -= Δin
    accumulated_out += Δout

    if sqrt_p' == next_tick.sqrt_price:
        // пересечение тика
        L += next_tick.liquidity_net * direction_sign
        flip_fee_growth_outside(next_tick)
        match_limit_orders_at_tick(next_tick, ...)        // см. products/clmm/math
        pool.tick_current = next_tick.tick_index
    sqrt_p = sqrt_p'
Каждый single_step использует текущий L пула. L изменяется только при пересечении инициализированного тика. Ликвидность между тиками постоянна, что позволяет получить замкнутую формулу для шага. liquidity_net в тике — это знаковая сумма ликвидностей позиций, начинающихся в этом тике, минус те, что заканчиваются там. При пересечении вверх добавляем liquidity_net; при пересечении вниз вычитаем. Когда в пуле открыты лимитные ордера на тике, шаг пересечения тика также оппортунистически потребляет часть входного объёма свопа для заполнения этих ордеров (FIFO по группам). Алгоритм сопоставления и динамическая комиссия, которая может быть применена поверх базового шага, документированы в products/clmm/math; они не изменяют замкнутые формулы одного шага выше.

Аккумуляторы роста комиссий

CLMM отслеживает комиссии на единицу активной ликвидности, за каждую сторону, глобально и по тикам:
fee_growth_global_0_x64     // Q64.64, монотонно возрастающий
fee_growth_global_1_x64
tick.fee_growth_outside_0_x64   // "комиссии, начисленные, пока этот тик был вне активного диапазона"
tick.fee_growth_outside_1_x64
На каждом single_step:
step_lp_fee = (Δin · fee_rate) · (1 − protocol_fraction − fund_fraction)
fee_growth_global += step_lp_fee · 2^64 / L     // только для входной стороны
(Значение fee_growth_global для другой стороны не меняется на этом шаге, так как никакой токен с той стороны не был выплачен как входной.) При пересечении тика программа меняет fee_growth_outside:
tick.fee_growth_outside_0_x64 = fee_growth_global_0_x64 − tick.fee_growth_outside_0_x64
tick.fee_growth_outside_1_x64 = fee_growth_global_1_x64 − tick.fee_growth_outside_1_x64
“Outside” (вне диапазона) определяется относительно tick_current. Когда tick_current находится выше тика, outside означает “ниже”. Когда tick_current ниже, outside означает “выше”. Смена меняет интерпретацию.

fee_growth_inside для позиции

При заданной позиции [tick_lower, tick_upper] и текущем tick_current:
if tick_current >= tick_upper:
    inside = tick_lower.fee_growth_outside − tick_upper.fee_growth_outside
else if tick_current < tick_lower:
    inside = tick_upper.fee_growth_outside − tick_lower.fee_growth_outside
else:     // позиция внутри диапазона
    inside = fee_growth_global
           − tick_lower.fee_growth_outside
           − tick_upper.fee_growth_outside
Невыплаченные комиссии позиции для стороны токена s:
tokens_owed_s += L · (fee_growth_inside_s − fee_growth_inside_last_s) / 2^64
fee_growth_inside_last_s = fee_growth_inside_s
Это обновление выполняется при каждом взаимодействии с позицией (IncreaseLiquidity, DecreaseLiquidity, CollectFees).

Пример с расчётами — пересечение одного тика

Пул (упрощённо):
  • sqrt_p_x64 = 2^64 · 1.0 = 2^64 (цена = 1.0)
  • L = 1_000_000
  • tick_current = 0
  • Следующий инициализированный тик ниже: tick = −60, sqrt_price = 1.0001^(−30) ≈ 0.99700, liquidity_net = −400_000 (этот тик заканчивает позицию, поэтому нисходящее пересечение удаляет 400k)
  • Процент комиссии: 0.25%
Своп: Δin = 10_000 token0, направление = 0→1. Шаг 1 — до sqrt_target = 0.99700 · 2^64:
amount_in_to_target = L · (1/sqrt_target − 1/sqrt_p)
                    = 1_000_000 · (1/0.99700 − 1/1.0)
                    ≈ 1_000_000 · 0.003009
                    ≈ 3_009
3 009 < 10 000, поэтому заполняем этот шаг полностью:
Δin_step  = 3_009 / (1 − 0.0025)  = 3_017    // с учётом комиссии
Δout_step = L · (sqrt_p − sqrt_target) ≈ 1_000_000 · 0.00299 ≈ 2_990
sqrt_p    = 0.99700 · 2^64
tick_current = −60
L         = 1_000_000 + (−400_000)  = 600_000         // пересекли тик
fee_growth_outside на тике −60 обновляется
Δin_remaining = 10_000 − 3_017 = 6_983
Шаг 2 — с новым L = 600_000: Следующий инициализированный тик (скажем, tick = −120) находится на sqrt = 0.99402. Пересчитываем amount_in_to_target:
amount_in_to_target = 600_000 · (1/0.99402 − 1/0.99700)
                    ≈ 600_000 · 0.003010
                    ≈ 1_806
Всё ещё меньше Δin_remaining. Пересекаем снова. Продолжаем до тех пор, пока Δin_remaining не достигнет нуля. Полная последовательность Δout накапливается в финальный выход свопа.

Инициализация и охрана от переполнения

  • MIN_SQRT_PRICE_X64 и MAX_SQRT_PRICE_X64 соответствуют tick = ±443636. Любой своп, который попытается вытолкнуть sqrt_p за пределы этого диапазона, будет отменён.
  • Параметр sqrt_price_limit пользователя должен находиться в том же интервале; программа проверяет это.
  • Произведения L · Δsqrt вычисляются в u256, а затем сдвигаются обратно в u128 для предотвращения переполнения.

Различия с Uniswap v3

  • Oracle. ObservationState Raydium хранит буфер (block_timestamp, tick_cumulative, seconds_per_liquidity_cumulative) в виде кольца; немного другой формат провода, чем у Uniswap, но такая же математика TWAP.
  • Token-2022. CLMM Raydium поддерживает мелинты Token-2022; вариант с комиссией за передачу требует дополнительных корректировок объёмов до и после свопа. См. algorithms/token-2022-transfer-fees.
  • Bitmap тиков. Raydium упаковывает bitmap инициализированных тиков в [u64; 16] на пул для быстрого find_next_initialized_tick; Uniswap использует on-chain маппинг per-word. Компромисс между размером хранилища и стоимостью поиска.
  • Слоты вознаграждений. Raydium поддерживает 3 потока вознаграждений на пул с отдельными счётчиками reward_growth_global_x64; такая же структура, как у аккумулятора роста комиссий.

Указатели

  • products/clmm/math — on-chain реализация и пример с расчётами с фактическими полями структуры CLMM.
  • products/clmm/ticks-and-positions — решётка тиков, семантика liquidity_net/gross, активного диапазона.
  • products/clmm/fees — аккумулятор роста комиссий в действии.
Источники:
  • Whitepaper Uniswap v3 (каноническое выведение математики sqrt-price).
  • Исходный код программы Raydium CLMM.