死磕PancakeSwap V3(六):费用系统与预言机
本文是「死磕PancakeSwap V3」系列的第六篇,深入剖析V3的费用分配机制和TWAP预言机系统的完整实现。
系列导航
| 序号 | 标题 | 核心内容 |
|---|---|---|
| 01 | PancakeSwap V3概述 | 发展历程、集中流动性、V3特色 |
| 02 | Tick机制与价格数学 | Tick设计、价格转换算法 |
| 03 | 架构与合约设计 | Factory、Pool合约结构 |
| 04 | 交换机制深度解析 | swap函数、价格发现 |
| 05 | 流动性管理与头寸 | Position、mint/burn |
| 06 | 费用系统与预言机 | 费用分配、TWAP |
| 07 | V3与Uniswap V3对比 | 差异点、优化、适用场景 |
| 08 | 多链部署与特性适配 | BNB Chain、Ethereum、跨链策略 |
| 09 | 集成开发指南 | SDK使用、交易构建、最佳实践 |
| 10 | 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:#ffeb3b style ACTIVE fill:#c8e6c9 style INACTIVE fill:#ffcdd2
1.2 费率等级
PancakeSwap V3支持多个费率等级,每个等级绑定特定的tick间距:
| 费率 | 百万分比 | Tick间距 | 适用场景 |
|---|---|---|---|
| 0.01% | 100 | 1 | 稳定币对(USDT/USDC) |
| 0.05% | 500 | 10 | 相关资产(WBTC/renBTC) |
| 0.25% | 2500 | 50 | 主流币对(CAKE/BNB)- PancakeSwap特色 |
| 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:#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 V3 | Uniswap 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效率对比
- 生态整合对比
- 适用场景选择