帳戶總覽
一個運行中的 CLMM 流動性池由以下幾類帳戶組成。除了兩個 mint 及其 vault 之外,所有帳戶皆由 CLMM 程式擁有。
| 帳戶 | 用途 | 每個池的數量 |
|---|
AmmConfig | 費率層級:交易費率、協議分成、基金分成、預設 tick 間距。在同一費率層級的所有池中共用。 | 1(共用) |
PoolState | 當前 sqrt_price_x64、當前 tick、總流動性、全域費用成長、獎勵資訊、觀察指標。 | 1 |
TickArrayState | 由 TICK_ARRAY_SIZE 個相鄰 tick 組成的區塊。僅在需要時才初始化。 | 0 ≤ N ≤ 範圍 |
TickArrayBitmapExtension | 用於追蹤超出 PoolState 內聯 bitmap 範圍之 tick 陣列的溢出 bitmap。 | 0 或 1 |
PersonalPositionState | 每個 LP 倉位一個。儲存範圍、流動性,以及上次更新時的費用/獎勵成長快照。授權方 = NFT 持有者。 | 每個倉位 1 個 |
| 倉位 NFT mint | 供給量為 1 的 Mint,與 PersonalPositionState 關聯。轉移 NFT 即等同轉移倉位。 | 每個倉位 1 個 |
ObservationState | 用於 TWAP 的價格觀察環形緩衝區。 | 1 |
token_0_vault、token_1_vault | 持有流動性池餘額的代幣帳戶。由池授權方擁有。 | 2 |
DynamicFeeConfig | 動態費用機制的可重複使用參數集。透過 create_customizable_pool 創建的池可選擇啟用。由管理員管理。 | 共用(依索引) |
LimitOrderState | 每個開放限價單一個。記錄擁有者、tick、方向、總金額、已結算輸出快照。 | 每個訂單 1 個 |
LimitOrderNonce | 每個 (錢包, nonce_index) 的計數器,用於推導唯一的訂單 PDA。 | 每個(錢包、索引)1 個 |
PoolState
流動性池的即時狀態,在每次 swap 和每次倉位變更時都會被讀取。
// programs/amm/src/states/pool.rs
pub struct PoolState {
pub bump: [u8; 1],
pub amm_config: Pubkey, // fee tier binding
pub owner: Pubkey, // admin (multisig)
pub token_mint_0: Pubkey,
pub token_mint_1: Pubkey,
pub token_vault_0: Pubkey,
pub token_vault_1: Pubkey,
pub observation_key: Pubkey,
pub mint_decimals_0: u8,
pub mint_decimals_1: u8,
pub tick_spacing: u16, // inherited from amm_config at init
pub liquidity: u128, // total active (in-range) liquidity
pub sqrt_price_x64: u128, // Q64.64 of sqrt(price)
pub tick_current: i32, // current tick index
pub padding3: u16,
pub padding4: u16,
// Global fee growth per unit of liquidity, Q64.64.
pub fee_growth_global_0_x64: u128,
pub fee_growth_global_1_x64: u128,
// Accrued-but-not-swept protocol fees (per mint).
pub protocol_fees_token_0: u64,
pub protocol_fees_token_1: u64,
// Reserved padding for future upgrades.
pub padding5: [u128; 4],
// Status bitmask. Bits 0-5: open-position, decrease-liquidity,
// collect-fee, collect-reward, swap, limit-order. A set bit disables
// the corresponding operation.
pub status: u8,
// Fee-collection mode (CollectFeeOn).
// 0 = FromInput (deduct fee from the swap input — Uniswap-V3 default)
// 1 = Token0Only (always deduct fee from token0 vault)
// 2 = Token1Only (always deduct fee from token1 vault)
pub fee_on: u8,
pub padding: [u8; 6],
// Live reward streams (up to REWARD_NUM = 3).
pub reward_infos: [RewardInfo; 3],
// Inline bitmap tracking initialized tick-arrays in the primary range.
pub tick_array_bitmap: [u64; 16],
// Reserved padding for future upgrades.
pub padding6: [u64; 4],
pub fund_fees_token_0: u64,
pub fund_fees_token_1: u64,
pub open_time: u64, // currently disabled by the program
pub recent_epoch: u64,
// Per-pool dynamic-fee state. Zero-valued unless the pool was
// created with `enable_dynamic_fee = true` via create_customizable_pool.
pub dynamic_fee_info: DynamicFeeInfo,
// Reserved for future upgrades.
pub padding1: [u64; 14],
pub padding2: [u64; 32],
}
你實際上會用到的欄位:
sqrt_price_x64 與 tick_current 代表流動性池的價格狀態。每次 swap 時會同步更新。tick_current 是 log_{1.0001}(price) 的下取整值。
liquidity 是有效流動性——即所有範圍包含 tick_current 的倉位其 L 值的總和。每當 swap 穿越一個 tick,或每當倉位被開啟、關閉或調整大小時,此值都會改變。
fee_growth_global_{0,1}_x64 是整個流動性池歷史中每單位流動性累積獲得的費用。倉位透過讀取此值來計算應得的費用。
tick_spacing 在初始化時鎖定至 AmmConfig,之後不會改變。它決定哪些 tick 索引可作為倉位的端點。
tick_array_bitmap 是一個內聯 bitmap,覆蓋現價附近常用的 tick 範圍。對於倉位延伸至遠端的流動性池,溢出追蹤則存放於獨立的 TickArrayBitmapExtension 帳戶中。
fee_on 在流動性池創建時固定。0(FromInput)重現經典 Uniswap-V3 的行為;1 和 2 會將 swap 費用路由至訂單簿的單一方向——詳見 products/clmm/fees 中的取捨說明。
dynamic_fee_info 儲存動態費用附加費的波動率狀態。啟用後,每次 swap 都會在 AmmConfig.trade_fee_rate 之上重新計算 dynamic_fee_component。結構說明詳見下方的 DynamicFeeInfo;未啟用動態費用的流動性池此結構全部為零。
AmmConfig
pub struct AmmConfig {
pub bump: u8,
pub index: u16, // uses "amm_config"+u16 seed
pub owner: Pubkey, // admin
pub protocol_fee_rate: u32, // fraction of trade fee to protocol, denom 1e6
pub trade_fee_rate: u32, // trade fee in 1e6ths of volume
pub tick_spacing: u16, // default spacing for pools using this config
pub fund_fee_rate: u32, // fraction of trade fee to fund, denom 1e6
pub padding_u32: u32,
pub fund_owner: Pubkey,
pub padding: [u64; 3],
}
一組典型的 CLMM 費率層級(請對照 GET https://api-v3.raydium.io/main/clmm-config 確認):
| 索引 | trade_fee_rate | Tick 間距 | 典型用途 |
|---|
| 0 | 100(0.01%) | 1 | 穩定幣對,如 USDC/USDT |
| 1 | 500(0.05%) | 10 | 相關性高的藍籌幣對 |
| 2 | 2_500(0.25%) | 60 | 標準幣對 |
| 3 | 10_000(1.00%) | 120 | 高波動或長尾幣對 |
protocol_fee_rate 和 fund_fee_rate 均為交易費用的分成比例,慣例與 CPMM 相同。詳見 products/clmm/fees。
TickArrayState
CLMM 不會為每個 tick 儲存單獨的記錄,否則將產生數十億個帳戶。取而代之的是,它將 TICK_ARRAY_SIZE 個相鄰 tick(通常為 60 或 88,依程式版本而定)分組至一個 TickArrayState 中,並在首次使用時才惰性創建。
pub const TICK_ARRAY_SIZE: usize = 60;
pub const TICK_ARRAY_SIZE_USIZE: usize = 60;
pub struct TickArrayState {
pub pool_id: Pubkey,
pub start_tick_index: i32, // lowest tick in this array
pub ticks: [TickState; TICK_ARRAY_SIZE], // 60 entries
pub initialized_tick_count: u8,
pub recent_epoch: u64,
pub padding: [u8; 107],
}
pub struct TickState {
pub tick: i32,
pub liquidity_net: i128, // ΔL when crossing this tick upward
pub liquidity_gross: u128, // total L referencing this tick
pub fee_growth_outside_0_x64: u128, // see math.mdx
pub fee_growth_outside_1_x64: u128,
pub reward_growths_outside_x64: [u128; 3],
// Limit-order bookkeeping. All zero for ticks that have never carried
// a limit order. See products/clmm/math for the matching algorithm.
pub order_phase: u64, // monotonic FIFO cohort id
pub orders_amount: u64, // unfilled tokens in current cohort
pub part_filled_orders_remaining: u64, // remaining tokens of partially-filled cohort
pub unfilled_ratio_x64: u128, // Q64.64; starts at 1.0 and shrinks as fills occur
pub padding: [u32; 3],
}
四個限價單欄位在從未用於限價單的 tick 上全部為零。當訂單在某個 tick 上開啟時,程式會以一系列批次(cohort)來追蹤它們:
order_phase 是批次 ID。每當一個批次從「全部未成交」轉換為「部分成交」時遞增。
orders_amount 是當前(最新)批次的輸入代幣總量。
part_filled_orders_remaining 追蹤正在被持續 swap 填充的前一個批次。
unfilled_ratio_x64 是附在批次上的 Q64.64 乘數:當某次 swap 填充了該批次的 X%,此比率便乘以 (1 − X)。每個開啟的訂單在開倉時會儲存自己的 (order_phase, unfilled_ratio_x64) 快照,因此結算數學只需比較快照即可。
規則:
- 倉位端點 tick t 必須滿足
t % tick_spacing == 0。程式會拒絕不符合間距的倉位。
- tick 所屬的陣列位於
floor(t / (TICK_ARRAY_SIZE * tick_spacing)) * (TICK_ARRAY_SIZE * tick_spacing)。
- tick 陣列採惰性初始化:第一個接觸到未初始化陣列的倉位或 swap 會創建它,並支付租金。
- 程式永遠不會關閉 tick 陣列。一旦分配後,即使其中所有 tick 的
liquidity_gross 都歸零,它也會在流動性池的整個生命週期內持續存在。後續的倉位和 swap 可免費重複使用現有帳戶,無需額外租金。不存在由 ClosePosition 驅動的 tick 陣列清理路徑。
TickArrayBitmapExtension
PoolState.tick_array_bitmap(內聯)涵蓋「接近現價」的範圍——±1,024 個 tick 陣列。超出該範圍(對應極端 tick 值)時,程式會維護一個擴充帳戶:
pub struct TickArrayBitmapExtension {
pub pool_id: Pubkey,
pub positive_tick_array_bitmap: [[u64; 8]; 14],
pub negative_tick_array_bitmap: [[u64; 8]; 14],
}
如果你的倉位範圍屬於「正常」範圍,完全不需要考慮擴充帳戶。全範圍倉位(例如 (MIN_TICK, MAX_TICK))才需要用到它;SDK 會自動為你解析。
一個 CLMM 倉位是由三個帳戶加一個 mint 組成的組合:
倉位 NFT mint
一個供給量為 1 的 SPL Token mint。mint 的地址是確定性 PDA;持有者錢包中的倉位 NFT 只是一個持有該單一代幣的 ATA。轉移 NFT 是倉位易手的方式——程式將授權綁定至 NFT ATA 餘額的當前持有者,而非狀態中儲存的某個 Pubkey。
PersonalPositionState
每個開放倉位一個,以 NFT mint 為鍵。
pub struct PersonalPositionState {
pub bump: [u8; 1],
pub nft_mint: Pubkey, // this position's NFT mint
pub pool_id: Pubkey,
pub tick_lower_index: i32,
pub tick_upper_index: i32,
pub liquidity: u128, // this position's L
// Fee-growth snapshots at last time the position was touched.
pub fee_growth_inside_0_last_x64: u128,
pub fee_growth_inside_1_last_x64: u128,
pub token_fees_owed_0: u64, // accrued since last collect
pub token_fees_owed_1: u64,
pub reward_infos: [PositionRewardInfo; 3],
pub recent_epoch: u64,
pub padding: [u64; 7],
}
pub struct PositionRewardInfo {
pub growth_inside_last_x64: u128,
pub reward_amount_owed: u64,
}
ProtocolPositionState(已棄用)
舊版 CLMM 在 ProtocolPositionState PDA 中儲存每個 (pool, tick_lower, tick_upper) 組合的彙總帳務。新版已不再創建或讀取此帳戶。 為了 ABI 相容性,該欄位仍以 UncheckedAccount 的形式出現在 OpenPosition、IncreaseLiquidity、DecreaseLiquidity 的帳戶清單中,但程式不會對其進行寫入。鏈上現有的帳戶屬於殘留資料;管理員可呼叫 CloseProtocolPosition 回收其租金。彙總範圍帳務現在直接從 TickArrayState 中的兩個端點 tick(liquidity_gross、liquidity_net,以及每個 tick 的 fee_growth_outside_* / reward_growths_outside_x64)推導而來。費用成長內部公式 fee_growth_inside = global − outside_lower − outside_upper 在不需要彙總倉位帳戶的情況下依然有效。
Observation
pub const OBSERVATION_NUM: usize = 100;
pub struct Observation {
pub block_timestamp: u32,
pub tick_cumulative: i64, // Σ tick_current × Δt
pub padding: [u64; 4],
}
pub struct ObservationState {
pub initialized: bool,
pub recent_epoch: u64,
pub observation_index: u16,
pub pool_id: Pubkey,
pub observations: [Observation; OBSERVATION_NUM], // 100 entries
pub padding: [u64; 4],
}
CLMM 的觀察緩衝區儲存的是累積 tick,而非累積價格。外部使用者透過 (tick_cumulative[t1] − tick_cumulative[t0]) / (t1 − t0) 計算某段時間內的幾何平均價格,再以 price = 1.0001 ** tick 轉換。詳見 algorithms/clmm-math。
DynamicFeeConfig 與 DynamicFeeInfo
動態費用參數分存於兩個位置。可重複使用的範本——DynamicFeeConfig——由管理員管理,並在選擇加入的流動性池之間共用。每個池的運行時狀態——DynamicFeeInfo——則嵌入於 PoolState 中,並在每次 swap 時更新。
DynamicFeeConfig
// programs/amm/src/states/pool_fee.rs
pub struct DynamicFeeConfig {
pub index: u16, // identifier; PDA seed component
pub filter_period: u16, // seconds — within this window the volatility reference is held
pub decay_period: u16, // seconds — beyond this window the reference fully decays
pub reduction_factor: u16, // fixed-point in [1, 10_000); applied at decay
pub dynamic_fee_control: u32, // fixed-point in (0, 100_000); fee-rate gain
pub max_volatility_accumulator: u32, // ceiling on the volatility accumulator
pub padding: [u64; 8],
}
PDA 種子:["dynamic_fee_config", index.to_be_bytes()]。透過 create_dynamic_fee_config(需管理員權限)創建,並透過 update_dynamic_fee_config 修改。以 enable_dynamic_fee = true 創建的流動性池,會在創建時將設定的五個校準參數(filter_period、decay_period、reduction_factor、dynamic_fee_control、max_volatility_accumulator)快照至自身的 DynamicFeeInfo 中;之後對 DynamicFeeConfig 的編輯不會追溯影響現有流動性池。
DynamicFeeInfo(嵌入於 PoolState)
pub struct DynamicFeeInfo {
pub filter_period: u16,
pub decay_period: u16,
pub reduction_factor: u16,
pub dynamic_fee_control: u32,
pub max_volatility_accumulator: u32,
pub tick_spacing_index_reference: i32, // tick-spacing-units; reference for next swap
pub volatility_reference: u32, // running floor for the accumulator
pub volatility_accumulator: u32, // current cumulative volatility (capped)
pub last_update_timestamp: u64,
pub padding: [u8; 46],
}
後四個欄位為狀態;前五個欄位為從 DynamicFeeConfig 複製的校準參數。費用計算公式與衰減規則詳見 products/clmm/math 與 products/clmm/fees。
公式使用的常數:
| 常數 | 值 | 含義 |
|---|
VOLATILITY_ACCUMULATOR_SCALE | 10_000 | 波動率累加器的精度粒度 |
REDUCTION_FACTOR_DENOMINATOR | 10_000 | reduction_factor 的分母 |
DYNAMIC_FEE_CONTROL_DENOMINATOR | 100_000 | dynamic_fee_control 的分母 |
MAX_FEE_RATE_NUMERATOR | 100_000 | 最終費率的硬性上限為 10% |
LimitOrderState
每個開放限價單一個帳戶。
// programs/amm/src/states/limit_order.rs
pub struct LimitOrderState {
pub pool_id: Pubkey,
pub owner: Pubkey,
pub tick_index: i32,
pub zero_for_one: bool, // direction: true sells token0 for token1
pub order_phase: u64, // snapshot of TickState.order_phase at open time
pub total_amount: u64, // input-token amount placed
pub filled_amount: u64, // informational; computed precisely on settle
pub settle_base: u64, // unfilled remainder at last settle/decrease
pub settled_output: u64, // cumulative output-token paid to owner
pub open_time: u64,
pub unfilled_ratio_x64: u128, // Q64.64 snapshot of TickState.unfilled_ratio_x64 at open
pub padding: [u64; 4],
}
生命週期:
- 開啟——用戶呼叫
open_limit_order,存入 total_amount 的輸入代幣,訂單被綁定至某個 TickState 批次。
- (可選)增加 / 減少——
increase_limit_order 增加 total_amount;decrease_limit_order 退還未成交代幣(以及截至當時的已結算輸出)。
- 結算——當批次完全或部分成交後,訂單擁有者或運維守護者呼叫
settle_limit_order,將輸出代幣推送至擁有者的 ATA。
- 關閉——一旦
unfilled_amount == 0,該帳戶即可關閉。租金始終退還至 owner。
PDA 種子:[owner.as_ref(), limit_order_nonce.key().as_ref(), limit_order_nonce.order_nonce.to_be_bytes().as_ref()]。因此,訂單 PDA 對於每個 (owner, nonce_index, order_nonce) 組合都是唯一的。
LimitOrderNonce
每個 (錢包, nonce_index) 的計數器,讓單一用戶能並行執行多條限價單管線而不發生 PDA 碰撞。
pub struct LimitOrderNonce {
pub user_wallet: Pubkey,
pub nonce_index: u8, // user-chosen, 0..255
pub order_nonce: u64, // monotonic, incremented every time a new order is opened
pub padding: [u64; 4],
}
PDA 種子:[user_wallet.as_ref(), &[nonce_index]]。大多數客戶端使用 nonce_index = 0,並讓 order_nonce 承載基數。
推導關鍵帳戶
import { PublicKey } from "@solana/web3.js";
const CLMM_PROGRAM_ID = new PublicKey(
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK"
); // see reference/program-addresses
function i32ToBytes(n: number): Buffer {
const b = Buffer.alloc(4);
b.writeInt32BE(n);
return b;
}
export function deriveClmmAccounts(
ammConfig: PublicKey,
token0Mint: PublicKey, // must already be sorted
token1Mint: PublicKey,
) {
const [poolState] = PublicKey.findProgramAddressSync(
[Buffer.from("pool"), ammConfig.toBuffer(),
token0Mint.toBuffer(), token1Mint.toBuffer()],
CLMM_PROGRAM_ID,
);
const [observation] = PublicKey.findProgramAddressSync(
[Buffer.from("observation"), poolState.toBuffer()],
CLMM_PROGRAM_ID,
);
const [tickArrayBitmapExtension] = PublicKey.findProgramAddressSync(
[Buffer.from("pool_tick_array_bitmap_extension"), poolState.toBuffer()],
CLMM_PROGRAM_ID,
);
return { poolState, observation, tickArrayBitmapExtension };
}
export function deriveTickArray(
pool: PublicKey,
startTickIndex: number,
) {
const [tickArray] = PublicKey.findProgramAddressSync(
[Buffer.from("tick_array"), pool.toBuffer(), i32ToBytes(startTickIndex)],
CLMM_PROGRAM_ID,
);
return tickArray;
}
export function deriveDynamicFeeConfig(index: number) {
const idx = Buffer.alloc(2);
idx.writeUInt16BE(index);
const [pda] = PublicKey.findProgramAddressSync(
[Buffer.from("dynamic_fee_config"), idx],
CLMM_PROGRAM_ID,
);
return pda;
}
export function deriveLimitOrderNonce(
wallet: PublicKey,
nonceIndex: number,
) {
const [pda] = PublicKey.findProgramAddressSync(
[wallet.toBuffer(), Buffer.from([nonceIndex & 0xff])],
CLMM_PROGRAM_ID,
);
return pda;
}
export function deriveLimitOrder(
wallet: PublicKey,
nonceAccount: PublicKey,
orderNonce: bigint,
) {
const nonceBytes = Buffer.alloc(8);
nonceBytes.writeBigUInt64BE(orderNonce);
const [pda] = PublicKey.findProgramAddressSync(
[wallet.toBuffer(), nonceAccount.toBuffer(), nonceBytes],
CLMM_PROGRAM_ID,
);
return pda;
}
export function derivePersonalPosition(nftMint: PublicKey) {
const [personalPosition] = PublicKey.findProgramAddressSync(
[Buffer.from("position"), nftMint.toBuffer()],
CLMM_PROGRAM_ID,
);
return personalPosition;
}
確切的種子字串應始終對照鏈上 IDL 及 reference/program-addresses 進行雙重確認。
生命週期快速參考
| 事件 | 創建的帳戶 | 銷毀的帳戶 |
|---|
CreatePool | poolState、observation、token_0_vault、token_1_vault | — |
OpenPosition[WithToken22Nft] | NFT mint + ATA、personalPosition,可能新增 tickArrayState,若不存在則新增 tickArrayBitmapExtension | — |
IncreaseLiquidity | 可能新增 tickArrayState | — |
DecreaseLiquidity | — | 可能清除 tick 條目(但 tickArrayState 本身不會關閉) |
ClosePosition | — | NFT mint、personalPosition |
SwapV2 | 可能新增 tickArrayState | — |
OpenLimitOrder | limitOrderState,可能新增 limitOrderNonce(按需初始化),可能新增 tickArrayState | — |
IncreaseLimitOrder | — | — |
DecreaseLimitOrder | — | 若訂單已完全消耗則關閉 limitOrderState |
SettleLimitOrder | — | — |
CloseLimitOrder | — | limitOrderState(租金 → owner) |
CreateDynamicFeeConfig | dynamicFeeConfig | — |
CreateCustomizablePool | poolState、observation、vault——與 CreatePool 相同。若 enable_dynamic_fee = true 則快照 dynamicFeeConfig。 | — |
CollectRewards | — | — |
UpdateRewardInfos | — | — |
CloseProtocolPosition(管理員) | — | 殘留的 protocolPositionState(租金 → 管理員) |
TickArrayState 帳戶永遠不會被程式關閉——它們在流動性池的整個生命週期內持續存在。一旦 tick 陣列被初始化,即使其中所有 tick 的 liquidity_gross 均歸零,它仍會保留在鏈上。重複使用現有 tick 陣列是免費的;只有第一個接觸從未初始化陣列的倉位才需要支付租金。
各主題閱讀指引
來源: