本頁內容由 AI 自動翻譯,所有內容以英文版本為準。查看英文版 →
為什麼需要 ticks
CLMM 的流動性集中在價格範圍內。為了在鏈上使範圍易於處理,價格被量化為整數 ticks,其中每個 tick 是前一個的常數倍數: 一個 tick 對應 0.01% 的價格變動,或約 1 個基點。映射關係如下:MIN_TICK 和 MAX_TICK 的選擇確保 sqrt_price_x64 在兩端都能適配 u128。每個 pool 都強制 tick_lower >= MIN_TICK 和 tick_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 = 60 和 TICK_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_gross 和 liquidity_net
每個已初始化的 tick 儲存兩個流動性值:
liquidity_gross— 所有將此 tick 作為任一端點引用的 positions 的L之和。當liquidity_gross達到零時,tick 變為未初始化,可以從位圖中移除。liquidity_net— 當價格向上跨越此 tick(在 tick 空間中從左到右)時,pool 級別liquidity的帶符號變化。如果此 tick 是大小為L的 position 的下界,它貢獻+L;如果它是該 position 的上界,它貢獻−L。
- Position A:
tick_lower = -120,tick_upper = 0,流動性L_A = 100。 - Position B:
tick_lower = -60,tick_upper = 60,流動性L_B = 50。
不同
tick_current 值的 pool 級別 liquidity:
tick_current = -180:liquidity = 0(任何 position 之前)tick_current = -90:liquidity = 100(僅在 A 內)tick_current = -30:liquidity = 150(在 A 和 B 內)tick_current = 30:liquidity = 50(僅在 B 內)tick_current = 90:liquidity = 0(超過兩者)
liquidity_net(可能為負)添加到 PoolState.liquidity。這正是 Uniswap v3 的機制。
Positions 作為 NFTs
Raydium CLMM position 是一個 NFT。開啟 position 會將一個全新的 mint(供應量為 1)鑄造到呼叫者的錢包中,該 mint 的權限由 CLMM 程式持有。程式將 position 所有權綁定到在 CPI 時在該 mint 的 ATA 中持有餘額的任何人。 後果:- Positions 通常是可轉移的。 錢包可以通過轉移 NFT 來出售或空投 position。新持有者隨後可以呼叫
CollectRewards、IncreaseLiquidity等。例外是在受限發行人路徑下凍結的 position。 - Positions 在 CLMM 外可尋址。 市場和錢包像顯示其他 NFTs 一樣顯示 positions。SDK 在 mint 元數據上設置合理的
name/symbol。 - Position 的 PDA 源自 NFT mint。 你可以找到
PersonalPositionState而無需知道誰當前持有它。
受限發行人 positions
每個在 2026-08 升級後建立的 position NFT mint 都會記錄其 CLMMpool_state 作為凍結權限。這並不意味著每個新 position 都被凍結。 對於普通 pools 和每個非匹配 position,NFT 代幣帳戶保持未凍結和可轉移。pool PDA 無法在 CLMM 程式外簽名,CLMM 不公開通用凍結指令。
凍結需要以下兩個條件都滿足:
- position 通過
OpenPositionV2或OpenPositionWithToken22Nft開啟。 - 至少一個 pool vault mint 從程式的硬編碼受限發行人列表中攜帶凍結權限。
OpenPosition V1 不應用此篩選器。請參閱 reference/program-addresses 以獲取當前列表。
凍結的 position:
- 無法將其 NFT 轉移到另一個代幣帳戶。
- 無法更改 NFT 代幣帳戶的所有者。
- 當記錄的所有者簽名時,仍可以增加或減少流動性並收集費用或獎勵。
- 仍可以關閉。
ClosePosition使用 pool PDA 解凍 NFT 帳戶,然後在同一指令中燒毀 NFT 並關閉 position 帳戶。
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 時,程式強制執行:
tick_lower < tick_upper。tick_lower % tick_spacing == 0和tick_upper % tick_spacing == 0。MIN_TICK <= tick_lower和tick_upper <= MAX_TICK。- 呼叫者已提供包含
tick_lower和tick_upper的 tick 陣列 — 要麼已初始化,要麼通過同一交易中的init_tick_array。 - 位圖擴展帳戶,如果此 position 延伸到擴展範圍。
InvalidTickIndex、NotApproved 或 InsufficientLiquidity 而回滾,具體取決於哪個約束。請參閱 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。
常見整合陷阱
- 偏離間距的端點。 從目標價格計算 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 邊界參與其中。
- Accounts —
TickArrayState和PositionState佈局。 - Fees and rewards — 範圍內性如何控制費用累積。
algorithms/clmm-math— 集中流動性公式的共享推導。
raydium-io/raydium-clmm—tick_array、tick、position模組- “Uniswap v3 Core” 白皮書,§6 (ticks)、§7 (fee growth)

