死磕Uniswap V3(六):费用系统与预言机

本文是「死磕Uniswap V3」系列的第六篇,深入剖析V3的费用分配机制和TWAP预言机系统的完整实现。

系列导航

序号标题核心内容
01概述与集中流动性AMM演进、集中流动性原理
02Tick机制与价格数学Tick设计、价格转换算法
03架构与合约设计Factory、Pool合约结构
04交换机制深度解析swap函数、价格发现
05流动性管理与头寸Position、mint/burn
06费用系统与预言机费用分配、TWAP
07MEV与套利策略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%1001稳定币对
0.05%50010相关资产
0.30%300060主流币对
1.00%10000200高风险币对

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与私有交易池

参考资料