本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →
为什么需要 ticks
CLMM 的流动性集中在价格范围内。为了在链上使范围易于处理,价格被量化为整数 ticks,其中每个 tick 是前一个的常数倍数: 一个 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 会因 InvalidTickIndex 而回滚。
常见的已发布间距:
间距越粗,需要初始化的 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 数组 — 它们使用位图跳过未初始化的数组。SDK 的 open-position 流程检查选定的范围,计算它接触的 tick 数组列表,如果有缺失的数组,会在与OpenPosition 相同的交易中添加 init_tick_array 指令。
Tick 数组不会被关闭
一旦 tick 数组被初始化,它就会在池的整个生命周期内持续存在。程序不提供关闭 tick 数组的路径,即使initialized_tick_count 返回到零也不行。Tick 数组没有租金恢复;第一个接触数组的 position 支付的租金会永久锁定在该账户中。这是一个有意的权衡:重用现有 tick 数组对后续每个 position 都是免费的,所以一个交易频繁的池只需为每个 (pool, start_tick_index) 槽支付一次租金,无论流动性变化如何。
位图
找到”当前 tick 左/右的下一个已初始化 tick”必须很快 — swap 可能会跨越许多 ticks。池在PoolState 中为 tick 0 周围 ±1,024 个数组的范围内存储一个每 tick 数组 1 位的位图。在该范围之外(全范围 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。
- Position A:
tick_lower = -120,tick_upper = 0,流动性L_A = 100。 - Position B:
tick_lower = -60,tick_upper = 60,流动性L_B = 50。
不同
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(超过两者)
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 都将其 CLMMpool_state 记录为冻结权限。这并不意味着每个新 position 都被冻结。 对于普通池和每个非匹配 position,NFT 代币账户保持未冻结和可转移。池 PDA 无法在 CLMM 程序外签名,CLMM 不提供通用冻结指令。
冻结需要满足以下两个条件:
- Position 通过
OpenPositionV2或OpenPositionWithToken22Nft打开。 - 至少一个池金库 mint 从程序的硬编码受限发行者列表中携带冻结权限。
OpenPosition V1 不应用此过滤器。参见 reference/program-addresses 了解当前列表。
冻结的 position:
- 无法将其 NFT 转移到另一个代币账户。
- 无法更改 NFT 代币账户的所有者。
- 当记录的所有者签名时,仍可以增加或减少流动性并收集费用或奖励。
- 仍可以关闭。
ClosePosition使用池 PDA 解冻 NFT 账户,然后在同一指令中销毁 NFT 并关闭 position 账户。
Token-2022 positions
CLMM 可以通过OpenPositionV2 在经典 SPL Token 下铸造 position NFT,或通过 OpenPositionWithToken22Nft 在 Token-2022 下铸造。两个 V2 路径都检查池金库 mints 并应用相同的受限发行者冻结规则。OpenPosition V1 是遗留的经典代币路径,无法服务具有 Token-2022 金库 mints 的池。钱包和市场兼容性不同;Raydium 的 UI 跟踪两个 NFT 程序。
允许范围规则
在OpenPosition 时,程序强制执行:
tick_lower < tick_upper。tick_lower % tick_spacing == 0和tick_upper % tick_spacing == 0。MIN_TICK <= tick_lower和tick_upper <= MAX_TICK。- 调用者已提供包含
tick_lower和tick_upper的 tick 数组 — 要么已初始化,要么通过同一交易中的init_tick_array。 - 位图扩展账户,如果此 position 扩展到扩展范围。
InvalidTickIndex、NotApproved 或 InsufficientLiquidity 而回滚,具体取决于哪个约束。参见 reference/error-codes。
“范围内”vs”范围外”
当tick_lower <= tick_current < tick_upper 时,position 是范围内的。只有范围内的 positions 对 PoolState.liquidity 有贡献,因此只有它们赚取 swap 费用。
范围外的 position:
- 持有一个代币的 100%(其范围已经走过的那个)。具体来说,如果
tick_current < tick_lower,position 仅持有 token1(它已经被价格移动”卖出”);如果tick_current >= tick_upper,它仅持有 token0。 - 不赚取 swap 费用。
- 确实继续累积奖励,如果池的奖励流向范围外流动性发出 — 但 Raydium 的默认行为是”仅向范围内发出”,与 Uniswap v3 约定相匹配。参见
products/clmm/fees。
常见集成陷阱
- 非间距端点。 从目标价格计算 tick 的代码必须在将其传递给
OpenPosition之前对齐到tick_spacing的倍数。SDK 助手(TickUtils.getTickWithPriceAndTickspacing)会执行此操作;自制数学通常不会。 - 缺失 tick 数组。 打开宽 position 可能需要初始化多个 tick 数组;忘记将它们作为可写账户传递会导致回滚。SDK 的
openPositionFromBase为你返回列表。 - 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— 集中流动性公式的共享推导。
raydium-io/raydium-clmm—tick_array、tick、position模块- “Uniswap v3 Core” 白皮书,§6(ticks),§7(费用增长)

