Uniswap V3 第10章 TWAP 预言机:tickCumulative 与 observe

讲解 Uniswap V3 内置的 TWAP 价格预言机:tickCumulative 累积、observe 查询、几何平均价,以及相比 V2 的改进。

5 分钟阅读
Uniswap V3 第10章 TWAP 预言机:tickCumulative 与 observe

Uniswap V3 第 10 章:TWAP 价格预言机

V3 内置了比 V2 更好用的时间加权平均价(TWAP)预言机。它累积的不是”价格”,而是 tick(价格的对数),并把历史观测点存进环形数组,让你能直接查询任意过去时刻的累积值。这一章讲清 tickCumulativeobserve,以及它相比 V2 的改进。


目录


1. V3 预言机的改进

回忆 V2 TWAP(第 7 章)的局限:

  • V2 累积的是算术价格 priceCumulative = Σ(price × dt),得到的是算术平均价
  • V2 只存”最新”的累积值,你要自己在 t1t2 各记一次、自己存历史。

V3 的改进:

  • 累积 tick(价格的对数),得到几何平均价(更符合价格的乘性本质)。
  • 内置一个环形数组存历史观测点,提供 observe(secondsAgos) 让你直接查过去任意时刻的累积值,无需自己存。

2. tickCumulative:累积的是 tick

V3 的每个观测点(observation)记录:

tickCumulative = Σ (当前 tick × 持续秒数)
secondsPerLiquidityCumulative = Σ (dt / 活跃流动性)   // 另一个量,用于流动性挖矿等
blockTimestamp

tickCumulativetick 对时间的积分。和 V2 的累积价格思路一样,但累积的对象是 tick 而非 price。


3. 几何平均价:为什么累积 tick 更优雅

回忆 price = 1.0001^tick,所以 tick = log_1.0001(price)tick 就是价格的对数

对 tick 取算术平均,等价于对价格取几何平均

平均 tick = (tickCumulative(t2) − tickCumulative(t1)) / (t2 − t1)
TWAP 价格 = 1.0001^(平均 tick)   ← 这是几何平均价

为什么几何平均更好?

  • 价格是乘性的(涨 10% 再跌 10% 不回到原点)。几何平均尊重这种乘性结构,对”价格翻倍”和”价格减半”对称看待。
  • 几何平均价对极端值不那么敏感,更稳健。
  • 数学上 tick 是整数、累积是简单加法,链上实现干净。

4. observe:查询任意时刻的累积值

function observe(uint32[] calldata secondsAgos)
    external view returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s);
  • 你传入”几秒前”的列表,比如 [3600, 0] 表示”1 小时前”和”现在”。
  • 返回这些时刻的 tickCumulative
  • 内部从环形数组里找最接近的两个观测点,线性插值出你要的精确时刻——所以你不用自己存历史,池子帮你存了。

这比 V2 方便太多:V2 你得自己部署合约定期记录,V3 一个 observe 调用搞定。


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

想算”过去 1 小时(3600 秒)的 TWAP”:

uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 3600;  // 1 小时前
secondsAgos[1] = 0;     // 现在
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgos);

int56 diff = tickCumulatives[1] - tickCumulatives[0];   // 累积 tick 之差
int24 avgTick = int24(diff / 3600);                      // 平均 tick

假设 tickCumulatives = [a, b]b - a = 3600 × 200,000 之类,则 avgTick = (b-a)/3600。这个 avgTick 就是过去 1 小时的(几何)平均 tick。

注意整数除法的取整方向(V3 官方 OracleLibrary 在余数为负时会向负无穷调整,保证一致性)。


6. 从平均 tick 还原价格

拿到 avgTick 后,用第 2 章的换算把它变成可读价格:

sqrtPriceX96 = TickMath.getSqrtRatioAtTick(avgTick)
price = (sqrtPriceX96 / 2^96)^2   (再按 token0/token1 方向和 decimals 调整)

periphery 的 OracleLibrary.consultgetQuoteAtTick 帮你封装了这套:给定 avgTick 和一个输入数量,直接返回换算后的输出数量。所以实战里通常:

avgTick = OracleLibrary.consult(pool, period)
amountOut = OracleLibrary.getQuoteAtTick(avgTick, amountIn, tokenIn, tokenOut)

7. 观测数组的容量与扩容

V3 池子默认只存最近 1 个观测点(cardinality = 1),意味着默认只能查很短的历史。要支持长窗口 TWAP,需要扩容观测数组

pool.increaseObservationCardinalityNext(newCardinality);
  • 这会预留更多存储槽来记录历史观测点。
  • 容量越大,能查询的历史越久(但占用更多存储,扩容要付 gas)。
  • 任何人都可以调用扩容(通常由依赖该预言机的协议或集成方提前调好)。

所以用 V3 池做长窗口预言机前,要确认它的 cardinality 足够;不够就先扩容并等待足够的观测点被写入。


8. 本章小结

  1. V3 预言机累积 tick(价格对数),得到几何平均价,比 V2 的算术平均更符合价格乘性。
  2. tickCumulative = Σ(tick × dt);平均 tick = (累积差) / (时间差),TWAP 价 = 1.0001^平均tick
  3. observe(secondsAgos) 直接查过去任意时刻的累积值(内部插值),无需自己存历史。
  4. OracleLibrary.consult + getQuoteAtTick 封装”算平均 tick → 换算价格”。
  5. 默认 cardinality=1,长窗口需 increaseObservationCardinalityNext 扩容观测数组。
  6. 抗操纵原理同 V2:时间是护城河,窗口越长越安全也越滞后。

9. 动手练习

对应课程的 TWAP 练习:用 observe 实现一个 TWAP 查询。

练习:实现 UniswapV3Twap

interface IUniswapV3Pool {
    function observe(uint32[] calldata secondsAgos)
        external view returns (int56[] memory tickCumulatives, uint160[] memory);
}

思路:

  1. getTwap(uint32 period)
    • 构造 secondsAgos = [period, 0]
    • observe,算 avgTick = (tickCumulatives[1] - tickCumulatives[0]) / int56(period)(注意负余数取整)。
    • TickMath.getSqrtRatioAtTick(avgTick)OracleLibrary.getQuoteAtTickavgTick 换成价格 / 报价。
  2. 测试:
    • 选一个真实池(如 WETH/USDT 0.05%)。
    • 先确认 cardinality 足够(不够先 increaseObservationCardinalityNextvm.warp + 做几笔 swap 写入观测点)。
    • getTwap(3600),断言返回价格在合理区间。
  3. 抗操纵验证:用一笔大额 swap 把现货价瞬间推偏,再 getTwap,验证 TWAP 几乎不动。

运行

forge test --evm-version cancun --fork-url $FORK_URL \
  --match-path test/UniswapV3Twap.t.sol -vvv

下一章(第 11 章 附录)介绍两个进阶话题:JIT 流动性 与 V3 套利思路。

💬 评论