账户总览
一个运行中的 CLMM 流动池由以下几类账户描述。除两个代币 mint 及其 vault 外,所有账户均归属于 CLMM 程序。
| 账户 | 用途 | 每个流动池的数量 |
|---|
AmmConfig | 费率层级:交易手续费率、协议分成、基金分成、默认 tick 间距。在同一层级的所有流动池之间共享。 | 1(共享) |
PoolState | 当前 sqrt_price_x64、当前 tick、总流动性、全局手续费增长、奖励信息、观测指针。 | 1 |
TickArrayState | 一组 TICK_ARRAY_SIZE 个相邻 tick 的集合,按需惰性初始化。 | 0 ≤ N ≤ 范围上限 |
TickArrayBitmapExtension | 溢出位图,追踪超出 PoolState 内联位图范围的 tick 数组。 | 0 或 1 |
PersonalPositionState | 每个 LP 仓位对应一个,存储范围、流动性以及最后记录的手续费/奖励增长。授权方为 NFT 持有者。 | 每个仓位 1 个 |
| Position 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 是一个内联位图,覆盖当前价格附近的常用 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 组成:
Position NFT mint
供应量为 1 的 SPL Token mint。Mint 地址是确定性 PDA,持有者钱包中的 position 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 兼容性,该插槽在 OpenPosition / IncreaseLiquidity / DecreaseLiquidity 的账户列表中仍以 UncheckedAccount 形式出现,但程序不再向其写入。链上现有的该类账户为历史遗留;管理员可调用 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 返还未成交代币(以及截至当时已结算的输出代币)。
- 结算 — 当批次全部或部分成交后,持有者或运营 keeper 调用
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 数组无需额外成本;只有第一个触及从未初始化的数组的仓位才需支付其租金。
各主题阅读指引
来源: