This repository has been archived by the owner on Jun 30, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
UniswapV3Price.sol
344 lines (294 loc) · 15.4 KB
/
UniswapV3Price.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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
// SPDX-License-Identifier: AGPL-3.0
pragma solidity 0.8.15;
import {ERC20} from "solmate/tokens/ERC20.sol";
// Libraries
import {UniswapV3OracleHelper as OracleHelper} from "libraries/UniswapV3/Oracle.sol";
import {Deviation} from "libraries/Deviation.sol";
import {FullMath} from "libraries/FullMath.sol";
// Uniswap V3
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol";
// Bophades
import "modules/PRICE/PRICE.v2.sol";
/// @title UniswapV3Price
/// @author 0xJem
/// @notice Provides prices derived from the TWAP of a Uniswap V3 pool
contract UniswapV3Price is PriceSubmodule {
using FullMath for uint256;
// ========== CONSTANTS ========== //
/// @notice The maximum number of decimals allowed for a token in order to prevent overflows
uint8 internal constant BASE_10_MAX_EXPONENT = 30;
/// @notice The minimum length of the TWAP observation window in seconds
/// From testing, a value under 19 seconds is rejected by `OracleLibrary.getQuoteAtTick()`
uint32 internal constant TWAP_MINIMUM_OBSERVATION_SECONDS = 19;
/// @notice The parameters for a Uniswap V3 pool
/// @param pool The address of the pool
/// @param observationWindowSeconds The length of the TWAP observation window in seconds
struct UniswapV3Params {
IUniswapV3Pool pool;
uint32 observationWindowSeconds;
uint16 maxDeviationBps;
}
/// @notice The minimum tick that can be used in a pool, as defined by UniswapV3 libraries
int24 internal constant MIN_TICK = -887272;
/// @notice The maximum tick that can be used in a pool, as defined by UniswapV3 libraries
int24 internal constant MAX_TICK = -MIN_TICK;
/// @notice Represents a deviation of 100% from the TWAP
uint16 internal constant DEVIATION_BASE = 10_000;
// ========== ERRORS ========== //
/// @notice The decimals of the asset are out of bounds
/// @param asset_ The address of the asset
/// @param assetDecimals_ The number of decimals of the asset
/// @param maxDecimals_ The maximum number of decimals allowed
error UniswapV3_AssetDecimalsOutOfBounds(
address asset_,
uint8 assetDecimals_,
uint8 maxDecimals_
);
/// @notice The lookup token was not found in the pool
/// @param pool_ The address of the pool
/// @param asset_ The address of the asset
error UniswapV3_LookupTokenNotFound(address pool_, address asset_);
/// @notice The output decimals are out of bounds
/// @param outputDecimals_ The number of decimals of the output
/// @param maxDecimals_ The maximum number of decimals allowed
error UniswapV3_OutputDecimalsOutOfBounds(uint8 outputDecimals_, uint8 maxDecimals_);
/// @notice The pool specified in the parameters is invalid
/// @param paramsIndex_ The index of the parameter
/// @param pool_ The address of the pool
error UniswapV3_ParamsPoolInvalid(uint8 paramsIndex_, address pool_);
/// @notice The pool tokens are invalid
/// @param pool_ The address of the pool
/// @param tokenIndex_ The index of the token
/// @param token_ The address of the token
error UniswapV3_PoolTokensInvalid(address pool_, uint8 tokenIndex_, address token_);
/// @notice The pool is invalid
/// @dev This is triggered if the pool reverted when called,
/// and indicates that the feed address is not a UniswapV3 pool.
///
/// @param pool_ The address of the pool
error UniswapV3_PoolTypeInvalid(address pool_);
/// @notice Triggered if `pool_` is locked, which indicates re-entrancy
///
/// @param pool_ The address of the affected Uniswap V3 pool
error UniswapV3_PoolReentrancy(address pool_);
/// @notice The calculated pool price deviates from the TWAP by more than the maximum deviation.
///
/// @param pool_ The address of the pool
/// @param baseInQuoteTWAP_ The calculated TWAP price in terms of the quote token
/// @param baseInQuotePrice_ The calculated current price in terms of the quote token
error UniswapV3_PriceMismatch(
address pool_,
uint256 baseInQuoteTWAP_,
uint256 baseInQuotePrice_
);
// ========== STATE VARIABLES ========== //
// ========== CONSTRUCTOR ========== //
constructor(Module parent_) Submodule(parent_) {}
// ========== SUBMODULE FUNCTIONS =========== //
/// @inheritdoc Submodule
function SUBKEYCODE() public pure override returns (SubKeycode) {
return toSubKeycode("PRICE.UNIV3");
}
/// @inheritdoc Submodule
function VERSION() public pure override returns (uint8 major, uint8 minor) {
major = 1;
minor = 0;
}
// ========== TOKEN PRICE FUNCTIONS ========== //
/// @notice Obtains the price of `lookupToken_` in USD, using the TWAP from the specified Uniswap V3 oracle.
/// @dev This function will revert if:
/// - The value of `params.observationWindowSeconds` is less than `TWAP_MINIMUM_OBSERVATION_SECONDS`
/// - Any token decimals or `outputDecimals_` are high enough to cause an overflow
/// - Any tokens in the pool are not set
/// - `lookupToken_` is not in the pool
/// - The calculated time-weighted tick is outside the bounds of int24
///
/// NOTE: as a UniswapV3 pool can be manipulated using multi-block MEV, the TWAP values
/// can also be manipulated. Price feeds are a preferred source of price data. Use this function with caution.
/// See https://chainsecurity.com/oracle-manipulation-after-merge/
///
/// @param lookupToken_ The token to determine the price of.
/// @param outputDecimals_ The number of decimals to return the price in
/// @param params_ Pool parameters of type `UniswapV3Params`
/// @return Price in the scale of `outputDecimals_`
function getTokenTWAP(
address lookupToken_,
uint8 outputDecimals_,
bytes calldata params_
) external view returns (uint256) {
UniswapV3Params memory params = abi.decode(params_, (UniswapV3Params));
(
address quoteToken,
uint8 quoteTokenDecimals,
uint8 lookupTokenDecimals
) = _checkPoolAndTokenParams(lookupToken_, outputDecimals_, params.pool);
uint256 baseInQuotePrice = OracleHelper.getTWAPRatio(
address(params.pool),
params.observationWindowSeconds,
lookupToken_,
quoteToken,
lookupTokenDecimals
);
// Get the price of {quoteToken} in USD
// Decimals: outputDecimals_
// PRICE will revert if the price cannot be determined or is 0.
(uint256 quoteInUsdPrice, ) = _PRICE().getPrice(quoteToken, PRICEv2.Variant.CURRENT);
// Calculate final price in USD
// Decimals: outputDecimals_
return baseInQuotePrice.mulDiv(quoteInUsdPrice, 10 ** quoteTokenDecimals);
}
/// @notice Obtains the price of `lookupToken_` in USD, using the current Slot0 price from the specified Uniswap V3 oracle.
/// @dev This function will revert if:
/// - The current price differs from the TWAP by more than `maxDeviationBps_`
/// - The value of `params.observationWindowSeconds` is less than `TWAP_MINIMUM_OBSERVATION_SECONDS`
/// - Any token decimals or `outputDecimals_` are high enough to cause an overflow
/// - Any tokens in the pool are not set
/// - `lookupToken_` is not in the pool
/// - The calculated time-weighted tick is outside the bounds of int24
///
/// NOTE: as a UniswapV3 pool can be manipulated using multi-block MEV, the TWAP values
/// can also be manipulated. Price feeds are a preferred source of price data. Use this function with caution.
/// See https://chainsecurity.com/oracle-manipulation-after-merge/
///
/// @param lookupToken_ The token to determine the price of.
/// @param outputDecimals_ The number of decimals to return the price in
/// @param params_ Pool parameters of type `UniswapV3Params`
/// @return Price in the scale of `outputDecimals_`
function getTokenPrice(
address lookupToken_,
uint8 outputDecimals_,
bytes calldata params_
) external view returns (uint256) {
UniswapV3Params memory params = abi.decode(params_, (UniswapV3Params));
(
address quoteToken,
uint8 quoteTokenDecimals,
uint8 lookupTokenDecimals
) = _checkPoolAndTokenParams(lookupToken_, outputDecimals_, params.pool);
// Get the TWAP price of the lookup token in terms of the quote token
uint256 baseInQuoteTWAP = OracleHelper.getTWAPRatio(
address(params.pool),
params.observationWindowSeconds,
lookupToken_,
quoteToken,
lookupTokenDecimals
);
// Get the current price of the lookup token in terms of the quote token
(, int24 currentTick, , , , , bool unlocked) = params.pool.slot0();
// Check for re-entrancy
if (unlocked == false) revert UniswapV3_PoolReentrancy(address(params.pool));
uint256 baseInQuotePrice = OracleLibrary.getQuoteAtTick(
currentTick,
uint128(10 ** lookupTokenDecimals),
lookupToken_,
quoteToken
);
// Check if the absolute deviation between the lookup and reserves price differs by more than reservesDeviationBps
// If so, the reserves may be manipulated
if (
// `isDeviatingWithBpsCheck()` will revert if `deviationBps` is invalid.
Deviation.isDeviatingWithBpsCheck(
baseInQuotePrice,
baseInQuoteTWAP,
params.maxDeviationBps,
DEVIATION_BASE
)
) {
revert UniswapV3_PriceMismatch(address(params.pool), baseInQuoteTWAP, baseInQuotePrice);
}
// Get the price of {quoteToken} in USD
// Decimals: outputDecimals_
// PRICE will revert if the price cannot be determined or is 0.
(uint256 quoteInUsdPrice, ) = _PRICE().getPrice(quoteToken, PRICEv2.Variant.CURRENT);
// Calculate final price in USD
// Decimals: outputDecimals_
return baseInQuotePrice.mulDiv(quoteInUsdPrice, 10 ** quoteTokenDecimals);
}
// ========== INTERNAL FUNCTIONS ========== //
/// @notice Performs checks to ensure that the pool, the tokens, and the decimals are valid.
/// @dev This function will revert if:
/// - Any token decimals or `outputDecimals_` are high enough to cause an overflow
/// - Any tokens in the pool are not set
/// - `lookupToken_` is not in the pool
///
/// @param lookupToken_ The token to determine the price of
/// @param outputDecimals_ The decimals of `baseToken`
/// @param pool_ The Uniswap V3 pool to use
/// @return The `quoteToken`, its decimals, and the decimals of `lookupToken_`
function _checkPoolAndTokenParams(
address lookupToken_,
uint8 outputDecimals_,
IUniswapV3Pool pool_
) internal view returns (address, uint8, uint8) {
if (address(pool_) == address(0)) revert UniswapV3_ParamsPoolInvalid(0, address(pool_));
try pool_.slot0() returns (uint160, int24, uint16, uint16, uint16, uint8, bool) {
// Do nothing
} catch (bytes memory) {
// Handle a non-UniswapV3 pool
revert UniswapV3_PoolTypeInvalid(address(pool_));
}
address quoteToken;
{
bool lookupTokenFound;
try pool_.token0() returns (address token) {
// Check if token is zero address, revert if so
if (token == address(0))
revert UniswapV3_PoolTokensInvalid(address(pool_), 0, token);
// If token is the lookup token, set lookupTokenFound to true
// Otherwise, it should be the quote token
// If lookup token isn't found, quote token will be set twice,
// but this is fine since the function will revert anyway
if (token == lookupToken_) {
lookupTokenFound = true;
} else {
quoteToken = token;
}
} catch (bytes memory) {
// Handle a non-UniswapV3 pool
revert UniswapV3_PoolTypeInvalid(address(pool_));
}
try pool_.token1() returns (address token) {
// Check if token is zero address, revert if so
if (token == address(0))
revert UniswapV3_PoolTokensInvalid(address(pool_), 1, token);
// If token is the lookup token, set lookupTokenFound to true
// Otherwise, it should be the quote token
// If lookup token isn't found, quote token will be set twice,
// but this is fine since the function will revert anyway
if (token == lookupToken_) {
lookupTokenFound = true;
} else {
quoteToken = token;
}
} catch (bytes memory) {
// Handle a non-UniswapV3 pool
revert UniswapV3_PoolTypeInvalid(address(pool_));
}
// If lookup token wasn't found, revert
if (!lookupTokenFound)
revert UniswapV3_LookupTokenNotFound(address(pool_), lookupToken_);
}
// Validate output decimals are not too high
if (outputDecimals_ > BASE_10_MAX_EXPONENT)
revert UniswapV3_OutputDecimalsOutOfBounds(outputDecimals_, BASE_10_MAX_EXPONENT);
uint8 quoteTokenDecimals = ERC20(quoteToken).decimals();
uint8 lookupTokenDecimals = ERC20(lookupToken_).decimals();
// Avoid overflows with decimal normalisation
if (quoteTokenDecimals > BASE_10_MAX_EXPONENT)
revert UniswapV3_AssetDecimalsOutOfBounds(
quoteToken,
quoteTokenDecimals,
BASE_10_MAX_EXPONENT
);
// lookupTokenDecimals must be less than 38 to avoid overflow when cast to uint128
// BASE_10_MAX_EXPONENT is less than 38, so this check is safe
if (lookupTokenDecimals > BASE_10_MAX_EXPONENT)
revert UniswapV3_AssetDecimalsOutOfBounds(
lookupToken_,
lookupTokenDecimals,
BASE_10_MAX_EXPONENT
);
return (quoteToken, quoteTokenDecimals, lookupTokenDecimals);
}
}