# 价格预言机的使用总结（二）：UniswapV2篇

By [Keegan小钢](https://paragraph.com/@keeganlee) · 2022-04-25

---

前言
--

该系列的[前一篇](https://mp.weixin.qq.com/s?__biz=MzA5OTI1NDE0Mw==&mid=2652494404&idx=1&sn=9ab4817ecf08d25e9e77c42f7b861a98&chksm=8b685054bc1fd94222705f677be927831187c3aea23fc81a77a30fa0d4a6c9ce7d7bb8d9115c&token=141263052&lang=zh_CN&scene=21#wechat_redirect)文章介绍了 **Chainlink** 价格预言机的使用，其目前也被大部分 DeFi 应用所使用，但依然存在局限性。首先是所支持的 Token 的覆盖率还不全，尤其是长尾资产，大多还未支持，比如 **SHIB**，目前只在 BSC 主网有 **SHIB/USD** 的 **Price Feed**，而其它网络的都还没有，连 **Ethereum** 的都还没支持。其次，有些资产的偏差阈值较大，价格更新也比较慢，可能长达十几二十个小时才会更新价格，比如 **BNT**。

这时候就需要考虑其它价格预言机了，而 UniswapV2 和 UniswapV3 都是不错的选择。

本篇先来聊聊如何使用 UniswapV2 作为价格预言机。

UniswapV2 价格预言机
---------------

UniswapV2 使用的价格预言机称为 **TWAP（Time-Weighted Average Price）**，即**时间加权平均价格**。不同于链下聚合的 Chainlink 取自多个不同交易所的数据作为数据源，TWAP 的数据源来自于 Uniswap 自身的交易数据，价格的计算也都是在链上执行的，因此，TWAP 属于链上预言机。

TWAP 的原理比较简单，首先，在 **UniswapV2Pair** 合约中，会存储两个变量 **price0CumulativeLast** 和 **price1CumulativeLast**，在 **\_update()** 函数中会更新这两个变量，其相关代码如下：

    contract UniswapV2Pair {
      ...
      uint32 private blockTimestampLast;
      uint public price0CumulativeLast;
      uint public price1CumulativeLast;
      ...
      // update reserves and, on the first call per block, price accumulators
      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;
        ...
      }
    }
    

price0CumulativeLast 和 price1CumulativeLast 分别记录了 token0 和 token1 的累计价格。所谓累计价格，其代表的是**整个合约历史中每一秒的 Uniswap 价格总和**。且只会在每个区块第一笔交易时执行累加计算，累加的值不是当前区块的第一笔交易的价格，而是在这之前的最后一笔交易的价格，所以至少也是上个区块的价格。取自之前区块的价格，可以大大提高操控价格的成本，所以自然也提高了安全性。

![](https://storage.googleapis.com/papyrus_images/df59f0fc3e1078a1ce5ed66fa69f00ba9b8abdd476a0bb7906555a7e57645678.png)

如上图所示，合约的第一个区块为 Block 122，这时候，价格和时间差都为 0，所以累计价格也为  0。到了下一个区块 Block 123，这时候取自上个区块的最后一口价格 10.2，且经过的时间差为 7，因此就可以计算出累计价格 priceCumulative = 10.2 \* 7 = 71.4。再到下个区块 Block 124，取自上一口价格 10.3，两个区块间的时间差为 8，那此时的累计价格就变成了 71.4 + (10.3 \* 8) = 153.8。Block 125 的时候也同理，上口价格为 10.5，区块时间差为 5，所以最新的累计价格就变成了 153.8 + (10.5 \* 5) = 206.3。

有了这个基础之后，就可以计算 TWAP 了。

固定时间窗口 TWAP
-----------

![](https://storage.googleapis.com/papyrus_images/b7aa8666b749fc2a576783cfc0152595e783dde4607f5a0b09720b414dabdfd7.png)

计算 TWAP 的原理也是非常简单，如上图所示，这是计算时间间隔为 1 小时的 TWAP，取自开始和结束时的累计价格和两区块当时的时间戳，两者的累计价格相减，再除以两者之间的时间差，就算出这 1 小时内的 TWAP 价格了。

这是 TWAP 最简单的计算方式，也称为固定时间窗口的 TWAP。下面来讲讲具体如何实现。

Uniswap 官方也有提供了一个示例代码来计算固定时间窗口的 TWAP，其代码放在 v2-periphery 项目中：

*   [https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol](https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol)
    

该示例代码也比较简单，我们直接贴上代码看看：

    pragma solidity =0.6.6;
    
    import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
    import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
    import '@uniswap/lib/contracts/libraries/FixedPoint.sol';
    
    import '../libraries/UniswapV2OracleLibrary.sol';
    import '../libraries/UniswapV2Library.sol';
    
    // fixed window oracle that recomputes the average price for the entire period once every period
    // note that the price average is only guaranteed to be over at least 1 period, but may be over a longer period
    contract ExampleOracleSimple {
        using FixedPoint for *;
    
        uint public constant PERIOD = 24 hours;
    
        IUniswapV2Pair immutable pair;
        address public immutable token0;
        address public immutable token1;
    
        uint    public price0CumulativeLast;
        uint    public price1CumulativeLast;
        uint32  public blockTimestampLast;
        FixedPoint.uq112x112 public price0Average;
        FixedPoint.uq112x112 public price1Average;
    
        constructor(address factory, address tokenA, address tokenB) public {
            IUniswapV2Pair _pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, tokenA, tokenB));
            pair = _pair;
            token0 = _pair.token0();
            token1 = _pair.token1();
            price0CumulativeLast = _pair.price0CumulativeLast(); // fetch the current accumulated price value (1 / 0)
            price1CumulativeLast = _pair.price1CumulativeLast(); // fetch the current accumulated price value (0 / 1)
            uint112 reserve0;
            uint112 reserve1;
            (reserve0, reserve1, blockTimestampLast) = _pair.getReserves();
            require(reserve0 != 0 && reserve1 != 0, 'ExampleOracleSimple: NO_RESERVES'); // ensure that there's liquidity in the pair
        }
    
        function update() external {
            (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) =
                UniswapV2OracleLibrary.currentCumulativePrices(address(pair));
            uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
    
            // ensure that at least one full period has passed since the last update
            require(timeElapsed >= PERIOD, 'ExampleOracleSimple: PERIOD_NOT_ELAPSED');
    
            // overflow is desired, casting never truncates
            // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed
            price0Average = FixedPoint.uq112x112(uint224((price0Cumulative - price0CumulativeLast) / timeElapsed));
            price1Average = FixedPoint.uq112x112(uint224((price1Cumulative - price1CumulativeLast) / timeElapsed));
    
            price0CumulativeLast = price0Cumulative;
            price1CumulativeLast = price1Cumulative;
            blockTimestampLast = blockTimestamp;
        }
    
        // note this will always return 0 before update has been called successfully for the first time.
        function consult(address token, uint amountIn) external view returns (uint amountOut) {
            if (token == token0) {
                amountOut = price0Average.mul(amountIn).decode144();
            } else {
                require(token == token1, 'ExampleOracleSimple: INVALID_TOKEN');
                amountOut = price1Average.mul(amountIn).decode144();
            }
        }
    }
    

**PERIOD** 指定为了 24 小时，说明这个示例计算 TWAP 的固定时间窗口为 24 小时，即每隔 24 小时才更新一次价格。

该示例也只保存一个交易对的价格，即 **token0-token1** 的价格。**price0Average** 和 **price1Average** 分别就是 token0 和 token1 的 TWAP 价格。比如，token0 为 WETH，token1 为 USDC，那 price0Average 就是 WETH 对 USDC 的价格，而 price1Average 则是 USDC 对 WETH 的价格。

**update()** 函数就是更新 TWAP 价格的函数，这一般需要链下程序的定时任务来触发，按照这个示例的话，就是链下的定时任务需要每隔 24 小时就定时触发调用 update() 函数。

**update()** 函数的实现逻辑也和上面所述的公式一致：

1.  读取出当前最新的累计价格和当前的时间戳；
    
2.  计算出当前时间和上一次更新价格时的时间差 timeElapsed，要求该时间差需要达 24 小时；
    
3.  根据公式 TWAP = (priceCumulative - priceCumulativeLast) / timeElapsed 计算得到最新的 TWAP，即 **priceAverage**；
    
4.  更新 priceCumulativeLast 和 blockTimestampLast 为当前最新的累计价格和时间戳。
    

不过，有一点需要注意，因为 **priceCumulative** 本身计算存储时是做了**左移 112 位**的操作的，所以计算所得的 **priceAverage** 也是左移了 112 位的。

**consult()** 函数则可查询出用 TWAP 价格计算可兑换的数量。比如，token0 为 WETH，token1 为 USDC，假设 WETH 的价格为 3000 USDC，查询 consult() 时，若传入的参数 token 为 token0 的地址，amountIn 为 2，那输出的 amountOut 则为 3000 \* 2 = 6000，可理解为若支付 2 WETH，就可根据价格换算成 6000 USDC。

滑动时间窗口 TWAP
-----------

固定时间窗口 TWAP 的原理和实现，比较简单，但其最大的不足就是价格变化不够平滑，时间窗口越长，价格变化就可能会越陡峭。因此，在实际应用中，更多其实是用滑动时间窗口的 TWAP。

所谓滑动时间窗口 TWAP，就是说，计算 TWAP 的时间窗口并非固定的，而是滑动的。这种算法的主要原理就是将时间窗口划分为多个**时间片段**，每过一个**时间片段**，时间窗口就会往右滑动一格，如下图所示：

![](https://storage.googleapis.com/papyrus_images/9d7f2917811c30493f7b18a204d0e0bb851924c5a1b76ca3fc04e61a7e497d4a.png)

上图所示的时间窗口为 1 小时，划分为了 6 个时间片段，每个时间片段则为 10 分钟。那每过 10 分钟，整个时间窗口就会往右滑动一格。而计算 TWAP 时的公式则没有变，依然还是取自时间窗口的起点和终点。如果时间窗口为 24 小时，按照固定时间窗口算法，每隔 24 小时 TWAP 价格才会更新，但使用滑动时间窗口算法后，假设时间片段为 1 小时，则 TWAP 价格是每隔 1 小时就会更新。

Uniswap 官方也同样提供了这种滑动时间窗口 TWAP 实现的示例代码，其 Github 地址为：

*   [https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleSlidingWindowOracle.sol](https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleSlidingWindowOracle.sol)
    

我们也贴上代码看看：

    pragma solidity =0.6.6;
    
    import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
    import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';
    import '@uniswap/lib/contracts/libraries/FixedPoint.sol';
    
    import '../libraries/SafeMath.sol';
    import '../libraries/UniswapV2Library.sol';
    import '../libraries/UniswapV2OracleLibrary.sol';
    
    // sliding window oracle that uses observations collected over a window to provide moving price averages in the past
    // `windowSize` with a precision of `windowSize / granularity`
    // note this is a singleton oracle and only needs to be deployed once per desired parameters, which
    // differs from the simple oracle which must be deployed once per pair.
    contract ExampleSlidingWindowOracle {
        using FixedPoint for *;
        using SafeMath for uint;
    
        struct Observation {
            uint timestamp;
            uint price0Cumulative;
            uint price1Cumulative;
        }
    
        address public immutable factory;
        // the desired amount of time over which the moving average should be computed, e.g. 24 hours
        uint public immutable windowSize;
        // the number of observations stored for each pair, i.e. how many price observations are stored for the window.
        // as granularity increases from 1, more frequent updates are needed, but moving averages become more precise.
        // averages are computed over intervals with sizes in the range:
        //   [windowSize - (windowSize / granularity) * 2, windowSize]
        // e.g. if the window size is 24 hours, and the granularity is 24, the oracle will return the average price for
        //   the period:
        //   [now - [22 hours, 24 hours], now]
        uint8 public immutable granularity;
        // this is redundant with granularity and windowSize, but stored for gas savings & informational purposes.
        uint public immutable periodSize;
    
        // mapping from pair address to a list of price observations of that pair
        mapping(address => Observation[]) public pairObservations;
    
        constructor(address factory_, uint windowSize_, uint8 granularity_) public {
            require(granularity_ > 1, 'SlidingWindowOracle: GRANULARITY');
            require(
                (periodSize = windowSize_ / granularity_) * granularity_ == windowSize_,
                'SlidingWindowOracle: WINDOW_NOT_EVENLY_DIVISIBLE'
            );
            factory = factory_;
            windowSize = windowSize_;
            granularity = granularity_;
        }
    
        // returns the index of the observation corresponding to the given timestamp
        function observationIndexOf(uint timestamp) public view returns (uint8 index) {
            uint epochPeriod = timestamp / periodSize;
            return uint8(epochPeriod % granularity);
        }
    
        // returns the observation from the oldest epoch (at the beginning of the window) relative to the current time
        function getFirstObservationInWindow(address pair) private view returns (Observation storage firstObservation) {
            uint8 observationIndex = observationIndexOf(block.timestamp);
            // no overflow issue. if observationIndex + 1 overflows, result is still zero.
            uint8 firstObservationIndex = (observationIndex + 1) % granularity;
            firstObservation = pairObservations[pair][firstObservationIndex];
        }
    
        // update the cumulative price for the observation at the current timestamp. each observation is updated at most
        // once per epoch period.
        function update(address tokenA, address tokenB) external {
            address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
    
            // populate the array with empty observations (first call only)
            for (uint i = pairObservations[pair].length; i < granularity; i++) {
                pairObservations[pair].push();
            }
    
            // get the observation for the current period
            uint8 observationIndex = observationIndexOf(block.timestamp);
            Observation storage observation = pairObservations[pair][observationIndex];
    
            // we only want to commit updates once per period (i.e. windowSize / granularity)
            uint timeElapsed = block.timestamp - observation.timestamp;
            if (timeElapsed > periodSize) {
                (uint price0Cumulative, uint price1Cumulative,) = UniswapV2OracleLibrary.currentCumulativePrices(pair);
                observation.timestamp = block.timestamp;
                observation.price0Cumulative = price0Cumulative;
                observation.price1Cumulative = price1Cumulative;
            }
        }
    
        // given the cumulative prices of the start and end of a period, and the length of the period, compute the average
        // price in terms of how much amount out is received for the amount in
        function computeAmountOut(
            uint priceCumulativeStart, uint priceCumulativeEnd,
            uint timeElapsed, uint amountIn
        ) private pure returns (uint amountOut) {
            // overflow is desired.
            FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112(
                uint224((priceCumulativeEnd - priceCumulativeStart) / timeElapsed)
            );
            amountOut = priceAverage.mul(amountIn).decode144();
        }
    
        // returns the amount out corresponding to the amount in for a given token using the moving average over the time
        // range [now - [windowSize, windowSize - periodSize * 2], now]
        // update must have been called for the bucket corresponding to timestamp `now - windowSize`
        function consult(address tokenIn, uint amountIn, address tokenOut) external view returns (uint amountOut) {
            address pair = UniswapV2Library.pairFor(factory, tokenIn, tokenOut);
            Observation storage firstObservation = getFirstObservationInWindow(pair);
    
            uint timeElapsed = block.timestamp - firstObservation.timestamp;
            require(timeElapsed <= windowSize, 'SlidingWindowOracle: MISSING_HISTORICAL_OBSERVATION');
            // should never happen.
            require(timeElapsed >= windowSize - periodSize * 2, 'SlidingWindowOracle: UNEXPECTED_TIME_ELAPSED');
    
            (uint price0Cumulative, uint price1Cumulative,) = UniswapV2OracleLibrary.currentCumulativePrices(pair);
            (address token0,) = UniswapV2Library.sortTokens(tokenIn, tokenOut);
    
            if (token0 == tokenIn) {
                return computeAmountOut(firstObservation.price0Cumulative, price0Cumulative, timeElapsed, amountIn);
            } else {
                return computeAmountOut(firstObservation.price1Cumulative, price1Cumulative, timeElapsed, amountIn);
            }
        }
    }
    

要实现滑动时间窗口算法，就需要将时间分段，还需要保存每个时间段的 **priceCumulative**。在这实现的示例代码中，定义了结构体 **Observation**，用来保存每个时间片段的数据，包括两个 token 的 **priceCumulative** 和记录的时间点 **timestamp**。还定义了 **pairObservations** 用来存储每个 pair 的 **Observation** 数组，而数组实际的长度取决于将整个时间窗口划分为多少个时间片段。

**windowSize** 表示时间窗口大小，比如 24 小时，**granularity** 是划分的时间片段数量，比如 24 段，**periodSize** 则是每时间片段的大小，比如 1 小时，是由 windowSize / granularity 计算所得。这几个值都在构造函数中进行了初始化。

触发 **update()** 函数则更新存储最新时间片段的 observation，如时间片段大小为 1 小时，即每隔 1 小时就要触发 update() 函数一次。因为这个示例中是支持多个 pair 的，所以 update() 时需要指定所要更新的两个 token。

而查询当前 TWAP 价格的计算就在 **consult()** 函数里实现了。首先，先获取到当前时间窗口里的第一个时间片段的 observation，也算出当前时间与第一个 observation 时间的时间差，且读取出当前最新的 priceCumulative，之后就在 computeAmountOut() 函数里计算得到最新的 TWAP 价格 priceAverage，且根据 amountIn 算出了 amountOut 并返回。

总结
--

本文我们主要介绍了被广泛使用的一种链上预言机 **TWAP（时间加权平均价格）**，且介绍了**固定时间窗口**和**滑点时间窗口**两种算法的 TWAP。虽然，TWAP 是由 Uniswap 推出的，但因为很多其他 DEX 也采用了和 Uniswap 一样的底层实现，如 SushiSwap、PancakeSwap 等，所以这些 DEX 也可以用同样的算法计算出对应的 TWAP。

但使用 UniswapV2 的 TWAP，其主要缺陷就是需要链下程序定时触发 update() 函数，存在维护成本。**UniswapV3** 的 TWAP 则解决了这个问题，下一篇会来聊聊其具体是如何实现的。

* * *

文章首发于「Keegan小钢」公众号：

[https://mp.weixin.qq.com/s?\_\_biz=MzA5OTI1NDE0Mw==&mid=2652494441&idx=1&sn=57a97690390b93770c5a906dce4157c8&chksm=8b685079bc1fd96f9ab60cc1b41b8642abf807a13a37c12f05a280be2e03f3a9288a047b5739&token=1584634265&lang=zh\_CN#rd](https://mp.weixin.qq.com/s?__biz=MzA5OTI1NDE0Mw==&mid=2652494441&idx=1&sn=57a97690390b93770c5a906dce4157c8&chksm=8b685079bc1fd96f9ab60cc1b41b8642abf807a13a37c12f05a280be2e03f3a9288a047b5739&token=1584634265&lang=zh_CN#rd)

---

*Originally published on [Keegan小钢](https://paragraph.com/@keeganlee/uniswapv2)*
