Эта страница переведена с помощью ИИ. За эталон принимается английская версия.Открыть английскую версию →
Эта страница содержит рабочие формулы, соглашения о фиксированной точке и пошаговые процедуры, используемые программой CLMM. Для понимания логики самой кривой концентрированной ликвидности — почему
L = sqrt(x · y) имеет значение — см. algorithms/clmm-math. На этой странице предполагается, что вы уже прочитали тот материал.Представление sqrt-цены
CLMM хранит цену какsqrt_price_x64 — квадратный корень из цены token1-за-token0, в виде числа с фиксированной точкой Q64.64:
где p = token1_amount / token0_amount. Работа с sqrt вместо p линеаризует математику свопа (дельты сумм токенов становятся линейными по Δsqrt_price), а фиксированная точка x64 сохраняет точность при свопах через множество тиков.
Преобразование тик ↔ sqrt-цена предвычисляется через bit-by-bit логарифмическую аппроксимацию:
реализованную как поиск по таблице с возведением в степень в tick_math::get_sqrt_price_at_tick.
Ликвидность как каноническая единица
Внутри диапазона[sqrt_a, sqrt_b] (где sqrt_a < sqrt_b) позиция с ликвидностью L отображается на суммы токенов следующим образом. Пусть sqrt_c = sqrt_price_x64 — текущая цена пула.
Все три тождества вытекают из инварианта
x = L / sqrt_p, y = L · sqrt_p, который концентрированная ликвидность удовлетворяет внутри диапазона.
Интеграторы обычно нуждаются в обратном: дан депозит amount0 / amount1, вычислить максимальное L, которое подходит в диапазон. Метод SDK LiquidityMath.getLiquidityFromTokenAmounts делает это. Формула для случая в диапазоне:
Какая сторона ограничивает, определяет фактическое потребленное соотношение; другая сторона может иметь остаток.
Шаг свопа на одном тике
Своп происходит шагами. Каждый шаг либо (a) потребляет всю доступную входную сумму в текущем диапазоне тика без пересечения тика, либо (b) перемещает цену ровно на следующий инициализированный тик. Дан текущее состояние(sqrt_c, L) и своп вниз (token0 входит, token1 выходит, sqrt_price уменьшается — цена котируется как token1/token0, поэтому добавление token0 её снижает), следующий инициализированный тик ниже находится в sqrt_t < sqrt_c. Внутри этого микроинтервала связь между входной суммой и ценой:
и
Своп с входом token1 — зеркальное отражение: sqrt_price растёт в сторону следующего тика выше, и две формулы меняют, какая конечная точка вычитается. Программа кодирует направление как zero_for_one (true, когда входной mint — это token_mint_0), и своп zero_for_one зажимается в сторону MIN_SQRT_PRICE_X64.
Программа делает одно из двух:
-
Вся входная сумма подходит? Если оставшаяся входная сумма (после комиссии) меньше
Δamount0для достиженияsqrt_t, решить для новойsqrt_c'точно: (для свопа с точной входной суммойtoken0 → token1). Своп завершается на этом шаге без пересечения тика. -
Входная сумма превышает
Δamount0? Установитьsqrt_c' = sqrt_t, пересечь тик (применитьliquidity_net), уменьшить оставшуюся входную сумму наΔamount0, увеличить выходную сумму наΔamount1и повторить.
token1 → token0, цена идёт вниз), формулы имеют sqrt_c и sqrt_t поменянными местами и инверсию в другом слоте.
Полная реализация на Rust находится в raydium-clmm/programs/amm/src/libraries/swap_math.rs. Логика там соответствует SwapMath.computeSwapStep Uniswap v3 один в один.
Комиссии на каждом шаге
Торговые комиссии берутся с входной суммы на каждом шаге, то же соглашение, что и в CPMM:L_i, которая оставалась в диапазоне во время этого свопа, позже прочитает L_i · Δfee_growth_global / 2^{64} причитающихся токенов.
Доли протокола и фонда накапливаются в PoolState.protocol_fees_token_{0,1} и PoolState.fund_fees_token_{0,1} соответственно, идентично CPMM. Они собираются инструкциями CollectProtocolFee / CollectFundFee.
Рост комиссий снаружи и внутри
Сложная часть учёта комиссий CLMM: позиция зарабатывает комиссии только пока цена пула находится внутри её диапазона. Пул отслеживает кумулятивные комиссии глобально; позиция должна знать кумулятивные комиссии пока находится внутри своего конкретного диапазона. Решение — аккумулятор на основе тиков. Каждый тик хранит:- Если цена пула находится выше этого тика (
tick_current >= this_tick),fee_growth_outside = fee_growth_global. (Всё заработанное до сих пор — это “снаружи” — то есть ниже — этого тика, относительно текущей цены.) - Иначе
fee_growth_outside = 0.
fee_growth_outside этого тика:
Инвариант, который это сохраняет: для любого тика t, fee_growth_outside(t) равна комиссиям, которые накопились пока tick_current находился на противоположной стороне t.
Рост комиссий внутри диапазона [tick_lower, tick_upper] затем выводится:
Что хранит позиция и что она читает
PersonalPositionState хранит fee_growth_inside_0_last_x64 и fee_growth_inside_1_last_x64: значения fee_growth_inside в последний раз, когда позиция была затронута.
При любом последующем обращении (увеличение, уменьшение, сбор), программа:
- Вычисляет текущие
fee_growth_inside_{0,1}_x64используя формулу выше. - Вычисляет
Δ = fee_growth_inside_now − fee_growth_inside_last(модульное вычитание на u128). - Добавляет
Δ × position.liquidity / 2^{64}кtokens_fees_owed_{0,1}. - Обновляет
fee_growth_inside_lastна новое значение.
CollectFees / DecreaseLiquidity, против tokens_fees_owed.
Награды
Каждый из до 3 потоков вознаграждений пула использует ту же механику роста-внутри, в своём собственном аккумулятореreward_growth_global_x64. В момент эмиссии:
— эмиссии масштабируются обратно пропорционально активной ликвидности, поэтому более плотный пул платит каждой позиции пропорционально меньше в секунду, но в целом большему числу позиций. Причитающееся вознаграждение на позицию:
и выплачивается DecreaseLiquidity / DecreaseLiquidityV2 — нет отдельной инструкции сбора вознаграждения. См. products/clmm/fees.
Рабочий пример: своп с точной входной суммой
Предположим:tick_spacing = 60sqrt_price_x64 = 1 × 2^{64}— цена = 1.0, поэтомуtick_current = 0.- Активная ликвидность
L = 1_000_000 × 2^{64}. - Следующий инициализированный тик ниже:
t = −60(sqrt_price_b ≈0.997005 × 2^{64}). Своп с входом token0 движется вниз, поэтому тик выше здесь не имеет значения. - Ставка торговой комиссии: 500 (0.05%).
SwapBaseInput с точной входной суммой 1,000 token0.
Шаг 1 — комиссии:
999 < 3004.4, поэтому вся входная сумма подходит без пересечения тика.
Шаг 3 — новая цена:
sqrt_c' приземляется немного ниже sqrt_c, что является правильным направлением: своп token0 → token1 добавляет token0 в пул и поэтому снижает цену token1/token0.
Шаг 4 — выходная сумма:
trade_fee_rate × protocol_fee_rate / 1e6 (и аналогично для фонда); доля LP поступает в fee_growth_global_0_x64.
Сопоставление лимитных ордеров во время свопа
Когда шаг свопа пересекает тик, на котором открыты лимитные ордера, эти ордера потребляют входную сумму свопа перед кривой LP, по точной цене тика. Сопоставление FIFO внутри тика по когортеorder_phase.
Состояние на когорту в TickState
orders_amount и наследуют следующий order_phase; они не могут заполниться, пока предыдущая когорта полностью не потреблена.
Шаг сопоставления
Псевдокод для сопоставления, которое происходит при каждом пересечении тика во время свопа:SettleLimitOrder (или DecreaseLimitOrder). Пул просто отслеживает, насколько заполнена когорта, через unfilled_ratio_x64. Каждый LimitOrderState хранит свой собственный снимок (order_phase, unfilled_ratio_x64) в момент открытия, поэтому расчёт сводится к:
Взаимодействие с кривой LP
В шаге свопа сопоставление лимитных ордеров происходит на тике (нулевойΔsqrt_price); потребление кривой LP происходит между тиками. Порядок поэтому:
- Пересечь тик
t_cross(сначала применить изменение LPliquidity_net, так как это то, как это делает Uniswap-V3). - Заполнить любые лимитные ордера, находящиеся на
t_cross. - Продолжить вдоль кривой LP к следующему инициализированному тику или до исчерпания
swap_input.
Вывод динамической комиссии
PoolState.dynamic_fee_info несёт состояние волатильности. Каждый шаг свопа вычисляет ставку комиссии за шаг как:
где:
- —
DYNAMIC_FEE_CONTROL_DENOMINATOR - —
VOLATILITY_ACCUMULATOR_SCALE vol_acc— аккумулятор за своп после применения правила обновления нижеtick_spacing— изPoolState.tick_spacing
Обновление аккумулятора
Два правила применяются каждый своп, по порядку: Затухание. Опорный уровень затухает на основе времени с момента последнего обновления: Накопление. Новый аккумулятор — это опорный уровень плюс расстояние в тиках, пройденное с момента предыдущего опорного индекса:tick_spacing_index_reference () в единицах интервала тиков, не в сырых тиках: .
Почему парабола в расстоянии тиков
Возведение аккумулятора в квадрат означает, что комиссия растёт как квадрат того, насколько далеко цена прошла от своей опорной точки. Эмпирически это соответствует масштабированию дисперсии цены при случайном блуждании: 2× отклонение в тиках подразумевает 4× подразумеваемую волатильность, поэтому взимает 4× надбавку. Параметрdynamic_fee_control калибрует абсолютный уровень.
Окно filter_period предотвращает крошечные колебания в доли секунды (например, MEV-боты с сэндвичами) от раздувания аккумулятора. Окно decay_period предотвращает один прошлый скачок от взимания комиссий бесконечно долго после того, как рынок успокоился.
Численная устойчивость
- Все промежуточные произведения проходят через арифметику
u128илиu256. CLMM использует помощникиU128Sqrtи паттерныFullMath::mulDivнапрямую перенесённые из Uniswap v3. - Округление деления выбирается за шаг для обеспечения инварианта
k' ≥ kлокально.SwapBaseInputокругляет выход вниз;SwapBaseOutputокругляет вход вверх. - Пересечения тиков, которые снижают
PoolState.liquidityдо нуля, разрешены (цена может пройти через “яму ликвидности”), но своп просто переходит к следующему инициализированному тику без потребления входной суммы, не взимая комиссию. - Защита от переполнения:
sqrt_price_x64сохраняется в включающем диапазоне[MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64], соответствующем[MIN_TICK, MAX_TICK]. Своп, который попытался бы выйти за любую границу, откатывается сSqrtPriceLimitOverflow.
Что дальше
products/clmm/ticks-and-positionsдля того, как карта тиков участвует в обходе.products/clmm/feesдля подробного разбора математики комиссий/вознаграждений.algorithms/clmm-mathдля выводов заL = sqrt(x · y)и формулами диапазон-vs-ликвидность.
raydium-io/raydium-clmm—libraries/swap_math.rs,libraries/tick_math.rs- Whitepaper “Uniswap v3 Core”, §6–7

