18.Uniswap V3 手续费计算(一)

核心摘要 (Key Takeaways)

  • V3手续费的复杂性根源:与V2的全范围流动性不同,Uniswap V3的集中流动性允许LP在自定义价格区间(ticks)提供流动性,这使得手续费的计算和分配变得极其复杂。
  • 全局手续费累加器 feeGrowthGlobal:Uniswap V3引入了一个全局状态变量 feeGrowthGlobal,它代表每单位流动性可以获得的总手续费。这个值会随着每笔交易单调递增。
  • 手续费按token in收取:手续费只对交易中“进入”池子的代币收取。因此,系统需要为交易对中的两种代币(token0token1)分别维护两个独立的feeGrowthGlobal变量,导致其增长呈现阶梯状。
  • LP的核心关注点 feeGrowthInside:LP只关心在其提供的流动性区间内产生的手су费。因此,需要从全局feeGrowthGlobal中剥离掉区间外的部分,得到feeGrowthInside
  • 精巧的边界记录机制 feeGrowthOutside:V3通过在每个tick边界上记录一个名为feeGrowthOutside的值,来巧妙地计算任意区间的feeGrowthInside。当价格穿越一个tick时,该tickfeeGrowthOutside值会被更新,从而精确追踪内外手续费的累积情况。

1. Uniswap V2 vs. V3 手续费机制对比

1.1 Uniswap V2:简单直接

  • 流动性模型:流动性平均分布在从 0+∞ 的整个价格曲线上。
  • 手续费收取
    • 对每笔交易收取固定的千分之三(0.3%)手续费。
    • 手续费只对 token in (交易中流入池子的代币)收取。
    • 案例
      • 用 X 换 Y:收取 0.003 * X,实际用于交易的是 0.997 * X
      • 用 Y 换 X:收取 0.003 * Y,实际用于交易的是 0.997 * Y
  • 手续费分配:收取的手续费会自动累积到流动性池中,为LP实现自动复投增值,无需任何额外操作。

1.2 Uniswap V3:挑战重重

  • 核心挑战集中流动性 (Concentrated Liquidity)
    • 不同的LP可以在不同的价格区间(由tick lowertick upper定义,视频中也称为PAPB)提供流动性。
    • 手续费档位:V3提供了多个手续费档位供流动性池选择,如 0.01%, 0.05%, 0.3%, 1%
  • 需要解决的问题
    1. 按比例分配:在同一个tick区间内,手续费需按LP提供的流动性比例进行分配。
    2. tick计算:当交易价格跨越tick边界,导致总流动性发生变化时,如何精确计算每个LP在其活跃区间内应得的手续费。

2. 手续费分配的基本原理与 feeGrowth 的引入

2.1 单次交易的分配逻辑

  • 核心思想:LP在某个价格区间获得的手续费,等于该区间产生的总手续费乘以该LP提供的流动性占比
  • 案例:单次跨tick交易
    • 背景
      • 一个LP在某个范围内提供了流动性,大小为 S
      • 一笔交易发生,使得价格从tick_0移动到tick_1
      • tick0tick_0区间的总流动性为L0L_0,产生的总手续费为F0F_0
      • tick_1区间的总流动性为L1L_1,产生的总手续费为F1F_1
    • 计算LP应得手续费:该LP获得的手续费 FeeSFee_S 是其在每个tick区间所获手续费的总和。 FeeS=(F0SL0)+(F1SL1) Fee_S = \left(F_0 \cdot \frac{S}{L_0}\right) + \left(F_1 \cdot \frac{S}{L_1}\right)

2.2 feeGrowthGlobal:全局手续费累加器

  • 概念推导:将上述逻辑扩展到 N 次交易,LP的总手续费可以表示为: FeeS=Si=0NFiLi Fee_S = S \cdot \sum_{i=0}^{N} \frac{F_i}{L_i}
  • 定义:Uniswap V3将公式中求和的部分 i=0NFiLi\sum_{i=0}^{N} \frac{F_i}{L_i} 定义为一个全局变量,称为 feeGrowthGlobal (视频中简称为FG)。
    • 物理含义feeGrowthGlobal 代表从系统部署开始,每单位流动性 (S=1) 累计可以获得的总手续费。
    • 简化公式:LP的总手续费 FeeS=SfeeGrowthGlobalFee_S = S \cdot feeGrowthGlobal
  • 核心特性
    1. 全局状态feeGrowthGlobal 是一个存储在合约中的全局变量,随每次交易更新。
    2. 单调递增:只要有交易发生,feeGrowthGlobal 的值就会不断累积,永远不会减少。
    3. 按代币分离:由于手续费只对 token in 收取,因此合约为交易对中的两个代币分别维护了两个feeGrowthGlobal变量:
      • **feeGrowthGlobal0** (针对 token0)
      • **feeGrowthGlobal1** (针对 token1)
    4. 阶梯式增长
      • 当价格上涨时(例如,用token0token1),feeGrowthGlobal0会增加,而feeGrowthGlobal1保持不变(呈水平线)。
      • 当价格下跌时(例如,用token1token0),feeGrowthGlobal1会增加,而feeGrowthGlobal0保持不变。
      • 这导致每个feeGrowthGlobal变量的增长曲线都呈现出阶梯状

3. 从全局到局部:计算 feeGrowthInside

  • LP的视角:一个在特定价格区间 [il,iu][i_l, i_u] (tick lower, tick upper) 提供流动性的LP,只关心在这个区间内(inside)累积的手续费,区间外(outside)的与他无关。
  • 核心概念
    • feeGrowthInside:在LP指定的 [il,iu][i_l, i_u] 区间内累积的每单位流动性手续费。
    • feeGrowthOutside:在该区间之外累积的手续费。
  • 计算逻辑feeGrowthInside 等于全局的 feeGrowthGlobal 减去所有在区间之外累积的部分。
    • 白皮书与视频中的公式 (变量名来自视频讲解): FR(il,iu)=FgFB(il)FA(iu) F_{R(i_l, i_u)} = F_g - F_{B(i_l)} - F_{A(i_u)}
    • 公式解读
      • FR(il,iu)F_{R(i_l, i_u)}:我们想求的 feeGrowthInside (Range Fee)。
      • FgF_g:当前的全局 feeGrowthGlobal
      • FB(il)F_{B(i_l)}:所有低于下边界 ili_l 的手续费累积 (Fee Below)。
      • FA(iu)F_{A(i_u)}:所有高于上边界 iui_u 的手续费累积 (Fee Above)。

4. feeGrowthOutside:精巧的边界状态记录机制

为了高效计算任意区间的FB(il)F_{B(i_l)}FA(iu)F_{A(i_u)},V3合约并不直接记录它们,而是采用了一个更巧妙的机制:在每个被激活的tick记录一个状态变量 **feeGrowthOutside** (视频中简称为FO(i)F_{O(i)})。

4.1 feeGrowthOutside 的初始化

当一个LP在当前价格ici_c时,添加 [il,iu][i_l, i_u] 区间的流动性,其边界tickfeeGrowthOutside值会按以下规则初始化:

  • 规则
    • 如果一个tick ii 的位置小于等于当前价格ici_c (iici \le i_c),则 FO(i)=FgF_{O(i)} = F_g (设为当前的全局feeGrowthGlobal)。
    • 如果tick ii 的位置大于当前价格ici_c (i>ici > i_c),则 FO(i)=0F_{O(i)} = 0
  • 逻辑解释:这个初始化的目的是记录“在我加入之前,已经累积了多少手续费”。对于低于当前价格的下边界ili_l,它将所有历史手续费都记录为“外部”,因为这些都与该LP无关。

4.2 feeGrowthOutside 的更新

  • 触发时机:当交易价格穿越 (cross) 某个tick ii 时。
  • 更新规则FO(i)new=FgFO(i)old F_{O(i)_{\text{new}}} = F_g - F_{O(i)_{\text{old}}}
  • 逻辑解释:这是一个巧妙的“翻转”操作。每次穿越边界时,合约会用穿越瞬间的全局FgF_g减去该边界上记录的旧FO(i)F_{O(i)}值。这使得feeGrowthOutside能动态追踪边界内外的费用划分,从而可以方便地计算出任意区间的内部增长。

5. 场景推演:feeGrowthInside 的计算过程

5.1 可视化流程图 (Mermaid Diagram)

flowchart TD
    A[LP在 i_l, i_u 区间添加流动性] --> B[初始化边界值];
    B --> C[根据当前价格 i_c];
    C --> C1[若 i_l 小于等于 i_c, F_O(i_l) = F_g0];
    C --> C2[若 i_u 大于 i_c, F_O(i_u) = 0];
  
    subgraph " "
    direction LR
    C1 --> D;
    C2 --> D;
    end

    D[交易发生, 价格变动] --> E{价格是否穿越边界?};
  
    E -- 否, 在区间内变动 --> F[feeGrowthInside = 当前F_g - F_g0];
  
    E -- 是 --> H{穿越哪个边界?};
    H -- 向右穿越 i_u --> I[更新 F_O(i_u) = F_g1 - F_O(i_u)_old];
    I --> J[feeGrowthInside = F_g1 - F_g0];
  
    H -- 向左穿越 i_l --> K[更新 F_O(i_l) = F_g1 - F_O(i_l)_old];
    K --> L[feeGrowthInside = F_g1 - F_g0];

    subgraph " "
    direction LR
    F --> M;
    J --> M;
    L --> M;
    end
  
    M[计算LP总手续费 = S 乘以 feeGrowthInside];
flowchart TD
    A[LP在 i_l, i_u 区间添加流动性] --> B[初始化边界值];
    B --> C[根据当前价格 i_c];
    C --> C1[若 i_l 小于等于 i_c, F_O(i_l) = F_g0];
    C --> C2[若 i_u 大于 i_c, F_O(i_u) = 0];
  
    subgraph " "
    direction LR
    C1 --> D;
    C2 --> D;
    end

    D[交易发生, 价格变动] --> E{价格是否穿越边界?};
  
    E -- 否, 在区间内变动 --> F[feeGrowthInside = 当前F_g - F_g0];
  
    E -- 是 --> H{穿越哪个边界?};
    H -- 向右穿越 i_u --> I[更新 F_O(i_u) = F_g1 - F_O(i_u)_old];
    I --> J[feeGrowthInside = F_g1 - F_g0];
  
    H -- 向左穿越 i_l --> K[更新 F_O(i_l) = F_g1 - F_O(i_l)_old];
    K --> L[feeGrowthInside = F_g1 - F_g0];

    subgraph " "
    direction LR
    F --> M;
    J --> M;
    L --> M;
    end
  
    M[计算LP总手续费 = S 乘以 feeGrowthInside];
flowchart TD
    A[LP在 i_l, i_u 区间添加流动性] --> B[初始化边界值];
    B --> C[根据当前价格 i_c];
    C --> C1[若 i_l 小于等于 i_c, F_O(i_l) = F_g0];
    C --> C2[若 i_u 大于 i_c, F_O(i_u) = 0];
  
    subgraph " "
    direction LR
    C1 --> D;
    C2 --> D;
    end

    D[交易发生, 价格变动] --> E{价格是否穿越边界?};
  
    E -- 否, 在区间内变动 --> F[feeGrowthInside = 当前F_g - F_g0];
  
    E -- 是 --> H{穿越哪个边界?};
    H -- 向右穿越 i_u --> I[更新 F_O(i_u) = F_g1 - F_O(i_u)_old];
    I --> J[feeGrowthInside = F_g1 - F_g0];
  
    H -- 向左穿越 i_l --> K[更新 F_O(i_l) = F_g1 - F_O(i_l)_old];
    K --> L[feeGrowthInside = F_g1 - F_g0];

    subgraph " "
    direction LR
    F --> M;
    J --> M;
    L --> M;
    end
  
    M[计算LP总手续费 = S 乘以 feeGrowthInside];

流程解释: 该图展示了从LP添加流动性开始,如何根据价格变动情况(是否穿越边界)来更新feeGrowthOutside值,并最终计算出区间内的手续费增长feeGrowthInside。这是一个包含条件判断的逻辑过程,因此流程图是最佳的可视化工具。

5.2 具体计算案例

  • 前提:LP在T=0时刻添加流动性,此时全局feeGrowthGlobalFg0F_{g0}。根据初始化规则,我们得到FO(il)=Fg0F_{O(i_l)} = F_{g0}FO(iu)=0F_{O(i_u)} = 0

  • 案例一:价格在区间内变动(未穿越边界)

    • 过程:价格在ili_liui_u之间波动,但从未穿越它们。
    • 计算:经过一段时间后,全局feeGrowthGlobal增长到FgF_g
    • feeGrowthInside = 当前全局feeGrowth - 初始全局feeGrowth = FgFg0F_g - F_{g0}
  • 案例二:向右穿越上限 iui_u

    • 过程:价格上涨,在T=1时刻穿越了上边界iui_u,此时的全局feeGrowthGlobalFg1F_{g1}
    • 边界更新
      • FO(il)F_{O(i_l)}未被穿越,保持Fg0F_{g0}不变。
      • FO(iu)F_{O(i_u)}被穿越,更新为:FO(iu)new=Fg1FO(iu)old=Fg10=Fg1F_{O(i_u)_{\text{new}}} = F_{g1} - F_{O(i_u)_{\text{old}}} = F_{g1} - 0 = F_{g1}
    • 计算:在这个穿越瞬间,LP在区间内累积的feeGrowthInsideFg1Fg0F_{g1} - F_{g0}
  • 案例三:向左穿越下限 ili_l

    • 过程:价格下跌,在T=1时刻穿越了下边界ili_l,此时的全局feeGrowthGlobalFg1F_{g1}
    • 边界更新
      • FO(iu)F_{O(i_u)}未被穿越,保持0不变。
      • FO(il)F_{O(i_l)}被穿越,更新为:FO(il)new=Fg1FO(il)old=Fg1Fg0F_{O(i_l)_{\text{new}}} = F_{g1} - F_{O(i_l)_{\text{old}}} = F_{g1} - F_{g0}
    • 计算:在这个穿越瞬间,LP在区间内累积的feeGrowthInside同样是Fg1Fg0F_{g1} - F_{g0}

通过这套精巧的机制,Uniswap V3实现了在任意价格区间内对LP手续费的精确追踪和计算。