死磕Uniswap V3(六):费用系统与预言机
本文是「死磕Uniswap V3」系列的第六篇,深入剖析V3的费用分配机制和TWAP预言机系统的完整实现。
系列导航
| 序号 | 标题 | 核心内容 |
|---|---|---|
| 01 | 概述与集中流动性 | AMM演进、集中流动性原理 |
| 02 | Tick机制与价格数学 | Tick设计、价格转换算法 |
| 03 | 架构与合约设计 | Factory、Pool合约结构 |
| 04 | 交换机制深度解析 | swap函数、价格发现 |
| 05 | 流动性管理与头寸 | Position、mint/burn |
| 06 | 费用系统与预言机 | 费用分配、TWAP |
| 07 | MEV与套利策略 | JIT、三明治攻击 |
1. 费用系统概述
1.1 V3费用架构
flowchart TB subgraph 费用来源 SWAP[交易费用] end subgraph 费用分配 LP[LP费用] PROTOCOL[协议费用] end subgraph LP费用分配 ACTIVE[活跃流动性LP] INACTIVE[非活跃LP] end SWAP --> LP SWAP --> PROTOCOL LP --> ACTIVE LP -.->|不分配| INACTIVE NOTE[只有当前价格区间内的<br/>活跃LP才能获得费用] style NOTE fill:#fff3e0 style ACTIVE fill:#c8e6c9 style INACTIVE fill:#ffcdd2
1.2 费率等级
V3支持多个费率等级,每个等级绑定特定的tick间距:
| 费率 | 百万分比 | Tick间距 | 适用场景 |
|---|---|---|---|
| 0.01% | 100 | 1 | 稳定币对 |
| 0.05% | 500 | 10 | 相关资产 |
| 0.30% | 3000 | 60 | 主流币对 |
| 1.00% | 10000 | 200 | 高风险币对 |
1.3 费用计算的核心挑战
flowchart LR subgraph TradGroup["传统方案"] T1[遍历所有LP] T2[计算每个LP份额] T3[分配费用] T4["O(n)复杂度<br/>Gas成本高"] T1 --> T2 T2 --> T3 end subgraph V3Group["V3方案"] V1[记录全局增长率] V2[LP领取时计算差值] V3[乘以流动性数量] V4["O(1)复杂度<br/>Gas成本低"] V1 --> V2 V2 --> V3 end style V3Group fill:#c8e6c9 style TradGroup fill:#ffcdd2
2. 费用增长率机制
2.1 全局费用增长率
池子维护两个全局费用增长累积器:
// 每单位流动性累积的token0费用
uint256 public override feeGrowthGlobal0X128;
// 每单位流动性累积的token1费用
uint256 public override feeGrowthGlobal1X128;数学定义:
feeGrowthGlobalX128 = Σ(fee_i / L_i) × 2^128
其中:
- fee_i: 第i笔交易产生的费用
- L_i: 第i笔交易时的活跃流动性
2.2 更新机制
sequenceDiagram participant Trader as 交易者 participant Pool as 池子合约 participant State as 状态变量 Trader->>Pool: swap(...) Pool->>Pool: 计算交易费用 Note over Pool: feeAmount = amountIn × feePips / 1e6 Pool->>Pool: 计算费用增长 Note over Pool: growth = feeAmount × 2^128 / liquidity Pool->>State: 更新全局增长率 Note over State: feeGrowthGlobalX128 += growth
2.3 代码实现
// 在swap循环中更新费用增长率
if (state.liquidity > 0) {
state.feeGrowthGlobalX128 += FullMath.mulDiv(
step.feeAmount, // 本步产生的费用
FixedPoint128.Q128, // 2^128 归一化因子
state.liquidity // 当前活跃流动性
);
}3. 区间费用计算
3.1 “Outside”概念
每个tick维护”outside”费用增长率,表示tick另一侧累积的费用:
flowchart LR subgraph RightGroup["价格在tick右侧时"] L1["feeGrowthOutside = tick左侧累积费用"] R1["tick右侧费用 = global - outside"] end subgraph LeftGroup["价格在tick左侧时"] L2["feeGrowthOutside = tick右侧累积费用"] R2["tick左侧费用 = global - outside"] end style RightGroup fill:#e3f2fd style LeftGroup fill:#fff3e0
3.2 inside费用计算
flowchart TB subgraph 计算步骤 S1[确定下界below费用] S2[确定上界above费用] S3[计算inside费用] end subgraph 公式 F["feeGrowthInside = global - below - above"] end S1 --> S2 --> S3 --> F style 公式 fill:#c8e6c9
3.3 完整实现
function getFeeGrowthInside(
mapping(int24 => Tick.Info) storage self,
int24 tickLower,
int24 tickUpper,
int24 tickCurrent,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128
) internal view returns (
uint256 feeGrowthInside0X128,
uint256 feeGrowthInside1X128
) {
Info storage lower = self[tickLower];
Info storage upper = self[tickUpper];
// 计算下界以下的费用增长
uint256 feeGrowthBelow0X128;
uint256 feeGrowthBelow1X128;
if (tickCurrent >= tickLower) {
// 价格在下界之上,outside就是below
feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
feeGrowthBelow1X128 = lower.feeGrowthOutside1X128;
} else {
// 价格在下界之下,需要翻转
feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128;
}
// 计算上界以上的费用增长
uint256 feeGrowthAbove0X128;
uint256 feeGrowthAbove1X128;
if (tickCurrent < tickUpper) {
// 价格在上界之下,outside就是above
feeGrowthAbove0X128 = upper.feeGrowthOutside0X128;
feeGrowthAbove1X128 = upper.feeGrowthOutside1X128;
} else {
// 价格在上界之上,需要翻转
feeGrowthAbove0X128 = feeGrowthGlobal0X128 - upper.feeGrowthOutside0X128;
feeGrowthAbove1X128 = feeGrowthGlobal1X128 - upper.feeGrowthOutside1X128;
}
// 计算区间内的费用增长
feeGrowthInside0X128 = feeGrowthGlobal0X128 - feeGrowthBelow0X128 - feeGrowthAbove0X128;
feeGrowthInside1X128 = feeGrowthGlobal1X128 - feeGrowthBelow1X128 - feeGrowthAbove1X128;
}3.4 计算示例
flowchart TB subgraph InitGroup["初始状态"] G["feeGrowthGlobal = 1000"] TL["tickLower.outside = 200"] TU["tickUpper.outside = 300"] P["当前价格在区间内"] end subgraph CalcGroup["计算过程"] B["below = 200 (直接使用)"] A["above = 300 (直接使用)"] I["inside = 1000 - 200 - 300 = 500"] end subgraph LPFeeGroup["LP费用计算"] L["LP流动性 = 100"] LAST["lastInside = 400"] FEE["应得费用 = (500 - 400) × 100 / 2^128"] end G --> B TL --> B TU --> A P --> B P --> A B --> I A --> I I --> FEE L --> FEE LAST --> FEE style LPFeeGroup fill:#c8e6c9
4. Tick跨越时的费用处理
4.1 “翻转”技巧
当价格跨越tick时,需要更新outside值:
function cross(
mapping(int24 => Tick.Info) storage self,
int24 tick,
uint256 feeGrowthGlobal0X128,
uint256 feeGrowthGlobal1X128,
uint160 secondsPerLiquidityCumulativeX128,
int56 tickCumulative,
uint32 time
) internal returns (int128 liquidityNet) {
Tick.Info storage info = self[tick];
// 翻转outside值
// 新的outside = global - 旧的outside
info.feeGrowthOutside0X128 = feeGrowthGlobal0X128 - info.feeGrowthOutside0X128;
info.feeGrowthOutside1X128 = feeGrowthGlobal1X128 - info.feeGrowthOutside1X128;
// 同样处理预言机相关的outside值
info.secondsPerLiquidityOutsideX128 =
secondsPerLiquidityCumulativeX128 - info.secondsPerLiquidityOutsideX128;
info.tickCumulativeOutside = tickCumulative - info.tickCumulativeOutside;
info.secondsOutside = time - info.secondsOutside;
liquidityNet = info.liquidityNet;
}4.2 翻转原理图解
flowchart TB subgraph "跨越前 (价格在tick左侧)" B1["outside = 累积到tick右侧的费用"] B2["inside(左区间) = global - outside"] end subgraph "跨越操作" OP["outside_new = global - outside_old"] end subgraph "跨越后 (价格在tick右侧)" A1["outside = 累积到tick左侧的费用"] A2["inside(右区间) = global - outside"] end B1 --> OP --> A1 NOTE["关键:简单减法实现了<br/>inside和outside的互换"] style 跨越操作 fill:#fff3e0 style NOTE fill:#e8f5e9
5. 协议费用
5.1 协议费用结构
// Slot0中的feeProtocol字段
// 低4位:token0的协议费率
// 高4位:token1的协议费率
uint8 feeProtocol;
// 协议费用累积
struct ProtocolFees {
uint128 token0;
uint128 token1;
}
ProtocolFees public override protocolFees;5.2 协议费用计算
flowchart LR subgraph 交易费用 TOTAL[总费用 = amountIn × feePips] end subgraph 分配 PROTOCOL_SHARE["协议份额 = 总费用 / feeProtocol"] LP_SHARE["LP份额 = 总费用 - 协议份额"] end TOTAL --> PROTOCOL_SHARE TOTAL --> LP_SHARE style 分配 fill:#e3f2fd
5.3 代码实现
// 在swap循环中计算协议费用
if (cache.feeProtocol > 0) {
// 协议费用 = 交易费用 / 协议费率
uint256 delta = step.feeAmount / cache.feeProtocol;
step.feeAmount -= delta; // 从LP费用中扣除
state.protocolFee += uint128(delta); // 累积到协议费用
}
// 循环结束后更新协议费用
if (zeroForOne) {
if (state.protocolFee > 0) {
protocolFees.token0 += state.protocolFee;
}
} else {
if (state.protocolFee > 0) {
protocolFees.token1 += state.protocolFee;
}
}5.4 协议费用提取
function collectProtocol(
address recipient,
uint128 amount0Requested,
uint128 amount1Requested
) external override lock onlyFactoryOwner returns (
uint128 amount0,
uint128 amount1
) {
// 计算实际可提取数量
amount0 = amount0Requested > protocolFees.token0
? protocolFees.token0
: amount0Requested;
amount1 = amount1Requested > protocolFees.token1
? protocolFees.token1
: amount1Requested;
// 更新状态
if (amount0 > 0) {
if (amount0 == protocolFees.token0) amount0--;
protocolFees.token0 -= amount0;
TransferHelper.safeTransfer(token0, recipient, amount0);
}
if (amount1 > 0) {
if (amount1 == protocolFees.token1) amount1--;
protocolFees.token1 -= amount1;
TransferHelper.safeTransfer(token1, recipient, amount1);
}
emit CollectProtocol(msg.sender, recipient, amount0, amount1);
}6. 预言机系统概述
6.1 TWAP预言机
V3内置时间加权平均价格(TWAP)预言机:
flowchart TB subgraph TWAP计算 T1["时间点1: tick=100, time=1000"] T2["时间点2: tick=102, time=1500"] T3["时间点3: tick=98, time=2000"] end subgraph 累积值 C1["tickCumulative1 = 100×1000"] C2["tickCumulative2 = 100×1000 + 102×500"] C3["tickCumulative3 = ... + 98×500"] end subgraph TWAP公式 F["TWAP = (tickCumulative2 - tickCumulative1) / (time2 - time1)"] end T1 --> C1 T2 --> C2 T3 --> C3 C1 & C2 --> F style TWAP公式 fill:#c8e6c9
6.2 预言机的优势
| 特性 | 说明 |
|---|---|
| 抗操纵性 | 攻击者需要持续维持异常价格才能影响TWAP |
| 历史数据 | 可查询任意历史时间点的价格 |
| Gas效率 | O(1)查询复杂度 |
| 灵活性 | 可配置观察窗口大小 |
7. Observation数据结构
7.1 观察者结构
struct Observation {
// 记录时的区块时间戳
uint32 blockTimestamp;
// tick的累积值(用于计算TWAP)
int56 tickCumulative;
// 每流动性秒数的累积值
uint160 secondsPerLiquidityCumulativeX128;
// 是否已初始化
bool initialized;
}
// 固定大小的环形数组
Oracle.Observation[65535] public override observations;7.2 存储布局
flowchart LR subgraph ArrayGroup["observations数组 (环形)"] O0["[0]<br/>旧数据"] O1["[1]"] O2["[2]"] DOTS["..."] ON["[index]<br/>最新"] ON1["[index+1]<br/>下一个写入位置"] O0 --> O1 O1 --> O2 O2 --> DOTS DOTS --> ON ON --> ON1 ON1 -.->|环形| O0 end style ON fill:#c8e6c9
7.3 关键状态变量
// 在Slot0中
struct Slot0 {
// ... 其他字段
uint16 observationIndex; // 当前观察者索引
uint16 observationCardinality; // 当前容量
uint16 observationCardinalityNext; // 目标容量
// ...
}8. 预言机写入机制
8.1 写入时机
预言机在以下情况更新:
- swap:当价格发生变化时
- mint/burn:当流动性变化且是区块内首次操作时
8.2 写入函数
function write(
Observation[65535] storage self,
uint16 index,
uint32 blockTimestamp,
int24 tick,
uint128 liquidity,
uint16 cardinality,
uint16 cardinalityNext
) internal returns (uint16 indexUpdated, uint16 cardinalityUpdated) {
Observation memory last = self[index];
// 同一区块内不重复写入
if (last.blockTimestamp == blockTimestamp) return (index, cardinality);
// 检查是否需要扩容
if (cardinalityNext > cardinality && index == (cardinality - 1)) {
cardinalityUpdated = cardinalityNext;
} else {
cardinalityUpdated = cardinality;
}
// 计算下一个索引(环形)
indexUpdated = (index + 1) % cardinalityUpdated;
// 写入新观察值
self[indexUpdated] = transform(last, blockTimestamp, tick, liquidity);
}8.3 Transform函数
function transform(
Observation memory last,
uint32 blockTimestamp,
int24 tick,
uint128 liquidity
) private pure returns (Observation memory) {
// 计算时间差
uint32 delta = blockTimestamp - last.blockTimestamp;
return Observation({
blockTimestamp: blockTimestamp,
// 累积tick值
tickCumulative: last.tickCumulative +
int56(tick) * int56(uint56(delta)),
// 累积每流动性秒数
secondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 +
((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)),
initialized: true
});
}8.4 累积值计算图解
sequenceDiagram participant Last as 上一观察 participant Current as 当前状态 participant New as 新观察 Last->>Current: 时间差 delta = now - last.time Last->>Current: 获取当前tick Current->>New: tickCumulative = last.tickCumulative + tick × delta Current->>New: secondsPerLiquidity += delta / liquidity New->>New: 记录当前时间戳
9. 预言机查询机制
9.1 查询接口
function observe(uint32[] calldata secondsAgos)
external
view
override
noDelegateCall
returns (
int56[] memory tickCumulatives,
uint160[] memory secondsPerLiquidityCumulativeX128s
);9.2 单点查询实现
function observeSingle(
Observation[65535] storage self,
uint32 time,
uint32 secondsAgo,
int24 tick,
uint16 index,
uint128 liquidity,
uint16 cardinality
) internal view returns (
int56 tickCumulative,
uint160 secondsPerLiquidityCumulativeX128
) {
if (secondsAgo == 0) {
// 查询当前时刻
Observation memory last = self[index];
if (last.blockTimestamp != time) {
// 需要补充当前区块的累积
return transform(last, time, tick, liquidity);
}
return (last.tickCumulative, last.secondsPerLiquidityCumulativeX128);
}
// 查询历史时刻
uint32 target = time - secondsAgo;
// 找到目标时间点前后的观察值
(Observation memory beforeOrAt, Observation memory atOrAfter) =
getSurroundingObservations(
self, time, target, tick, index, liquidity, cardinality
);
if (target == beforeOrAt.blockTimestamp) {
// 精确命中
return (beforeOrAt.tickCumulative, beforeOrAt.secondsPerLiquidityCumulativeX128);
} else if (target == atOrAfter.blockTimestamp) {
return (atOrAfter.tickCumulative, atOrAfter.secondsPerLiquidityCumulativeX128);
} else {
// 需要线性插值
uint32 observationTimeDelta = atOrAfter.blockTimestamp - beforeOrAt.blockTimestamp;
uint32 targetDelta = target - beforeOrAt.blockTimestamp;
return (
beforeOrAt.tickCumulative +
((atOrAfter.tickCumulative - beforeOrAt.tickCumulative) /
int56(uint56(observationTimeDelta))) *
int56(uint56(targetDelta)),
beforeOrAt.secondsPerLiquidityCumulativeX128 +
uint160(
(uint256(
atOrAfter.secondsPerLiquidityCumulativeX128 -
beforeOrAt.secondsPerLiquidityCumulativeX128
) * targetDelta) / observationTimeDelta
)
);
}
}9.3 二分查找
function binarySearch(
Observation[65535] storage self,
uint32 time,
uint32 target,
uint16 index,
uint16 cardinality
) private view returns (
Observation memory beforeOrAt,
Observation memory atOrAfter
) {
// 确定搜索范围
uint256 l = (index + 1) % cardinality; // 最老的观察
uint256 r = l + cardinality - 1; // 最新的观察
uint256 i;
while (true) {
i = (l + r) / 2;
beforeOrAt = self[i % cardinality];
if (!beforeOrAt.initialized) {
l = i + 1;
continue;
}
atOrAfter = self[(i + 1) % cardinality];
bool targetAtOrAfter = lte(time, beforeOrAt.blockTimestamp, target);
if (targetAtOrAfter && lte(time, target, atOrAfter.blockTimestamp)) {
break; // 找到了
}
if (!targetAtOrAfter) {
r = i - 1;
} else {
l = i + 1;
}
}
}9.4 查询流程图
flowchart TB START[observe查询] --> CHECK{secondsAgo == 0?} CHECK -->|是| CURRENT[返回当前累积值] CHECK -->|否| CALC_TARGET[计算目标时间点] CALC_TARGET --> BINARY[二分查找] BINARY --> FOUND{精确命中?} FOUND -->|是| RETURN_EXACT[直接返回] FOUND -->|否| INTERPOLATE[线性插值] INTERPOLATE --> RETURN[返回插值结果] RETURN_EXACT --> RETURN CURRENT --> RETURN style INTERPOLATE fill:#fff3e0
10. 预言机扩容机制
10.1 扩容接口
function increaseObservationCardinalityNext(uint16 observationCardinalityNext)
external
override
lock
noDelegateCall
{
uint16 observationCardinalityNextOld = slot0.observationCardinalityNext;
uint16 observationCardinalityNextNew = observations.grow(
observationCardinalityNextOld,
observationCardinalityNext
);
slot0.observationCardinalityNext = observationCardinalityNextNew;
if (observationCardinalityNextOld != observationCardinalityNextNew) {
emit IncreaseObservationCardinalityNext(
observationCardinalityNextOld,
observationCardinalityNextNew
);
}
}10.2 Grow函数
function grow(
Observation[65535] storage self,
uint16 current,
uint16 next
) internal returns (uint16) {
require(current > 0, 'I');
// 无需扩容
if (next <= current) return current;
// 预初始化新槽位以节省后续写入的gas
for (uint16 i = current; i < next; i++) {
self[i].blockTimestamp = 1;
}
return next;
}10.3 扩容时机
flowchart TB subgraph TriggerGroup["扩容触发"] T1["用户调用increaseObservationCardinalityNext"] T2["设置observationCardinalityNext"] T1 --> T2 end subgraph ActualGroup["实际扩容"] A1["写入时检查: index == cardinality - 1?"] A2["且 cardinalityNext > cardinality?"] A3["更新cardinality = cardinalityNext"] A1 -->|是| A2 A2 -->|是| A3 end T2 --> A1 style ActualGroup fill:#e8f5e9
11. TWAP计算示例
11.1 计算30分钟TWAP
// 查询30分钟和当前的累积值
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 1800; // 30分钟前
secondsAgos[1] = 0; // 当前
(int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
// 计算TWAP tick
int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
int24 arithmeticMeanTick = int24(tickCumulativesDelta / 1800);
// 转换为价格
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(arithmeticMeanTick);11.2 TWAP vs 即时价格
flowchart LR subgraph 即时价格 I1[当前slot0.sqrtPriceX96] I2[可被单笔交易操纵] I3[波动大] end subgraph TWAP T1[时间加权平均] T2[操纵成本高] T3[更平滑稳定] end style TWAP fill:#c8e6c9 style 即时价格 fill:#ffcdd2
11.3 抗操纵分析
flowchart TB subgraph IntentGroup["攻击者意图"] A1[想把TWAP从100提高到200] A2[时间窗口30分钟] end subgraph CostGroup["攻击成本"] C1["需要30分钟内持续维持price=200"] C2["套利者会不断套利"] C3["攻击者承担全部滑点损失"] end subgraph ResultGroup["结论"] R["攻击成本远超潜在收益"] end A1 --> C1 A2 --> C2 C1 --> C3 C2 --> C3 C3 --> R style ResultGroup fill:#c8e6c9
12. 本章小结
12.1 核心概念回顾
mindmap root((费用与预言机)) 费用系统 全局增长率 区间费用计算 outside翻转技巧 协议费用分配 预言机系统 Observation结构 累积值机制 二分查找 线性插值 TWAP 时间加权平均 抗操纵性 历史查询 优化设计 O(1)费用计算 环形数组存储 按需扩容
12.2 关键设计总结
| 设计要点 | 实现方式 | 效果 |
|---|---|---|
| 费用计算 | 增长率差值 | O(1)复杂度 |
| 区间定位 | outside翻转 | 优雅的边界处理 |
| 历史存储 | 环形数组 | 固定存储开销 |
| 时间查询 | 二分+插值 | O(log n)查询 |
| 抗操纵 | 时间加权 | 提高攻击成本 |
12.3 使用建议
flowchart LR subgraph FeeGroup["费用收取"] F1[定期调用burn(0)结算费用] F2[使用collect提取] F1 --> F2 end subgraph OracleGroup["预言机使用"] O1[根据需要设置cardinality] O2[选择合适的时间窗口] O3[结合其他预言机验证] O1 --> O2 O2 --> O3 end style FeeGroup fill:#e3f2fd style OracleGroup fill:#fff3e0
下一篇预告
在下一篇文章中,我们将深入探讨MEV与套利策略,包括:
- JIT(Just-in-Time)流动性攻击
- 三明治攻击的原理与防护
- 跨池套利策略
- Tick边界套利
- Flashbots与私有交易池