Skip to main content
本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →

为什么需要 ticks

CLMM 的流动性集中在价格范围内。为了在链上使范围易于处理,价格被量化为整数 ticks,其中每个 tick 是前一个的常数倍数: price(i)=1.0001 i\text{price}(i) = 1.0001^{\,i} 一个 tick 对应 0.01% 的价格变动,或约 1 个基点。映射关系如下: MIN_TICK 和 MAX_TICK 的选择确保 sqrt_price_x64 在两端都能适配 u128。每个池都强制 tick_lower >= MIN_TICK 和 tick_upper <= MAX_TICK。实际上,Web UI 会将范围限制在更窄的范围内,以防止用户将流动性锁定在无法到达的 ticks 中。

Tick 间距

池的 AmmConfig 固定了一个 tick 间距 — position 唯一允许用作端点的 ticks。如果 tick_spacing = 60,只有 ticks …, −120, −60, 0, 60, 120, … 是有效的。尝试以端点 31 打开 position 会因 TickAndSpacingNotMatch 而回滚。 常见的已发布间距: 间距越粗,需要初始化的 tick 数组越少,打开宽 position 的成本越低,价格边界越模糊。波动性交易对通常在 120 间距等级中;稳定币对在 1 间距等级中。

Tick 数组

池不会在单独的账户中存储每个 tick 的状态。相反,TICK_ARRAY_SIZE 个相邻的 ticks(当前 Raydium CLMM 中为 60 个)被打包到单个 TickArrayState 中。数组的第一个 tick 是其 start_tick_index,它覆盖恰好 TICK_ARRAY_SIZE * tick_spacing 个整数 tick 单位。 对于 tick_spacing = 60 和 TICK_ARRAY_SIZE = 60:
  • 每个 tick 数组跨越 60 × 60 = 3600 个整数 ticks。
  • start_tick_index 是 3600 的倍数:…, -7200, -3600, 0, 3600, 7200, …。
在 tick_spacing = 60 时,position 端点 t = 2040 位于 start_tick_index = 0 的 tick 数组中。Position 端点 t = 4200 位于 start_tick_index = 3600 的数组中。

何时创建数组

Tick 数组是延迟初始化的:第一个 引用其中任何 tick 的 position 会初始化该数组并支付租金。Swaps 不会初始化 tick 数组 — 它们使用位图跳过未初始化的数组。不存在单独的初始化 tick 数组指令:OpenPosition* 和 IncreaseLiquidity* 会在内部调用 TickArrayState::get_or_create_tick_array,并由 payer 出资分配缺失的数组。你只需传入 tick 数组的 PDA —— 即使它仍未初始化、仍归系统程序所有。

Tick 数组不会被关闭

一旦 tick 数组被初始化,它就会在池的整个生命周期内持续存在。程序不提供关闭 tick 数组的路径,即使 initialized_tick_count 返回到零也不行。Tick 数组没有租金恢复;第一个接触数组的 position 支付的租金会永久锁定在该账户中。这是一个有意的权衡:重用现有 tick 数组对后续每个 position 都是免费的,所以一个交易频繁的池只需为每个 (pool, start_tick_index) 槽支付一次租金,无论流动性变化如何。

位图

找到”当前 tick 左/右的下一个已初始化 tick”必须很快 — swap 可能会跨越许多 ticks。池在 PoolState 中内联存储一个每 tick 数组 1 位的位图,覆盖 tick 0 周围 ±512 个数组的范围(TICK_ARRAY_BITMAP_SIZE = 512;该字段是 [u64; 16] = 1,024 位,按 512 个负索引 / 512 个正索引平分,即数组索引 −512 … +511)。在该范围之外(全范围 positions、特殊设置),TickArrayBitmapExtension 提供溢出。 Swap 遍历位图:lowest_set_bit_above(tick_current_array_index) 给出 swap 正在跨越的一侧的下一个具有已初始化 tick 的数组。在该数组内,类似的位扫描定位下一个已初始化的 tick。

liquidity_gross 和 liquidity_net

每个已初始化的 tick 存储两个流动性值:
  • liquidity_gross — 所有将此 tick 作为端点引用的 positions 的 L 之和。当 liquidity_gross 达到零时,tick 变为未初始化,可以从位图中移除。
  • liquidity_net — 当价格向上跨越此 tick(在 tick 空间中从左到右)时,对池级 liquidity 的有符号变化。如果此 tick 是大小为 L 的 position 的下界,它贡献 +L;如果它是该 position 的上界,它贡献 −L。
工作示例:同一池上的两个 positions。
  • Position A:tick_lower = -120,tick_upper = 0,流动性 L_A = 100。
  • Position B:tick_lower = -60,tick_upper = 60,流动性 L_B = 50。
逐 tick 状态: 不同 tick_current 值的池级 liquidity:
  • tick_current = -180:liquidity = 0(任何 position 之前)
  • tick_current = -90:liquidity = 100(仅在 A 内)
  • tick_current = -30:liquidity = 150(在 A 和 B 内)
  • tick_current = 30:liquidity = 50(仅在 B 内)
  • tick_current = 90:liquidity = 0(超过两者)
在 swap 期间的每次 tick 跨越时,程序将 liquidity_net(可能为负)添加到 PoolState.liquidity。这正是 Uniswap v3 的机制。

Positions 作为 NFTs

Raydium CLMM position 是一个 NFT。打开 position 会将一个全新的 mint(供应量为 1)铸造到调用者的钱包中,该 mint 的权限由 CLMM 程序拥有。程序将 position 所有权关联到在 CPI 时在该 mint 的 ATA 中持有余额的任何人。 后果:
  • Positions 通常是可转移的。 钱包可以通过转移 NFT 来出售或空投 position。新持有者随后可以调用 CollectRewards、IncreaseLiquidity 等。例外是在受限发行者路径下冻结的 position。
  • Positions 在 CLMM 外可寻址。 市场和钱包像显示其他 NFTs 一样显示 positions。SDK 在 mint 元数据上设置合理的 name/symbol。
  • Position 的 PDA 从 NFT mint 派生。 你可以找到 PersonalPositionState 而无需知道当前持有者是谁。

受限发行者 positions

2026-08 升级后创建的每个 position NFT mint 都将其 CLMM pool_state 记录为冻结权限。这并不意味着每个新 position 都被冻结。 对于普通池和每个非匹配 position,NFT 代币账户保持未冻结和可转移。池 PDA 无法在 CLMM 程序外签名,CLMM 不提供通用冻结指令。 冻结需要满足以下两个条件:
  1. Position 通过 OpenPositionV2 或 OpenPositionWithToken22Nft 打开。
  2. 至少一个池金库 mint 从程序的硬编码受限发行者列表中携带冻结权限。
只有当两个条件都满足时,CLMM 才会在铸造后立即冻结新创建的 position NFT 账户。OpenPosition V1 不应用此过滤器。参见 reference/program-addresses 了解当前列表。 冻结的 position:
  • 无法将其 NFT 转移到另一个代币账户。
  • 无法更改 NFT 代币账户的所有者。
  • 当记录的所有者签名时,仍可以增加或减少流动性并收集费用或奖励。
  • 仍可以关闭。ClosePosition 使用池 PDA 解冻 NFT 账户,然后在同一指令中销毁 NFT 并关闭 position 账户。
现有 positions 不会被迁移或追溯冻结。升级前创建的 Position NFT mints 保留其之前的冻结权限设置。
关闭冻结 position 的客户端必须将 position 的 pool_state 作为 ClosePosition 的第一个剩余账户附加。声明的 IDL 账户列表保持不变,因此较旧的客户端可以成功打开冻结 position,但稍后可能因 AccountLack 而无法关闭它。在支持这些池之前更新关闭构建器。

Token-2022 positions

CLMM 可以通过 OpenPositionV2 在经典 SPL Token 下铸造 position NFT,或通过 OpenPositionWithToken22Nft 在 Token-2022 下铸造。两个 V2 路径都检查池金库 mints 并应用相同的受限发行者冻结规则。OpenPosition V1 是遗留的经典代币路径,无法服务具有 Token-2022 金库 mints 的池。钱包和市场兼容性不同;Raydium 的 UI 跟踪两个 NFT 程序。

允许范围规则

在 OpenPosition 时,程序强制执行:
  1. tick_lower < tick_upper。
  2. tick_lower % tick_spacing == 0 和 tick_upper % tick_spacing == 0。
  3. MIN_TICK <= tick_lower 和 tick_upper <= MAX_TICK。
  4. 调用者已提供包含 tick_lower 和 tick_upper 的 tick 数组 PDA。它们不必已初始化 —— 程序会自己创建缺失的那一个。
  5. 位图扩展账户,如果此 position 扩展到扩展范围。
如果任何检查失败,指令会回滚:TickInvalidOrder(规则 1)、TickAndSpacingNotMatch(规则 2)、InvalidTickIndex(规则 3)或 MissingTickArrayBitmapExtensionAccount(规则 5);NotApproved 对应另一种情况,即 pool_state.status 设置了开仓位。参见 reference/error-codes。

“范围内”vs”范围外”

当 tick_lower <= tick_current < tick_upper 时,position 是范围内的。只有范围内的 positions 对 PoolState.liquidity 有贡献,因此只有它们赚取 swap 费用。 范围外的 position:
  • 持有一个代币的 100%。如果 tick_current < tick_lower —— 价格在范围下方 —— position 完全是 token0;如果 tick_current >= tick_upper —— 价格在范围上方 —— 则完全是 token1。价格以 token1/token0 计价,因此被上涨价格走过的 position 最终持有报价资产。
  • 不赚取 swap 费用。
  • 确实继续累积奖励,如果池的奖励流向范围外流动性发出 — 但 Raydium 的默认行为是”仅向范围内发出”,与 Uniswap v3 约定相匹配。参见 products/clmm/fees。
管理 CLMM positions 的 LPs 花费大部分精力将 positions 保持在范围内,因为价格会移动。

常见集成陷阱

  • 非间距端点。 从目标价格计算 tick 的代码必须在将其传递给 OpenPosition 之前对齐到 tick_spacing 的倍数,否则会因 TickAndSpacingNotMatch 回滚。SDK 助手是 TickUtil.getPriceAndTick({ price, mintADecimals, mintBDecimals, zeroForOne, tickSpacing })(注意:TickUtil,单数形式);自制数学通常会跳过对齐。
  • 缺失 tick 数组。 打开宽 position 会接触两个 tick 数组;忘记将它们作为可写账户传递会导致回滚。它们不需要事先存在 —— 程序会创建缺失的那一个 —— 但该账户必须在列表中。
  • Swap 后的陈旧 tick。 tick_current 可以在一次 swap 中跨越许多 ticks。如果你的 UX 从一个 RPC 调用显示”当前 tick”,然后在稍后的调用中打开 position,相对于实时价格的相对位置可能相差数十个 ticks。在签名前重新获取。
  • 带有额外元数据的 Position NFTs。 如果你构建一个识别 Raydium positions 的钱包,使用 position PDA / 程序数据,而不是硬编码的元数据字段。新 position mints 在创建时使用池 PDA 作为 mint 和冻结权限;在铸造单个 NFT 后移除 mint 权限。
  • 假设每个 position 都是可转移的。 在显示转移、市场、托管或销毁和赚取操作之前,读取 NFT 代币账户的 isFrozen 状态。

接下来去哪里

  • Math — swap 步进和费用增长推导,tick 边界参与其中。
  • Accounts — TickArrayState 和 PositionState 布局。
  • Fees and rewards — 范围内性如何控制费用累积。
  • algorithms/clmm-math — 集中流动性公式的共享推导。
来源: