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

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

系列导航

序号标题核心内容
01PancakeSwap V3概述发展历程、集中流动性、V3特色
02Tick机制与价格数学Tick设计、价格转换算法
03架构与合约设计Factory、Pool合约结构
04交换机制深度解析swap函数、价格发现
05流动性管理与头寸Position、mint/burn
06费用系统与预言机费用分配、TWAP
07V3与Uniswap V3对比差异点、优化、适用场景
08多链部署与特性适配BNB Chain、Ethereum、跨链策略
09集成开发指南SDK使用、交易构建、最佳实践
10MEV与套利策略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:#ffeb3b
    style ACTIVE fill:#c8e6c9
    style INACTIVE fill:#ffcdd2

1.2 费率等级

PancakeSwap V3支持多个费率等级,每个等级绑定特定的tick间距:

费率百万分比Tick间距适用场景
0.01%1001稳定币对(USDT/USDC)
0.05%50010相关资产(WBTC/renBTC)
0.25%250050主流币对(CAKE/BNB)- PancakeSwap特色
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:#ffeb3b
    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           // 当前活跃流动性
    );
}

为什么乘以2^128?

  • 提高精度,避免小数运算
  • Q128定点数格式:整数部分在高位,小数部分在低位
  • 费用增长率是每单位流动性的累积费用,通常数值很小

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:#ffeb3b

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
) {
    Tick.Info storage lower = self[tickLower];
    Tick.Info storage upper = self[tickUpper];
 
    // 计算下界feeGrowthBelow
    uint256 feeGrowthBelow0X128;
    uint256 feeGrowthBelow1X128;
    if (tickCurrent < tickLower) {
        feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128;
    } else {
        feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = lower.feeGrowthOutside1X128;
    }
 
    // 计算上界feeGrowthAbove
    uint256 feeGrowthAbove0X128;
    uint256 feeGrowthAbove1X128;
    if (tickCurrent < tickUpper) {
        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 费用分配示例

场景

  • LP在[tickLower=1900, tickUpper=2100]提供流动性L=1000
  • 当前tick=1950(在区间内)
  • 交易产生token0费用100
sequenceDiagram
    participant Pool as Pool
    participant Lower as Tick 1900
    participant Upper as Tick 2100
    participant Position as Position

    Pool->>Pool: 更新feeGrowthGlobal0
    Note over Pool: feeGrowthGlobal0 += 100 * 2^128 / 1000<br/>= 0.1 * 2^128

    Pool->>Position: 记录feeGrowthInside快照
    Note over Position: feeGrowthInside0LastX128<br/>= 当前feeGrowthInside0

    Note over Pool: ...更多交易发生...

    Pool->>Position: 计算累积费用
    Note over Position: ΔfeeGrowthInside0 = 5 * 2^128<br/>LP费用 = 5 * 2^128 * 1000 = 5000 * 2^128

4. 协议费用

4.1 协议费率设置

struct FeeProtocol {
    uint16 feeProtocol0;
    uint16 feeProtocol1;
}

费率说明

  • feeProtocol0: token0协议费率(单位:1/100000)
  • feeProtocol1: token1协议费率(单位:1/100000)
  • 例如:feeProtocol0 = 1000,表示1%的协议费

4.2 协议费用计算

// 在swap中计算协议费用
if (feeProtocol > 0) {
    uint256 protocolFee = FullMath.mulDiv(
        step.feeAmount,
        feeProtocol,
        1e6
    );
    state.protocolFee += uint128(protocolFee);
    step.feeAmount -= protocolFee;
}

费用分配

graph LR
    subgraph Total[总费用]
        T[100%]
    end

    subgraph Distribution[分配]
        D1[LP费用<br/>如97%]
        D2[协议费用<br/>如3%]
    end

    subgraph Protocol[协议费用去向]
        P1[CAKE回购]
        P2[社区金库]
        P3[开发基金]
    end

    T --> D1
    T --> D2
    D2 --> P1
    D2 --> P2
    D2 --> P3

    style Total fill:#ffeb3b
    style Distribution fill:#c8e6c9

4.3 PancakeSwap的协议费特色

特性PancakeSwap V3Uniswap V3
费率设置社区治理投票决定协议团队决定
灵活性高(可调整)中等
用途CAKE回购、社区治理团队运营
透明度完全透明相对透明

5. TWAP预言机

5.1 预言机概述

mindmap
  root((TWAP预言机))
    设计目标
      抗操纵性
      低Gas成本
      历史数据
    核心机制
      时间加权平均价格
      累积tick
      秒数累积
    应用场景
      DEX价格
      借贷协议
      衍生品定价

5.2 Observation数据结构

struct Observation {
    uint32 blockTimestamp;
    int56 tickCumulative;            // tick × elapsedSeconds
    uint160 secondsPerLiquidityCumulativeX128;
    bool initialized;
}

字段说明

  • blockTimestamp: 记录时间戳
  • tickCumulative: 累积的tick值(用于计算TWAP)
  • secondsPerLiquidityCumulativeX128: 时间/流动性累积
  • initialized: 是否已初始化

5.3 观察数组

// 固定大小的观察数组
uint256 public override observationCardinality;
uint256 public override observationCardinalityNext;
uint16 public override observationIndex;
Observation[65535] public override observations;

设计原理

  • 固定大小数组,避免动态分配
  • 循环使用(observationIndex指向最新观察)
  • 可配置的观察数量

6. TWAP计算原理

6.1 基本概念

TWAP = (tickCumulative_end - tickCumulative_start) / (timestamp_end - timestamp_start)

其中:
- tickCumulative = Σ(tick_i × deltaSeconds_i)
- deltaSeconds_i: 第i个时间间隔的秒数

6.2 观察更新机制

sequenceDiagram
    participant Pool as Pool
    participant Observation as Observation数组

    Pool->>Pool: 获取当前时间戳
    Pool->>Pool: 计算时间差
    Pool->>Pool: 更新tickCumulative
    Note over Pool: tickCumulative += tick × (blockTimestamp - lastTimestamp)

    Pool->>Observation: 记录新观察
    Note over Observation: observations[observationIndex] = {<br/>  timestamp: blockTimestamp,<br/>  tickCumulative: tickCumulative<br/>}

    Pool->>Pool: 更新索引
    Note over Pool: observationIndex = (observationIndex + 1) % cardinality

6.3 更新代码实现

function _updateObservation(
    uint32 blockTimestamp
) private {
    Observation memory last = observations[observationIndex];
 
    if (blockTimestamp == last.blockTimestamp) {
        // 同一个区块内不更新
        return;
    }
 
    uint56 timeDelta = blockTimestamp - last.blockTimestamp;
    uint16 cardinality = observationCardinality;
 
    // 更新累积值
    int56 tickCumulative = last.tickCumulative +
        int56(uint56(tick)) * int56(timeDelta);
 
    uint160 secondsPerLiquidityCumulativeX128 =
        last.secondsPerLiquidityCumulativeX128 +
        uint160((uint256(timeDelta) << 128) / (liquidity > 0 ? liquidity : 1));
 
    // 写入新观察
    observations[observationIndex] = Observation({
        blockTimestamp: blockTimestamp,
        tickCumulative: tickCumulative,
        secondsPerLiquidityCumulativeX128: secondsPerLiquidityCumulativeX128,
        initialized: true
    });
 
    // 更新索引
    observationIndex = (observationIndex + 1) & (cardinality - 1);
}

7. 查询TWAP价格

7.1 observe函数

function observe(
    uint32[] calldata secondsAgos
) external view override returns (
    int56[] memory tickCumulatives,
    uint160[] memory secondsPerLiquidityCumulativeX128s
);

参数

  • secondsAgos: 询问的历史时间点数组,如[0, 3600]表示当前和1小时前

返回

  • tickCumulatives: 对应时间点的累积tick值
  • secondsPerLiquidityCumulativeX128s: 对应时间点的流动性加权时间

7.2 TWAP计算示例

// 查询最近1小时的TWAP
const pool = new ethers.Contract(
    poolAddress,
    poolAbi,
    provider
);
 
const observations = await pool.observe([0, 3600]);
const [currentTickCumulative, oldTickCumulative] = observations.tickCumulatives;
 
// 计算TWAP
const twapTick = (currentTickCumulative - oldTickCumulative) / 3600;
const twapPrice = 1.0001 ** twapTick;

7.3 使用场景

graph TB
    subgraph PriceOracle["价格预言机"]
        PO[TWAP价格]
    end

    subgraph Lending["借贷协议"]
        L1[抵押品估值]
        L2[清算价格]
        L3[利率调整]
    end

    subgraph Derivatives["衍生品"]
        D1[期权定价]
        D2[永续合约]
        D3[指数基金]
    end

    subgraph Aggregator["价格聚合"]
        A1[套利检测]
        A2[跨链价格]
        A3[指数构建]
    end

    PO --> L1
    PO --> L2
    PO --> L3
    PO --> D1
    PO --> D2
    PO --> D3
    PO --> A1
    PO --> A2
    PO --> A3

    style PO fill:#ffeb3b

8. PancakeSwap V3的预言机优化

8.1 多链适配

graph LR
    subgraph BNBChain["BNB Chain"]
        B1[短区块时间<br/>3秒]
        B2[更多观察点]
        B3[更精细TWAP]
    end

    subgraph Ethereum["Ethereum"]
        E1[长区块时间<br/>12秒]
        E2[较少观察点]
        E3[标准TWAP]
    end

    B1 -->|优势| E1
    B2 -->|优势| E2

    style BNBChain fill:#ffeb3b
    style Ethereum fill:#e3f2fd

8.2 Gas优化

mindmap
  root((预言机Gas优化))
    观察更新
      同区块跳过
      最小时间间隔
    存储优化
      紧凑数据结构
      循环数组
    查询优化
      批量查询
      预计算结果
    多链适配
      不同参数
      灵活配置

9. 实际应用示例

9.1 查询TWAP价格

import { Pool } from '@pancakeswap/sdk';
 
const pool = new Pool(
    CAKE,
    WBNB,
    2500,  // 0.25% fee
    sqrtPriceX96,
    liquidity,
    tick
);
 
// 查询最近1小时TWAP
const poolContract = new ethers.Contract(
    poolAddress,
    poolAbi,
    provider
);
 
const observations = await poolContract.observe([0, 3600]);
const tickCumulatives = observations[0];
 
// 计算TWAP tick
const twapTick = (tickCumulatives[0].sub(tickCumulatives[1])).div(3600);
const twapPrice = Math.pow(1.0001, twapTick);

9.2 查询LP费用

// 查询position可领取的费用
const poolContract = new ethers.Contract(
    poolAddress,
    poolAbi,
    provider
);
 
const position = await positionManager.positions(tokenId);
 
// 获取当前费用增长
const feeGrowthInside = await poolContract.feeGrowthInside(
    position.tickLower,
    position.tickUpper
);
 
// 计算累积费用
const tokensOwed0 = feeGrowthInside[0].sub(position.feeGrowthInside0LastX128)
    .mul(position.liquidity)
    .div(BigNumber.from(2).pow(128));
 
const tokensOwed1 = feeGrowthInside[1].sub(position.feeGrowthInside1LastX128)
    .mul(position.liquidity)
    .div(BigNumber.from(2).pow(128));
 
console.log(`Token0 owed: ${ethers.utils.formatEther(tokensOwed0)}`);
console.log(`Token1 owed: ${ethers.utils.formatEther(tokensOwed1)}`);

10. 本章小结

10.1 费用系统核心要点

mindmap
  root((费用系统<br/>核心要点))
    全局费用增长率
      feeGrowthGlobalX128
      每单位流动性累积
      O(1)复杂度
    区间费用计算
      Outside概念
      Inside费用
      Tick更新
    协议费用
      可配置费率
      社区治理
      CAKE回购
    费用领取
      Position快照
      增量计算
      按需提取

10.2 预言机核心要点

mindmap
  root((预言机<br/>核心要点))
    TWAP原理
      时间加权平均
      抗操纵性
      历史数据
    Observation
      tickCumulative
      时间戳
      循环数组
    更新机制
      每次交易更新
      同区块跳过
      累积计算
    查询功能
      observe函数
      多时间点
      价格衍生

下一篇预告

在下一篇文章中,我们将深入探讨V3与Uniswap V3对比,包括:

  • 技术架构差异
  • Gas效率对比
  • 生态整合对比
  • 适用场景选择

参考资料