19.Uniswap V3 手续费计算(二)
Uniswap V3 手续费计算深度解析 (二)
核心摘要 (Key Takeaways)
- 核心公式: 为特定价格区间 计算手续费的核心思想是:用全局累计手续费 减去该区间下方的累计手续费 和上方的累计手续费 。公式为:。
- 关键变量 : Uniswap V3 引入了一个名为
feeOutside(笔记中记为 ) 的关键变量。每个 ticki都会记录这个值,它巧妙地追踪了当价格在 ticki的“外部”时所累积的单位流动性手续费。 - 的更新机制: 当价格穿越一个 tick
i时,该 tick 对应的 会进行一次“翻转”更新。更新规则为:。这个机制是整个计算逻辑的精髓。 - 推导关系: 通过 和当前全局手续费 ,可以精确推导出任意时刻的 和 。它们的值取决于当前价格 相对于目标 tick
i的位置。
1. 背景:V3 手续费计算的复杂性
-
根本原因: 集中流动性 (Concentrated Liquidity)。与 V2 不同,V3 的流动性提供者 (LP) 将资金集中在特定的价格区间内。
-
带来的问题: 一个 LP 只能在当前市场价格处于其设定的流动性区间内时,才能赚取手续费。当交易导致价格移出该区间,LP 就停止赚取手续费,这部分手续费应归属于其他区间的 LP。
-
目标: 精确计算出在特定 LP 的价格区间内发生的交易对手续费的贡献。
-
案例:单一 LP 的手续费归属
- 一个用户在价格区间
[-5, +5](以 tick 为单位) 内提供了流动性(下图中紫色部分)。 - 全局手续费累积变量
feeGrowthGlobal() 会随着每一次符合方向的交易而持续增长(下图中绿色曲线)。当交易是反方向时,该值保持不变,手续费累积在另一个 token 的feeGrowthGlobal变量上。 - 当交易价格在
[-5, +5]区间内时,产生的手续费应归属于该用户。 - 当交易价格超出
[-5, +5]区间时(例如,价格移动到+6或-6),这部分交易消耗的是其他 LP 的流动性,产生的手续费与该用户无关。 - 因此,为了计算该用户的应得手续费,必须从全局累计手续费中,减去价格在其区间下方和上方时产生的手续费。
- 一个用户在价格区间
2. 核心计算框架
手续费计算的核心公式是:
- : 在价格区间 (lower tick, upper tick) 内累积的单位流动性手续费。这是我们要求解的目标。
- :
feeGrowthGlobal,一个全局变量,记录了自池子创建以来累积的总单位流动性手续费。 - :
feeBelow,在价格低于下边界 的区域内累积的单位流动性手续费。 - :
feeAbove,在价格高于上边界 的区域内累积的单位流动性手续费。
挑战: 如何高效、准确地计算出任意时刻的 和 ?
3. 解决方案:feeOutside 变量 (f_{o,i})
为了解决上述挑战,Uniswap V3 为每一个 tick i 都引入并维护了一个状态变量:feeOutside (记为 )。
3.1. f_{o,i} 的定义与规则
记录的是当价格在 tick i 的“外部”时,全局手续费 的累积值。它的行为由两条核心规则定义:
-
初始化规则: 当一个 tick
i首次被激活(即成为某个流动性区间的边界)时,其 被初始化。- 如果当前价格 (价格在 tick
i之上),则 。 - 如果当前价格 (价格在 tick
i之下),则 。
- 如果当前价格 (价格在 tick
-
更新规则: 每当价格穿越 (cross) tick
i时, 会被更新。更新公式为:
这个“翻转”操作是整个机制的核心,它巧妙地在 和 之间切换记录基准,从而跟踪外部手续费的增长。
3.2. feeOutside 的更新流程 (Mermaid 图)
图表类型选择说明: 此处选择流程图是因为它最能清晰地表达一个包含条件判断的逻辑流程。它直观地展示了系统是如何根据价格是否穿越 tick 这一条件来决定是否执行更新操作的。时序图或甘特图不适合描述这种算法逻辑。
flowchart TD
A[开始] --> B{价格 i_c 发生变化}
B --> C{价格是否穿越了 Tick i ?}
C -- 否 --> B
C -- 是 --> D[记录当前全局手续费 f_g]
D --> E[执行更新 f_o,i = f_g - f_o,i_old]
E --> B
flowchart TD
A[开始] --> B{价格 i_c 发生变化}
B --> C{价格是否穿越了 Tick i ?}
C -- 否 --> B
C -- 是 --> D[记录当前全局手续费 f_g]
D --> E[执行更新 f_o,i = f_g - f_o,i_old]
E --> B
flowchart TD
A[开始] --> B{价格 i_c 发生变化}
B --> C{价格是否穿越了 Tick i ?}
C -- 否 --> B
C -- 是 --> D[记录当前全局手续费 f_g]
D --> E[执行更新 f_o,i = f_g - f_o,i_old]
E --> Bflowchart TD
A[开始] --> B{价格 i_c 发生变化}
B --> C{价格是否穿越了 Tick i ?}
C -- 否 --> B
C -- 是 --> D[记录当前全局手续费 f_g]
D --> E[执行更新 f_o,i = f_g - f_o,i_old]
E --> B
图表解释:
这个流程描述了 的动态更新机制。系统持续监控价格 的变化。只有在价格恰好穿越某个 tick i 的瞬间,才会触发该 tick 的 更新。更新操作利用穿越时的全局手续费 和旧的 值,计算出新的 。如果价格没有穿越任何 tick,则所有 tick 的 保持不变。
4. 推导 f_b 和 f_a 与 f_{o,i} 的关系
通过分析价格多次穿越 tick 的过程,我们可以找到 / 和 之间的稳定关系。
4.1. 推导 f_b(i_l) (手续费 below lower tick)
-
场景分析: 假设我们关注下边界 tick 。我们通过一个详细的例子来观察当价格在 上下来回穿越时, 和 的变化。
-
案例:价格多次穿越下边界
- 下图展示了 随时间(交易)的变化。曲线的上升段表示手续费在累积,平坦段表示交易方向相反,不为我们关注的 token 累积手续费。
- 表示在关键时间点(穿越 tick 的瞬间)的 值。
- (直观计算): 阴影部分的面积总和,代表了所有价格低于 时累积的手续费。
- (按规则计算): 根据上一节的初始化和更新规则计算出的值。
-
推导结论: 通过对比在不同时间段内 和 的值,可以总结出以下规律:
- 当 (当前价格在 tick 之上) 时:
- 此时,价格在 的“内部”,所有低于 的手续费累积都已成为历史。 经过翻转后,其值正好等于所有低于 的历史手续费之和。
- 因此,下方的费用 。
- 当 (当前价格在 tick 之下) 时:
- 此时,价格在 的“外部”。 记录了上方所有累积费用的总和。
- 因此,下方的费用 。
- 当 (当前价格在 tick 之上) 时:
-
最终公式 for :
4.2. 推导 f_a(i_u) (手续费 above upper tick)
-
场景分析: 使用完全相同的逻辑,我们分析上边界 tick 。
-
案例:价格多次穿越上边界
- 与 的推导类似,我们观察 变化以及价格穿越 的过程。
- (直观计算): 阴影部分的面积总和,代表了所有价格高于 时累积的手续费。
-
推导结论:
- 当 (当前价格在 tick 之下) 时: 价格在 的“内部”, 记录的就是高于 的历史手续费之和,所以 。
- 当 (当前价格在 tick 之上) 时: 价格在 的“外部”, 记录的是低于 的手续费,所以高于 的手续费 。
-
最终公式 for :
5. 综合案例演算
现在,我们将所有公式整合起来,通过三个典型案例来验证整个计算逻辑的正确性。
假设一个 LP 在区间 [i_l, i_u] 提供流动性。初始时,价格 在区间内,。
- 初始化:
- (因为 )
- (因为 )
案例 1: 价格始终在 [i_l, i_u] 区间内波动
- 过程: 价格从未穿越 或 。 从 增长到 。
- 状态: 和 始终保持初始值,未被更新。
- 计算:
- (因为 )
- (因为 )
- 结论: 结果符合直觉,LP 赚取了从 到 的全部手续费增长。
案例 2: 价格向下跌破下边界 i_l
- 过程: 价格下跌,在 时穿越 。最终 增长到 ,且 。
- 状态: 被穿越一次, 更新。 未被穿越。
- 计算:
- (因为 )
- (因为 )
- 结论: 结果符合直觉,LP 只赚取了价格在跌破 之前(即从 到 )的手续费。
案例 3: 价格向上涨破上边界 i_u
- 过程: 价格上涨,在 时穿越 。最终 增长到 ,且 。
- 状态: 被穿越一次, 更新。 未被穿越。
- 计算:
- (因为 )
- (因为 )
- 结论: 结果符合直觉,LP 只赚取了价格在涨破 之前(即从 到 )的手续费。