Skip to main content
本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →
本页描述了每个账户的布局和作用。规范的种子列在 reference/program-addresses 中。CLMM 池比 CPMM 池需要更多账户,因为流动性稀疏地分布在 tick 范围内;理解这种稀疏性是本页的重点。

账户清单

一个活跃的 CLMM 池由以下账户族组成。除了两个 mint 及其金库外,所有账户都由 CLMM 程序拥有。

PoolState

池的实时状态,在每次交换和每次头寸变化时读取。
你实际会接触的字段:
  • sqrt_price_x64tick_current 是池的价格状态。它们在每次交换时一起更新。tick_currentlog_{1.0001}(price) 的下界。
  • liquidity活跃流动性——所有范围包含 tick_current 的头寸的 L 值之和。每次交换穿过 tick 时以及每次打开/关闭/调整头寸大小时都会改变。
  • fee_growth_global_{0,1}_x64 是整个池历史中每单位流动性累积的费用。头寸读取这个值来计算欠它们的费用。
  • tick_spacing 在初始化时锁定到 AmmConfig,永远不会改变。它决定了哪些 tick 索引可以作为头寸端点。
  • tick_array_bitmap 是一个内联位图,覆盖现货价格周围的”接近”范围——±1,024 个 tick 数组。对于头寸范围很远的池,溢出跟踪位于单独的 TickArrayBitmapExtension 中。
  • fee_on 在池创建时固定。0(FromInput)复现经典 Uniswap-V3 行为。12 将交换费路由到账簿的单一一侧——参见 products/clmm/fees 了解权衡。
  • seed_index 对于通过 CreatePool / CreateCustomizablePool 创建的每个池都是 [0, 0](每对一个规范池)。非零值意味着池是通过 CreatePermissionedPool 创建的,索引是池 PDA 种子的一部分,允许多个池为同一 (config, mint0, mint1) 共存。要重新派生这样的池的地址,你必须知道它的 seed_index
  • dynamic_fee_info 携带动态费用加价的波动性状态。启用时,每次交换在 AmmConfig.trade_fee_rate 之上重新计算 dynamic_fee_component。布局在下面的 DynamicFeeInfo 中记录;没有动态费用的池将整个结构保留为零。

AmmConfig

一个典型的已发布 CLMM 费用等级集(确认对比 GET https://api-v3.raydium.io/main/clmm-config): protocol_fee_ratefund_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(内联)覆盖”接近现货”范围——±1,024 个 tick 数组。超出该范围(对于极端 tick 值),程序维护一个扩展账户:
如果你的头寸范围是”正常的”,你永远不会考虑扩展账户。全范围头寸(例如 (MIN_TICK, MAX_TICK))需要它;SDK 为你解决它。

头寸

CLMM 头寸是三个账户加一个 mint 的捆绑

Position NFT mint

一个供应量为 1 的 SPL Token 或 Token-2022 mint。所有者钱包中的头寸 NFT 是持有该单个代币的 ATA。程序将授权密钥设置为 NFT ATA 余额的当前持有者,而不是存储在状态中的 Pubkey。 新头寸 NFT mint 在铸造单个代币并移除 mint 权限之前将 pool_state 设置为其冻结权限。设置冻结权限本身不会冻结 NFT 账户。 账户保持未冻结和可转移,除非两个条件都成立:调用者使用 OpenPositionV2OpenPositionWithToken22Nft,并且至少一个底层金库 mint 的冻结权限出现在 CLMM 的受限发行者列表中。只有这样,CLMM 才会在铸造后冻结 NFT 账户。这不改变任何 PersonalPositionStatePoolState 字节。

PersonalPositionState

每个开放头寸一个。以 NFT mint 为键。

ProtocolPositionState(已弃用)

较早的 CLMM 版本在 ProtocolPositionState PDA 中存储每个 (pool, tick_lower, tick_upper) 的聚合簿记。较新的版本不再创建或读取此账户。 该槽仍然作为 UncheckedAccount 出现在 OpenPosition / IncreaseLiquidity / DecreaseLiquidity 账户列表中以保持 ABI 兼容性,但程序不写入它。链上现有账户是残留的;管理员可以调用 CloseProtocolPosition 来回收它们的租金。聚合范围簿记现在直接从两个端点 tick(liquidity_grossliquidity_netTickArrayState 中的每个 tick fee_growth_outside_* / reward_growths_outside_x64)派生。费用增长内部公式 fee_growth_inside = global − outside_lower − outside_upper 继续在没有聚合头寸账户的情况下工作。

Observation

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

DynamicFeeConfigDynamicFeeInfo

动态费用参数位于两个地方。可重用模板——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_perioddecay_periodreduction_factordynamic_fee_controlmax_volatility_accumulator)快照到自己的 DynamicFeeInfo 中;对 DynamicFeeConfig 的后续编辑不会追溯影响现有池。

DynamicFeeInfo(嵌入在 PoolState 中)

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

LimitOrderState

每个开放限价单一个账户。
生命周期:
  1. 打开 — 用户调用 open_limit_order,存入输入代币的 total_amount,订单绑定到 TickState 队列。
  2. (可选)增加/减少increase_limit_order 添加到 total_amountdecrease_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 PDA,该权限可以调用 CreatePermissionedPool。它除了创建它的权限外不存储任何东西。
PDA 种子:["permission", authority.as_ref()]。由管理员通过 CreatePermissionPda 创建,通过 ClosePermissionPda 拆除(租金退款给调用者)。两个管理员指令都接受程序 admin 或专用 permission_pda_admin 密钥。关闭 PDA 撤销授权——权限不能再创建额外的池,但它已创建的池不受影响。

派生关键账户

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

生命周期快速参考

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

在哪里阅读什么

来源: