Skip to content

Commit

Permalink
Sliding window oracle (Uniswap#24)
Browse files Browse the repository at this point in the history
* Initial commit of sliding window oracle

* Passing tests

* Add a couple more tests

* Add a retry to our gas test

* fetch live prices in consult

change naming conventions / update strategy

* Move to examples folder

* Fix tests, clean up comments

* Lint

* Add retries

* Remove only, add more retries because of how flaky this timestamp test is

* Use uint256 for block timestamps

* Modifier order

* Some clean up before writing more tests

* Add more tests, some additional cleanup

* More comprehensive testing on sliding window oracle

* Overflow safety test for observation index

* Overflow safety test for observation index

Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
  • Loading branch information
moodysalem and NoahZinsmeister committed May 1, 2020
1 parent ea3cefc commit 6d03bed
Show file tree
Hide file tree
Showing 4 changed files with 422 additions and 2 deletions.
2 changes: 1 addition & 1 deletion contracts/examples/ExampleOracleSimple.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ contract ExampleOracleSimple {
}

function update() external {
(uint32 blockTimestamp, uint price0Cumulative, uint price1Cumulative) =
(uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) =
UniswapV2OracleLibrary.currentCumulativePrices(address(pair));
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired

Expand Down
126 changes: 126 additions & 0 deletions contracts/examples/ExampleSlidingWindowOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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 correct epoch
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, now]
// update must have been called for the bucket corresponding to timestamp `now - period` as well as the bucket
// corresponding to `now`
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);
}
}
}
2 changes: 1 addition & 1 deletion contracts/libraries/UniswapV2OracleLibrary.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ library UniswapV2OracleLibrary {
// produces the cumulative price using counterfactuals to save gas and avoid a call to sync.
function currentCumulativePrices(
address pair
) internal view returns (uint32 blockTimestamp, uint price0Cumulative, uint price1Cumulative) {
) internal view returns (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) {
blockTimestamp = currentBlockTimestamp();
price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast();
price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast();
Expand Down
Loading

0 comments on commit 6d03bed

Please sign in to comment.