Uniswap作为链上的去中心化交易所,承载着价值发现的功能,即用户或其他链上合约可以通过Uniswap来获取代币的价格,Uniswap在这其中承担链上预言机的功能。
假设当前交易池中有1 Ether和2000 USDC,那么以太币的价格就是2000/1=2000 USDC,反之就是USDC的价格,因此价格就是一个比率,由于智能合约尚不支持小数,所以在Uniswap的代码中拓展了新的数据类型来存储价格,关于新的数据类型后面会专门做介绍。
然而仅仅使用瞬时的代币数目之比作为价格是不安全的,存在人为操纵价格预言机的风险,由于Uniswap提供了闪电贷功能,因此在某个闪电贷交易的瞬间,交易对内的代币余额会产生剧烈波动。在UniswapV2中为了解决这个问题,采用了TWAP(Time Weighted Average Price)即时间加权的价格预言机机制。
具体工作原理如下:
总结下来,TWAP的公式如下,这里的T是时间段,P是对应时间段的价格
在UniswapV2的合约中,只会记录分子部分,即记录每个时间段乘以单价的求和,而分母部分则需要使用方自行维护,交易对内有两个代币,所有有两个值来记录
通常我们只需关心某一时间区间内的代币价格,这是TWAP公式的历史价格公式:
假设我们的计价从T4开始,那么实际的计价公式应该如下:
前面已经提到,在合约内有一个变量会追踪分子的求和值,以token0的追踪计价变量price0Cumulativelast为例:
这个变量是记录了历史以来所有时间段的求和,那么我们只需要从T4开始的部分即可,计算方式也很简单,在T3时间点我们获取一个price0Cumulativelast变量的快照,在最新即T6时间点再获取一次,两次的差值即是T4-最新时间段内token0的计价和
我们自己也维护了最近的窗口持续时间和,即:T4+T5+T6
那么这段时间内的TWAP价格即可计算得出:(price0Cumulativelast-UpToTime3)/(T4+T5+T6)
具体到Uniswap的实现中,对于每个交易对都维护了两个变量price0Cumulativelast和price1Cumulativelast,在之前提到的_update
方法中进行求和,具体的代码如下:
blockTimestamp
timeElapsed
timeElapsed
>0时,即进入下一个区块时,才会累加价格*时间段UQ112x112
的特殊格式,了解它是为了记录小数即可,后面会专门讲解这里的优化blockTimestampLast
到最新的区块时间戳function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
...
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast;
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
blockTimestampLast = blockTimestamp;
...
}
基于时间加权的价格计算,会提高攻击者的操纵成本,因为需要连续控制多个区块,这样的攻击成本是很高的。
当价格产生剧烈波动时,由于有时间作为加权因素,预言机的价格无法在较短时间内反映出价格的波动,反而提供出过时的价格,尤其是在市场发生剧烈动荡时,这样的情况会导致Uniswap中的价格与外部市场产生较大的差异。
使用TWAP预言机仍然依赖链下的定时触发,存在维护成本与中心化问题。