diff --git a/.gitmodules b/.gitmodules index 5d4ffc22..13c6ee33 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,7 +19,6 @@ path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts branch = release-v4.9 - [submodule "lib/pyth-sdk-solidity"] path = lib/pyth-sdk-solidity url = https://github.com/pyth-network/pyth-sdk-solidity @@ -29,3 +28,6 @@ [submodule "lib/pendle-core-v2-public"] path = lib/pendle-core-v2-public url = https://github.com/pendle-finance/pendle-core-v2-public +[submodule "lib/v4-core"] + path = lib/v4-core + url = https://github.com/Uniswap/v4-core.git diff --git a/README.md b/README.md index 0127fca6..1dbd9f7f 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ An adapter's parameters and acceptance logic are easily observed on-chain. | [PendleOracle](src/adapter/pendle/PendleOracle.sol) | Onchain | TWAP | Pendle markets | pendle market, twap window | | [RateProviderOracle](src/adapter/rate/RateProviderOracle.sol) | Onchain | Rate | Balancer rate providers | rate provider | | [FixedRateOracle](src/adapter/fixed/FixedRateOracle.sol) | Onchain | Rate | Any | rate | +| [RigoblockOracle](src/adapter/rigoblock/RigoblockOracle.sol) | Onchain | TWAP | UniV4 pools | twap window | ## Usage diff --git a/foundry.toml b/foundry.toml index e6c0d9a9..94ff36ac 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = "src" out = "out" test = "test" libs = ["lib"] -solc = "0.8.23" +solc = "0.8.24" evm_version = "cancun" optimizer = true optimizer_runs = 100_000 diff --git a/lib/v4-core b/lib/v4-core new file mode 160000 index 00000000..59d3ecf5 --- /dev/null +++ b/lib/v4-core @@ -0,0 +1 @@ +Subproject commit 59d3ecf53afa9264a16bba0e38f4c5d2231f80bc diff --git a/remappings.txt b/remappings.txt index a7bf9eda..5c57c8f7 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,4 +5,5 @@ @openzeppelin/contracts=lib/openzeppelin-contracts/contracts/ @pyth/=lib/pyth-sdk-solidity/ ethereum-vault-connector/=lib/ethereum-vault-connector/src/ -@pendle/core-v2/=lib/pendle-core-v2-public/contracts/ \ No newline at end of file +@pendle/core-v2/=lib/pendle-core-v2-public/contracts/ +@uniswap/v4-core/=lib/v4-core/ \ No newline at end of file diff --git a/src/adapter/rigoblock/IOracle.sol b/src/adapter/rigoblock/IOracle.sol new file mode 100644 index 00000000..04253c65 --- /dev/null +++ b/src/adapter/rigoblock/IOracle.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.8.0 <0.9.0; + +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; + +interface IOracle { + /// @member index The index of the last written observation for the pool + /// @member cardinality The cardinality of the observations array for the pool + /// @member cardinalityNext The cardinality target of the observations array for the pool, which will replace cardinality when enough observations are written + struct ObservationState { + uint16 index; + uint16 cardinality; + uint16 cardinalityNext; + } + + function getState(PoolKey calldata key) external view returns (ObservationState memory state); + + function observe( + PoolKey calldata key, + uint32[] calldata secondsAgos + ) external view returns (int48[] memory tickCumulatives, uint144[] memory secondsPerLiquidityCumulativeX128s); +} \ No newline at end of file diff --git a/src/adapter/rigoblock/RigoblockOracle.sol b/src/adapter/rigoblock/RigoblockOracle.sol new file mode 100644 index 00000000..4365aec4 --- /dev/null +++ b/src/adapter/rigoblock/RigoblockOracle.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {Currency} from "@uniswap/v4-core/src/types/Currency.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; +import {BaseAdapter, Errors, IPriceOracle} from "../BaseAdapter.sol"; +import {IOracle} from "./IOracle.sol"; + +/// @title RigoblockOracle +/// @custom:security-contact security@rigoblock.com +/// @author Rigoblock (https://rigoblock.com/) +/// @notice Adapter for Rigoblock's Uniswap V4 TWAP oracle hook. +/// @dev This oracle supports quoting tokenA/tokenB and tokenB/tokenA of the given pool. +/// WARNING: READ THIS BEFORE DEPLOYING +/// The cardinality of the observation buffer must be grown sufficiently to accommodate for the chosen TWAP window. +/// The observation buffer must contain enough observations to accommodate for the chosen TWAP window. +/// The chosen pool must have enough total liquidity to resist manipulation. +/// The chosen pool must have had sufficient liquidity when past observations were recorded in the buffer. +contract RigoblockOracle is BaseAdapter { + /// @dev The minimum length of the TWAP window. + uint32 internal constant MIN_TWAP_WINDOW = 5 minutes; + /// @inheritdoc IPriceOracle + string public constant name = "RigoblockOracle"; + /// @notice One of the tokens in the pool. + address public immutable tokenA; + /// @notice The other token in the pool. + address public immutable tokenB; + /// @notice The desired length of the twap window. + uint32 public immutable twapWindow; + /// @notice The address of the BackGeoOracle hook. + address public immutable backGeoOracle; + + /// @notice Deploy a UniswapV3Oracle. + /// @dev The oracle will support tokenA/tokenB and tokenB/tokenA pricing. + /// @param _tokenA One of the tokens in the pool. + /// @param _tokenB The other token in the pool. + /// @param _twapWindow The desired length of the twap window. + /// @param _backGeoOracle The address of the Uniswap V4 BackGeoOracle oracle hook. + constructor(address _tokenA, address _tokenB, uint32 _twapWindow, address _backGeoOracle) { + if (_twapWindow < MIN_TWAP_WINDOW || _twapWindow > uint32(type(int32).max)) { + revert Errors.PriceOracle_InvalidConfiguration(); + } + tokenA = _tokenA; + tokenB = _tokenB; + twapWindow = _twapWindow; + backGeoOracle = _backGeoOracle; + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(tokenA), + currency1: Currency.wrap(tokenB), + fee: 0, + tickSpacing: TickMath.MAX_TICK_SPACING, + hooks: IHooks(backGeoOracle) + }); + IOracle.ObservationState memory state = IOracle(backGeoOracle).getState(key); + if (state.cardinality == 0) revert Errors.PriceOracle_InvalidConfiguration(); + } + + /// @notice Get a quote by calling the pool's TWAP oracle. + /// @param inAmount The amount of `base` to convert. + /// @param base The token that is being priced. Either `tokenA` or `tokenB`. + /// @param quote The token that is the unit of account. Either `tokenB` or `tokenA`. + /// @return The converted amount. + function _getQuote(uint256 inAmount, address base, address quote) internal view override returns (uint256) { + if (!((base == tokenA && quote == tokenB) || (base == tokenB && quote == tokenA))) { + revert Errors.PriceOracle_NotSupported(base, quote); + } + // Size limitation enforced by the pool. + if (inAmount > type(uint128).max) revert Errors.PriceOracle_Overflow(); + + uint32[] memory secondsAgos = new uint32[](2); + secondsAgos[0] = twapWindow; + + PoolKey memory key = PoolKey({ + currency0: Currency.wrap(tokenA), + currency1: Currency.wrap(tokenB), + fee: 0, + tickSpacing: TickMath.MAX_TICK_SPACING, + hooks: IHooks(backGeoOracle) + }); + + // Calculate the mean tick over the twap window. + (int48[] memory tickCumulatives,) = IOracle(backGeoOracle).observe(key, secondsAgos); + int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0]; + int24 tick = int24(tickCumulativesDelta / int56(uint56(twapWindow))); + if (tickCumulativesDelta < 0 && (tickCumulativesDelta % int56(uint56(twapWindow)) != 0)) tick--; + return OracleLibrary.getQuoteAtTick(tick, uint128(inAmount), base, quote); + } +}