/
OracleSlippage.sol
executable file
·171 lines (149 loc) · 8.06 KB
/
OracleSlippage.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// 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');
}
}