126 lines
6.3 KiB
Solidity
126 lines
6.3 KiB
Solidity
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);
|
|
}
|
|
}
|
|
}
|