fluid-contracts-public/contracts/oracle/implementations/uniV3OracleImpl.sol

305 lines
15 KiB
Solidity

// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.21;
import { ErrorTypes } from "../errorTypes.sol";
import { FullMath } from "../libraries/FullMath.sol";
import { TickMath } from "../libraries/TickMath.sol";
import { OracleUtils } from "../libraries/oracleUtils.sol";
import { IUniswapV3Pool } from "../interfaces/external/IUniswapV3Pool.sol";
import { Error as OracleError } from "../error.sol";
/// @title Uniswap V3 Oracle implementation
/// @notice This contract is used to get the exchange rate from from a Uniswap V3 Pool,
/// including logic to check against TWAP max deltas.
/// @dev Uses 5 secondsAgos[] values and 3 TWAP maxDeltas:
/// e.g. [240, 60, 15, 1, 0] -> [price240to60, price60to15, price 15to1, currentPrice]
/// delta checks: price240to60 vs currentPrice, price60to15 vs currentPrice and 15to1 vs currentPrice.
abstract contract UniV3OracleImpl is OracleError {
/// @dev Uniswap V3 Pool to check for the exchange rate
IUniswapV3Pool internal immutable _POOL;
/// @dev Flag to invert the price or not (to e.g. for WETH/USDC pool return prive of USDC per 1 WETH)
bool internal immutable _UNIV3_INVERT_RATE;
/// @dev Uniswap oracle delta for TWAP1 in 1e2 percent. If uniswap price TWAP1 is out of this delta,
/// current price fetching reverts. E.g. for delta of TWAP 240 -> 60 vs current price
uint256 internal immutable _UNI_TWAP1_MAX_DELTA_PERCENT;
/// @dev Uniswap oracle delta for TWAP2 in 1e2 percent. If uniswap price TWAP2 is out of this delta,
/// current price fetching reverts. E.g. for delta of TWAP 60 -> 15 vs current price
uint256 internal immutable _UNI_TWAP2_MAX_DELTA_PERCENT;
/// @dev Uniswap oracle delta for TWAP3 in 1e2 percent. If uniswap price TWAP3 is out of this delta,
/// current price fetching reverts. E.g. for delta of TWAP 15 -> 1 vs current price
uint256 internal immutable _UNI_TWAP3_MAX_DELTA_PERCENT;
/// @dev Uniswap oracle seconds ago for twap, 1. value, e.g. 240
uint256 internal immutable _UNI_SECONDS_AGO_1;
/// @dev Uniswap oracle seconds ago for twap, 2. value, e.g. 60
uint256 internal immutable _UNI_SECONDS_AGO_2;
/// @dev Uniswap oracle seconds ago for twap, 3. value, e.g. 15
uint256 internal immutable _UNI_SECONDS_AGO_3;
/// @dev Uniswap oracle seconds ago for twap, 4. value, e.g. 1
uint256 internal immutable _UNI_SECONDS_AGO_4;
/// @dev Uniswap oracle seconds ago for twap, 5. value, e.g. 0
uint256 internal immutable _UNI_SECONDS_AGO_5;
/// @dev Uniswap TWAP1 interval duration.
int256 internal immutable _UNI_TWAP1_INTERVAL;
/// @dev Uniswap TWAP2 interval duration.
int256 internal immutable _UNI_TWAP2_INTERVAL;
/// @dev Uniswap TWAP3 interval duration.
int256 internal immutable _UNI_TWAP3_INTERVAL;
/// @dev Uniswap TWAP4 interval duration.
int256 internal immutable _UNI_TWAP4_INTERVAL;
/// @dev stored array lengths to optimize gas
uint256 internal constant _SECONDS_AGOS_LENGTH = 5;
uint256 internal constant _TWAP_DELTAS_LENGTH = 3;
/// @dev constant value for price scaling to reduce gas usage
uint256 internal immutable _UNIV3_PRICE_SCALER_MULTIPLIER;
/// @dev constant value for inverting price to reduce gas usage
uint256 internal immutable _UNIV3_INVERT_PRICE_DIVIDEND;
struct UniV3ConstructorParams {
/// @param pool Uniswap V3 Pool to check for the exchange rate
IUniswapV3Pool pool;
/// @param invertRate Flag to invert the Uniswap price or not
bool invertRate;
/// @param tWAPMaxDeltaPercents Uniswap oracle delta for TWAP1-2-3 in 1e2 percent
uint256[_TWAP_DELTAS_LENGTH] tWAPMaxDeltaPercents;
/// @param secondsAgos Uniswap oracle seconds ago for the 3 TWAP values, from oldest to newest, e.g. [240, 60, 15, 1, 0]
uint32[_SECONDS_AGOS_LENGTH] secondsAgos;
}
/// @notice constructor sets the Uniswap V3 `pool_` to check for the exchange rate and the `invertRate_` flag.
/// E.g. `invertRate_` should be true if for the WETH/USDC pool it's expected that the oracle returns USDC per 1 WETH
constructor(UniV3ConstructorParams memory params_) {
if (address(params_.pool) == address(0)) {
revert FluidOracleError(ErrorTypes.UniV3Oracle__InvalidParams);
}
// sanity check that seconds agos values are ordered ascending, e.g. [240, 60, 15, 1, 0]
if (
params_.secondsAgos[0] <= params_.secondsAgos[1] ||
params_.secondsAgos[1] <= params_.secondsAgos[2] ||
params_.secondsAgos[2] <= params_.secondsAgos[3] ||
params_.secondsAgos[3] <= params_.secondsAgos[4]
) {
revert FluidOracleError(ErrorTypes.UniV3Oracle__InvalidSecondsAgos);
}
// sanity check that deltas are less than 100% and decreasing (as timespan is closer to current price):
// 1. delta must < 100%
// all following deltas must be <= than the previous one
if (
params_.tWAPMaxDeltaPercents[0] >= OracleUtils.HUNDRED_PERCENT_DELTA_SCALER ||
params_.tWAPMaxDeltaPercents[1] > params_.tWAPMaxDeltaPercents[0] ||
params_.tWAPMaxDeltaPercents[2] > params_.tWAPMaxDeltaPercents[1]
) {
revert FluidOracleError(ErrorTypes.UniV3Oracle__InvalidDeltas);
}
_UNI_SECONDS_AGO_1 = uint256(params_.secondsAgos[0]);
_UNI_SECONDS_AGO_2 = uint256(params_.secondsAgos[1]);
_UNI_SECONDS_AGO_3 = uint256(params_.secondsAgos[2]);
_UNI_SECONDS_AGO_4 = uint256(params_.secondsAgos[3]);
_UNI_SECONDS_AGO_5 = uint256(params_.secondsAgos[4]);
_UNI_TWAP1_INTERVAL = int256(uint256(params_.secondsAgos[0] - params_.secondsAgos[1]));
_UNI_TWAP2_INTERVAL = int256(uint256(params_.secondsAgos[1] - params_.secondsAgos[2]));
_UNI_TWAP3_INTERVAL = int256(uint256(params_.secondsAgos[2] - params_.secondsAgos[3]));
_UNI_TWAP4_INTERVAL = int256(uint256(params_.secondsAgos[3] - params_.secondsAgos[4]));
_UNI_TWAP1_MAX_DELTA_PERCENT = params_.tWAPMaxDeltaPercents[0]; // e.g. for TWAP 240 -> 60 vs current price
_UNI_TWAP2_MAX_DELTA_PERCENT = params_.tWAPMaxDeltaPercents[1]; // e.g. for TWAP 60 -> 15 vs current price
_UNI_TWAP3_MAX_DELTA_PERCENT = params_.tWAPMaxDeltaPercents[2]; // e.g. for TWAP 15 -> 1 vs current price
_POOL = params_.pool;
_UNIV3_INVERT_RATE = params_.invertRate;
// uniswapV3 returned price is already scaled to token decimals.
_UNIV3_PRICE_SCALER_MULTIPLIER = 10 ** OracleUtils.RATE_OUTPUT_DECIMALS;
// uniV3 invert price dividend happens on the already scaled by 1e27 result for price in token1 per 1 token0
_UNIV3_INVERT_PRICE_DIVIDEND = 10 ** (OracleUtils.RATE_OUTPUT_DECIMALS * 2);
}
/// @dev Get the last exchange rate from the pool's last observed value without any checks
/// @return exchangeRateUnsafe_ The exchange rate between the underlying asset and the peg asset in `OracleUtils.RATE_OUTPUT_DECIMALS`
function _getUniV3ExchangeRateUnsafe() internal view returns (uint256 exchangeRateUnsafe_) {
(uint160 sqrtPriceX96_, , , , , , ) = _POOL.slot0();
exchangeRateUnsafe_ = _UNIV3_INVERT_RATE
? _invertUniV3Price(_getPriceFromSqrtPriceX96(sqrtPriceX96_))
: _getPriceFromSqrtPriceX96(sqrtPriceX96_);
}
/// @dev Get the last exchange rate from the pool's last observed value, checked against TWAP deviations.
/// @return exchangeRate_ The exchange rate between the underlying asset and the peg asset in `OracleUtils.RATE_OUTPUT_DECIMALS`
/// If 0 then the fetching the price failed or a delta was invalid.
function _getUniV3ExchangeRate() internal view returns (uint256 exchangeRate_) {
// build calldata bytes in a gas-optimized way without having to build an array / using abi.encode.
// gas efficient work around for Solidity not supporting immutable non-value types.
bytes memory data_ = abi.encodePacked(
hex"883bdbfd", // pack function selector
hex"0000000000000000000000000000000000000000000000000000000000000020", // pack start offset of dynamic array
_SECONDS_AGOS_LENGTH, // pack length of dynamic array
// pack seconds agos values:
_UNI_SECONDS_AGO_1,
_UNI_SECONDS_AGO_2,
_UNI_SECONDS_AGO_3,
_UNI_SECONDS_AGO_4,
_UNI_SECONDS_AGO_5
);
// get the tickCumulatives from Pool.observe()
(bool success_, bytes memory result_) = address(_POOL).staticcall(data_);
if (!success_) {
return 0;
}
int56[] memory tickCumulatives_ = abi.decode(result_, (int56[]));
unchecked {
{
int56 tickCumulativesDelta_ = (tickCumulatives_[_TWAP_DELTAS_LENGTH + 1] -
tickCumulatives_[_TWAP_DELTAS_LENGTH]);
// _UNI_TWAP4_INTERVAL can not be 0 because of constructor sanity checks
int24 arithmeticMeanTick_ = int24(tickCumulativesDelta_ / _UNI_TWAP4_INTERVAL);
// Always round to negative infinity, see UniV3 OracleLibrary
// https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/libraries/OracleLibrary.sol#L36
if (tickCumulativesDelta_ < 0 && (tickCumulativesDelta_ % _UNI_TWAP4_INTERVAL != 0)) {
arithmeticMeanTick_--;
}
// get the current uniswap price, which is the last tick cumulatives interval, usually [..., 1, 0]
exchangeRate_ = _getPriceFromSqrtPriceX96(TickMath.getSqrtRatioAtTick(arithmeticMeanTick_));
}
if (_UNIV3_INVERT_RATE) {
exchangeRate_ = _invertUniV3Price(exchangeRate_);
}
// Check the latest Uniswap price is within the acceptable delta from each TWAP range
// TWAP 1 check
if (
_isInvalidTWAPDelta(
exchangeRate_,
tickCumulatives_[1] - tickCumulatives_[0],
_UNI_TWAP1_INTERVAL,
_UNI_TWAP1_MAX_DELTA_PERCENT
)
) {
return 0;
}
// TWAP 2 check
if (
_isInvalidTWAPDelta(
exchangeRate_,
tickCumulatives_[2] - tickCumulatives_[1],
_UNI_TWAP2_INTERVAL,
_UNI_TWAP2_MAX_DELTA_PERCENT
)
) {
return 0;
}
// TWAP 3 check
if (
_isInvalidTWAPDelta(
exchangeRate_,
tickCumulatives_[3] - tickCumulatives_[2],
_UNI_TWAP3_INTERVAL,
_UNI_TWAP3_MAX_DELTA_PERCENT
)
) {
return 0;
}
}
}
/// @dev verifies that `exchangeRate_` is within `maxDelta_` for derived price from `tickCumulativesDelta_` and `interval_`.
/// returns true if delta is invalid
function _isInvalidTWAPDelta(
uint256 exchangeRate_,
int256 tickCumulativesDelta_,
int256 interval_, // can not be 0 because of constructor sanity checks
uint256 maxDelta_
) internal view returns (bool) {
unchecked {
int24 arithmeticMeanTick_ = int24(tickCumulativesDelta_ / interval_);
// Always round to negative infinity, see UniV3 OracleLibrary
// https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/libraries/OracleLibrary.sol#L36
if (tickCumulativesDelta_ < 0 && (tickCumulativesDelta_ % interval_ != 0)) {
arithmeticMeanTick_--;
}
// Get the price for the interval of the twap
uint256 price_ = _getPriceFromSqrtPriceX96(TickMath.getSqrtRatioAtTick(arithmeticMeanTick_));
if (_UNIV3_INVERT_RATE) {
price_ = _invertUniV3Price(price_);
}
// Check that the uniswapPrice is within DELTA of the Uniswap TWAP
maxDelta_ = (price_ * maxDelta_) / OracleUtils.HUNDRED_PERCENT_DELTA_SCALER;
if (exchangeRate_ > (price_ + maxDelta_) || exchangeRate_ < (price_ - maxDelta_)) {
// Uniswap last price is NOT within the delta
return true;
}
}
return false;
}
/// @notice returns all UniV3 oracle related data as utility for easy off-chain use / block explorer in a single view method
function uniV3OracleData()
public
view
returns (
IUniswapV3Pool uniV3Pool_,
bool uniV3InvertRate_,
uint32[] memory uniV3secondsAgos_,
uint256[] memory uniV3TwapDeltas_,
uint256 uniV3exchangeRateUnsafe_,
uint256 uniV3exchangeRate_
)
{
// Get the latest TWAP prices from the Uniswap Oracle for second intervals
uniV3secondsAgos_ = new uint32[](_SECONDS_AGOS_LENGTH);
uniV3secondsAgos_[0] = uint32(_UNI_SECONDS_AGO_1);
uniV3secondsAgos_[1] = uint32(_UNI_SECONDS_AGO_2);
uniV3secondsAgos_[2] = uint32(_UNI_SECONDS_AGO_3);
uniV3secondsAgos_[3] = uint32(_UNI_SECONDS_AGO_4);
uniV3secondsAgos_[4] = uint32(_UNI_SECONDS_AGO_5);
// Check the latest Uniswap price is within the acceptable delta from each TWAP range
uniV3TwapDeltas_ = new uint256[](_TWAP_DELTAS_LENGTH);
uniV3TwapDeltas_[0] = _UNI_TWAP1_MAX_DELTA_PERCENT;
uniV3TwapDeltas_[1] = _UNI_TWAP2_MAX_DELTA_PERCENT;
uniV3TwapDeltas_[2] = _UNI_TWAP3_MAX_DELTA_PERCENT;
return (
_POOL,
_UNIV3_INVERT_RATE,
uniV3secondsAgos_,
uniV3TwapDeltas_,
_getUniV3ExchangeRateUnsafe(),
_getUniV3ExchangeRate()
);
}
/// @dev Get the price from the sqrt price in `OracleUtils.RATE_OUTPUT_DECIMALS`
/// (see https://blog.uniswap.org/uniswap-v3-math-primer)
/// @param sqrtPriceX96_ The sqrt price to convert
function _getPriceFromSqrtPriceX96(uint160 sqrtPriceX96_) private view returns (uint256 priceX96_) {
return
FullMath.mulDiv(
uint256(sqrtPriceX96_) * uint256(sqrtPriceX96_),
_UNIV3_PRICE_SCALER_MULTIPLIER,
1 << 192 // 2^96 * 2
);
}
/// @dev Invert the price
/// @param price_ The price to invert
/// @return invertedPrice_ The inverted price in `OracleUtils.RATE_OUTPUT_DECIMALS`
function _invertUniV3Price(uint256 price_) private view returns (uint256 invertedPrice_) {
return _UNIV3_INVERT_PRICE_DIVIDEND / price_;
}
}