Uniswap V2 第7章 TWAP:抗操纵的时间加权平均价预言机

讲解 Uniswap V2 的累积价格机制如何构建时间加权平均价(TWAP)预言机,为什么它能抵抗瞬时价格操纵。

6 分钟阅读
Uniswap V2 第7章 TWAP:抗操纵的时间加权平均价预言机

Uniswap V2 第 7 章:时间加权平均价(TWAP)

现货价(y/x)极易被一笔大额交易操纵,直接拿来当预言机很危险。Uniswap V2 内置了一个累积价格机制,让任何人都能在链上构建时间加权平均价(TWAP)——价格被时间”摊平”,操纵成本极高。这一章讲清它的数学和实现。


目录


1. 为什么不能直接用现货价

现货价 y/x 是当前这一刻的池子比例。攻击者可以:

  1. 用一笔大额交易把 y/x 瞬间推到极端值。
  2. 在同一区块里,让某个依赖现货价的协议(借贷、清算等)读到这个被操纵的价格,做出错误决策。
  3. 反向交易把价格推回,几乎无成本(甚至用 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)

也就是”累积价格之差 ÷ 时间差”。你的预言机合约只需:

  1. t1 记下当时的 priceCumulative 和时间戳。
  2. 过一段时间(≥ 某个最小间隔)后,在 t2 再读一次。
  3. 两者相减、除以时间差,得到这段时间的平均价。

5. 案例:算一段时间的 TWAP

假设观察 WETH/USDC 池,token0=WETH:

时间现货价(USDC/WETH)持续该段累加
0~100s2000100s2000×100 = 200,000
100~400s2100300s2100×300 = 630,000
400~600s2050200s2050×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. 定点数与溢出的处理

两个工程细节:

  1. 定点数 UQ112x112:Solidity 没有小数,价格用 112.112 位定点数表示(整数部分 112 位、小数部分 112 位)。reserve 是 uint112,正好相配。
  2. 故意允许溢出price0CumulativeLast 会随时间一直增大,最终溢出 uint256。Uniswap 故意用 unchecked 让它环绕(wrap around)——因为你算的是两次累积值之差,只要两次观测间隔不超过约 2^32 秒(约 136 年),差值依然正确。这是个巧妙的设计:绝对值会溢出,但差值永远对。

8. 一个常见误区

误区:“TWAP 就是当前价格。”

不是。TWAP 是过去某段时间的平均价,天然滞后于现货价。

  • 它适合做”抗操纵的参考价”(借贷清算阈值等),不适合做”需要实时精确价格”的场景。
  • 窗口越长越抗操纵,但也越滞后。这是个权衡——和 Curve V2 的 EMA 预言机 ma_time 的权衡是同一个道理。

9. 本章小结

  1. 现货价易被瞬时操纵,不能直接当预言机。
  2. Pair 维护 累积价格 priceXCumulativeLast = Σ(现货价×持续秒数),由 _update 每区块最多累加一次(用上一区块的价)。
  3. TWAP = (累积价之差) / (时间差),是时间加权平均。
  4. 瞬时极端价因持续时间短,对 TWAP 贡献极小 → 抗操纵;窗口越长越安全也越滞后。
  5. 工程上用 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 步):

  1. 构造函数:存 pair、token0、token1,并记录初始的 price0CumulativeLastprice1CumulativeLastgetReserves 的时间戳到 updatedAt

  2. _getCurrentCumulativePrices():读 pair 的累积价;若 block.timestamp > blockTimestampLast,把 (现货价 × dt) 补加到累积价上(用 FixedPoint.fraction(reserve1, reserve0) 算 token0 现货价)。这样即使本区块还没人交易,也能得到”截至当前”的累积价。

  3. update():要求距上次 ≥ MIN_WAIT(如 300 秒);算 TWAP = (当前累积价 − 上次累积价) / dt,存进 price0Avg/price1Avg(UQ112x112);更新 last 值和 updatedAt

  4. 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 串起来,做一个真实的跨池套利合约。

💬 评论