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

Sqrt 价格表示

CLMM 将价格存储为 sqrt_price_x64 — token1 相对于 token0 的价格的平方根,表示为 Q64.64 定点数: sqrt_price_x64=p264\text{sqrt\_price\_x64} = \lfloor \sqrt{p} \cdot 2^{64} \rfloor 其中 p = token1_amount / token0_amount。使用 sqrt 而不是 p 可以线性化交换数学(代币数量增量与 Δsqrt_price 成线性关系),而 x64 定点表示在多 tick 交换中保持精度。 Tick ↔ sqrt 价格转换通过 bit-by-bit 对数近似预计算: sqrt_price_x64(t)264(1.0001)t/2\text{sqrt\_price\_x64}(t) \approx 2^{64} \cdot (1.0001)^{t/2} tick_math::get_sqrt_price_at_tick 中实现为基于查表的指数运算。

流动性作为规范单位

在范围 [sqrt_a, sqrt_b] 内(其中 sqrt_a < sqrt_b),流动性 L 的头寸映射到代币数量如下。设 sqrt_c = sqrt_price_x64 为池的当前价格。 这三个恒等式都来自于集中流动性在范围内满足的不变量 x = L / sqrt_py = L · sqrt_p 集成者通常需要反向计算:给定 amount0 / amount1 的存入,计算适合该范围的最大 L。SDK 的 LiquidityMath.getLiquidityFromTokenAmounts 完成此操作。在范围内情况的公式: L0=amount0sqrt_csqrt_bsqrt_bsqrt_c,L1=amount1sqrt_csqrt_a,L=min(L0,L1)L_0 = \text{amount0} \cdot \frac{\text{sqrt\_c} \cdot \text{sqrt\_b}}{\text{sqrt\_b} - \text{sqrt\_c}}, \qquad L_1 = \frac{\text{amount1}}{\text{sqrt\_c} - \text{sqrt\_a}}, \qquad L = \min(L_0, L_1) 哪一侧受限决定了实际消耗的比例;另一侧可能有剩余。

单个 tick 交换步骤

交换以步骤进行。每个步骤要么(a)消耗当前 tick 范围内的所有可用输入而不跨越 tick,要么(b)将价格精确移动到下一个已初始化的 tick。 给定当前状态 (sqrt_c, L)向下交换(token0 输入,token1 输出,sqrt_price 下降 — 价格以 token1/token0 报价,所以添加 token0 会降低它),下方的下一个已初始化 tick 位于 sqrt_t < sqrt_c。在这个微小区间内,输入和价格之间的关系为: Δamount0=L(1sqrt_t1sqrt_c)=L(sqrt_csqrt_t)sqrt_csqrt_t\Delta\text{amount0} = L \cdot \left( \frac{1}{\text{sqrt\_t}} - \frac{1}{\text{sqrt\_c}} \right) = \frac{L \cdot (\text{sqrt\_c} - \text{sqrt\_t})}{\text{sqrt\_c} \cdot \text{sqrt\_t}} 以及 Δamount1=L(sqrt_csqrt_t)\Delta\text{amount1} = L \cdot (\text{sqrt\_c} - \text{sqrt\_t}) token1 输入交换是镜像:sqrt_price 上升到下一个上方 tick,两个公式中哪个端点被减去会互换。程序将方向编码为 zero_for_one(当输入 mint 是 token_mint_0 时为 true),zero_for_one 交换向 MIN_SQRT_PRICE_X64 夹紧。 程序执行以下两种操作之一:
  • 整个输入是否适合? 如果剩余输入(扣除费用后)小于到达 sqrt_tΔamount0,精确求解新的 sqrt_c' sqrt_c=Lsqrt_cL+Δinputsqrt_c\text{sqrt\_c}' = \frac{L \cdot \text{sqrt\_c}}{L + \Delta\text{input} \cdot \text{sqrt\_c}} (对于精确输入 token0 → token1 交换)。交换在此步骤完成而不跨越 tick。
  • 输入超过 Δamount0 设置 sqrt_c' = sqrt_t,跨越 tick(应用 liquidity_net),剩余输入减少 Δamount0,输出增加 Δamount1,然后重复。
对于相反方向(token1 → token0,价格下降),公式中 sqrt_csqrt_t 互换,反演在另一个位置。 完整的 Rust 实现位于 raydium-clmm/programs/amm/src/libraries/swap_math.rs。那里的逻辑与 Uniswap v3 的 SwapMath.computeSwapStep 一一对应。

每个步骤的费用

交易费从每个步骤的输入金额中扣除,与 CPMM 的约定相同:
LP 部分通过更新全局费用增长累加器在当前范围内的流动性中分割: fee_growth_globalin+=lp_portion264L\text{fee\_growth\_global}_{\text{in}} \mathrel{+}= \text{lp\_portion} \cdot \frac{2^{64}}{L} — 即,它以每单位流动性的费用表示,Q64.64,因此大小为 L_i 的头寸在此交换中保持范围内将稍后读回 L_i · Δfee_growth_global / 2^{64} 欠付代币。 协议和基金部分分别累积到 PoolState.protocol_fees_token_{0,1}PoolState.fund_fees_token_{0,1},与 CPMM 相同。它们由 CollectProtocolFee / CollectFundFee 清扫。

范围外和范围内的费用增长

CLMM 费用计算的棘手部分:头寸仅在池的价格其范围时才赚取费用。池跟踪全局累积费用;头寸需要知道在其特定范围内的累积费用。 解决方案是基于 tick 的累加器。每个 tick 存储:
在 tick 初始化时:
  • 如果池的价格高于此 tick(tick_current >= this_tick),fee_growth_outside = fee_growth_global。(到目前为止赚取的所有费用相对于当前价格是”外部”的 — 即在此 tick 下方。)
  • 否则 fee_growth_outside = 0
当价格跨越 tick 时,程序翻转该 tick 的 fee_growth_outside fee_growth_outsidefee_growth_globalfee_growth_outside\text{fee\_growth\_outside} \gets \text{fee\_growth\_global} - \text{fee\_growth\_outside} 此不变量保持的是:对于任何 tick tfee_growth_outside(t) 等于在 tick_current 位于 t 的相反一侧时累积的费用。 范围 [tick_lower, tick_upper] 内的费用增长随后推导为:
这是 Uniswap-v3 费用增长公式,未改变。

头寸存储和读取的内容

PersonalPositionState 存储 fee_growth_inside_0_last_x64fee_growth_inside_1_last_x64:上次触及头寸时的 fee_growth_inside 值。 在任何后续触及(增加、减少、收集)时,程序:
  1. 使用上述公式计算当前 fee_growth_inside_{0,1}_x64
  2. 计算 Δ = fee_growth_inside_now − fee_growth_inside_last(u128 上的模减法)。
  3. Δ × position.liquidity / 2^{64} 添加到 tokens_fees_owed_{0,1}
  4. fee_growth_inside_last 更新为新值。
代币实际上仅在 CollectFees / DecreaseLiquidity 时从保管库中移出,针对 tokens_fees_owed

奖励

池的最多 3 个奖励流中的每一个都使用相同的增长范围内机制,在其自己的 reward_growth_global_x64 累加器中。在发放时: reward_growth_global+=emission_per_secondΔt264L\text{reward\_growth\_global} \mathrel{+}= \text{emission\_per\_second} \cdot \Delta t \cdot \frac{2^{64}}{L} — 发放与活跃流动性成反比,因此更密集的池每秒向每个头寸支付的比例更少,但总体上跨越更多头寸。每个头寸欠付的奖励为 reward_owed=(reward_growth_insidenowreward_growth_insidelast)L/264\text{reward\_owed} = (\text{reward\_growth\_inside}_{\text{now}} - \text{reward\_growth\_inside}_{\text{last}}) \cdot L / 2^{64} 并由 DecreaseLiquidity / DecreaseLiquidityV2 支付 — 没有独立的收集奖励指令。参见 /zh/products/clmm/fees

工作示例:精确输入交换

假设:
  • tick_spacing = 60
  • sqrt_price_x64 = 1 × 2^{64} — 价格 = 1.0,所以 tick_current = 0
  • 活跃流动性 L = 1_000_000 × 2^{64}
  • 下方的下一个已初始化 tick:t = −60(sqrt_price_b ≈ 0.997005 × 2^{64})。token0 输入交换向下移动,所以上方的 tick 在这里无关。
  • 交易费率:500(0.05%)。
用户:SwapBaseInput 精确输入 1,000 token0。 步骤 1 — 费用:
步骤 2 — 999 是否适合当前 tick 范围内?
999 < 3004.4,所以整个输入适合而不跨越 tick。 步骤 3 — 新价格:
sqrt_c' 略微低于 sqrt_c,这是正确的方向:token0 → token1 交换向池中添加 token0,因此降低 token1/token0 价格。 步骤 4 — 输出金额:
考虑舍入后,用户收到约 998 token1。费用(1 token0)在 LP、协议和基金之间分割,按 trade_fee_rate × protocol_fee_rate / 1e6(基金类似);LP 部分流入 fee_growth_global_0_x64

交换期间的限价单匹配

当交换步骤跨越持有开放限价单的 tick 时,这些订单在 LP 曲线之前以 tick 的精确价格消耗交换输入。匹配在 tick 内按 order_phase 队列 FIFO。

TickState 上的每队列状态

两队列布局存在是因为新订单可能在旧队列仍在填充时在 tick 上打开。新打开的订单加入 orders_amount 并继承下一个 order_phase;它们在前一个队列完全消耗之前无法填充。

匹配步骤

在交换期间每次 tick 跨越时发生的匹配的伪代码:
流向限价单所有者的输出代币不会在每次交换时转移。它们虚拟地位于池的输出保管库中,直到订单所有者调用 SettleLimitOrder(或 DecreaseLimitOrder)。池只是通过 unfilled_ratio_x64 跟踪队列现在填充了多少。每个 LimitOrderState 在打开时存储其自己的 (order_phase, unfilled_ratio_x64) 快照,因此结算简化为:
这个 O(1) 结算是整个队列设计的要点 — tick 可以填充任意多个订单而不产生每个订单的 gas。

与 LP 曲线的交互

在交换步骤中,限价单匹配发生在 tick (零 Δsqrt_price);LP 曲线消耗发生在 tick 之间。因此顺序为:
  1. 跨越 tick t_cross(首先应用 LP liquidity_net 变化,因为这是 Uniswap-V3 的做法)。
  2. 填充位于 t_cross 的任何限价单。
  3. 沿 LP 曲线继续到下一个已初始化的 tick 或 swap_input 耗尽。
限价单因此为交易者在恰好订单的 tick 价格处提供更多有效流动性(价格改进效果),代价是 LP 不在该部分交换量上赚取费用 — 交换的限价单部分对交换者是无费用的,因为限价单下单者充当做市商。动态费用附加费(如果启用)仍适用于同一交换的 LP 部分。

动态费用推导

PoolState.dynamic_fee_info 携带波动率状态。每个交换步骤计算每步费率为: fee_ratetotal=trade_fee_rateconfig+dynamic_fee_control(vol_acctick_spacing)2DctrlSvol2动态附加费\text{fee\_rate}_{\text{total}} = \text{trade\_fee\_rate}_{\text{config}} + \underbrace{\frac{\text{dynamic\_fee\_control} \cdot (\text{vol\_acc} \cdot \text{tick\_spacing})^2} {D_{\text{ctrl}} \cdot S_{\text{vol}}^2}}_{\text{动态附加费}} 其中:
  • Dctrl=100,000D_{\text{ctrl}} = 100{,}000DYNAMIC_FEE_CONTROL_DENOMINATOR
  • Svol=10,000S_{\text{vol}} = 10{,}000VOLATILITY_ACCUMULATOR_SCALE
  • vol_acc 是下面更新规则后的每交换累加器
  • tick_spacing 来自 PoolState.tick_spacing
结果被夹紧在 100,000/106=10%100{,}000 / 10^6 = 10\%

累加器更新

两个规则在每次交换时按顺序应用: 衰减。 参考底线根据自上次更新以来的时间衰减: vol_ref={0if Δt>decay_periodvol_accprevreduction_factor10,000if filter_period<Δtdecay_periodvol_refprevif Δtfilter_period\text{vol\_ref} = \begin{cases} 0 & \text{if } \Delta t > \text{decay\_period} \\ \text{vol\_acc}_{\text{prev}} \cdot \dfrac{\text{reduction\_factor}}{10{,}000} & \text{if } \text{filter\_period} < \Delta t \le \text{decay\_period} \\ \text{vol\_ref}_{\text{prev}} & \text{if } \Delta t \le \text{filter\_period} \end{cases} 累积。 新累加器是参考加上自前一个参考索引以来遍历的 tick 距离: vol_acc=min(vol_ref+treftnowSvol,max_vol_acc)\text{vol\_acc} = \min\left( \text{vol\_ref} + \left| t_{\text{ref}} - t_{\text{now}} \right| \cdot S_{\text{vol}}, \text{max\_vol\_acc} \right) tick_spacing_index_referencetreft_{\text{ref}})以 tick 间距单位而非原始 tick:tref=tick_current/tick_spacingt_{\text{ref}} = \lfloor \text{tick\_current} / \text{tick\_spacing} \rfloor

为什么在 tick 距离上是抛物线

对累加器平方意味着费用随价格从其参考点走开的距离的平方上升。经验上这与随机游走压力下价格的方差缩放相匹配:2× tick 偏移意味着 4× 隐含波动率,因此收费 4× 附加费。dynamic_fee_control 参数校准绝对水平。 filter_period 窗口防止微小的亚秒级振荡(例如 MEV 机器人三明治)膨胀累加器。decay_period 窗口防止单个过去的尖峰在市场平静后无限期地收费。

数值稳健性

  • 所有中间乘积通过 u128u256 形状的算术。CLMM 使用 U128Sqrt 辅助程序和 FullMath::mulDiv 模式,直接从 Uniswap v3 移植。
  • 除法舍入按步骤选择以在本地强制不变量 k' ≥ kSwapBaseInput舍入输出;SwapBaseOutput舍入输入。
  • PoolState.liquidity 降至零的 tick 跨越是允许的(价格可以遍历”流动性洞”),但交换只是推进到下一个已初始化的 tick 而不消耗输入,不收费。
  • 溢出防护:sqrt_price_x64 保持在包含范围 [MIN_SQRT_PRICE_X64, MAX_SQRT_PRICE_X64] 内,对应于 [MIN_TICK, MAX_TICK]。会推过任一边界的交换以 SqrtPriceLimitOverflow 还原。

接下来去哪里

来源: