Skip to content

Instantly share code, notes, and snippets.

@gorgos
Created January 9, 2021 20:16
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save gorgos/24166993b84da3015ad1dac58445778d to your computer and use it in GitHub Desktop.
Save gorgos/24166993b84da3015ad1dac58445778d to your computer and use it in GitHub Desktop.
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