UniswapV2 peripheral contract learning (7) - ExampleSlidingWindowOracle.sol

I remember a sentence in the circle of friends, if Defi is the crown of Ethereum, then Uniswap is the jewel in this crown. Uniswap is currently the V2 version. Compared to V1, its functions are more fully optimized, but its contract source code is not complicated. This article is a series of record articles for personal learning UniswapV2 source code.

1. Introduction to ExampleSlidingWindowOracle contract

This contract is the same as the ExampleOracleSimplecontract learned in the previous article , using UniswapV2 as the price oracle machine. But the two application scenarios are different:

  • ExampleOracleSimpleThe contract is used in a fixed window mode, in which historical data is not important, and the current price has the same weight as the historical price. Therefore, it is sufficient to record (update) the average price once per cycle.
  • ExampleSlidingWindowOracleUsed in sliding window mode, you can record price-related information multiple times in a cycle. The sliding window mode is also divided into two categories, one is the simple moving average, which means that each price calculation is equal weight. The other is the exponential moving average, the most recent price calculation has a greater weight value.

This contract is an implementation example of simple moving average. For more information on using UniswapV2 as a price oracle machine, please read its document Buiding an Oracle . The document also elaborates on overflow issues in price calculations.

2. Contract source code

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

Three, a brief study of the source code

Let's briefly study the source code of this contract. Personal understanding may not be correct, please leave a message to correct me.

  • The first line of the Solidity version

  • Lines 2-7 import 6 interface definitions or tool libraries, which is only one more than the previous article SafeMath.

  • Next is the contract note (description), which reveals several key points:

    1. The sliding window adopts the observer mode. The observation window size (time) is windowSize, and the precision is windowSize / granularity. The granularityliteral value here is the granularity, which actually means the stage. It is assumed here that it windowSizeis 24 hours, that is, the observation window is 24 hours. The granularity is 8, then the accuracy is 3 hours, that is, the average price can be recorded 8 times in a cycle, so that it is easier to see the price trend.
    2. For fixed parameters, this contract only needs to be deployed once, which is a singleton contract. The fixed window mode in the previous article requires a contract to be deployed for each trading pair.
  • The contract definition and the next two usinggrammars have been studied many times, and they are also very simple, so skip directly.

  • struct Observation {
          
          
        uint timestamp;
        uint price0Cumulative;
        uint price1Cumulative;
    }
    

    Defines a structure called the observer. It has three fields. The first field records the block time during the observation, and the latter two fields record the cumulative value of the price during the observation.

  • Next is the definition of four state variables, respectively factory,windowSize,granularity,periodSize. Their meanings are V2's factorycontract address, observation window size, granularity, and observation window accuracy (that is, the window size divided by the granularity). The notes here also mention a lot of content, such as the larger the granularity, the more frequent the update, and the more accurate the moving average price. There are mentioned, could have been through granularityand windowSizebe calculated periodSize, but in order to save more intuitive and Gas, also recorded as a state variable.

  • pairObservationsUse a map to record the observers of each trading pair. Observer is an array, and its length is granularity, which represents the number of observations.

  • Next is the constructor.

    • First, verify that the granularity cannot be 0, because it is a divisor. Although dividing by zero without verification will also report an error reset transaction, the requiremeaning is more clear.
    • Then verify that the observation window can be divisible by the granularity and periodSizeassign values at the same time . This is obvious, otherwise there will be a gap in the observation window. Note the syntax here. The (periodSize = windowSize_ / granularity_) * granularity_ == windowSize_,left end of the equation uses an expression as a multiplier. This is not supported in some programming languages, and expressions cannot be mixed with values. But in JavaScripta series of supported languages, the value on the left side of the expression is the value of the expression. Although this syntax is rarely used in Solidity, it shows that it is also supported.
    • Next, set the value of the previously defined state variable (observation parameter). Note that it granularityis of uint8type, that is, a container can record up to 255 times, which is enough.
  • Function observationIndexOfto get the observer index at a given time. It first divides by periodSizeto get the remainder, assuming it is called E (that is, how many precisions it contains). Because granularityof the uint8size, it is impossible to record all E data. So only take the modulo to recycle. Because an uintAND uint8operation is still one uint, it needs to be converted to at last uint8.

  • getFirstObservationInWindowThe function is a private function. The comment mentioned is the first observer to get the current new window. Its index adds 1 to the index of the current block record.

    Why add 1? Because the observer is cyclic. If the latest index is increased by 1, then its position is either empty or has an old value. Having an old value is equivalent to returning to the beginning of a window period. This function is used in subsequent calculations, so that the current block time minus the start time of the window period when calculating is exactly one window period.

    To prevent overflow, the modulo method is adopted, which of course is equivalent to direct type conversion. This is also mentioned in the core contract trading pair learning. The last thing to note is that because it is a private function, it is used internally. Therefore storage, Observationa variable of the type is returned , so that the reference will be passed when passing, avoiding the overhead of copying the object.

  • updatefunction. Update the cumulative price of the current block observer. It is mentioned in the comments that each period(precision) is updated at most once. The function parameters are the two token addresses of the trading pair.

    • First use the UniswapV2 tool library to calculate the trading pair address.
    • Next is a forloop, if the observer array of the trading pair is not initialized at this time, it is initialized with empty data. The length of the array is the granularitysame after initialization , so it will not be initialized a second time.
    • The next two lines of code get the observer information recorded in the current block.
    • Next, it is *uint* timeElapsed = *block*.timestamp - observation.timestamp;used to calculate the difference between the current block time and the time recorded by the current observer (the current recorded time can also be 0, that is, it has not been recorded).
    • Next is a ifsentence to determine whether the time difference is greater than the specified precision (record at most once within a precision). If the conditions are met, the UniswapV2 tool library is used to calculate the cumulative price of the current block and update the current observer record. This updates the current block observer's price cumulative value and block time (if the time interval requirement is met). Note here: Observers are recycled, and new ones will overwrite old ones.
  • computeAmountOutfunction. It is also a private function that uses the average price to calculate the quantity of a certain asset. Note that the calculation method of the average price is the same as mentioned in the previous article, that is, the cumulative price difference divided by the time interval (similar to the formula for calculating the average speed).

  • consultQuery function. Based on the average price during the entire window, given the quantity of one token, calculate the quantity of another token. Its parameters are the address and quantity of the input token, and the address of the token to be calculated.

    • A lot of information is mentioned in the function comments, such as the time range of the average price used in the query. In addition, the corresponding time period must have updated prices.
    • The first line of the function is used to calculate the trading pair address.
    • The second line gets the first observer of the new window.
    • The third line calculates the difference between the current block time and the time recorded by the first observer in the new window.
    • The fourth line verifies that the time difference must be less than one window period, that is, it cannot be updated for too long.
    • The fifth line is used to verify the lower limit of the time difference.
    • The sixth line is used to get the cumulative price difference of the current block.
    • The seventh line is used to sort the two token addresses of the input parameters.
    • The function finally calculates the number of tokens according to whether the input is token0still token1. Why use the first index calculation of the new window here? Refer to the getFirstObservationInWindowfunction description.

Four, summary

Note: Although the accuracy (stages) is divided according to the granularity in the window period, each stage records the observer block time and the cumulative price at the time. Its function is to reflect the price sliding, and the other is not to use the same accumulation. Price points (not fixed, relative to the fixed cumulative price points of the previous article) to calculate the average price. However, the average price is still calculated over the entire window period, not an average price within precision.

During each query, the window period of the query will periodslide one grid from the top to the right (a granularity), so it is called the sliding window vividly.

Each machine must use such predictions periodmust be updated cumulative value price, ad infinitum; otherwise, the starting position of the window during this periodtime, there will be circumstances query interval is greater than during the window, causing the query to fail. But as long as periodthe observer information is updated again , the query can be resumed.

Well, this study is over, the next time I plan to study examplesunder the catalog ExampleSwapToPrice.sol.

Due to limited personal ability, it is inevitable that there will be mistakes or incorrect understandings. Please leave a message for correction.

Guess you like

Origin blog.csdn.net/weixin_39430411/article/details/109262196