From 07abffa8f66929cde90769c121a1061b05a95dfc Mon Sep 17 00:00:00 2001 From: Gabriele Rigo Date: Fri, 27 Jun 2025 13:01:31 +0200 Subject: [PATCH 1/6] feat: add rigoblock oracle - add rigoblock oracle adapter - add oracle interface dep - add uni v4-core git submodule - update remappings.txt --- .gitmodules | 4 +- lib/v4-core | 1 + remappings.txt | 3 +- src/adapter/rigoblock/IOracle.sol | 22 ++++++ src/adapter/rigoblock/RigoblockOracle.sol | 81 +++++++++++++++++++++++ 5 files changed, 109 insertions(+), 2 deletions(-) create mode 160000 lib/v4-core create mode 100644 src/adapter/rigoblock/IOracle.sol create mode 100644 src/adapter/rigoblock/RigoblockOracle.sol 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/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..511b94b2 --- /dev/null +++ b/src/adapter/rigoblock/RigoblockOracle.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol"; +import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol"; +import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.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 pool key of the uniswap v4 pool. + PoolKey public immutable key; + + /// @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; + key = PoolKey({ + currency0: Currency.wrap(tokenA), + currency1: Currency.wrap(tokenB), + fee: 0, + tickSpacing: TickMath.MAX_TICK_SPACING, + hooks: IHooks(_backGeoOracle) + }); + IOracle.ObservationState memory state = oracle.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; + + // Calculate the mean tick over the twap window. + (int48[] memory tickCumulatives,) = IOracle(key.hooks).observe(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); + } +} From cf519a7403304036cf0877f7441dd7de37f6739f Mon Sep 17 00:00:00 2001 From: Gabriele Rigo Date: Fri, 27 Jun 2025 13:03:46 +0200 Subject: [PATCH 2/6] Remove broken redstone-oracles-monorepo submodule --- lib/redstone-oracles-monorepo | 1 - 1 file changed, 1 deletion(-) delete mode 160000 lib/redstone-oracles-monorepo diff --git a/lib/redstone-oracles-monorepo b/lib/redstone-oracles-monorepo deleted file mode 160000 index 2dc8e9a8..00000000 --- a/lib/redstone-oracles-monorepo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2dc8e9a8e3030b6805d640325547a9a937342f76 From 5e1dc97043f7af4354f2db06d227d429431f7def Mon Sep 17 00:00:00 2001 From: Gabriele Rigo Date: Fri, 27 Jun 2025 13:23:08 +0200 Subject: [PATCH 3/6] reactivate redstone submodule --- .gitmodules | 2 +- lib/redstone-oracles-monorepo | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 lib/redstone-oracles-monorepo diff --git a/.gitmodules b/.gitmodules index 13c6ee33..22139a0f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,7 +14,7 @@ url = https://github.com/Vectorized/solady [submodule "lib/redstone-oracles-monorepo"] path = lib/redstone-oracles-monorepo - url = https://github.com/redstone-finance/redstone-oracles-monorepo + url = https://github.com/redstone-finance/redstone-oracles-monorepo.git [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/lib/redstone-oracles-monorepo b/lib/redstone-oracles-monorepo new file mode 160000 index 00000000..2dc8e9a8 --- /dev/null +++ b/lib/redstone-oracles-monorepo @@ -0,0 +1 @@ +Subproject commit 2dc8e9a8e3030b6805d640325547a9a937342f76 From c37437c9db288c2d810d2e126a1b78be2faeda69 Mon Sep 17 00:00:00 2001 From: Gabriele Rigo Date: Fri, 27 Jun 2025 13:31:48 +0200 Subject: [PATCH 4/6] fix compilation bugs --- foundry.toml | 2 +- src/adapter/rigoblock/RigoblockOracle.sol | 26 ++++++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) 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/src/adapter/rigoblock/RigoblockOracle.sol b/src/adapter/rigoblock/RigoblockOracle.sol index 511b94b2..4365aec4 100644 --- a/src/adapter/rigoblock/RigoblockOracle.sol +++ b/src/adapter/rigoblock/RigoblockOracle.sol @@ -2,8 +2,10 @@ pragma solidity ^0.8.0; import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol"; -import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.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"; @@ -28,9 +30,8 @@ contract RigoblockOracle is BaseAdapter { address public immutable tokenB; /// @notice The desired length of the twap window. uint32 public immutable twapWindow; - - /// @notice The pool key of the uniswap v4 pool. - PoolKey public immutable key; + /// @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. @@ -45,14 +46,15 @@ contract RigoblockOracle is BaseAdapter { tokenA = _tokenA; tokenB = _tokenB; twapWindow = _twapWindow; - key = PoolKey({ + backGeoOracle = _backGeoOracle; + PoolKey memory key = PoolKey({ currency0: Currency.wrap(tokenA), currency1: Currency.wrap(tokenB), fee: 0, tickSpacing: TickMath.MAX_TICK_SPACING, - hooks: IHooks(_backGeoOracle) + hooks: IHooks(backGeoOracle) }); - IOracle.ObservationState memory state = oracle.getState(key); + IOracle.ObservationState memory state = IOracle(backGeoOracle).getState(key); if (state.cardinality == 0) revert Errors.PriceOracle_InvalidConfiguration(); } @@ -71,8 +73,16 @@ contract RigoblockOracle is BaseAdapter { 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(key.hooks).observe(secondsAgos); + (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--; From b77619745bdd8c9d211444f098e32e0a2c2e037d Mon Sep 17 00:00:00 2001 From: Gabriele Rigo Date: Fri, 27 Jun 2025 15:40:51 +0200 Subject: [PATCH 5/6] chore: update README - add Rigoblock oracle to README file --- README.md | 1 + 1 file changed, 1 insertion(+) 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 From 068cf56ccf8481828a450a0a5e0ebf6443df000a Mon Sep 17 00:00:00 2001 From: Gabriele Rigo Date: Fri, 27 Jun 2025 15:46:27 +0200 Subject: [PATCH 6/6] chore: remove modification to git submodule url --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 22139a0f..13c6ee33 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,7 +14,7 @@ url = https://github.com/Vectorized/solady [submodule "lib/redstone-oracles-monorepo"] path = lib/redstone-oracles-monorepo - url = https://github.com/redstone-finance/redstone-oracles-monorepo.git + url = https://github.com/redstone-finance/redstone-oracles-monorepo [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts