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

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.

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

Уровни комиссий

При создании CLMM-пул привязывается к AmmConfig; конфигурация определяет ставку торговой комиссии, доли протокола и фонда, а также шаг тиков (см. products/clmm/ticks-and-positions). Типичные опубликованные уровни (актуальные значения можно проверить через GET https://api-v3.raydium.io/main/clmm-config):
Индекс AmmConfigtrade_fee_rateШаг тиковТипичное применение
0100 (0,01%)1Стейблкоин-пары
1500 (0,05%)10Коррелированные blue-chip активы
22_500 (0,25%)60Стандартные пары
310_000 (1,00%)120Волатильные или низколиквидные пары
Ставка торговой комиссии задаётся в единицах 1/FEE_RATE_DENOMINATOR = 1/1_000_000 от объёма. Ставки протокола и фонда используют тот же знаменатель, но применяются к торговой комиссии, а не к объёму — так же, как в CPMM.

Распределение комиссии на каждом шаге свопа

На каждом шаге свопа (см. products/clmm/math):
step_trade_fee   = ceil(step_input * trade_fee_rate / 1_000_000)
step_protocol    = floor(step_trade_fee * protocol_fee_rate / 1_000_000)
step_fund        = floor(step_trade_fee * fund_fee_rate     / 1_000_000)
step_lp          = step_trade_fee - step_protocol - step_fund
  • step_lp поступает в fee_growth_global_{input_side}_x64, масштабированный по текущей активной ликвидности: fee_growth_global += step_lp × 2^64 / pool.liquidity.
  • step_protocol накапливается в PoolState.protocol_fees_token_{input_side} — извлекается через CollectProtocolFee.
  • step_fund накапливается в PoolState.fund_fees_token_{input_side} — извлекается через CollectFundFee.
Как и в CPMM, доли протокола и фонда хранятся в хранилищах, но исключены из представления ликвидности кривой: математика свопа читает pool.liquidity, который не раздут накопленными, но ещё не изъятыми комиссиями.

Почему комиссии ведутся по каждой стороне отдельно

В отличие от CPMM (где комиссия за своп всегда взимается в токене ввода и другая сторона пула не видит накопления протокола/фонда по данному свопу), в CLMM то же правило действует на каждом шаге: комиссии накапливаются в том токене, который является вводом на данном шаге. Поскольку в многотиковом свопе направление постоянно, все шаги взимают комиссию в одном токене — на практике комиссии конкретного свопа идут на одну сторону. Если пользователь меняет token0 → token1, растёт fee_growth_global_0_x64; fee_growth_global_1_x64 не изменяется. Позиции зарабатывают комиссии в token0 по этому свопу. Следующий своп может пойти в обратном направлении и зачислить fee_growth_global_1_x64. Со временем сбалансированный пул накапливает комиссии с обеих сторон.

Односторонняя комиссия (CollectFeeOn)

Пулы, созданные через CreateCustomizablePool, могут использовать нестандартный режим сбора комиссий. Режим фиксируется при создании пула и хранится в PoolState.fee_on.
Значение CollectFeeOnБайт fee_onПоведение
FromInput (по умолчанию)0Классический Uniswap-V3 — комиссия всегда вычитается из входного токена каждого шага свопа. Входной токен чередуется в зависимости от направления свопа.
Token0Only1Комиссия всегда номинирована в token0. При свопах 0→1 это входной токен (то же, что FromInput). При свопах 1→0 комиссия берётся из выходного токена свопа (token0).
Token1Only2Симметрично Token0Only — комиссия всегда в token1.
Зачем пулу выбирать Token0Only или Token1Only — чтобы дать LP единую предсказуемую валюту начисления. Пары вида MEMECOIN / USDC, где LP номинированы в долларах, выигрывают от Token1Only (комиссии всегда переходят в USDC); P&L LP тогда не зависит от того, какое направление торгов доминирует. Компромисс состоит в том, что при направлениях, где комиссия берётся из выходной стороны свопа, пользователь получает out − fee вместо out − ε от ввода, поэтому логика котировок должна вычитать комиссию из выходной стороны. computeAmountOut в SDK учитывает эту ветку из fee_on; клиентский код, читающий pool.fee_on напрямую, должен повторять вспомогательные функции PoolState:
pool.is_fee_on_input(zero_for_one: bool) -> bool   // true → fee is deducted from input
pool.is_fee_on_token0(zero_for_one: bool) -> bool  // for telemetry / accounting
Эффект на уровне LP — комиссия по-прежнему маршрутизируется через стандартные аккумуляторы fee_growth_global_{0,1}_x64 на каждом шаге свопа, поэтому позиции по-прежнему рассчитывают комиссии по той же формуле fee_growth_inside. Асимметрия касается только направления начисления по сторонам, но не математики. fee_on нельзя изменить после создания пула. Пулы, созданные через устаревший CreatePool, навсегда остаются в режиме FromInput.

Динамическая комиссия

Пулы, созданные с enable_dynamic_fee = true, применяют надбавку на основе волатильности поверх AmmConfig.trade_fee_rate. Механизм представляет собой упрощённый порт дизайна динамической комиссии Trader Joe / Meteora.

Состояние

PoolState.dynamic_fee_info содержит пять калибровочных параметров (снимок DynamicFeeConfig на момент создания пула) и четыре поля состояния, обновляемых при каждом свопе. Разметка байтов описана в products/clmm/accounts.

Обновление при каждом свопе

На каждом шаге свопа программа выполняет три подшага:
  1. Затухание эталона. Если now - last_update_timestamp > filter_period, эталон волатильности затухает:
    if elapsed > decay_period:
        volatility_reference = 0
    elif elapsed > filter_period:
        volatility_reference = volatility_accumulator * reduction_factor / 10_000
    # else: hold the previous reference
    
  2. Обновление аккумулятора. Новый аккумулятор — это эталон плюс абсолютное пройденное расстояние (в единицах tick_spacing), умноженное на масштаб гранулярности, ограниченное настроенным максимумом:
    delta_idx     = abs(tick_spacing_index_reference - current_tick_spacing_index)
    accumulator   = volatility_reference + delta_idx * 10_000   // VOLATILITY_ACCUMULATOR_SCALE
    accumulator   = min(accumulator, max_volatility_accumulator)
    
  3. Вычисление надбавки. Надбавка параболически зависит от аккумулятора (поскольку «тиковое расстояние» свопа возводится в квадрат в канонической формуле), масштабированная по усилению dynamic_fee_control:
    fee_increment_rate = dynamic_fee_control * (accumulator * tick_spacing)^2
                       / (100_000 * 10_000^2)
    fee_rate           = AmmConfig.trade_fee_rate + fee_increment_rate
    fee_rate           = min(fee_rate, 100_000)              // 10% cap
    
Ограничение в 10% (MAX_FEE_RATE_NUMERATOR = 100_000 в единицах 1e6) жёстко задано в качестве предохранителя; на практике хорошо настроенные конфигурации остаются значительно ниже этого порога.

Выбор параметров

Диапазоны по умолчанию, проверенные на пилотных пулах:
ПараметрТипичный диапазонПримечания
filter_period30 – 60 сУдерживает эталон при микроволатильности; меньше = более реактивно
decay_period300 – 1800 сПосле этого окна тишины комиссия возвращается к базовому значению
reduction_factor4_000 – 8_000От 10_000. Выше = повышенная комиссия «прилипает» дольше
dynamic_fee_control1_000 – 50_000От 100_000. Усиление кривой
max_volatility_accumulator100_000 – 10_000_000Ограничивает максимальный размер надбавки
Для калибровки проигрывайте исторические свопы офлайн по формуле, затем подбирайте dynamic_fee_control так, чтобы результирующая средняя комиссия соответствовала цели (например, 1,5× от базовой в дни с волатильностью 1σ, 5× в дни с 3σ).

Что видят LP

Доходы от динамической комиссии проходят через те же аккумуляторы, что и базовая комиссия — fee_growth_global_{0,1}_x64. Отдельного поля «динамический рост комиссий» нет. LP в волатильных пулах просто зарабатывают больше комиссий в периоды волатильности — никаких дополнительных действий или инструкций не требуется.

Что нужно знать интеграторам

  • Комиссия, которую возвращает котировка, может измениться между блоком N и блоком N+1 даже без изменения резервов пула — каждый своп сдвигает аккумулятор волатильности. Котировки Trade API действительны на блок котировки и могут отличаться на несколько bps, если реактивный пул получил своп между котировкой и исполнением.
  • volatility_accumulator и last_update_timestamp публично доступны в блокчейне — клиенты могут воспроизводить формулу на стороне клиента для офлайн-симуляций.

Учёт комиссий по позициям

Каждая позиция хранит на момент последнего обращения к ней:
  • fee_growth_inside_0_last_x64 и fee_growth_inside_1_last_x64 — специфичный для диапазона рост комиссий на момент снимка.
При каждом последующем обращении (IncreaseLiquidity, DecreaseLiquidity и неявно при любом переходе состояния, обновляющем рост комиссий по граничным тикам):
  1. Программа пересчитывает fee_growth_inside_{0,1}_x64 на основе глобального роста комиссий и снимков fee_growth_outside_* двух граничных тиков.
  2. Дельта добавляется к tokens_fees_owed_{0,1}, взвешенной по ликвидности позиции:
    Δ_fee_growth_inside_0 = fee_growth_inside_now_0 - fee_growth_inside_last_0
    tokens_fees_owed_0  += Δ_fee_growth_inside_0 * position.liquidity / 2^64
    
  3. fee_growth_inside_{0,1}_last_x64 обновляется.
Токены физически перемещаются только при DecreaseLiquidity или через выделенный путь CollectFees (в текущем наборе инструкций Raydium комиссии выплачиваются в рамках DecreaseLiquidity). Указать liquidity = 0 в вызове DecreaseLiquidity — это канонический способ «только собрать комиссии».

Позиции вне диапазона ничего не зарабатывают

Если диапазон позиции не содержит tick_current, вычисленный для неё fee_growth_inside ограничен сверху и не меняется, пока цена находится вне диапазона. Позиция прекращает накапливать комиссии до тех пор, пока цена не вернётся в её диапазон. Это не баг, а фича — именно так концентрированная ликвидность концентрирует доходность от комиссий так же, как и капитал.

Потоки вознаграждений

CLMM-пул может иметь до трёх активных потоков вознаграждений одновременно. Каждый поток представляет собой кортеж (reward mint, скорость эмиссии, время начала, время окончания), хранящийся в PoolState.reward_infos[i].
pub struct RewardInfo {
    pub reward_state: u8,               // Uninitialized | Initialized | Open | Ended
    pub open_time: u64,
    pub end_time: u64,
    pub last_update_time: u64,
    pub emissions_per_second_x64: u128, // Q64.64 reward tokens per second
    pub reward_total_emissioned: u64,
    pub reward_claimed: u64,
    pub token_mint:    Pubkey,
    pub token_vault:   Pubkey,
    pub authority:     Pubkey,           // who can SetRewardParams / fund
    pub reward_growth_global_x64: u128,  // accumulator, Q64.64
}

Цикл расчётов

Каждая инструкция, затрагивающая ликвидность (а также UpdateRewardInfos как самостоятельная инструкция), продвигает все активные потоки до now:
for each reward_info with state in {Open, Ended within grace}:
    elapsed         = min(now, end_time) − last_update_time
    if elapsed > 0 && pool.liquidity > 0:
        reward_growth_global_x64 += emissions_per_second_x64 × elapsed × 2^64 / pool.liquidity
        reward_total_emissioned  += emissions_per_second × elapsed
    last_update_time = min(now, end_time)
Если pool.liquidity == 0 на каком-либо интервале, эмиссии за этот интервал не распределяются (это невозможно — нет ликвидности в диапазоне, которой их начислять). Оставшийся бюджет остаётся в хранилище вознаграждений. Протоколы, которые запустили поток и забыли о нём, могут пополнить его или завершить через SetRewardParams.

Начисление вознаграждений по позиции

Аналогично комиссиям, с дополнительным измерением на каждый поток:
for each stream i:
    reward_growth_inside_now_i   = compute_inside_i(pool, tick_lower, tick_upper)
    Δ_i = reward_growth_inside_now_i - personal_position.reward_infos[i].growth_inside_last_x64
    personal_position.reward_infos[i].reward_amount_owed += Δ_i * personal_position.liquidity / 2^64
    personal_position.reward_infos[i].growth_inside_last_x64 = reward_growth_inside_now_i
Пользователи получают вознаграждения через CollectReward, который переводит reward_amount_owed из хранилища потока пользователю и обнуляет счётчик.

Только позиции в диапазоне получают вознаграждения

reward_growth_inside использует ту же формулу, что и fee_growth_inside — через аккумуляторы вне тиков — поэтому позиции за пределами текущего ценового диапазона не накапливают вознаграждения. Это отражает проектное решение Uniswap v3 «стимулы достаются активной ликвидности» и согласует интересы LP с покрытием спотовой цены.

Финансирование и завершение потоков

Поток создаётся через InitializeReward, который заблаговременно вносит весь бюджет (emissions_per_second × (end_time − open_time)) в хранилище вознаграждений потока. Программа отклонит InitializeReward, если баланса финансирующего не хватает. SetRewardParams может продлить end_time или увеличить скорость эмиссии; уменьшение любого из этих значений заблокировано, чтобы исключить rug-pull по уже обещанным LP эмиссиям. Когда now > end_time, поток переходит в состояние Ended, но его reward_growth_global_x64 продолжает читаться — LP могут вызывать CollectReward для исторически заработанных сумм ещё долго после прекращения эмиссий.

Административный сбор комиссий

ПодписантИнструкцияЭффект
amm_config.ownerCollectProtocolFeeПеревод protocol_fees_token_{0,1} получателю.
amm_config.fund_ownerCollectFundFeeПеревод fund_fees_token_{0,1} получателю.
Ни одна из них не двигает кривую — накопленные суммы уже находятся вне pool.liquidity. О том, кто владеет этими подписантами в mainnet, см. security/admin-and-multisig.

Взаимодействие с Token-2022

Комиссии и вознаграждения номинированы в одном из токенов пула или потока. Расширения Token-2022 ведут себя так же, как в CPMM:
  • Transfer fee на входном mint свопа. Пул получает amount_in − mint_transfer_fee. Входные данные шага программы CLMM вычисляются на основе нетто-суммы, поэтому аккумуляторы комиссий пула отражают токены, реально поступившие в хранилище.
  • Transfer fee на выходном mint. Пул отправляет amount_out; пользователь получает amount_out − mint_transfer_fee. Проверки проскальзывания следует выполнять относительно суммы, получаемой пользователем.
  • Transfer fee на reward mint. Эмиссии номинированы в единицах «в хранилище» на момент InitializeReward (финансирующий оплачивает transfer fee mint при пополнении хранилища). Выводы при CollectReward влекут ещё один transfer fee mint; LP должны учитывать небольшой дисконт на токены вознаграждений с transfer fee.
  • Non-transferable / confidential / group-member mints. Отклоняются при CreatePool / InitializeReward.
Совокупный эффект на многошаговый своп с transfer fee может быть существенным. Котировщики, игнорирующие это, будут давать завышенные обещания; справочный расчёт см. в algorithms/token-2022-transfer-fees.

Чтение комиссий и вознаграждений вне блокчейна

const pool = await raydium.clmm.getPoolInfoFromRpc(poolId);
const position = await raydium.clmm.getOwnerPositionInfo({
  wallet: owner.publicKey,
});

for (const p of position) {
  console.log("Position", p.nftMint.toBase58(),
              "range", p.tickLower, "→", p.tickUpper,
              "L", p.liquidity.toString(),
              "fees owed:", p.tokenFeesOwed0.toString(),
              p.tokenFeesOwed1.toString(),
              "rewards owed:", p.rewardInfos.map(r => r.rewardAmountOwed.toString()));
}
tokenFeesOwed* и rewardAmountOwed — это снимки с момента последнего обращения к позиции. Чтобы увидеть актуальные значения (учитывающие рост с тех пор), вызовите IncreaseLiquidity с нулевой ликвидностью в режиме симуляции или пересчитайте вручную, используя глобальные fee_growth_* и снимки двух граничных тиков.

Что изучить дальше

Источники: