Uniswap V3 第8章 费用算法:feeGrowth 如何公平分配手续费

讲解 Uniswap V3 最精妙的费用追踪机制:feeGrowthGlobal、feeGrowthOutside、feeGrowthInside,以及如何只给"价格经过其区间"的 LP 分配手续费。

6 分钟阅读
Uniswap V3 第8章 费用算法:feeGrowth 如何公平分配手续费

Uniswap V3 第 8 章:费用算法(Fee Growth)

这是 V3 最精妙、也最难懂的部分。难点在于:每个 LP 的区间不同,只有”价格经过其区间”的那段时间才该给他分手续费。V3 用一套 feeGrowth(费用增长)机制,不需要遍历所有 LP,就能精确算出每个头寸该得多少。这一章用通俗的方式把它讲透。


目录


1. 问题:怎么只给”在场”的 LP 分钱

V3 里每笔 swap 产生手续费,但这笔费只该分给当前价格落在其区间内的 LP(回忆第 3 章:只有区间内才”在场”提供流动性)。

朴素做法:每次 swap 遍历所有相关头寸、按比例记账——gas 爆炸,不可行

V3 的目标:用 O(1) 的记账,让每个 LP 在 collect 时能算出”我的区间在我持有期间,累计该分多少手续费”。秘诀是只记几个全局/per-tick 的累计量,靠减法还原任意区间、任意时段的费用。


2. feeGrowthGlobal:每单位流动性累计赚了多少

池子维护两个全局变量(两种代币各一个):

feeGrowthGlobal0X128   // 自池子诞生以来,每单位流动性累计赚到的 token0 手续费
feeGrowthGlobal1X128   // token1 同理

定义(概念上):

feeGrowthGlobal += 本次 swap 手续费 / 当前活跃流动性 L

注意它是**“每单位流动性”**的累计费用,而不是总费用。这样设计的好处:一个流动性为 L 的头寸,在某段时间赚的费 = L × (这段时间 feeGrowthGlobal 的增量)。把”分钱”变成了”乘法”。

X128 表示这是个 128 位小数的定点数(费用/流动性通常是小数)。


3. 核心技巧:用”差值”算区间内的费用

feeGrowthGlobal全价格的累计。但我们要的是某个区间 [tickLower, tickUpper] 内累计的费用增长。

核心思想(类比”前缀和”):

区间内累计 = 全局累计 − 低于区间的部分 − 高于区间的部分
feeGrowthInside = feeGrowthGlobal − feeGrowthBelow(lower) − feeGrowthAbove(upper)

只要能高效地知道”低于某 tick 累计了多少费”和”高于某 tick 累计了多少费”,就能用减法得到”区间内累计了多少费”。这就引出了 feeGrowthOutside


4. feeGrowthOutside:每个 tick 记一个”外侧累计”

每个已初始化的 tick 记录一个 feeGrowthOutside在这个 tick “外侧”累计的每单位流动性费用

“外侧”的定义随当前价格而变,约定:

  • feeGrowthOutside 记的是”当前价格那一侧的对面”累计的费用。
  • 当价格跨过这个 tick 时,feeGrowthOutside 会被翻转更新(第 8 节):feeGrowthOutside = feeGrowthGlobal − feeGrowthOutside

feeGrowthOutside 可以算出:

低于 lower 的累计 feeGrowthBelow(lower):
    若 当前tick ≥ lower:  feeGrowthBelow = feeGrowthOutside(lower)
    否则:                 feeGrowthBelow = feeGrowthGlobal − feeGrowthOutside(lower)

高于 upper 的累计 feeGrowthAbove(upper):
    若 当前tick < upper:  feeGrowthAbove = feeGrowthOutside(upper)
    否则:                 feeGrowthAbove = feeGrowthGlobal − feeGrowthOutside(upper)

不必死记这几个分支,理解**“outside 记的是相对当前价的某一侧,按当前价位置决定要不要用 global 减一下”**即可。


5. feeGrowthInside:区间内的费用增长

把上面拼起来:

feeGrowthInside = feeGrowthGlobal − feeGrowthBelow(lower) − feeGrowthAbove(upper)

这就是”区间 [lower, upper] 内,每单位流动性累计赚到的手续费”。它只随”价格在区间内时的 swap”增长,价格在区间外时不增长——正好实现了”只给在场 LP 分钱”。


6. 案例:一步步算 feeGrowthInside

设某时刻:

  • feeGrowthGlobal = 1000(每单位流动性累计费,示意值)
  • 头寸区间 [lower, upper]
  • feeGrowthOutside(lower) = 200feeGrowthOutside(upper) = 150
  • 当前 tick 在区间(lower ≤ 当前 < upper)

算 below(当前 tick ≥ lower):

feeGrowthBelow = feeGrowthOutside(lower) = 200

算 above(当前 tick < upper):

feeGrowthAbove = feeGrowthOutside(upper) = 150

区间内:

feeGrowthInside = 1000 − 200 − 150 = 650

含义:这个区间内,每单位流动性累计赚了 650(单位是 token/流动性的定点值)。


7. 头寸应得的手续费

每个头寸记录一个”上次结算时的 feeGrowthInside”叫 feeGrowthInsideLast。应得手续费:

应得 = liquidity × (feeGrowthInside_now − feeGrowthInsideLast)

即”从上次结算到现在,区间内每单位流动性的费用增长” × “我的流动性”。

  • 每次 mint / burn / collect 时,先用这个公式把新赚的费累加到 tokensOwed,再更新 feeGrowthInsideLast
  • 接 第 6 节:如果头寸 L=1,000,000,feeGrowthInsideLast=400,现在 feeGrowthInside=650,则新赚 1,000,000 × (650−400) = 2.5e8(定点单位,换算回真实代币)。

这就是为什么 V3 能不遍历所有 LP 却精确分费——每个头寸自己用两次 feeGrowthInside 的差乘以自己的 L 即可。


8. tick 翻转时如何维护 outside

feeGrowthOutside 的”外侧”含义依赖当前价在 tick 的哪一侧。所以每当 swap 跨过一个 tick(第 4 章的 Tick.cross),要翻转它:

feeGrowthOutside = feeGrowthGlobal − feeGrowthOutside

直觉:价格从 tick 一侧走到另一侧,“外侧”和”内侧”互换了,于是用 global 减一下把累计量翻到正确的参照系。初始化一个 tick 时,按”当前价格是否已经高于它”决定 outside 的初值。

这套机制保证:无论价格怎么来回穿越,任何时刻用第 4、5 节公式都能算出正确的 feeGrowthInside


9. 本章小结

  1. V3 要只给”价格经过其区间”的 LP 分手续费,且不能遍历所有 LP。
  2. feeGrowthGlobal:每单位流动性的累计手续费(+= 费/活跃L),把分钱变成乘法。
  3. 核心技巧:feeGrowthInside = global − below − above(前缀和式的减法)。
  4. feeGrowthOutside(每个 tick 一个)+ 当前 tick 位置 → 算出 below/above。
  5. 头寸应得费 = liquidity × (feeGrowthInside_now − feeGrowthInsideLast)
  6. swap 跨 tick 时翻转 feeGrowthOutside = global − feeGrowthOutside,维持参照系正确。

10. 动手练习

这是理解性练习,建议用脚本模拟,把抽象机制变具体。

练习:模拟 feeGrowth 分费

用 Python/Solidity 模拟一个简化池子(单一 token 的费用追踪):

  1. 维护 feeGrowthGlobal、若干 tick 的 feeGrowthOutside、当前 tick、活跃流动性 L
  2. 实现 swap(feeAmount)feeGrowthGlobal += feeAmount / L;若价格跨 tick,更新 L(liquidityNet)并翻转该 tick 的 feeGrowthOutside
  3. 实现 getFeeGrowthInside(lower, upper):按第 4、5 节公式返回。
  4. 实现 positionFees(L, lower, upper, last):返回 L × (inside − last)
  5. 测试场景:建两个区间不同的头寸,让价格只在其中一个头寸的区间内来回 swap,验证只有该头寸累积了手续费,另一个为 0。

进阶(链上)

  • 读真实 V3 池的 feeGrowthGlobal0X128 和某 tick 的 feeGrowthOutside0X128,对一个真实头寸用公式算出 feeGrowthInside,和 positions(tokenId) 里的 feeGrowthInside0LastX128 对比,理解 collect 时的结算。

运行

forge test --evm-version cancun --fork-url $FORK_URL \
  --match-path test/UniswapV3Fee.t.sol -vvv

下一章(第 9 章 Flash)讲 V3 的闪电贷:从池子借出任意代币、同区块归还本金 + 手续费。

💬 评论