Skip to main content
本頁內容由 AI 自動翻譯,所有內容以英文版本為準。查看英文版 →

Sqrt 價格表示

CLMM 將價格儲存為 sqrt_price_x64 — token1 對 token0 價格的平方根,表示為 Q64.64 定點數: sqrt_price_x64=⌊p⋅264⌋\text{sqrt\_price\_x64} = \lfloor \sqrt{p} \cdot 2^{64} \rfloor 其中 p = token1_amount / token0_amount。使用 sqrt 而非 p 可以線性化交換數學(代幣數量變化與 Δsqrt_price 呈線性關係),而 x64 定點表示在多 tick 交換中保持精度。 Tick ↔ sqrt 價格轉換透過 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 為池的當前價格。 這三個恆等式都來自於集中流動性在範圍內滿足的不變量 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) 哪一側受限決定了實際消耗的比例;另一側可能有剩餘。

單一 tick 交換步驟

交換以步驟進行。每個步驟要麼 (a) 消耗當前 tick 範圍內的所有可用輸入而不跨越 tick,要麼 (b) 將價格精確移動到下一個已初始化的 tick。 給定當前狀態 (sqrt_c, L) 和向下交換(token0 輸入、token1 輸出、sqrt_price 下降 — 價格以 token1/token0 報價,所以增加 token0 會降低它),下方的下一個已初始化 tick 位於 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 上升朝向上方的下一個 tick,兩個公式中哪個端點被減去會互換。程式將方向編碼為 zero_for_one(當輸入 mint 為 token_mint_0 時為 true),而 zero_for_one 交換會限制在 MIN_SQRT_PRICE_X64。 程式執行以下兩項之一:
  • 整個輸入是否適合? 如果剩餘輸入(扣除費用後)小於到達 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 交換)。交換在此步驟完成而不跨越 tick。
  • 輸入超過 Δamount0? 設定 sqrt_c' = sqrt_t,跨越 tick(應用 liquidity_net),剩餘輸入減少 Δamount0,輸出增加 Δamount1,然後重複。
對於相反方向(token1 → token0,價格下降),公式中 sqrt_c 和 sqrt_t 互換,反演在另一個位置。 完整的 Rust 實現位於 raydium-clmm/programs/amm/src/libraries/swap_math.rs。那裡的邏輯與 Uniswap v3 的 SwapMath.computeSwapStep 一一對應。

每個步驟的費用

交易費用從每個步驟的輸入金額中扣除,與 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 的累加器。每個 tick 儲存:
在 tick 初始化時刻:
  • 如果池的價格高於此 tick(tick_current >= this_tick),fee_growth_outside = fee_growth_global。(迄今為止賺取的所有費用相對於當前價格是「外部」— 即低於 — 此 tick。)
  • 否則 fee_growth_outside = 0。
當價格跨越 tick 時,程式翻轉該 tick 的 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} 此不變量保留:對於任何 tick 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} — 發放與活躍流動性成反比,因此更密集的池對每個頭寸每秒支付的比例更少,但總共跨越更多頭寸。每個頭寸欠付的獎勵為 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 支付 — 沒有獨立的收集獎勵指令。見 /zh-Hant/products/clmm/fees。

實例:精確輸入交換

假設:
  • tick_spacing = 60
  • sqrt_price_x64 = 1 × 2^{64} — 價格 = 1.0,所以 tick_current = 0。
  • 活躍流動性 L = 1_000_000 × 2^{64}。
  • 下方的下一個已初始化 tick:t = −60(sqrt_price_b ≈ 0.997005 × 2^{64})。token0 輸入交換向下移動,所以上方的 tick 在此無關。
  • 交易費率:500(0.05%)。
使用者:SwapBaseInput 精確輸入 1,000 token0。 步驟 1 — 費用:
步驟 2 — 999 是否適合當前 tick 範圍內?
999 < 3004.4,所以整個輸入適合而不跨越 tick。 步驟 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。

交換期間的限價單匹配

當交換步驟跨越持有開放限價單的 tick 時,這些訂單在 LP 曲線之前以 tick 的精確價格消耗交換輸入。匹配在 tick 內按 order_phase 隊列 FIFO。

TickState 上的每隊列狀態

兩隊列佈局存在是因為新訂單可能在舊隊列仍在填充時在 tick 上開啟。新開啟的訂單加入 orders_amount 並繼承下一個 order_phase;它們在前一個隊列完全消耗之前無法填充。

匹配步驟

在交換期間每次 tick 跨越時發生的匹配的虛擬代碼:
流向限價單所有者的輸出代幣不會在每次交換時轉移。它們虛擬地位於池的輸出金庫中,直到訂單所有者呼叫 SettleLimitOrder(或 DecreaseLimitOrder)。池只是透過 unfilled_ratio_x64 追蹤隊列現在填充了多少。每個 LimitOrderState 在開啟時儲存其自己的 (order_phase, unfilled_ratio_x64) 快照,因此結算簡化為:
這個 O(1) 結算是整個隊列設計的要點 — tick 可以填充任意多個訂單而不需要按訂單計算 gas。

與 LP 曲線的互動

在交換步驟中,限價單匹配發生在 tick 處(零 Δsqrt_price);LP 曲線消耗發生在 tick 之間。因此順序為:
  1. 跨越 tick t_cross(首先應用 LP liquidity_net 變化,因為這是 Uniswap-V3 的做法)。
  2. 填充位於 t_cross 的任何限價單。
  3. 沿著 LP 曲線繼續到下一個已初始化的 tick 或 swap_input 耗盡。
限價單因此為交易者在精確的訂單 tick 價格處提供更多有效流動性(價格改善效果),代價是 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\%。

累加器更新

每次交換按順序應用兩個規則: 衰減。 參考底線根據自上次更新以來的時間衰減: 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} 累積。 新累加器是參考加上自前一個參考索引以來遍歷的 tick 距離: 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}})以 tick 間距單位表示,不是原始 tick:tref=⌊tick_current/tick_spacing⌋t_{\text{ref}} = \lfloor \text{tick\_current} / \text{tick\_spacing} \rfloor。

為什麼在 tick 距離上是拋物線

平方累加器意味著費用隨著價格從其參考點走開的距離的平方上升。根據經驗,這與隨機遊走壓力下的價格方差縮放相匹配:2× tick 偏移意味著 4× 隱含波動率,因此收取 4× 附加費。dynamic_fee_control 參數校準絕對水平。 filter_period 窗口防止微小的亞秒級振盪(例如 MEV 機器人三明治攻擊)膨脹累加器。decay_period 窗口防止單一過去的尖峰在市場平靜後無限期地收取費用。

數值穩健性

  • 所有中間乘積都通過 u128 或 u256 形狀的算術。CLMM 直接使用 U128Sqrt 幫助程序和 FullMath::mulDiv 模式,從 Uniswap v3 移植。
  • 除法四捨五入按步驟選擇以在本地強制不變量 k' ≥ k。SwapBaseInput 將輸出向下四捨五入;SwapBaseOutput 將輸入向上四捨五入。
  • 將 PoolState.liquidity 降至零的 tick 跨越是允許的(價格可以遍歷「流動性洞」),但交換只是推進到下一個已初始化的 tick 而不消耗輸入,不收取費用。
  • 溢出防護:sqrt_price_x64 保持在包含範圍 [MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64] 內,對應於 [MIN_TICK, MAX_TICK]。會將價格推過任一邊界的交換以 SqrtPriceLimitOverflow 還原。

後續步驟

來源: