Documentation Index
Fetch the complete documentation index at: https://docs.raydium.io/llms.txt
Use this file to discover all available pages before exploring further.
本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →
Tick 存在的原因
CLMM 的流动性集中在价格范围内。为了在链上使范围易于处理,价格被量化为整数 tick,其中每个 tick 是前一个的常数倍数: 一个 tick 对应 0.01% 的价格变动,或约 1 个基点。映射如下:Tick 索引 i | 价格倍数 |
|---|---|
0 | 1.0000 |
100 | 1.0100(≈ +1.00%) |
-100 | 0.9900(≈ −0.99%) |
10000 | 2.7181(≈ e) |
MAX_TICK = 443636 | ≈ 1.84e19 |
MIN_TICK = -443636 | ≈ 5.42e-20 |
MIN_TICK 和 MAX_TICK 的选择保证 sqrt_price_x64 在两端都能适配 u128。每个池都强制 tick_lower >= MIN_TICK 和 tick_upper <= MAX_TICK。实际上,Web UI 会将范围限制在更窄的范围内,以防止用户将流动性锁定在无法到达的 tick。
Tick 间距
池的AmmConfig 固定了一个 tick 间距 — 持仓只能用此间距作为端点。如果 tick_spacing = 60,只有 …, −120, −60, 0, 60, 120, … 这些 tick 有效。尝试用端点 31 打开持仓会返回 InvalidTickIndex 错误。
常见的公开间距:
| 手续费等级 | trade_fee_rate | Tick 间距 | 每个 tick 持仓的最粗价格步长 |
|---|---|---|---|
| 0.01% | 100 | 1 | 0.01% |
| 0.05% | 500 | 10 | 0.10% |
| 0.25% | 2500 | 60 | 0.60% |
| 1.00% | 10000 | 120 | 1.21% |
Tick 数组
池不会在单独的账户中存储按 tick 的状态。相反,TICK_ARRAY_SIZE 个相邻的 tick(当前 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个整数 tick。 start_tick_index是 3600 的倍数:…, -7200, -3600, 0, 3600, 7200, …。
t = 2040 在 tick_spacing = 60 时位于 start_tick_index = 0 的 tick 数组中。持仓端点 t = 4200 位于 start_tick_index = 3600 的数组中。
何时创建数组
Tick 数组是懒加载的:引用其内任何 tick 的第一个持仓初始化该数组并支付租金。交换不初始化 tick 数组 — 它们使用位图跳过未初始化的数组。SDK 的打开持仓流程检查所选范围,计算其接触的 tick 数组列表,并在同一交易中与OpenPosition 一起添加 init_tick_array 指令(如果有遗漏)。
Tick 数组不被关闭
一旦 tick 数组被初始化,它就在池的生命周期内持续存在。程序不提供关闭 tick 数组的路径,即使initialized_tick_count 返回到零。Tick 数组没有租金恢复;第一个接触数组的持仓支付的租金被永久锁定在该账户中。这是一个有意的权衡:重用现有 tick 数组对每个后续持仓都是免费的,因此繁忙交易的池只需为每个 (pool, start_tick_index) 槽支付一次租金成本,无论变动多频繁。
位图
找到”当前 tick 左侧/右侧的下一个初始化 tick”必须很快 — 一次交换可能会跨越许多 tick。池在PoolState 中存储一个围绕 tick 0 的 ±1,024 数组范围的 1 位每 tick 数组位图。超出该范围(全范围持仓、异常设置),TickArrayBitmapExtension 提供溢出。
交换遍历位图:lowest_set_bit_above(tick_current_array_index) 给出交换正在跨越的一侧的下一个具有初始化 tick 的数组。在该数组内,类似的位扫描定位下一个初始化 tick。
liquidity_gross 和 liquidity_net
每个初始化的 tick 存储两个流动性值:
liquidity_gross— 将此 tick 作为任一端点的所有持仓的L之和。当liquidity_gross降至零时,该 tick 变为未初始化,可从位图中移除。liquidity_net— 当价格向上跨越此 tick(在 tick 空间中从左到右)时,对池级liquidity的有符号变化。如果此 tick 是大小为L的持仓的下界,它贡献+L;如果是上界,贡献−L。
- 持仓 A:
tick_lower = -120,tick_upper = 0,流动性L_A = 100。 - 持仓 B:
tick_lower = -60,tick_upper = 60,流动性L_B = 50。
| Tick | 涉及方 | liquidity_gross | liquidity_net |
|---|---|---|---|
-120 | A 下界 | 100 | +100 |
-60 | B 下界 | 50 | +50 |
0 | A 上界 | 100 | −100 |
60 | B 上界 | 50 | −50 |
tick_current 值的池级 liquidity:
tick_current = -180:liquidity = 0(任何持仓之前)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 机制。
作为 NFT 的持仓
Raydium CLMM 持仓是 NFT。打开持仓会将一个全新的铸币(供应量为 1)铸入调用者的钱包,该铸币的权限由 CLMM 程序持有。程序将持仓所有权关联到 CPI 时该铸币的 ATA 中持有余额的任何人。 结果:- 持仓是可转移的。 钱包可以通过转移 NFT 来出售或空投持仓。新持有人可以随后调用
CollectRewards、IncreaseLiquidity等。 - 持仓在 CLMM 外可寻址。 市场和钱包将持仓显示为其他 NFT。SDK 在铸币元数据上设置合理的
name/symbol。 - 持仓的 PDA 从 NFT 铸币衍生。 你可以找到
PersonalPositionState而无需知道谁当前持有它。
Token-2022 持仓
较新的 CLMM 池可以在 Token-2022 下铸造持仓,而不是经典的 SPL Token。程序暴露两个并行的打开指令 —OpenPosition 和 OpenPositionWithToken22Nft — 除了哪个代币程序拥有 NFT 铸币外,语义相同。钱包和市场兼容性有所不同;Raydium 的 UI 追踪两者。
允许范围规则
在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。 - 位图扩展账户,如果此持仓扩展到扩展范围。
InvalidTickIndex、NotApproved 或 InsufficientLiquidity 之一,取决于哪个约束。见 reference/error-codes。
“在范围内”与”在范围外”
当tick_lower <= tick_current < tick_upper 时,持仓是在范围内。只有在范围内的持仓对 PoolState.liquidity 有贡献,因此只有它们赚取交换费。
范围外的持仓:
- 持有 100% 的一个代币(其范围已走过的那个)。具体来说,如果
tick_current < tick_lower,持仓仅持有 token1(它已被价格移动”卖出”);如果tick_current >= tick_upper,仅持有 token0。 - 不赚取交换费。
- 确实继续累积奖励,如果池的奖励流向范围外流动性发送 — 但 Raydium 的默认行为是”仅向范围内发送”,符合 Uniswap v3 约定。见
products/clmm/fees。
常见集成陷阱
- 间距外端点。 从目标价格计算 tick 的代码必须在将其传给
OpenPosition之前捕捉到tick_spacing的倍数。SDK 帮助程序(TickUtils.getTickWithPriceAndTickspacing)会做这个;自创数学通常不会。 - 遗漏 tick 数组。 打开宽范围持仓可能需要初始化多个 tick 数组;忘记将它们作为可写账户传递会导致返回。SDK 的
openPositionFromBase为你返回列表。 - 交换后的陈旧 tick。
tick_current在一次交换中可以跨越多个 tick。如果你的 UX 显示来自一个 RPC 调用的”当前 tick”,然后在后来的调用中打开持仓,相对于实时价格的持仓相对位置可能相差数十个 tick。在签署前重新获取。 - 带有额外元数据的持仓 NFT。 如果你建立一个识别 Raydium 持仓的钱包,通过其铸币权限(= CLMM 程序的 PDA)检测它们,而不是通过硬编码的元数据字段。
接下来去哪里
- 数学 — 交换步骤和 tick 边界参与的费用增长推导。
- 账户 —
TickArrayState和PositionState布局。 - 费用和奖励 — 范围内状态如何门控费用累积。
algorithms/clmm-math— 集中流动性公式的共享推导。
raydium-io/raydium-clmm—tick_array、tick、position模块- “Uniswap v3 Core” 白皮书,§6(tick),§7(费用增长)


