ExampleSlidingWindowOracle with DAI + WETH for Kovan.
pragma solidity 0.6.6; | |
pragma experimental ABIEncoderV2; | |
import "https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/interfaces/IUniswapV2Pair.sol"; | |
import "https://github.com/Uniswap/uniswap-lib/blob/master/contracts/libraries/FixedPoint.sol"; | |
import "https://github.com/Uniswap/uniswap-lib/blob/master/contracts/libraries/FullMath.sol"; | |
import "https://github.com/Uniswap/uniswap-lib/blob/master/contracts/libraries/Babylonian.sol"; | |
import "https://github.com/Uniswap/uniswap-lib/blob/master/contracts/libraries/BitMath.sol"; | |
library SafeMath { | |
function add(uint x, uint y) internal pure returns (uint z) { | |
require((z = x + y) >= x, 'ds-math-add-overflow'); | |
} | |
function sub(uint x, uint y) internal pure returns (uint z) { | |
require((z = x - y) <= x, 'ds-math-sub-underflow'); | |
} | |
function mul(uint x, uint y) internal pure returns (uint z) { | |
require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow'); | |
} | |
} | |
library UniswapV2Library { | |
using SafeMath for uint; | |
// returns sorted token addresses, used to handle return values from pairs sorted in this order | |
function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { | |
require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); | |
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); | |
require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); | |
} | |
// calculates the CREATE2 address for a pair without making any external calls | |
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { | |
(address token0, address token1) = sortTokens(tokenA, tokenB); | |
pair = address(uint(keccak256(abi.encodePacked( | |
hex'ff', | |
factory, | |
keccak256(abi.encodePacked(token0, token1)), | |
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash | |
)))); | |
} | |
// fetches and sorts the reserves for a pair | |
function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { | |
(address token0,) = sortTokens(tokenA, tokenB); | |
(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves(); | |
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); | |
} | |
// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset | |
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) { | |
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT'); | |
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); | |
amountB = amountA.mul(reserveB) / reserveA; | |
} | |
// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset | |
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) { | |
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT'); | |
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); | |
uint amountInWithFee = amountIn.mul(997); | |
uint numerator = amountInWithFee.mul(reserveOut); | |
uint denominator = reserveIn.mul(1000).add(amountInWithFee); | |
amountOut = numerator / denominator; | |
} | |
// given an output amount of an asset and pair reserves, returns a required input amount of the other asset | |
function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) { | |
require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT'); | |
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY'); | |
uint numerator = reserveIn.mul(amountOut).mul(1000); | |
uint denominator = reserveOut.sub(amountOut).mul(997); | |
amountIn = (numerator / denominator).add(1); | |
} | |
// performs chained getAmountOut calculations on any number of pairs | |
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { | |
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); | |
amounts = new uint[](path.length); | |
amounts[0] = amountIn; | |
for (uint i; i < path.length - 1; i++) { | |
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); | |
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); | |
} | |
} | |
// performs chained getAmountIn calculations on any number of pairs | |
function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { | |
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); | |
amounts = new uint[](path.length); | |
amounts[amounts.length - 1] = amountOut; | |
for (uint i = path.length - 1; i > 0; i--) { | |
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); | |
amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); | |
} | |
} | |
} | |
library UniswapV2OracleLibrary { | |
using FixedPoint for *; | |
// helper function that returns the current block timestamp within the range of uint32, i.e. [0, 2**32 - 1] | |
function currentBlockTimestamp() internal view returns (uint32) { | |
return uint32(block.timestamp % 2 ** 32); | |
} | |
// produces the cumulative price using counterfactuals to save gas and avoid a call to sync. | |
function currentCumulativePrices( | |
address pair | |
) internal view returns (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) { | |
blockTimestamp = currentBlockTimestamp(); | |
price0Cumulative = IUniswapV2Pair(pair).price0CumulativeLast(); | |
price1Cumulative = IUniswapV2Pair(pair).price1CumulativeLast(); | |
// if time has elapsed since the last update on the pair, mock the accumulated price values | |
(uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast) = IUniswapV2Pair(pair).getReserves(); | |
if (blockTimestampLast != blockTimestamp) { | |
// subtraction overflow is desired | |
uint32 timeElapsed = blockTimestamp - blockTimestampLast; | |
// addition overflow is desired | |
// counterfactual | |
price0Cumulative += uint(FixedPoint.fraction(reserve1, reserve0)._x) * timeElapsed; | |
// counterfactual | |
price1Cumulative += uint(FixedPoint.fraction(reserve0, reserve1)._x) * timeElapsed; | |
} | |
} | |
} | |
// 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 = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; | |
address public immutable tokenA = 0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa; // DAI | |
address public immutable tokenB = 0xd0A1E359811322d97991E03f863a0C30C2cF029C; // WETH | |
// 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; | |
uint256 tmp; | |
constructor(uint windowSize_, uint8 granularity_) public { | |
require(granularity_ > 1, 'SlidingWindowOracle: GRANULARITY'); | |
require( | |
(periodSize = windowSize_ / granularity_) * granularity_ == windowSize_, | |
'SlidingWindowOracle: WINDOW_NOT_EVENLY_DIVISIBLE' | |
); | |
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]; | |
} | |
function getFirstObservationInWindow2(address pair) external view returns (Observation memory 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() 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; | |
} | |
} | |
function getPair() external view returns (address) { | |
return UniswapV2Library.pairFor(factory, tokenA, tokenB); | |
} | |
function getPrice() external view returns (uint,uint) { | |
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); | |
(uint price0Cumulative, uint price1Cumulative,) = UniswapV2OracleLibrary.currentCumulativePrices(pair); | |
return (price0Cumulative,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); | |
} | |
} | |
function getTimeElapsed(address tokenIn, address tokenOut) external view returns (uint) { | |
address pair = UniswapV2Library.pairFor(factory, tokenIn, tokenOut); | |
Observation storage firstObservation = getFirstObservationInWindow(pair); | |
uint timeElapsed = block.timestamp - firstObservation.timestamp; | |
return timeElapsed; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment