forked from mico/idle_moloch
172 lines
8.1 KiB
Solidity
172 lines
8.1 KiB
Solidity
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
pragma solidity =0.7.6;
|
|
pragma abicoder v2;
|
|
|
|
import '../interfaces/IOracleSlippage.sol';
|
|
|
|
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
|
|
import '@uniswap/v3-periphery/contracts/base/BlockTimestamp.sol';
|
|
import '@uniswap/v3-periphery/contracts/libraries/Path.sol';
|
|
import '@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol';
|
|
import '@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol';
|
|
import '@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol';
|
|
|
|
abstract contract OracleSlippage is IOracleSlippage, PeripheryImmutableState, BlockTimestamp {
|
|
using Path for bytes;
|
|
|
|
/// @dev Returns the tick as of the beginning of the current block, and as of right now, for the given pool.
|
|
function getBlockStartingAndCurrentTick(IUniswapV3Pool pool)
|
|
internal
|
|
view
|
|
returns (int24 blockStartingTick, int24 currentTick)
|
|
{
|
|
uint16 observationIndex;
|
|
uint16 observationCardinality;
|
|
(, currentTick, observationIndex, observationCardinality, , , ) = pool.slot0();
|
|
|
|
// 2 observations are needed to reliably calculate the block starting tick
|
|
require(observationCardinality > 1, 'NEO');
|
|
|
|
// If the latest observation occurred in the past, then no tick-changing trades have happened in this block
|
|
// therefore the tick in `slot0` is the same as at the beginning of the current block.
|
|
// We don't need to check if this observation is initialized - it is guaranteed to be.
|
|
(uint32 observationTimestamp, int56 tickCumulative, , ) = pool.observations(observationIndex);
|
|
if (observationTimestamp != uint32(_blockTimestamp())) {
|
|
blockStartingTick = currentTick;
|
|
} else {
|
|
uint256 prevIndex = (uint256(observationIndex) + observationCardinality - 1) % observationCardinality;
|
|
(uint32 prevObservationTimestamp, int56 prevTickCumulative, , bool prevInitialized) =
|
|
pool.observations(prevIndex);
|
|
|
|
require(prevInitialized, 'ONI');
|
|
|
|
uint32 delta = observationTimestamp - prevObservationTimestamp;
|
|
blockStartingTick = int24((tickCumulative - prevTickCumulative) / delta);
|
|
}
|
|
}
|
|
|
|
/// @dev Virtual function to get pool addresses that can be overridden in tests.
|
|
function getPoolAddress(
|
|
address tokenA,
|
|
address tokenB,
|
|
uint24 fee
|
|
) internal view virtual returns (IUniswapV3Pool pool) {
|
|
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, PoolAddress.getPoolKey(tokenA, tokenB, fee)));
|
|
}
|
|
|
|
/// @dev Returns the synthetic time-weighted average tick as of secondsAgo, as well as the current tick,
|
|
/// for the given path. Returned synthetic ticks always represent tokenOut/tokenIn prices,
|
|
/// meaning lower ticks are worse.
|
|
function getSyntheticTicks(bytes memory path, uint32 secondsAgo)
|
|
internal
|
|
view
|
|
returns (int256 syntheticAverageTick, int256 syntheticCurrentTick)
|
|
{
|
|
bool lowerTicksAreWorse;
|
|
|
|
uint256 numPools = path.numPools();
|
|
address previousTokenIn;
|
|
for (uint256 i = 0; i < numPools; i++) {
|
|
// this assumes the path is sorted in swap order
|
|
(address tokenIn, address tokenOut, uint24 fee) = path.decodeFirstPool();
|
|
IUniswapV3Pool pool = getPoolAddress(tokenIn, tokenOut, fee);
|
|
|
|
// get the average and current ticks for the current pool
|
|
int256 averageTick;
|
|
int256 currentTick;
|
|
if (secondsAgo == 0) {
|
|
// we optimize for the secondsAgo == 0 case, i.e. since the beginning of the block
|
|
(averageTick, currentTick) = getBlockStartingAndCurrentTick(pool);
|
|
} else {
|
|
(averageTick, ) = OracleLibrary.consult(address(pool), secondsAgo);
|
|
(, currentTick, , , , , ) = IUniswapV3Pool(pool).slot0();
|
|
}
|
|
|
|
if (i == numPools - 1) {
|
|
// if we're here, this is the last pool in the path, meaning tokenOut represents the
|
|
// destination token. so, if tokenIn < tokenOut, then tokenIn is token0 of the last pool,
|
|
// meaning the current running ticks are going to represent tokenOut/tokenIn prices.
|
|
// so, the lower these prices get, the worse of a price the swap will get
|
|
lowerTicksAreWorse = tokenIn < tokenOut;
|
|
} else {
|
|
// if we're here, we need to iterate over the next pool in the path
|
|
path = path.skipToken();
|
|
previousTokenIn = tokenIn;
|
|
}
|
|
|
|
// accumulate the ticks derived from the current pool into the running synthetic ticks,
|
|
// ensuring that intermediate tokens "cancel out"
|
|
bool add = (i == 0) || (previousTokenIn < tokenIn ? tokenIn < tokenOut : tokenOut < tokenIn);
|
|
if (add) {
|
|
syntheticAverageTick += averageTick;
|
|
syntheticCurrentTick += currentTick;
|
|
} else {
|
|
syntheticAverageTick -= averageTick;
|
|
syntheticCurrentTick -= currentTick;
|
|
}
|
|
}
|
|
|
|
// flip the sign of the ticks if necessary, to ensure that the lower ticks are always worse
|
|
if (!lowerTicksAreWorse) {
|
|
syntheticAverageTick *= -1;
|
|
syntheticCurrentTick *= -1;
|
|
}
|
|
}
|
|
|
|
/// @dev Cast a int256 to a int24, revert on overflow or underflow
|
|
function toInt24(int256 y) private pure returns (int24 z) {
|
|
require((z = int24(y)) == y);
|
|
}
|
|
|
|
/// @dev For each passed path, fetches the synthetic time-weighted average tick as of secondsAgo,
|
|
/// as well as the current tick. Then, synthetic ticks from all paths are subjected to a weighted
|
|
/// average, where the weights are the fraction of the total input amount allocated to each path.
|
|
/// Returned synthetic ticks always represent tokenOut/tokenIn prices, meaning lower ticks are worse.
|
|
/// Paths must all start and end in the same token.
|
|
function getSyntheticTicks(
|
|
bytes[] memory paths,
|
|
uint128[] memory amounts,
|
|
uint32 secondsAgo
|
|
) internal view returns (int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) {
|
|
require(paths.length == amounts.length);
|
|
|
|
OracleLibrary.WeightedTickData[] memory weightedSyntheticAverageTicks =
|
|
new OracleLibrary.WeightedTickData[](paths.length);
|
|
OracleLibrary.WeightedTickData[] memory weightedSyntheticCurrentTicks =
|
|
new OracleLibrary.WeightedTickData[](paths.length);
|
|
|
|
for (uint256 i = 0; i < paths.length; i++) {
|
|
(int256 syntheticAverageTick, int256 syntheticCurrentTick) = getSyntheticTicks(paths[i], secondsAgo);
|
|
weightedSyntheticAverageTicks[i].tick = toInt24(syntheticAverageTick);
|
|
weightedSyntheticCurrentTicks[i].tick = toInt24(syntheticCurrentTick);
|
|
weightedSyntheticAverageTicks[i].weight = amounts[i];
|
|
weightedSyntheticCurrentTicks[i].weight = amounts[i];
|
|
}
|
|
|
|
averageSyntheticAverageTick = OracleLibrary.getWeightedArithmeticMeanTick(weightedSyntheticAverageTicks);
|
|
averageSyntheticCurrentTick = OracleLibrary.getWeightedArithmeticMeanTick(weightedSyntheticCurrentTicks);
|
|
}
|
|
|
|
/// @inheritdoc IOracleSlippage
|
|
function checkOracleSlippage(
|
|
bytes memory path,
|
|
uint24 maximumTickDivergence,
|
|
uint32 secondsAgo
|
|
) external view override {
|
|
(int256 syntheticAverageTick, int256 syntheticCurrentTick) = getSyntheticTicks(path, secondsAgo);
|
|
require(syntheticAverageTick - syntheticCurrentTick < maximumTickDivergence, 'TD');
|
|
}
|
|
|
|
/// @inheritdoc IOracleSlippage
|
|
function checkOracleSlippage(
|
|
bytes[] memory paths,
|
|
uint128[] memory amounts,
|
|
uint24 maximumTickDivergence,
|
|
uint32 secondsAgo
|
|
) external view override {
|
|
(int256 averageSyntheticAverageTick, int256 averageSyntheticCurrentTick) =
|
|
getSyntheticTicks(paths, amounts, secondsAgo);
|
|
require(averageSyntheticAverageTick - averageSyntheticCurrentTick < maximumTickDivergence, 'TD');
|
|
}
|
|
}
|