Uniswap V3 第 10 章:TWAP 价格预言机
V3 内置了比 V2 更好用的时间加权平均价(TWAP)预言机。它累积的不是”价格”,而是 tick(价格的对数),并把历史观测点存进环形数组,让你能直接查询任意过去时刻的累积值。这一章讲清
tickCumulative、observe,以及它相比 V2 的改进。
目录
- 1. V3 预言机的改进
- 2. tickCumulative:累积的是 tick
- 3. 几何平均价:为什么累积 tick 更优雅
- 4. observe:查询任意时刻的累积值
- 5. 案例:算一段时间的 TWAP
- 6. 从平均 tick 还原价格
- 7. 观测数组的容量与扩容
- 8. 本章小结
- 9. 动手练习
1. V3 预言机的改进
回忆 V2 TWAP(第 7 章)的局限:
- V2 累积的是算术价格
priceCumulative = Σ(price × dt),得到的是算术平均价。 - V2 只存”最新”的累积值,你要自己在
t1和t2各记一次、自己存历史。
V3 的改进:
- 累积 tick(价格的对数),得到几何平均价(更符合价格的乘性本质)。
- 内置一个环形数组存历史观测点,提供
observe(secondsAgos)让你直接查过去任意时刻的累积值,无需自己存。
2. tickCumulative:累积的是 tick
V3 的每个观测点(observation)记录:
tickCumulative = Σ (当前 tick × 持续秒数)
secondsPerLiquidityCumulative = Σ (dt / 活跃流动性) // 另一个量,用于流动性挖矿等
blockTimestamp
tickCumulative 是 tick 对时间的积分。和 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.consult 和 getQuoteAtTick 帮你封装了这套:给定 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. 本章小结
- V3 预言机累积 tick(价格对数),得到几何平均价,比 V2 的算术平均更符合价格乘性。
tickCumulative = Σ(tick × dt);平均 tick =(累积差) / (时间差),TWAP 价 =1.0001^平均tick。observe(secondsAgos)直接查过去任意时刻的累积值(内部插值),无需自己存历史。- 用
OracleLibrary.consult+getQuoteAtTick封装”算平均 tick → 换算价格”。 - 默认
cardinality=1,长窗口需increaseObservationCardinalityNext扩容观测数组。 - 抗操纵原理同 V2:时间是护城河,窗口越长越安全也越滞后。
9. 动手练习
对应课程的 TWAP 练习:用 observe 实现一个 TWAP 查询。
练习:实现 UniswapV3Twap
interface IUniswapV3Pool {
function observe(uint32[] calldata secondsAgos)
external view returns (int56[] memory tickCumulatives, uint160[] memory);
}
思路:
- 写
getTwap(uint32 period):- 构造
secondsAgos = [period, 0]。 - 调
observe,算avgTick = (tickCumulatives[1] - tickCumulatives[0]) / int56(period)(注意负余数取整)。 - 用
TickMath.getSqrtRatioAtTick(avgTick)或OracleLibrary.getQuoteAtTick把avgTick换成价格 / 报价。
- 构造
- 测试:
- 选一个真实池(如 WETH/USDT 0.05%)。
- 先确认
cardinality足够(不够先increaseObservationCardinalityNext并vm.warp+ 做几笔 swap 写入观测点)。 - 调
getTwap(3600),断言返回价格在合理区间。
- 抗操纵验证:用一笔大额 swap 把现货价瞬间推偏,再
getTwap,验证 TWAP 几乎不动。
运行
forge test --evm-version cancun --fork-url $FORK_URL \
--match-path test/UniswapV3Twap.t.sol -vvv
下一章(第 11 章 附录)介绍两个进阶话题:JIT 流动性 与 V3 套利思路。