Uniswap V3 第 8 章:费用算法(Fee Growth)
这是 V3 最精妙、也最难懂的部分。难点在于:每个 LP 的区间不同,只有”价格经过其区间”的那段时间才该给他分手续费。V3 用一套
feeGrowth(费用增长)机制,不需要遍历所有 LP,就能精确算出每个头寸该得多少。这一章用通俗的方式把它讲透。
目录
- 1. 问题:怎么只给”在场”的 LP 分钱
- 2. feeGrowthGlobal:每单位流动性累计赚了多少
- 3. 核心技巧:用”差值”算区间内的费用
- 4. feeGrowthOutside:每个 tick 记一个”外侧累计”
- 5. feeGrowthInside:区间内的费用增长
- 6. 案例:一步步算 feeGrowthInside
- 7. 头寸应得的手续费
- 8. tick 翻转时如何维护 outside
- 9. 本章小结
- 10. 动手练习
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) = 200,feeGrowthOutside(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. 本章小结
- V3 要只给”价格经过其区间”的 LP 分手续费,且不能遍历所有 LP。
feeGrowthGlobal:每单位流动性的累计手续费(+= 费/活跃L),把分钱变成乘法。- 核心技巧:
feeGrowthInside = global − below − above(前缀和式的减法)。 feeGrowthOutside(每个 tick 一个)+ 当前 tick 位置 → 算出 below/above。- 头寸应得费 =
liquidity × (feeGrowthInside_now − feeGrowthInsideLast)。 - swap 跨 tick 时翻转
feeGrowthOutside = global − feeGrowthOutside,维持参照系正确。
10. 动手练习
这是理解性练习,建议用脚本模拟,把抽象机制变具体。
练习:模拟 feeGrowth 分费
用 Python/Solidity 模拟一个简化池子(单一 token 的费用追踪):
- 维护
feeGrowthGlobal、若干 tick 的feeGrowthOutside、当前 tick、活跃流动性L。 - 实现
swap(feeAmount):feeGrowthGlobal += feeAmount / L;若价格跨 tick,更新 L(liquidityNet)并翻转该 tick 的feeGrowthOutside。 - 实现
getFeeGrowthInside(lower, upper):按第 4、5 节公式返回。 - 实现
positionFees(L, lower, upper, last):返回L × (inside − last)。 - 测试场景:建两个区间不同的头寸,让价格只在其中一个头寸的区间内来回 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 的闪电贷:从池子借出任意代币、同区块归还本金 + 手续费。