Uniswap V2 第 7 章:时间加权平均价(TWAP)
现货价(
y/x)极易被一笔大额交易操纵,直接拿来当预言机很危险。Uniswap V2 内置了一个累积价格机制,让任何人都能在链上构建时间加权平均价(TWAP)——价格被时间”摊平”,操纵成本极高。这一章讲清它的数学和实现。
目录
- 1. 为什么不能直接用现货价
- 2. 累积价格 priceCumulative
- 3. _update:每个区块累加一次
- 4. TWAP 的核心公式
- 5. 案例:算一段时间的 TWAP
- 6. 为什么 TWAP 抗操纵
- 7. 定点数与溢出的处理
- 8. 一个常见误区
- 9. 本章小结
- 10. 动手练习
1. 为什么不能直接用现货价
现货价 y/x 是当前这一刻的池子比例。攻击者可以:
- 用一笔大额交易把
y/x瞬间推到极端值。 - 在同一区块里,让某个依赖现货价的协议(借贷、清算等)读到这个被操纵的价格,做出错误决策。
- 反向交易把价格推回,几乎无成本(甚至用 flash swap 一笔完成)。
所以直接用现货价当预言机 = 给攻击者送钱。解决方案是用一段时间的平均价。
2. 累积价格 priceCumulative
Uniswap V2 的 Pair 维护两个状态变量:
price0CumulativeLast // token0 以 token1 计价的"累积价格"
price1CumulativeLast // token1 以 token0 计价的"累积价格"
“累积价格”的定义是 价格对时间的积分:
priceCumulative = Σ (现货价 × 该价格持续的秒数)
直觉:它把”每个时刻的价格 × 持续时间”不断累加。它本身的绝对值没意义,但两个时间点的累积价格之差 ÷ 时间差 = 这段时间的平均价。
3. _update:每个区块累加一次
Pair 在每次储备变化时调用内部函数 _update,其中累加价格(简化):
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // 距上次更新的秒数
if (timeElapsed > 0 && reserve0 != 0 && reserve1 != 0) {
// 累加:用"上一次的"储备算现货价,乘以经过的时间
price0CumulativeLast += uint(UQ112x112.encode(reserve1).uqdiv(reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(reserve0).uqdiv(reserve1)) * timeElapsed;
}
blockTimestampLast = blockTimestamp;
关键点:
- 累加用的是上一次的储备(即上个区块结束时的价格),乘以这段时间
timeElapsed。 - 每个区块最多累加一次(基于区块时间戳)。
- 这意味着一笔交易刚把价格推到极端,它要等到下一个区块才会以那个价格开始计入累积——给了套利者一整个区块去把价格拉回,操纵很难持续。
4. TWAP 的核心公式
要算 [t1, t2] 这段时间的时间加权平均价:
TWAP = (priceCumulative(t2) − priceCumulative(t1)) / (t2 − t1)
也就是”累积价格之差 ÷ 时间差”。你的预言机合约只需:
- 在
t1记下当时的priceCumulative和时间戳。 - 过一段时间(≥ 某个最小间隔)后,在
t2再读一次。 - 两者相减、除以时间差,得到这段时间的平均价。
5. 案例:算一段时间的 TWAP
假设观察 WETH/USDC 池,token0=WETH:
| 时间 | 现货价(USDC/WETH) | 持续 | 该段累加 |
|---|---|---|---|
| 0~100s | 2000 | 100s | 2000×100 = 200,000 |
| 100~400s | 2100 | 300s | 2100×300 = 630,000 |
| 400~600s | 2050 | 200s | 2050×200 = 410,000 |
priceCumulative(0) = 0(基准)priceCumulative(600) = 200,000 + 630,000 + 410,000 = 1,240,000
这 600 秒的 TWAP:
TWAP = (1,240,000 − 0) / 600 = 2,066.67 USDC/WETH
注意这是按时间加权的平均:2100 这个价格持续了 300 秒(权重最大),所以 TWAP 偏向它。如果中间有人把价格瞬间砸到 100 但只持续了 1 秒,它对 TWAP 的贡献只有 100×1 / 600 ≈ 0.17,几乎可忽略——这就是抗操纵。
6. 为什么 TWAP 抗操纵
要把 TWAP 拉偏,攻击者必须让被操纵的价格持续很长时间,而不是一闪而过。但:
- 价格一旦偏离市场,套利者会立刻反向交易把它拉回(赚走攻击者的钱)。
- 要维持偏离,攻击者得在每个区块都顶住套利压力,成本随时间线性累积、随窗口长度暴增。
所以选一个足够长的 TWAP 窗口(比如 30 分钟),操纵成本会高到不划算。时间是 TWAP 的护城河。
7. 定点数与溢出的处理
两个工程细节:
- 定点数 UQ112x112:Solidity 没有小数,价格用 112.112 位定点数表示(整数部分 112 位、小数部分 112 位)。
reserve是 uint112,正好相配。 - 故意允许溢出:
price0CumulativeLast会随时间一直增大,最终溢出 uint256。Uniswap 故意用unchecked让它环绕(wrap around)——因为你算的是两次累积值之差,只要两次观测间隔不超过约 2^32 秒(约 136 年),差值依然正确。这是个巧妙的设计:绝对值会溢出,但差值永远对。
8. 一个常见误区
误区:“TWAP 就是当前价格。”
不是。TWAP 是过去某段时间的平均价,天然滞后于现货价。
- 它适合做”抗操纵的参考价”(借贷清算阈值等),不适合做”需要实时精确价格”的场景。
- 窗口越长越抗操纵,但也越滞后。这是个权衡——和 Curve V2 的 EMA 预言机
ma_time的权衡是同一个道理。
9. 本章小结
- 现货价易被瞬时操纵,不能直接当预言机。
- Pair 维护 累积价格
priceXCumulativeLast = Σ(现货价×持续秒数),由_update每区块最多累加一次(用上一区块的价)。 - TWAP = (累积价之差) / (时间差),是时间加权平均。
- 瞬时极端价因持续时间短,对 TWAP 贡献极小 → 抗操纵;窗口越长越安全也越滞后。
- 工程上用 UQ112x112 定点数,并故意允许累积值溢出(只看差值,2^32 秒内正确)。
10. 动手练习
对应课程的 TWAP 练习:实现一个 TWAP 预言机合约。
练习:实现 UniswapV2Twap
interface IUniswapV2Pair {
function token0() external view returns (address);
function token1() external view returns (address);
function price0CumulativeLast() external view returns (uint256);
function price1CumulativeLast() external view returns (uint256);
function getReserves() external view returns (uint112, uint112, uint32);
}
实现要点(4 步):
-
构造函数:存 pair、token0、token1,并记录初始的
price0CumulativeLast、price1CumulativeLast和getReserves的时间戳到updatedAt。 -
_getCurrentCumulativePrices():读 pair 的累积价;若block.timestamp > blockTimestampLast,把(现货价 × dt)补加到累积价上(用FixedPoint.fraction(reserve1, reserve0)算 token0 现货价)。这样即使本区块还没人交易,也能得到”截至当前”的累积价。 -
update():要求距上次 ≥MIN_WAIT(如 300 秒);算TWAP = (当前累积价 − 上次累积价) / dt,存进price0Avg/price1Avg(UQ112x112);更新 last 值和updatedAt。 -
consult(tokenIn, amountIn):返回amountOut = TWAP(tokenIn) × amountIn(用FixedPoint.mul(...).decode144())。
测试流程:
- 部署你的 Twap 合约指向某个池子。
vm.warp推进时间 + 做几笔 swap 改变价格。- 调
update(),再consult(WETH, 1e18),断言返回的均价在合理区间。 - 多窗口对比:制造一次瞬时极端价,验证它对 TWAP 影响很小。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/UniswapV2Twap.t.sol -vvv
下一章(第 8 章 应用:闪电兑换套利)把前面的 swap + flash swap 串起来,做一个真实的跨池套利合约。