Skip to main content
このページは AI による自動翻訳です。すべての内容は英語版を正とします。英語版を表示 →
このページは運用ドキュメントです。CLMM プログラムで使用される公式、固定小数点規約、ステップスルーを示します。集中流動性曲線そのものの背景にある理由(なぜ L = sqrt(x · y) が重要なのか)については、algorithms/clmm-math を参照してください。このページは、そちらを読んでいることを前提としています。

Sqrt-price 表現

CLMM は価格を sqrt_price_x64 として保存します。これは token1-per-token0 価格の平方根を Q64.64 固定小数点数で表したものです: sqrt_price_x64=⌊p⋅264⌋\text{sqrt\_price\_x64} = \lfloor \sqrt{p} \cdot 2^{64} \rfloor ここで p = token1_amount / token0_amount です。p の代わりに sqrt で計算することで、スワップ数学が線形化されます(トークン量のデルタが Δsqrt_price に対して線形になります)。また x64 固定小数点により、多くのティックにわたるスワップでも精度が保たれます。 ティック ↔ sqrt-price 変換は bit-by-bit ログ近似を使用して事前計算されます: sqrt_price_x64(t)≈264⋅(1.0001)t/2\text{sqrt\_price\_x64}(t) \approx 2^{64} \cdot (1.0001)^{t/2} これは tick_math::get_sqrt_price_at_tick でルックアップベースの指数計算として実装されています。

リクイディティを正規単位として

範囲 [sqrt_a, sqrt_b](sqrt_a < sqrt_b)内のポジションで、リクイディティ L はトークン量に次のようにマップされます。sqrt_c = sqrt_price_x64 をプールの現在の価格とします。 3 つの恒等式すべては、集中流動性が範囲内で満たす不変式 x = L / sqrt_p、y = L · sqrt_p から導出されます。 インテグレーターは通常、逆を求めます。amount0 / amount1 のデポジットが与えられたとき、範囲に収まる最大の L を計算します。SDK の LiquidityMath.getLiquidityFromTokenAmounts がこれを行います。範囲内ケースの公式は: L0=amount0⋅sqrt_c⋅sqrt_bsqrt_b−sqrt_c,L1=amount1sqrt_c−sqrt_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) どちらかが制約になるかで、実際に消費される比率が決まります。もう一方は余剰が残る可能性があります。

単一ティック スワップ ステップ

スワップは ステップ で進行します。各ステップは (a) 現在のティック範囲内で利用可能なすべての入力を消費してティックを越えないか、(b) 価格を次の初期化されたティックに正確に移動させます。 現在の状態 (sqrt_c, L) と 下向き スワップ(token0 入力、token1 出力、sqrt_price 減少 — 価格は token1/token0 として引用されるため、token0 を追加すると価格が低下します)が与えられたとき、次の初期化されたティックは sqrt_t < sqrt_c に位置します。このマイクロ区間内で、入力と価格の関係は: Δamount0=L⋅(1sqrt_t−1sqrt_c)=L⋅(sqrt_c−sqrt_t)sqrt_c⋅sqrt_t\Delta\text{amount0} = L \cdot \left( \frac{1}{\text{sqrt\_t}} - \frac{1}{\text{sqrt\_c}} \right) = \frac{L \cdot (\text{sqrt\_c} - \text{sqrt\_t})}{\text{sqrt\_c} \cdot \text{sqrt\_t}} および Δamount1=L⋅(sqrt_c−sqrt_t)\Delta\text{amount1} = L \cdot (\text{sqrt\_c} - \text{sqrt\_t}) token1 入力スワップはミラーイメージです。sqrt_price は次のティック上に向かって上昇し、2 つの公式はどちらのエンドポイントが減算されるかが入れ替わります。プログラムは方向を zero_for_one(入力ミントが token_mint_0 の場合は true)としてエンコードし、zero_for_one スワップは MIN_SQRT_PRICE_X64 に向かってクランプされます。 プログラムは次の 2 つのいずれかを実行します:
  • 入力全体が収まるか? フィー後の残りの入力が sqrt_t に到達するための Δamount0 より少ない場合、新しい sqrt_c' を正確に解きます: sqrt_c′=L⋅sqrt_cL+Δinput⋅sqrt_c\text{sqrt\_c}' = \frac{L \cdot \text{sqrt\_c}}{L + \Delta\text{input} \cdot \text{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 にあります。そこのロジックは Uniswap v3 の SwapMath.computeSwapStep と 1 対 1 で一致します。

各ステップのフィー

トレードフィーは各ステップの 入力 量から取られます。CPMM と同じ規約です:
LP ポーション は、グローバル フィー成長アキュムレーターを更新することで、現在範囲内のリクイディティ全体に分割されます: fee_growth_globalin+=lp_portion⋅264L\text{fee\_growth\_global}_{\text{in}} \mathrel{+}= \text{lp\_portion} \cdot \frac{2^{64}}{L} つまり、リクイディティ単位あたりのフィー として表示され、Q64.64 です。そのため、このスワップ全体で範囲内にとどまったサイズ 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 を 反転 します: fee_growth_outside←fee_growth_global−fee_growth_outside\text{fee\_growth\_outside} \gets \text{fee\_growth\_global} - \text{fee\_growth\_outside} これが保持する不変式:任意のティック t に対して、fee_growth_outside(t) は tick_current が t の反対側にあった間に発生したフィーに等しい。 範囲 [tick_lower, tick_upper] 内のフィー成長 は次のように導出されます:
これは Uniswap-v3 フィー成長公式で、変更されていません。

ポジションが保存するもの、読み取るもの

PersonalPositionState は fee_growth_inside_0_last_x64 と fee_growth_inside_1_last_x64 を保存します。これらはポジションが最後にタッチされたときの fee_growth_inside 値です。 その後のタッチ(増加、減少、収集)では、プログラムは:
  1. 上記の公式を使用して現在の fee_growth_inside_{0,1}_x64 を計算します。
  2. Δ = fee_growth_inside_now − fee_growth_inside_last(u128 上のモジュラー減算)を計算します。
  3. Δ × position.liquidity / 2^{64} を tokens_fees_owed_{0,1} に追加します。
  4. fee_growth_inside_last を新しい値に更新します。
トークンは実際には CollectFees / DecreaseLiquidity でのみ、tokens_fees_owed に対してボールトから移動します。

リワード

プールの最大 3 つのリワード ストリームは、それぞれ独自の reward_growth_global_x64 アキュムレーターで同じ成長内部メカニズムを使用します。エミッション時: reward_growth_global+=emission_per_second⋅Δt⋅264L\text{reward\_growth\_global} \mathrel{+}= \text{emission\_per\_second} \cdot \Delta t \cdot \frac{2^{64}}{L} — エミッションはアクティブ リクイディティに反比例してスケーリングされるため、より密度の高いプールは各ポジションに 1 秒あたり比例して少なく支払いますが、合計でより多くのポジションに支払います。ポジションあたりのリワード支払いは reward_owed=(reward_growth_insidenow−reward_growth_insidelast)⋅L/264\text{reward\_owed} = (\text{reward\_growth\_inside}_{\text{now}} - \text{reward\_growth\_inside}_{\text{last}}) \cdot L / 2^{64} で、DecreaseLiquidity / DecreaseLiquidityV2 によって支払われます。スタンドアロンの収集リワード命令はありません。products/clmm/fees を参照してください。

実例:正確入力スワップ

次を想定します:
  • tick_spacing = 60
  • sqrt_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 — フィー:
ステップ 2 — 999 は現在のティック範囲内に収まるか?
999 < 3004.4 なので、入力全体はティックを越えずに収まります。 ステップ 3 — 新しい価格:
つまり sqrt_c' は sqrt_c より わずかに下 に着地します。これは正しい方向です。token0 → token1 スワップはプールに token0 を追加するため、token1/token0 価格を低下させます。 ステップ 4 — 出力量:
丸めを考慮した後、ユーザーは ≈ 998 token1 を受け取ります。フィー(1 token0)は trade_fee_rate × protocol_fee_rate / 1e6(ファンドも同様)によって LP、プロトコル、ファンド間で分割されます。LP ポーションは fee_growth_global_0_x64 に流れます。

スワップ中のリミットオーダー マッチング

スワップ ステップがオープン リミットオーダーを保持するティックを越えるとき、これらのオーダーはティックの正確な価格で LP 曲線 前に スワップ入力を消費します。マッチングは order_phase コホート内でティック内で FIFO です。

TickState のコホートごとの状態

2 コホート レイアウトが存在する理由は、古いコホートがまだ埋められている 間に 新しいオーダーがティックで開かれる可能性があるためです。新しく開かれたオーダーは orders_amount に参加し、次の order_phase を継承します。前のコホートが完全に消費されるまで埋めることはできません。

マッチング ステップ

スワップ中の各ティック交差で発生するマッチングの疑似コード:
リミットオーダー所有者に向かう出力トークンはスワップごとに転送 されません。スワップ入力が完全に消費されるまで、プールの出力ボールトに仮想的に座ります。プールは単に unfilled_ratio_x64 を介してコホートがどの程度埋められたかを追跡します。各 LimitOrderState はオープン時に独自の (order_phase, unfilled_ratio_x64) スナップショットを保存するため、決済は以下に帰着します:
この O(1) 決済がコホート設計の全体的なポイントです。ティックは任意の数のオーダーを、オーダーごとのガスなしで埋めることができます。

LP 曲線との相互作用

スワップ ステップでは、リミットオーダー マッチングはティック で 発生します(ゼロ Δsqrt_price)。LP 曲線消費はティック 間 で発生します。したがって順序は:
  1. ティック t_cross を越えます(LP liquidity_net 変更を最初に適用します。これは Uniswap-V3 がどのように行うかです)。
  2. t_cross に座っているリミットオーダーを埋めます。
  3. LP 曲線に沿って次の初期化されたティックまたは swap_input 枯渇まで続行します。
リミットオーダーは、トレーダーに正確にオーダーのティック価格で より多くの 有効リクイディティを与えます(価格改善効果)。ただし、LP はそのスワップ量の部分に対してフィーを獲得しません。リミットオーダー部分のトレードはスワッパーにとってフィーなしです。リミットオーダー配置者がメーカーとして機能しているためです。動的フィー サーチャージ(有効な場合)は、同じスワップの LP 部分に引き続き適用されます。

動的フィー導出

PoolState.dynamic_fee_info はボラティリティ状態を保持します。各スワップ ステップは、ステップごとのフィー レートを次のように計算します: fee_ratetotal=trade_fee_rateconfig+dynamic_fee_control⋅(vol_acc⋅tick_spacing)2Dctrl⋅Svol2⏟動的サーチャージ\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{動的サーチャージ}} ここで:
  • Dctrl=100,000D_{\text{ctrl}} = 100{,}000 — DYNAMIC_FEE_CONTROL_DENOMINATOR
  • Svol=10,000S_{\text{vol}} = 10{,}000 — VOLATILITY_ACCUMULATOR_SCALE
  • vol_acc は以下の更新ルール後のスワップごとのアキュムレーター
  • tick_spacing は PoolState.tick_spacing から
結果は 100,000/106=10%100{,}000 / 10^6 = 10\% でクランプされます。

アキュムレーター更新

2 つのルールが各スワップで順番に適用されます: 減衰。 参照フロアは最後の更新からの経過時間に基づいて減衰します: vol_ref={0if Δt>decay_periodvol_accprev⋅reduction_factor10,000if filter_period<Δt≤decay_periodvol_refprevif Δt≤filter_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} 蓄積。 新しいアキュムレーターは参照プラス前の参照インデックス以降に走査されたティック距離です: vol_acc=min⁡(vol_ref+∣tref−tnow∣⋅Svol,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}})はティック間隔単位です。生のティックではありません:tref=⌊tick_current/tick_spacing⌋t_{\text{ref}} = \lfloor \text{tick\_current} / \text{tick\_spacing} \rfloor。

ティック距離で放物線である理由

アキュムレーターを二乗することは、フィーが価格が参照ポイントから歩いた距離の 二乗 として上昇することを意味します。経験的には、これはランダムウォーク圧力下での価格の分散スケーリングと一致します。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 で戻ります。

次に進む場所

ソース: