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

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.

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

Инвариант

CPMM поддерживает классический инвариант константного произведения на двух своих хранилищах: xy=kx \cdot y = k где x — это баланс vault0 после всех комиссий передачи Token-2022 при получении, и аналогично для y. Каждый свап должен оставить k' ≥ k после учёта торговых комиссий, зачисленных LP (протокол, фонд и комиссии создателя не учитываются при расчёте k — они находятся в хранилище, но исключаются из представления кривой, см. Комиссии на кривой ниже). Таким образом, k монотонно растёт с течением времени по мере начисления комиссий LP. Цена LP-доли определяется резервами пула, а не k: цена LP в token0=xlpSupply,цена LP в token1=ylpSupply\text{цена LP в token0} = \frac{x}{\text{lpSupply}}, \qquad \text{цена LP в token1} = \frac{y}{\text{lpSupply}} Сжигание ΔLP LP-токенов возвращает ровно ΔLP × x / lpSupply token0 и ΔLP × y / lpSupply token1. При депозите или выводе ни кривая, ни k не движутся — цена меняется только при свопах.

Модель комиссий на пути свопа

CPMM применяет две независимо установленные комиссии на каждом свопе:
  • Торговая комиссия берётся на входе с коэффициентом AmmConfig.trade_fee_rate. Затем она разделяется на доли LP, протокола и фонда (доля LP остаётся в хранилище и увеличивает k; доли протокола и фонда извлекаются из учёта хранилища).
  • Комиссия создателя (активна только когда enable_creator_fee == true) берётся с коэффициентом AmmConfig.creator_fee_rate. Она берётся на входе или выходе в зависимости от PoolState.creator_fee_on и направления свопа (см. /ru/products/cpmm/fees#с-какой-стороны-торговли-берутся-комиссии). Это отдельная корзина — никогда не доля торговой комиссии.
Пусть:
  • FEE_RATE_DENOMINATOR = 1_000_000
  • trade_fee_rate — из AmmConfig, например 2500 = 0,25% от соответствующего объёма
  • creator_fee_rate — из AmmConfig, например 1000 = 0,10% от соответствующего объёма
  • protocol_fee_rate, fund_fee_rate — в единицах 1/FEE_RATE_DENOMINATOR торговой комиссии, а не объёма
Когда комиссия создателя берётся на входе:
total_input_fee = ceil(amount_in * (trade_fee_rate + creator_fee_rate) / FEE_RATE_DENOMINATOR)
creator_fee     = floor(total_input_fee * creator_fee_rate / (trade_fee_rate + creator_fee_rate))
trade_fee       = total_input_fee - creator_fee
amount_in_after_fees = amount_in - total_input_fee
Когда комиссия создателя берётся на выходе:
trade_fee            = ceil(amount_in * trade_fee_rate / FEE_RATE_DENOMINATOR)
amount_in_after_fees = amount_in - trade_fee
amount_out_curve     = curve_output(amount_in_after_fees, ...)
creator_fee          = ceil(amount_out_curve * creator_fee_rate / FEE_RATE_DENOMINATOR)
amount_out           = amount_out_curve - creator_fee
В обоих случаях торговая комиссия разделяется одинаково:
protocol_fee   = floor(trade_fee * protocol_fee_rate / FEE_RATE_DENOMINATOR)
fund_fee       = floor(trade_fee * fund_fee_rate     / FEE_RATE_DENOMINATOR)
lp_fee         = trade_fee - protocol_fee - fund_fee     // creator_fee НЕ вычитается здесь
Сумма protocol_fee + fund_fee + creator_fee хранится в хранилищах, но отслеживается отдельно в состоянии пула (protocol_fees_token*, fund_fees_token*, creator_fees_token*). Когда проверка инварианта константного произведения проверяет k' ≥ k, она использует балансы хранилища минус все три накопленные, но неизвлечённые комиссии — таким образом LP захватывают только lp_fee. См. /ru/products/cpmm/fees для инструкций по сбору и выполненных численных примеров.

SwapBaseInput (вход точен)

“Пользователь дает нам ровно amount_in входного токена и получает как минимум minimum_amount_out выходного токена.” Пока игнорируем Token-2022:
amount_in_after_trade_fee = amount_in - trade_fee
amount_out                = y − (x * y) / (x + amount_in_after_trade_fee)
По алгебре: amount_out=yΔxnetx+Δxnet\text{amount\_out} = \frac{y \cdot \Delta x_{\text{net}}}{x + \Delta x_{\text{net}}} где Δx_net = amount_in_after_trade_fee. Затем программа обновляет учет хранилища так, чтобы часть trade_fee, полагающаяся протоколу/фонду/создателю, находилась в корзинах “накопленных” комиссий (не включаются в следующее x кривой), тогда как доля LP присоединяется к x для следующего свопа.

Token-2022 на входе

Если входной токен имеет расширение transfer-fee, монета вычитает свою комиссию при передаче от пользователя → хранилище. Поэтому хранилище фактически получает amount_in − transfer_fee_in(amount_in). Программа CPMM поэтому вычисляет:
amount_actually_received = amount_in − transfer_fee_in(amount_in)
trade_fee                = ceil(amount_actually_received * trade_fee_rate / FEE_RATE_DENOMINATOR)
amount_in_after_trade_fee = amount_actually_received − trade_fee
и запускает кривую против amount_in_after_trade_fee. Это важно, потому что цена кривой вычисляется исходя из чистой суммы, поступившей в хранилище, а не из объявленной суммы пользователя.

Token-2022 на выходе

Если выходной токен имеет комиссию передачи, пул отправляет amount_out из своего хранилища пользователю. Монета затем снимает свою комиссию в пути, поэтому пользователь получает amount_out − transfer_fee_out(amount_out). Программа вычисляет amount_out из кривой как обычно, но задача интегратора — преобразовать число “отправка из хранилища” пула в число “получение пользователем” при показе котировок.

Проверка скольжения

После вычисления amount_out:
require(amount_out >= minimum_amount_out, "AmountSpecifiedLessThanMinimum")
Если выходной токен берёт комиссию передачи, SDK применяет комиссию передачи перед установкой minimum_amount_out, так чтобы константа скольжения была номинирована в том, что пользователь фактически получит, а не в том, что отправит хранилище.

SwapBaseOutput (выход точен)

“Пользователь получит ровно amount_out выходного токена и готов заплатить до maximum_amount_in входного токена.” Инвертируя кривую для Δx_net: Δxnet=xamount_outyamount_out\Delta x_{\text{net}} = \left\lceil \frac{x \cdot \text{amount\_out}}{y - \text{amount\_out}} \right\rceil Потолок важен — он гарантирует k' ≥ k после целочисленного усечения. Затем:
// Работаем в обратном направлении от чистого входа к брутто входу.
// комиссия применяется на брутто, поэтому:
//   чистое = брутто − ceil(брутто * ставка / D)
//       ≈ брутто * (D − ставка) / D
// инвертируем с потолком в нужных местах:
gross_needed = ceil(Δx_net * D / (D − trade_fee_rate))
На Token-2022 входе оборачиваем:
gross_needed_before_mint_fee
  = inflate_for_transfer_fee(gross_needed, input_mint)
так чтобы пользователь заплатил достаточно, чтобы после вычета комиссии передачи монеты пул получил gross_needed.

Проверка скольжения

require(gross_needed_before_mint_fee <= maximum_amount_in, "AmountSpecifiedExceedsMaximum")

Выполненный пример

Состояние пула, игнорируя Token-2022:
  • x = 1_000_000_000_000 (1 000 000,000000 token0, 6 знаков)
  • y = 2_000_000_000_000 (2 000 000,000000 token1, 6 знаков)
  • AmmConfig: trade_fee_rate = 2500, protocol_fee_rate = 120_000, fund_fee_rate = 40_000, creator_fee_rate = 0
Пользователь: SwapBaseInput с amount_in = 1_000_000_000 (1 000,000000 token0). Комиссия создателя отключена (enable_creator_fee = false).
trade_fee                = ceil(1_000_000_000 * 2500 / 1_000_000)       = 2_500_000
  protocol_fee           = floor(2_500_000 * 120_000 / 1_000_000)       = 300_000
  fund_fee               = floor(2_500_000 *  40_000 / 1_000_000)       = 100_000
  lp_fee                 = 2_500_000 − 300_000 − 100_000                 = 2_100_000
creator_fee              = 0                                              // отключено

amount_in_after_trade_fee = 1_000_000_000 − 2_500_000                    = 997_500_000

amount_out = y − (x * y) / (x + Δx_net)
           = 2_000_000_000_000
             − (1_000_000_000_000 * 2_000_000_000_000)
               / (1_000_000_000_000 + 997_500_000)
           ≈ 1_995_015_009

new_vault0_raw   = x + amount_in                                        = 1_001_000_000_000
new_vault1       = y − amount_out                                       ≈ 1_998_004_984_991

// Из 1_000_000_000 полученных в vault0, 400_000 — это "накопленная комиссия"
// (протокол + фонд), которую кривая должна исключить:
curve_x          = new_vault0_raw − (protocol_fees_token0 + fund_fees_token0)
                 = 1_001_000_000_000 − 400_000
                 = 1_000_999_600_000

k' = curve_x * new_vault1 ≈ 2.000_002_501_E24  ≥  k = 2.0E24   ✓
Если бы у того же пула было enable_creator_fee = true с creator_fee_rate = 1000 (0,10%) на входе, программа взимала бы total_input_fee = ceil(1_000_000_000 * 3500 / 1_000_000) = 3_500_000, затем разделила бы это как creator_fee = 1_000_000 и trade_fee = 2_500_000. Арифметика протокол/фонд/LP на trade_fee не изменяется по сравнению с примером выше — комиссия создателя — это отдельная корзина, накопленная в creator_fees_token0 и исключённая из curve_x вместе с корзинами протокола и фонда. Если входной токен имеет комиссию передачи Token-2022 в размере 1%, хранилище получает 990_000_000 токенов вместо 1_000_000_000, и все последующие вычисления используют эту чистую сумму.

Правило обновления наблюдений

При каждом свопе программа оценивает, нужно ли добавить новое наблюдение в кольцевой буфер:
let since_last = now − observations[head].block_timestamp;
if since_last >= MIN_OBSERVATION_INTERVAL {
    let price0 = (vault1 << 32) / vault0;            // Q32.32-ish
    let price1 = (vault0 << 32) / vault1;
    let head' = (head + 1) % OBSERVATION_NUM;
    observations[head'] = Observation {
        block_timestamp: now,
        cumulative_token0_price_x32:
            observations[head].cumulative_token0_price_x32 + price0 * since_last,
        cumulative_token1_price_x32:
            observations[head].cumulative_token1_price_x32 + price1 * since_last,
    };
    head = head';
}
Два свойства:
  • Кумулятивная цена, не спотовая цена. Одно наблюдение — это не цена. Чтобы получить TWAP за период от t0 до t1, прочитайте наблюдения, ближайшие к каждому концу, и вычислите (cumulative(t1) − cumulative(t0)) / (t1 − t0).
  • Образцы ограничены по частоте. Последовательные свопы в одном слоте могут делить одно наблюдение. Чтение наблюдения сразу после свопа может поэтому выглядеть устаревшим на один слот — это нормально.
Подробнее в /ru/products/clmm/accounts.

Комиссии на кривой

Это тонкая часть, стоящая того, чтобы её выделить. Арифметика кривой работает с чистыми балансами хранилища — то есть с опубликованным балансом SPL минус накопленные комиссии протокола, фонда и создателя (все три — независимые корзины — см. /ru/products/cpmm/fees). Конкретная картина:
raw_vault_balance   = что возвращает RPC getTokenAccountBalance
accrued_fees        = protocol_fees_token{0,1} + fund_fees_token{0,1} + creator_fees_token{0,1}
curve_balance       = raw_vault_balance − accrued_fees
invariant           = curve_balance0 * curve_balance1
Последствия для интеграторов:
  • Не котируйте из сырых балансов. Сначала вычтите поля накопленных комиссий, или вызовите SwapBaseInput как имитацию и возьмите его результат.
  • CollectProtocolFee перемещает токены из хранилища. После сбора raw_vault_balance падает, но curve_balance остаётся неизменным; цена пула не движется. Это намеренно.

Точность и переполнение

  • Вся арифметика кривой использует промежуточные значения u128 для предотвращения переполнения при x * y.
  • Деление округляется к нулю, за исключением SwapBaseOutput’s Δx_net, который округляется вверх, и вычисления комиссии, которое округляется вверх на trade_fee и вниз на подразделения. Эти направления округления выбраны так, чтобы инвариант никогда не уменьшался из-за целочисленного усечения.
  • Пулы с экстремальными соотношениями хранилищ (миллиарды : 1) могут попадать в полы точности при малых торговлях; программа возвращает ZeroTradingTokens в этом случае. См. /ru/reference/error-codes.

Куда дальше

Источники: