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

為什麼需要 ticks

CLMM 的流動性集中在價格範圍內。為了在鏈上使範圍易於處理,價格被量化為整數 ticks,其中每個 tick 是前一個的常數倍數: price(i)=1.0001i\text{price}(i) = 1.0001^{\,i} 一個 tick 對應 0.01% 的價格變動,或約 1 個基點。映射關係如下: MIN_TICKMAX_TICK 的選擇確保 sqrt_price_x64 在兩端都能適配 u128。每個 pool 都強制 tick_lower >= MIN_TICKtick_upper <= MAX_TICK。實際上,web UI 會將範圍限制在更窄的範圍內,以防止使用者將流動性鎖定在無法到達的 ticks。

Tick 間距

pool 的 AmmConfig 固定了一個 tick 間距 — position 唯一允許用作端點的 ticks。如果 tick_spacing = 60,只有 ticks …, −120, −60, 0, 60, 120, … 是有效的。嘗試開啟端點為 31 的 position 會因 InvalidTickIndex 而回滾。 常見的已發佈間距: 間距越粗,需要初始化的 tick 陣列越少,開啟寬 position 的成本越低,價格邊界越模糊。波動性交易對通常位於 120 間距層級;穩定幣位於 1 間距層級。

Tick 陣列

pool 不會在單獨的帳戶中儲存每個 tick 的狀態。相反,TICK_ARRAY_SIZE 個相鄰的 ticks(當前 Raydium CLMM 中為 60 個)被打包到單個 TickArrayState 中。陣列的第一個 tick 是其 start_tick_index,它覆蓋恰好 TICK_ARRAY_SIZE * tick_spacing 個整數 tick 單位。 對於 tick_spacing = 60TICK_ARRAY_SIZE = 60
  • 每個 tick 陣列跨越 60 × 60 = 3600 個整數 ticks。
  • start_tick_index 是 3600 的倍數:…, -7200, -3600, 0, 3600, 7200, …
tick_spacing = 60 時,position 端點 t = 2040 位於 start_tick_index = 0 的 tick 陣列中。position 端點 t = 4200 位於 start_tick_index = 3600 的陣列中。

何時建立陣列

tick 陣列是延遲初始化的:第一個引用其中任何 tick 的 position 會初始化該陣列並支付租金。Swaps 不會初始化 tick 陣列 — 它們使用位圖跳過未初始化的陣列。SDK 的 open-position 流程會檢查選定的範圍,計算它接觸的 tick 陣列列表,並在同一交易中與 OpenPosition 一起添加 init_tick_array 指令(如果有任何缺失)。

Tick 陣列不會被關閉

一旦 tick 陣列被初始化,它就會在 pool 的整個生命週期內持續存在。程式不會公開關閉 tick 陣列的路徑,即使 initialized_tick_count 回到零也不會。tick 陣列沒有租金恢復;第一個接觸陣列的 position 支付的租金會永久鎖定在該帳戶中。這是一個刻意的權衡:重複使用現有 tick 陣列對每個後續 position 都是免費的,因此一個交易量大的 pool 只需為每個 (pool, start_tick_index) 槽位支付一次租金,無論流動性變動如何。

位圖

找到「當前 tick 左/右的下一個已初始化 tick」必須很快 — swap 可能會跨越許多 ticks。pool 在 PoolState 中為 tick 0 周圍 ±1,024 個陣列的範圍內存儲 1 位每 tick 陣列的位圖。在該範圍之外(全範圍 positions、異常設置),TickArrayBitmapExtension 提供溢出。 swap 遍歷位圖:lowest_set_bit_above(tick_current_array_index) 給出 swap 正在跨越的一側的下一個具有已初始化 tick 的陣列。在該陣列內,類似的位掃描定位下一個已初始化的 tick。

liquidity_grossliquidity_net

每個已初始化的 tick 儲存兩個流動性值:
  • liquidity_gross — 所有將此 tick 作為任一端點引用的 positions 的 L 之和。當 liquidity_gross 達到零時,tick 變為未初始化,可以從位圖中移除。
  • liquidity_net — 當價格向上跨越此 tick(在 tick 空間中從左到右)時,pool 級別 liquidity帶符號變化。如果此 tick 是大小為 L 的 position 的下界,它貢獻 +L;如果它是該 position 的上界,它貢獻 −L
實例:同一 pool 上的兩個 positions。
  • Position A:tick_lower = -120tick_upper = 0,流動性 L_A = 100
  • Position B:tick_lower = -60tick_upper = 60,流動性 L_B = 50
逐 tick 狀態: 不同 tick_current 值的 pool 級別 liquidity
  • tick_current = -180liquidity = 0(任何 position 之前)
  • tick_current = -90liquidity = 100(僅在 A 內)
  • tick_current = -30liquidity = 150(在 A 和 B 內)
  • tick_current = 30liquidity = 50(僅在 B 內)
  • tick_current = 90liquidity = 0(超過兩者)
在 swap 期間每次 tick 跨越時,程式將 liquidity_net(可能為負)添加到 PoolState.liquidity。這正是 Uniswap v3 的機制。

Positions 作為 NFTs

Raydium CLMM position 是一個 NFT。開啟 position 會將一個全新的 mint(供應量為 1)鑄造到呼叫者的錢包中,該 mint 的權限由 CLMM 程式持有。程式將 position 所有權綁定到在 CPI 時在該 mint 的 ATA 中持有餘額的任何人 後果:
  • Positions 通常是可轉移的。 錢包可以通過轉移 NFT 來出售或空投 position。新持有者隨後可以呼叫 CollectRewardsIncreaseLiquidity 等。例外是在受限發行人路徑下凍結的 position。
  • Positions 在 CLMM 外可尋址。 市場和錢包像顯示其他 NFTs 一樣顯示 positions。SDK 在 mint 元數據上設置合理的 name/symbol
  • Position 的 PDA 源自 NFT mint。 你可以找到 PersonalPositionState 而無需知道誰當前持有它。

受限發行人 positions

每個在 2026-08 升級後建立的 position NFT mint 都會記錄其 CLMM pool_state 作為凍結權限。這並不意味著每個新 position 都被凍結。 對於普通 pools 和每個非匹配 position,NFT 代幣帳戶保持未凍結和可轉移。pool PDA 無法在 CLMM 程式外簽名,CLMM 不公開通用凍結指令。 凍結需要以下兩個條件都滿足:
  1. position 通過 OpenPositionV2OpenPositionWithToken22Nft 開啟。
  2. 至少一個 pool vault mint 從程式的硬編碼受限發行人列表中攜帶凍結權限。
只有當兩個條件都滿足時,CLMM 才會在鑄造後立即凍結新建立的 position NFT 帳戶。OpenPosition V1 不應用此篩選器。請參閱 reference/program-addresses 以獲取當前列表。 凍結的 position:
  • 無法將其 NFT 轉移到另一個代幣帳戶。
  • 無法更改 NFT 代幣帳戶的所有者。
  • 當記錄的所有者簽名時,仍可以增加或減少流動性並收集費用或獎勵。
  • 仍可以關閉。ClosePosition 使用 pool PDA 解凍 NFT 帳戶,然後在同一指令中燒毀 NFT 並關閉 position 帳戶。
現有 positions 不會被遷移或追溯凍結。在升級前建立的 position NFT mints 保留其先前的凍結權限設置。
關閉凍結 position 的客戶端必須將 position 的 pool_state 作為 ClosePosition 的第一個剩餘帳戶附加。聲明的 IDL 帳戶列表保持不變,因此較舊的客戶端可以成功開啟凍結 position,但稍後可能因 AccountLack 而無法關閉它。在支持這些 pools 之前更新 close builder。

Token-2022 positions

CLMM 可以通過 OpenPositionV2 在經典 SPL Token 下鑄造 position NFT,或通過 OpenPositionWithToken22Nft 在 Token-2022 下鑄造。兩個 V2 路徑都檢查 pool vault mints 並應用相同的受限發行人凍結規則。OpenPosition V1 是舊版經典代幣路徑,無法服務具有 Token-2022 vault mints 的 pool。錢包和市場相容性不同;Raydium 的 UI 跟蹤兩個 NFT 程式。

允許範圍規則

OpenPosition 時,程式強制執行:
  1. tick_lower < tick_upper
  2. tick_lower % tick_spacing == 0tick_upper % tick_spacing == 0
  3. MIN_TICK <= tick_lowertick_upper <= MAX_TICK
  4. 呼叫者已提供包含 tick_lowertick_upper 的 tick 陣列 — 要麼已初始化,要麼通過同一交易中的 init_tick_array
  5. 位圖擴展帳戶,如果此 position 延伸到擴展範圍。
如果任何檢查失敗,指令會因 InvalidTickIndexNotApprovedInsufficientLiquidity 而回滾,具體取決於哪個約束。請參閱 reference/error-codes

「範圍內」vs「範圍外」

tick_lower <= tick_current < tick_upper 時,position 是範圍內。只有範圍內的 positions 對 PoolState.liquidity 有貢獻,因此只有它們才能賺取 swap 費用。 範圍外的 position:
  • 持有一個代幣的 100%(其範圍已經走過的代幣)。具體來說,如果 tick_current < tick_lower,position 只持有 token1(它已經被價格移動「賣出」);如果 tick_current >= tick_upper,它只持有 token0。
  • 賺取 swap 費用。
  • 確實如果 pool 的獎勵流向範圍外流動性發出獎勵,則繼續累積獎勵 — 但 Raydium 的預設行為是「僅向範圍內發出」,符合 Uniswap v3 慣例。請參閱 products/clmm/fees
管理 CLMM positions 的 LPs 花費大部分注意力將 positions 保持在範圍內,因為價格會移動。

常見整合陷阱

  • 偏離間距的端點。 從目標價格計算 tick 的程式碼必須在將其傳遞給 OpenPosition 之前對齊到 tick_spacing 的倍數。SDK 幫助程式(TickUtils.getTickWithPriceAndTickspacing)會執行此操作;自製數學通常不會。
  • 缺少 tick 陣列。 開啟寬 position 可能需要初始化多個 tick 陣列;忘記將它們作為可寫帳戶傳遞會導致回滾。SDK 的 openPositionFromBase 為你返回列表。
  • Swap 後的陳舊 tick。 tick_current 可以在一次 swap 中跨越許多 ticks。如果你的 UX 從一個 RPC 呼叫顯示「當前 tick」,然後在稍後的呼叫中開啟 position,相對於實時價格的相對 position 可能偏離數十個 ticks。在簽名前重新獲取。
  • 帶有額外元數據的 Position NFTs。 如果你構建一個識別 Raydium positions 的錢包,使用 position PDA / 程式數據,而不是硬編碼的元數據欄位。新 position mints 在建立時使用 pool PDA 作為 mint 和凍結權限;在鑄造單個 NFT 後移除 mint 權限。
  • 假設每個 position 都是可轉移的。 在顯示轉移、市場、託管或燒毀與賺取操作之前,讀取 NFT 代幣帳戶的 isFrozen 狀態。

接下來去哪裡

  • Math — swap 逐步執行和費用增長推導,tick 邊界參與其中。
  • AccountsTickArrayStatePositionState 佈局。
  • Fees and rewards — 範圍內性如何控制費用累積。
  • algorithms/clmm-math — 集中流動性公式的共享推導。
來源: