Skip to main content
本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →
本页介绍每个账户的布局和作用。种子(Seeds)是规范的,列在 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

自 2026-09 升级以来,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 中,该状态在首次使用时被懒加载创建。
四个限价单字段在任何从未用于限价单的 tick 上都为零。当在某个 tick 上开启订单时,程序将它们追踪为一系列队列:
  • 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 继续在没有聚合头寸账户的情况下工作。

观察

CLMM 的观察缓冲区存储累积 tick,而不是累积价格。外部消费者从 (tick_cumulative[t1] − tick_cumulative[t0]) / (t1 − t0) 计算区间上的几何平均价格,然后 price = 1.0001 ** tick。参见 algorithms/clmm-math。

DynamicFeeConfig 和 DynamicFeeInfo

动态费用参数存储在两个地方。可重用的模板——DynamicFeeConfig——由管理员管理并在选择加入的池之间共享。每个池的运行时状态——DynamicFeeInfo——嵌入在 PoolState 中并由每次交换更新。

DynamicFeeConfig

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 中)

底部四个字段是状态;顶部五个是从 DynamicFeeConfig 复制的校准。费用数学和衰减规则记录在 products/clmm/math 和 products/clmm/fees 下。 公式使用的常数:

LimitOrderState

每个开启的限价单一个账户。
生命周期:
  1. 开启 — 用户调用 open_limit_order,存入 total_amount 的输入代币,订单绑定到 TickState 队列。
  2. (可选)增加 / 减少 — increase_limit_order 增加 total_amount;decrease_limit_order 返回未成交代币(以及到该点为止的任何已结算输出)。
  3. 结算 — 当队列被完全或部分填充时,所有者或运营保管人调用 settle_limit_order 将输出代币推送到所有者的 ATA。
  4. 关闭 — 一旦 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

每个 (wallet, nonce_index) 对应一个计数器,允许单个用户运行多条并行的限价单管道,而不会在 PDA 上发生碰撞。
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 所有账户时请忽略它们。

推导关键账户

确切的种子字符串应始终与链上 IDL 和 reference/program-addresses 进行双重检查。

生命周期快速参考

TickArrayState 账户永远不会被程序关闭 — 它们在池的整个生命周期内持续存在。一旦 tick 数组被初始化,即使其中的每个 tick 都返回到 liquidity_gross == 0,它仍会保留在链上。重用现有 tick 数组是免费的;只有第一个接触从未初始化数组的头寸才需支付其租金。

阅读指南

来源: