Summary of the use of price oracles (2): UniswapV2

foreword

The previous article in this series introduced the use of the Chainlink price oracle, which is currently used by most DeFi applications, but still has limitations. First of all, the coverage of the supported tokens is not complete, especially long-tail assets, most of which are not yet supported, such as SHIB . Currently, only the BSC mainnet has the SHIB / USD Price Feed , while other networks have not, even Ethereum. are not supported yet. Secondly, the deviation threshold of some assets is relatively large, and the price update is relatively slow, and it may take up to ten or twenty hours to update the price, such as BNT .

At this time, other price oracles need to be considered, and UniswapV2 and UniswapV3 are both good choices.

In this article, let’s talk about how to use UniswapV2 as a price oracle.

UniswapV2 Price Oracle

The price oracle used by UniswapV2 is called TWAP (Time-Weighted Average Price) , which is the time-weighted average price . Unlike the off-chain aggregated Chainlink, which uses data from multiple different exchanges as the data source, the data source of TWAP comes from Uniswap’s own transaction data, and the price calculation is also performed on the chain. Therefore, TWAP belongs to the chain. oracle.

The principle of TWAP is relatively simple. First, in the UniswapV2Pair contract, two variables price0CumulativeLast and price1CumulativeLast will be stored , and these two variables will be updated in the _update() function. The relevant codes are as follows:

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 and price1CumulativeLast record the cumulative price of token0 and token1 respectively. The so-called cumulative price represents the sum of Uniswap prices per second in the entire contract history . And only the cumulative calculation is performed at the first transaction of each block. The accumulated value is not the price of the first transaction of the current block, but the price of the last transaction before that, so at least it is the last transaction. The price of the block. The price taken from the previous block can greatly increase the cost of manipulating the price, so it naturally improves the security.

Summary of the use of price oracles (2): UniswapV2

As shown in the figure above, the first block of the contract is Block 122. At this time, the price and time difference are both 0, so the cumulative price is also 0. When it comes to the next block, Block 123, the price of the last bite taken from the previous block is 10.2, and the elapsed time difference is 7, so the cumulative price priceCumulative = 10.2 * 7 = 71.4 can be calculated. Then go to the next block, Block 124, which is taken from the previous price of 10.3, and the time difference between the two blocks is 8. Then the cumulative price at this time becomes 71.4 + (10.3 * 8) = 153.8. The same is true for Block 125, the catch price is 10.5, and the block time difference is 5, so the latest cumulative price becomes 153.8 + (10.5 * 5) = 206.3.

With this foundation in place, TWAP can be calculated.

Fixed time window TWAP

Summary of the use of price oracles (2): UniswapV2

The principle of calculating TWAP is also very simple. As shown in the figure above, this is TWAP with a calculation time interval of 1 hour. It is taken from the cumulative price at the beginning and the end and the timestamp of the two blocks at that time. Divide by the time difference between the two to calculate the TWAP price in the 1 hour.

This is the simplest way to calculate TWAP, also known as TWAP with fixed time windows. Let’s talk about how to achieve it.

Uniswap official also provides a sample code to calculate TWAP with a fixed time window, the code is placed in the v2-periphery project:

The sample code is also relatively simple, let’s paste the code directly to see:

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 is specified as 24 hours, indicating that the fixed time window for calculating TWAP in this example is 24 hours, that is, the price is updated every 24 hours.

The example also only saves the price of one trading pair, the price of token0-token1 . price0Average and price1Average are the TWAP prices of token0 and token1 respectively. For example, token0 is WETH, token1 is USDC, then price0Average is the price of WETH to USDC, and price1Average is the price of USDC to WETH.

The update() function is the function to update the TWAP price, which generally needs to be triggered by the scheduled task of the off-chain program. According to this example, the off-chain scheduled task needs to periodically trigger the update() function every 24 hours.

The implementation logic of the update() function is also consistent with the above formula:

  1. Read out the current latest cumulative price and current timestamp;
  2. Calculate the time difference timeElapsed between the current time and the last time the price was updated, and the time difference is required to be 24 hours;
  3. Calculate the latest TWAP according to the formula TWAP = (priceCumulative – priceCumulativeLast) / timeElapsed, namely priceAverage ;
  4. Update priceCumulativeLast and blockTimestampLast to the current latest cumulative price and timestamp.

However, there is one thing to note, because priceCumulative itself performs a 112-bit left-shift operation when calculating and storing, so the calculated priceAverage is also left-shifted by 112-bit.

The consult() function can query the convertible amount calculated with the TWAP price. For example, token0 is WETH and token1 is USDC. Suppose the price of WETH is 3000 USDC. When querying consult(), if the incoming parameter token is the address of token0 and the amountIn is 2, the output amountOut is 3000 * 2 = 6000 , it can be understood that if you pay 2 WETH, it can be converted into 6000 USDC according to the price.

Sliding time window TWAP

The principle and implementation of fixed time window TWAP is relatively simple, but its biggest disadvantage is that the price change is not smooth enough. The longer the time window, the steeper the price change may be. Therefore, in practical applications, more TWAP with sliding time windows is actually used.

The so-called sliding time window TWAP means that the time window for calculating TWAP is not fixed, but sliding. The main principle of this algorithm is to divide the time window into multiple time segments . After each time segment , the time window will slide one grid to the right, as shown in the following figure:

Summary of the use of price oracles (2): UniswapV2

The time window shown in the figure above is 1 hour, divided into 6 time slices, each time slice is 10 minutes. Then every 10 minutes, the entire time window will slide one space to the right. The formula for calculating TWAP has not changed, and is still taken from the start and end points of the time window. If the time window is 24 hours, according to the fixed time window algorithm, the TWAP price will be updated every 24 hours, but after using the sliding time window algorithm, assuming that the time segment is 1 hour, the TWAP price will be updated every 1 hour.

Uniswap officials also provide sample code for this sliding time window TWAP implementation, whose Github address is:

Let’s also paste the code to see:

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);
        }
    }
}

To implement the sliding time window algorithm, it is necessary to divide the time into segments, and also to save the priceCumulative of each time segment . In the sample code of this implementation, the structure Observation is defined to save the data of each time segment, including the priceCumulative of the two tokens and the recorded time point timestamp . PairObservations is also defined to store the Observation array for each pair , and the actual length of the array depends on how many time segments the entire time window is divided into.

windowSize represents the size of the time window, such as 24 hours, granularity is the number of divided time segments, such as 24 segments, periodSize is the size of each time segment, such as 1 hour, which is calculated by windowSize / granularity. These values ​​are initialized in the constructor.

When the update() function is triggered , the observation that stores the latest time segment is updated. For example, if the time segment size is 1 hour, the update() function is triggered every 1 hour. Because multiple pairs are supported in this example, two tokens to be updated need to be specified in update().

The calculation of querying the current TWAP price is implemented in the consult() function. First, obtain the observation of the first time segment in the current time window, calculate the time difference between the current time and the first observation time, and read the latest priceCumulative, and then calculate it in the computeAmountOut() function. The latest TWAP price, priceAverage, calculates the amountOut according to the amountIn and returns it.

Summarize

In this paper, we mainly introduce a widely used on-chain oracle TWAP (Time Weighted Average Price) , and introduce the TWAP algorithm of fixed time window and slippage time window . Although TWAP was launched by Uniswap, because many other DEXs also use the same underlying implementation as Uniswap, such as SushiSwap, PancakeSwap, etc., these DEXs can also use the same algorithm to calculate the corresponding TWAP.

However, the main drawback of using UniswapV2’s TWAP is that the off-chain program needs to trigger the update() function regularly, and there is a maintenance cost. The TWAP of UniswapV3 solves this problem, and the next article will talk about how it is implemented.

Posted by:CoinYuppie,Reprinted with attribution to:https://coinyuppie.com/summary-of-the-use-of-price-oracles-2-uniswapv2/
Coinyuppie is an open information publishing platform, all information provided is not related to the views and positions of coinyuppie, and does not constitute any investment and financial advice. Users are expected to carefully screen and prevent risks.

Like (0)
Donate Buy me a coffee Buy me a coffee
Previous 2022-04-25 23:49
Next 2022-04-25 23:51

Related articles