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 ExampleOracleSimple
contract learned in the previous article , using UniswapV2 as the price oracle machine. But the two application scenarios are different:
ExampleOracleSimple
The 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.ExampleSlidingWindowOracle
Used 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:
- The sliding window adopts the observer mode. The observation window size (time) is
windowSize
, and the precision iswindowSize / granularity
. Thegranularity
literal value here is the granularity, which actually means the stage. It is assumed here that itwindowSize
is 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. - 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 sliding window adopts the observer mode. The observation window size (time) is
-
The contract definition and the next two
using
grammars 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'sfactory
contract 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 throughgranularity
andwindowSize
be calculatedperiodSize
, but in order to save more intuitive and Gas, also recorded as a state variable. -
pairObservations
Use 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
require
meaning is more clear. - Then verify that the observation window can be divisible by the granularity and
periodSize
assign 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 inJavaScript
a 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
granularity
is ofuint8
type, that is, a container can record up to 255 times, which is enough.
- 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
-
Function
observationIndexOf
to get the observer index at a given time. It first divides byperiodSize
to get the remainder, assuming it is called E (that is, how many precisions it contains). Becausegranularity
of theuint8
size, it is impossible to record all E data. So only take the modulo to recycle. Because anuint
ANDuint8
operation is still oneuint
, it needs to be converted to at lastuint8
. -
getFirstObservationInWindow
The 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
,Observation
a variable of the type is returned , so that the reference will be passed when passing, avoiding the overhead of copying the object. -
update
function. Update the cumulative price of the current block observer. It is mentioned in the comments that eachperiod
(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
for
loop, 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 thegranularity
same 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
if
sentence 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.
-
computeAmountOut
function. 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). -
consult
Query 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
token0
stilltoken1
. Why use the first index calculation of the new window here? Refer to thegetFirstObservationInWindow
function 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 period
slide one grid from the top to the right (a granularity), so it is called the sliding window vividly.
Each machine must use such predictions period
must be updated cumulative value price, ad infinitum; otherwise, the starting position of the window during this period
time, there will be circumstances query interval is greater than during the window, causing the query to fail. But as long as period
the observer information is updated again , the query can be resumed.
Well, this study is over, the next time I plan to study examples
under 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.