本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →
reference/program-addresses 中。CLMM 池比 CPMM 池需要更多账户,因为流动性在 tick 范围内稀疏分布;理解这种稀疏性是本页的重点。
账户清单
一个活跃的 CLMM 池由以下账户族组成。除了两个 mint 及其金库外,所有账户都由 CLMM 程序拥有。PoolState
池的实时状态,在每次交换和每次头寸变化时读取。
sqrt_price_x64和tick_current是池的价格状态。它们在每次交换时一起更新。tick_current是log_{1.0001}(price)的下界。liquidity是活跃流动性——所有范围包含tick_current的头寸的L值之和。每次交换穿过 tick 以及每次头寸打开/关闭/调整大小时都会改变。fee_growth_global_{0,1}_x64是整个池历史中每单位流动性赚取的累积费用。头寸读取这些值来计算应付给它们的费用。tick_spacing在初始化时锁定到AmmConfig,永不改变。它决定了哪些 tick 索引可以作为头寸端点。tick_array_bitmap是一个内联位图,覆盖现货价格周围常用的 tick 范围。对于头寸范围很广的池,溢出跟踪位于单独的TickArrayBitmapExtension中。fee_on在池创建时固定。0(FromInput)复现经典 Uniswap-V3 行为。1和2将交换费路由到账簿的单一一侧——参见products/clmm/fees了解权衡。seed_index对于通过CreatePool/CreateCustomizablePool创建的每个池都是[0, 0](每个交易对一个规范池)。非零值意味着池是通过CreateCustomizablePoolV2(2026-10-22 之前名为CreatePermissionedPool)创建的,索引是池 PDA 种子的一部分,允许同一(config, mint0, mint1)存在多个池。要重新派生这样的池的地址,你必须知道它的seed_index。dynamic_fee_info携带动态费用附加费的波动性状态。启用时,每次交换在AmmConfig.trade_fee_rate之上重新计算dynamic_fee_component。布局在下面的DynamicFeeInfo中记录;没有动态费用的池将整个结构保留为零。
AmmConfig
CreateAmmConfig 将程序的硬编码 protocol_fee_owner::ID 写入 owner,将 fund_fee_owner::ID 写入 fund_owner。在此之前,它将管理员签名者的密钥复制到两者中。现有配置保持创建或轮换时的状态,因此从账户中读取两个字段。UpdateAmmConfig 参数 3 / 4 仍然轮换它们。地址在 reference/program-addresses 中。
一个典型的已发布 CLMM 费用等级集(根据 GET https://api-v3.raydium.io/main/clmm-config 确认):
protocol_fee_rate 和 fund_fee_rate 是交易费的分数;与 CPMM 相同的约定。参见 products/clmm/fees。
TickArrayState
CLMM 不会为每个 tick 存储单独的记录。那样会产生数十亿个账户。相反,它将相邻的 TICK_ARRAY_SIZE 个 tick(已初始化或未初始化,通常为 60 或 88,取决于程序版本)分组到一个 TickArrayState 中,该状态在首次使用时被懒加载创建。
order_phase是队列 id。每当队列从”全部未成交”转变为”部分成交”时,它就会递增。orders_amount是当前(最新)队列的输入代币总额。part_filled_orders_remaining追踪正在被进行中的交换填充的前一个队列。unfilled_ratio_x64是在队列上携带的 Q64.64 乘数:当交换填充队列的 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 数组被懒加载初始化:第一个接触未初始化数组的头寸或交换会创建它,并支付租金。
- tick 数组永远不会被程序关闭。一旦分配,它就会在池的整个生命周期内持续存在,即使其中的每个 tick 都回到
liquidity_gross == 0。后续的头寸和交换会重用现有账户,无需额外租金。没有由ClosePosition驱动的 tick 数组清理路径。
TickArrayBitmapExtension
PoolState.tick_array_bitmap(内联)覆盖”接近现货”范围——±512 个 tick 数组(TICK_ARRAY_BITMAP_SIZE = 512;[u64; 16] 字段是 1,024 位,在负和正起始索引之间均匀分割,因此数组索引 −512 … +511)。在该范围之外(对于极端 tick 值),程序维护一个扩展账户:
(MIN_TICK, MAX_TICK))需要它;SDK 会为你解决。
头寸
CLMM 头寸是三个账户加一个 mint 的捆绑:头寸 NFT mint
一个供应量为 1 的 SPL Token 或 Token-2022 mint。所有者钱包中的头寸 NFT 是持有该单个代币的 ATA。程序将授权密钥设置为 NFT 的 ATA 余额的当前持有者,而不是存储在状态中的 Pubkey。 新头寸 NFT mint 在铸造单个代币并移除 mint 权限之前,将pool_state 设置为其冻结权限。设置冻结权限本身不会冻结 NFT 账户。 除非满足以下两个条件,否则账户保持未冻结和可转移状态:调用者使用 OpenPositionV2 或 OpenPositionWithToken22Nft,并且至少一个底层保险库 mint 的冻结权限出现在 CLMM 的受限发行人列表中。只有这样,CLMM 才会在铸造后冻结 NFT 账户。这不会改变任何 PersonalPositionState 或 PoolState 字节。
PersonalPositionState
每个开启的头寸一个。以 NFT mint 为密钥。
ProtocolPositionState(已弃用)
较早的 CLMM 版本在
ProtocolPositionState PDA 中存储每个 (pool, tick_lower, tick_upper) 的聚合簿记。较新的版本不再创建或读取此账户。 该槽仍然作为 UncheckedAccount 出现在 OpenPosition / IncreaseLiquidity / DecreaseLiquidity 账户列表中以保持 ABI 兼容性,但程序不会写入它。链上现有的账户是遗留的;管理员可以调用 CloseProtocolPosition 来回收它们的租金。聚合范围簿记现在直接从两个端点 tick(liquidity_gross、liquidity_net 和每个 tick 的 fee_growth_outside_* / reward_growths_outside_x64)在 TickArrayState 中派生。费用增长内部公式 fee_growth_inside = global − outside_lower − outside_upper 继续在没有聚合头寸账户的情况下工作。观察
(tick_cumulative[t1] − tick_cumulative[t0]) / (t1 − t0) 计算区间上的几何平均价格,然后 price = 1.0001 ** tick。参见 algorithms/clmm-math。
DynamicFeeConfig 和 DynamicFeeInfo
动态费用参数存储在两个地方。可重用的模板——DynamicFeeConfig——由管理员管理并在选择加入的池之间共享。每个池的运行时状态——DynamicFeeInfo——嵌入在 PoolState 中并由每次交换更新。
DynamicFeeConfig
["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 中)
DynamicFeeConfig 复制的校准。费用数学和衰减规则记录在 products/clmm/math 和 products/clmm/fees 下。
公式使用的常数:
LimitOrderState
每个开启的限价单一个账户。
- 开启 — 用户调用
open_limit_order,存入total_amount的输入代币,订单绑定到TickState队列。 - (可选)增加 / 减少 —
increase_limit_order增加total_amount;decrease_limit_order返回未成交代币(以及到该点为止的任何已结算输出)。 - 结算 — 当队列被完全或部分填充时,所有者或运营保管人调用
settle_limit_order将输出代币推送到所有者的 ATA。 - 关闭 — 一旦
unfilled_amount == 0,账户就可以关闭。租金始终返回给owner。
[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
每个 (wallet, nonce_index) 对应一个计数器,允许单个用户运行多条并行的限价单管道,而不会在 PDA 上发生碰撞。
[user_wallet.as_ref(), &[nonce_index]]。大多数客户端使用 nonce_index = 0,让 order_nonce 承载基数。
Permission(已弃用)
Permission 账户在 2026-10-22 升级 中被移除。它曾用于限制 CreatePermissionedPool,现在已改为无权限的 CreateCustomizablePoolV2。程序不再读取或关闭这些账户,CreatePermissionPda / ClosePermissionPda 指令已删除。升级前创建的 PDA(["permission", authority],280 字节)仍保留在链上作为惰性数据;扫描 CLMM 所有账户时请忽略它们。
推导关键账户
reference/program-addresses 进行双重检查。
生命周期快速参考
TickArrayState 账户永远不会被程序关闭 — 它们在池的整个生命周期内持续存在。一旦 tick 数组被初始化,即使其中的每个 tick 都返回到 liquidity_gross == 0,它仍会保留在链上。重用现有 tick 数组是免费的;只有第一个接触从未初始化数组的头寸才需支付其租金。
阅读指南
- Tick 数学和范围机制:
products/clmm/ticks-and-positions。 - 交换遍历和费用增长数学:
products/clmm/math。 - 指令账户列表:
products/clmm/instructions。 - 费用和奖励累积:
products/clmm/fees。 - 规范程序 ID 和种子:
reference/program-addresses。

