From 4f80dc484fba963f2757b40afc78fc0f0467c771 Mon Sep 17 00:00:00 2001 From: frontier159 <103474701+frontier159@users.noreply.github.com> Date: Fri, 3 May 2024 15:06:19 +1000 Subject: [PATCH] Add ezETH/ETH, rsETH/ETH, swETH/ETH, rswETH/ETH, weETH/ETH exchange rate oracles --- foundry.toml | 3 +- ...EzEthToEthExchangeRateChainlinkAdapter.sol | 44 +++++++++++++ ...RsEthToEthExchangeRateChainlinkAdapter.sol | 34 ++++++++++ ...swEthToEthExchangeRateChainlinkAdapter.sol | 29 +++++++++ ...SwEthToEthExchangeRateChainlinkAdapter.sol | 29 +++++++++ ...WeEthToEthExchangeRateChainlinkAdapter.sol | 29 +++++++++ ...stEthStEthExchangeRateChainlinkAdapter.sol | 12 ++-- src/interfaces/IERC20.sol | 6 ++ .../IMinimalAggregatorV3Interface.sol} | 2 +- src/interfaces/etherfi/IWeEth.sol | 7 +++ src/interfaces/kelp/IKelpLRTConfig.sol | 6 ++ src/interfaces/kelp/IKelpLRTOracle.sol | 6 ++ .../interfaces => interfaces/lido}/IStEth.sol | 0 src/interfaces/renzo/IRenzoOracle.sol | 10 +++ src/interfaces/renzo/IRenzoRestakeManager.sol | 7 +++ src/interfaces/swell/IRswETH.sol | 6 ++ src/interfaces/swell/ISwETH.sol | 6 ++ ...EthToEthExchangeRateChainlinkAdapter.t.sol | 62 +++++++++++++++++++ ...EthToEthExchangeRateChainlinkAdapter.t.sol | 54 ++++++++++++++++ ...EthToEthExchangeRateChainlinkAdapter.t.sol | 49 +++++++++++++++ ...EthToEthExchangeRateChainlinkAdapter.t.sol | 49 +++++++++++++++ ...EthToEthExchangeRateChainlinkAdapter.t.sol | 49 +++++++++++++++ ...thStEthExchangeRateChainlinkAdapter.t.sol} | 14 +++-- 23 files changed, 499 insertions(+), 14 deletions(-) create mode 100644 src/exchange-rate-adapters/EzEthToEthExchangeRateChainlinkAdapter.sol create mode 100644 src/exchange-rate-adapters/RsEthToEthExchangeRateChainlinkAdapter.sol create mode 100644 src/exchange-rate-adapters/RswEthToEthExchangeRateChainlinkAdapter.sol create mode 100644 src/exchange-rate-adapters/SwEthToEthExchangeRateChainlinkAdapter.sol create mode 100644 src/exchange-rate-adapters/WeEthToEthExchangeRateChainlinkAdapter.sol rename src/{wsteth-exchange-rate-adapter => exchange-rate-adapters}/WstEthStEthExchangeRateChainlinkAdapter.sol (76%) create mode 100644 src/interfaces/IERC20.sol rename src/{wsteth-exchange-rate-adapter/interfaces/MinimalAggregatorV3Interface.sol => interfaces/IMinimalAggregatorV3Interface.sol} (94%) create mode 100644 src/interfaces/etherfi/IWeEth.sol create mode 100644 src/interfaces/kelp/IKelpLRTConfig.sol create mode 100644 src/interfaces/kelp/IKelpLRTOracle.sol rename src/{wsteth-exchange-rate-adapter/interfaces => interfaces/lido}/IStEth.sol (100%) create mode 100644 src/interfaces/renzo/IRenzoOracle.sol create mode 100644 src/interfaces/renzo/IRenzoRestakeManager.sol create mode 100644 src/interfaces/swell/IRswETH.sol create mode 100644 src/interfaces/swell/ISwETH.sol create mode 100644 test/exchange-rate-adapters/EzEthToEthExchangeRateChainlinkAdapter.t.sol create mode 100644 test/exchange-rate-adapters/RsEthToEthExchangeRateChainlinkAdapter.t.sol create mode 100644 test/exchange-rate-adapters/RswEthToEthExchangeRateChainlinkAdapter.t.sol create mode 100644 test/exchange-rate-adapters/SwEthToEthExchangeRateChainlinkAdapter.t.sol create mode 100644 test/exchange-rate-adapters/WeEthToEthExchangeRateChainlinkAdapter.t.sol rename test/{WstEthStEthExchangeRateChainlinkAdapterTest.sol => exchange-rate-adapters/WstEthStEthExchangeRateChainlinkAdapter.t.sol} (74%) diff --git a/foundry.toml b/foundry.toml index fad254c..353e3b7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,8 @@ out = "out" libs = ["lib"] optimizer_runs = 999999 # Etherscan does not support verifying contracts with more optimizer runs. via_ir = true -evm_version = "paris" +evm_version = "cancun" +verbosity = 3 [profile.default.fmt] wrap_comments = true diff --git a/src/exchange-rate-adapters/EzEthToEthExchangeRateChainlinkAdapter.sol b/src/exchange-rate-adapters/EzEthToEthExchangeRateChainlinkAdapter.sol new file mode 100644 index 0000000..d92cd08 --- /dev/null +++ b/src/exchange-rate-adapters/EzEthToEthExchangeRateChainlinkAdapter.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IRenzoRestakeManager} from "../interfaces/renzo/IRenzoRestakeManager.sol"; +import {IRenzoOracle} from "../interfaces/renzo/IRenzoOracle.sol"; +import {IERC20} from "../interfaces/IERC20.sol"; +import {IMinimalAggregatorV3Interface} from "../interfaces/IMinimalAggregatorV3Interface.sol"; + +/// @title EzEthToEthExchangeRateChainlinkAdapter +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice ezETH/ETH exchange rate price feed. +/// @dev This contract should only be deployed on Ethereum and used as a price feed for Morpho oracles. +contract EzEthToEthExchangeRateChainlinkAdapter is IMinimalAggregatorV3Interface { + /// @inheritdoc IMinimalAggregatorV3Interface + // @dev The calculated price has 18 decimals precision, whatever the value of `decimals`. + uint8 public override constant decimals = 18; + + /// @notice The description of the price feed. + string public constant description = "ezETH/ETH exchange rate"; + + /// @notice The address of the Renzo restake manager in Ethereum. + IRenzoRestakeManager public constant RENZO_RESTAKE_MANAGER = IRenzoRestakeManager(0x74a09653A083691711cF8215a6ab074BB4e99ef5); + + /// @notice The address of the Renzo ezETH token in Ethereum + IERC20 public constant EZ_ETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + + /// @inheritdoc IMinimalAggregatorV3Interface + /// @dev Returns zero for roundId, startedAt, updatedAt and answeredInRound. + /// @dev Silently overflows if `calculateRedeemAmount`'s return value is greater than `type(int256).max`. + function latestRoundData() external override view returns (uint80, int256, uint256, uint256, uint80) { + (,, uint256 _currentValueInProtocol) = RENZO_RESTAKE_MANAGER.calculateTVLs(); + + // This returns the percentage of TVL that matches the percentage of ezETH being burned + // baseAsset is safely assumed to be the ezETH ERC20 + uint256 rate = IRenzoOracle(RENZO_RESTAKE_MANAGER.renzoOracle()).calculateRedeemAmount( + 1 ether, + EZ_ETH.totalSupply(), + _currentValueInProtocol + ); + + return (0, int256(rate), 0, 0, 0); + } +} diff --git a/src/exchange-rate-adapters/RsEthToEthExchangeRateChainlinkAdapter.sol b/src/exchange-rate-adapters/RsEthToEthExchangeRateChainlinkAdapter.sol new file mode 100644 index 0000000..5905cdd --- /dev/null +++ b/src/exchange-rate-adapters/RsEthToEthExchangeRateChainlinkAdapter.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IKelpLRTConfig} from "../interfaces/kelp/IKelpLRTConfig.sol"; +import {IKelpLRTOracle} from "../interfaces/kelp/IKelpLRTOracle.sol"; +import {IMinimalAggregatorV3Interface} from "../interfaces/IMinimalAggregatorV3Interface.sol"; + +/// @title RsEthToEthExchangeRateChainlinkAdapter +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice rsETH/ETH exchange rate price feed. +/// @dev This contract should only be deployed on Ethereum and used as a price feed for Morpho oracles. +contract RsEthToEthExchangeRateChainlinkAdapter is IMinimalAggregatorV3Interface { + /// @inheritdoc IMinimalAggregatorV3Interface + // @dev The calculated price has 18 decimals precision, whatever the value of `decimals`. + uint8 public override constant decimals = 18; + + /// @notice The description of the price feed. + string public constant description = "rsETH/ETH exchange rate"; + + /// @notice The address of the Kelp LRT Config contract in Ethereum. + IKelpLRTConfig public constant KELP_LRT_CONFIG = IKelpLRTConfig(0x947Cb49334e6571ccBFEF1f1f1178d8469D65ec7); + + bytes32 public constant LRT_ORACLE = keccak256("LRT_ORACLE"); + + /// @inheritdoc IMinimalAggregatorV3Interface + /// @dev Returns zero for roundId, startedAt, updatedAt and answeredInRound. + /// @dev Silently overflows if `rsETHPrice`'s return value is greater than `type(int256).max`. + function latestRoundData() external override view returns (uint80, int256, uint256, uint256, uint80) { + IKelpLRTOracle lrtOracle = IKelpLRTOracle(KELP_LRT_CONFIG.getContract(LRT_ORACLE)); + uint256 rate = lrtOracle.rsETHPrice(); + return (0, int256(rate), 0, 0, 0); + } +} diff --git a/src/exchange-rate-adapters/RswEthToEthExchangeRateChainlinkAdapter.sol b/src/exchange-rate-adapters/RswEthToEthExchangeRateChainlinkAdapter.sol new file mode 100644 index 0000000..0dbe81b --- /dev/null +++ b/src/exchange-rate-adapters/RswEthToEthExchangeRateChainlinkAdapter.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IRswETH} from "../interfaces/swell/IRswETH.sol"; +import {IMinimalAggregatorV3Interface} from "../interfaces/IMinimalAggregatorV3Interface.sol"; + +/// @title RswEthToEthExchangeRateChainlinkAdapter +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice rswETH/ETH exchange rate price feed. +/// @dev This contract should only be deployed on Ethereum and used as a price feed for Morpho oracles. +contract RswEthToEthExchangeRateChainlinkAdapter is IMinimalAggregatorV3Interface { + /// @inheritdoc IMinimalAggregatorV3Interface + // @dev The calculated price has 18 decimals precision, whatever the value of `decimals`. + uint8 public override constant decimals = 18; + + /// @notice The description of the price feed. + string public constant description = "rswETH/ETH exchange rate"; + + /// @notice The address of the Swell Network rswETH contract in Ethereum. + IRswETH public constant RSWETH = IRswETH(0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0); + + /// @inheritdoc IMinimalAggregatorV3Interface + /// @dev Returns zero for roundId, startedAt, updatedAt and answeredInRound. + /// @dev Silently overflows if `rswETHToETHRate`'s return value is greater than `type(int256).max`. + function latestRoundData() external override view returns (uint80, int256, uint256, uint256, uint80) { + return (0, int256(RSWETH.rswETHToETHRate()), 0, 0, 0); + } +} diff --git a/src/exchange-rate-adapters/SwEthToEthExchangeRateChainlinkAdapter.sol b/src/exchange-rate-adapters/SwEthToEthExchangeRateChainlinkAdapter.sol new file mode 100644 index 0000000..aa297b7 --- /dev/null +++ b/src/exchange-rate-adapters/SwEthToEthExchangeRateChainlinkAdapter.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {ISwETH} from "../interfaces/swell/ISwETH.sol"; +import {IMinimalAggregatorV3Interface} from "../interfaces/IMinimalAggregatorV3Interface.sol"; + +/// @title SwEthToEthExchangeRateChainlinkAdapter +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice swETH/ETH exchange rate price feed. +/// @dev This contract should only be deployed on Ethereum and used as a price feed for Morpho oracles. +contract SwEthToEthExchangeRateChainlinkAdapter is IMinimalAggregatorV3Interface { + /// @inheritdoc IMinimalAggregatorV3Interface + // @dev The calculated price has 18 decimals precision, whatever the value of `decimals`. + uint8 public override constant decimals = 18; + + /// @notice The description of the price feed. + string public constant description = "swETH/ETH exchange rate"; + + /// @notice The address of the Swell Network swETH contract in Ethereum. + ISwETH public constant SWETH = ISwETH(0xf951E335afb289353dc249e82926178EaC7DEd78); + + /// @inheritdoc IMinimalAggregatorV3Interface + /// @dev Returns zero for roundId, startedAt, updatedAt and answeredInRound. + /// @dev Silently overflows if `swETHToETHRate`'s return value is greater than `type(int256).max`. + function latestRoundData() external override view returns (uint80, int256, uint256, uint256, uint80) { + return (0, int256(SWETH.swETHToETHRate()), 0, 0, 0); + } +} diff --git a/src/exchange-rate-adapters/WeEthToEthExchangeRateChainlinkAdapter.sol b/src/exchange-rate-adapters/WeEthToEthExchangeRateChainlinkAdapter.sol new file mode 100644 index 0000000..6e32ce2 --- /dev/null +++ b/src/exchange-rate-adapters/WeEthToEthExchangeRateChainlinkAdapter.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {IWeEth} from "../interfaces/etherfi/IWeEth.sol"; +import {IMinimalAggregatorV3Interface} from "../interfaces/IMinimalAggregatorV3Interface.sol"; + +/// @title WeEthToEthExchangeRateChainlinkAdapter +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice weETH/ETH exchange rate price feed. +/// @dev This contract should only be deployed on Ethereum and used as a price feed for Morpho oracles. +contract WeEthToEthExchangeRateChainlinkAdapter is IMinimalAggregatorV3Interface { + /// @inheritdoc IMinimalAggregatorV3Interface + // @dev The calculated price has 18 decimals precision, whatever the value of `decimals`. + uint8 public override constant decimals = 18; + + /// @notice The description of the price feed. + string public constant description = "weETH/ETH exchange rate"; + + /// @notice The address of the weETH token on Ethereum. + IWeEth public constant WEETH = IWeEth(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); + + /// @inheritdoc IMinimalAggregatorV3Interface + /// @dev Returns zero for roundId, startedAt, updatedAt and answeredInRound. + /// @dev Silently overflows if `getRate()`'s return value is greater than `type(int256).max`. + function latestRoundData() external override view returns (uint80, int256, uint256, uint256, uint80) { + return (0, int256(WEETH.getRate()), 0, 0, 0); + } +} diff --git a/src/wsteth-exchange-rate-adapter/WstEthStEthExchangeRateChainlinkAdapter.sol b/src/exchange-rate-adapters/WstEthStEthExchangeRateChainlinkAdapter.sol similarity index 76% rename from src/wsteth-exchange-rate-adapter/WstEthStEthExchangeRateChainlinkAdapter.sol rename to src/exchange-rate-adapters/WstEthStEthExchangeRateChainlinkAdapter.sol index 6341c13..0d93c22 100644 --- a/src/wsteth-exchange-rate-adapter/WstEthStEthExchangeRateChainlinkAdapter.sol +++ b/src/exchange-rate-adapters/WstEthStEthExchangeRateChainlinkAdapter.sol @@ -1,16 +1,16 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity 0.8.21; +pragma solidity ^0.8.19; -import {IStEth} from "./interfaces/IStEth.sol"; -import {MinimalAggregatorV3Interface} from "./interfaces/MinimalAggregatorV3Interface.sol"; +import {IStEth} from "../interfaces/lido/IStEth.sol"; +import {IMinimalAggregatorV3Interface} from "../interfaces/IMinimalAggregatorV3Interface.sol"; /// @title WstEthStEthExchangeRateChainlinkAdapter /// @author Morpho Labs /// @custom:contact security@morpho.org /// @notice wstETH/stETH exchange rate price feed. /// @dev This contract should only be deployed on Ethereum and used as a price feed for Morpho oracles. -contract WstEthStEthExchangeRateChainlinkAdapter is MinimalAggregatorV3Interface { - /// @inheritdoc MinimalAggregatorV3Interface +contract WstEthStEthExchangeRateChainlinkAdapter is IMinimalAggregatorV3Interface { + /// @inheritdoc IMinimalAggregatorV3Interface // @dev The calculated price has 18 decimals precision, whatever the value of `decimals`. uint8 public constant decimals = 18; @@ -20,7 +20,7 @@ contract WstEthStEthExchangeRateChainlinkAdapter is MinimalAggregatorV3Interface /// @notice The address of stETH on Ethereum. IStEth public constant ST_ETH = IStEth(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); - /// @inheritdoc MinimalAggregatorV3Interface + /// @inheritdoc IMinimalAggregatorV3Interface /// @dev Returns zero for roundId, startedAt, updatedAt and answeredInRound. /// @dev Silently overflows if `getPooledEthByShares`'s return value is greater than `type(int256).max`. function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) { diff --git a/src/interfaces/IERC20.sol b/src/interfaces/IERC20.sol new file mode 100644 index 0000000..c7795bc --- /dev/null +++ b/src/interfaces/IERC20.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +interface IERC20 { + function totalSupply() external view returns (uint256); +} diff --git a/src/wsteth-exchange-rate-adapter/interfaces/MinimalAggregatorV3Interface.sol b/src/interfaces/IMinimalAggregatorV3Interface.sol similarity index 94% rename from src/wsteth-exchange-rate-adapter/interfaces/MinimalAggregatorV3Interface.sol rename to src/interfaces/IMinimalAggregatorV3Interface.sol index 22887af..98fd66b 100644 --- a/src/wsteth-exchange-rate-adapter/interfaces/MinimalAggregatorV3Interface.sol +++ b/src/interfaces/IMinimalAggregatorV3Interface.sol @@ -4,7 +4,7 @@ pragma solidity >=0.5.0; /// @dev Inspired by /// https://github.com/smartcontractkit/chainlink/blob/master/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol /// @dev This is the minimal feed interface required by `MorphoChainlinkOracleV2`. -interface MinimalAggregatorV3Interface { +interface IMinimalAggregatorV3Interface { /// @notice Returns the precision of the feed. function decimals() external view returns (uint8); diff --git a/src/interfaces/etherfi/IWeEth.sol b/src/interfaces/etherfi/IWeEth.sol new file mode 100644 index 0000000..9cc86ad --- /dev/null +++ b/src/interfaces/etherfi/IWeEth.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +interface IWeEth { + // Amount of weETH for 1 eETH + function getRate() external view returns (uint256); +} diff --git a/src/interfaces/kelp/IKelpLRTConfig.sol b/src/interfaces/kelp/IKelpLRTConfig.sol new file mode 100644 index 0000000..d9c83a7 --- /dev/null +++ b/src/interfaces/kelp/IKelpLRTConfig.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +interface IKelpLRTConfig { + function getContract(bytes32 contractId) external view returns (address); +} diff --git a/src/interfaces/kelp/IKelpLRTOracle.sol b/src/interfaces/kelp/IKelpLRTOracle.sol new file mode 100644 index 0000000..7b9db9a --- /dev/null +++ b/src/interfaces/kelp/IKelpLRTOracle.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +interface IKelpLRTOracle { + function rsETHPrice() external view returns (uint256); +} diff --git a/src/wsteth-exchange-rate-adapter/interfaces/IStEth.sol b/src/interfaces/lido/IStEth.sol similarity index 100% rename from src/wsteth-exchange-rate-adapter/interfaces/IStEth.sol rename to src/interfaces/lido/IStEth.sol diff --git a/src/interfaces/renzo/IRenzoOracle.sol b/src/interfaces/renzo/IRenzoOracle.sol new file mode 100644 index 0000000..f9e8b01 --- /dev/null +++ b/src/interfaces/renzo/IRenzoOracle.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +interface IRenzoOracle { + function calculateRedeemAmount( + uint256 _ezETHBeingBurned, + uint256 _existingEzETHSupply, + uint256 _currentValueInProtocol + ) external pure returns (uint256); +} diff --git a/src/interfaces/renzo/IRenzoRestakeManager.sol b/src/interfaces/renzo/IRenzoRestakeManager.sol new file mode 100644 index 0000000..51fb787 --- /dev/null +++ b/src/interfaces/renzo/IRenzoRestakeManager.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +interface IRenzoRestakeManager { + function calculateTVLs() external view returns (uint256[][] memory, uint256[] memory, uint256); + function renzoOracle() external view returns (address); +} diff --git a/src/interfaces/swell/IRswETH.sol b/src/interfaces/swell/IRswETH.sol new file mode 100644 index 0000000..8b41ced --- /dev/null +++ b/src/interfaces/swell/IRswETH.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +interface IRswETH { + function rswETHToETHRate() external view returns (uint256); +} diff --git a/src/interfaces/swell/ISwETH.sol b/src/interfaces/swell/ISwETH.sol new file mode 100644 index 0000000..54a27d2 --- /dev/null +++ b/src/interfaces/swell/ISwETH.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +interface ISwETH { + function swETHToETHRate() external view returns (uint256); +} diff --git a/test/exchange-rate-adapters/EzEthToEthExchangeRateChainlinkAdapter.t.sol b/test/exchange-rate-adapters/EzEthToEthExchangeRateChainlinkAdapter.t.sol new file mode 100644 index 0000000..db17cf9 --- /dev/null +++ b/test/exchange-rate-adapters/EzEthToEthExchangeRateChainlinkAdapter.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {vaultZero, feedZero} from "../helpers/Constants.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {MorphoChainlinkOracleV2} from "../../src/morpho-chainlink/MorphoChainlinkOracleV2.sol"; +import {EzEthToEthExchangeRateChainlinkAdapter} from "../../src/exchange-rate-adapters/EzEthToEthExchangeRateChainlinkAdapter.sol"; +import {IRenzoRestakeManager} from "../../src/interfaces/Renzo/IRenzoRestakeManager.sol"; +import {IRenzoOracle} from "../../src/interfaces/Renzo/IRenzoOracle.sol"; +import {IERC20} from "../../src/interfaces/IERC20.sol"; +import {AggregatorV3Interface} from "../../src/morpho-chainlink/interfaces/AggregatorV3Interface.sol"; + +contract EzEthToEthExchangeRateChainlinkAdapterTest is Test { + IRenzoRestakeManager public constant RENZO_RESTAKE_MANAGER = IRenzoRestakeManager(0x74a09653A083691711cF8215a6ab074BB4e99ef5); + IERC20 public constant EZ_ETH = IERC20(0xbf5495Efe5DB9ce00f80364C8B423567e58d2110); + + EzEthToEthExchangeRateChainlinkAdapter internal adapter; + MorphoChainlinkOracleV2 internal morphoOracle; + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 20066000); + require(block.chainid == 1, "chain isn't Ethereum"); + adapter = new EzEthToEthExchangeRateChainlinkAdapter(); + morphoOracle = new MorphoChainlinkOracleV2( + vaultZero, 1, AggregatorV3Interface(address(adapter)), feedZero, 18, vaultZero, 1, feedZero, feedZero, 18 + ); + } + + function test_decimals() public { + assertEq(adapter.decimals(), uint8(18)); + } + + function test_description() public { + assertEq(adapter.description(), "ezETH/ETH exchange rate"); + } + + function expectedRate() private view returns (uint256) { + (,, uint256 _currentValueInProtocol) = RENZO_RESTAKE_MANAGER.calculateTVLs(); + uint256 totalSupply = EZ_ETH.totalSupply(); + return IRenzoOracle(RENZO_RESTAKE_MANAGER.renzoOracle()).calculateRedeemAmount( + 1 ether, + totalSupply, + _currentValueInProtocol + ); + } + + function test_latestRoundData() public { + (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = + adapter.latestRoundData(); + assertEq(roundId, 0); + assertEq(uint256(answer), expectedRate()); + assertEq(answer, 1.011474492485111833e18); // Exchange rate queried at block 20066000 + assertEq(startedAt, 0); + assertEq(updatedAt, 0); + assertEq(answeredInRound, 0); + } + + function test_oracleEzEthToEthExchangeRate() public { + (, int256 expectedPrice,,,) = adapter.latestRoundData(); + assertEq(morphoOracle.price(), uint256(expectedPrice) * 10 ** (36 + 18 - 18 - 18)); + } +} diff --git a/test/exchange-rate-adapters/RsEthToEthExchangeRateChainlinkAdapter.t.sol b/test/exchange-rate-adapters/RsEthToEthExchangeRateChainlinkAdapter.t.sol new file mode 100644 index 0000000..91bee89 --- /dev/null +++ b/test/exchange-rate-adapters/RsEthToEthExchangeRateChainlinkAdapter.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {vaultZero, feedZero} from "../helpers/Constants.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {MorphoChainlinkOracleV2} from "../../src/morpho-chainlink/MorphoChainlinkOracleV2.sol"; +import {RsEthToEthExchangeRateChainlinkAdapter} from "../../src/exchange-rate-adapters/RsEthToEthExchangeRateChainlinkAdapter.sol"; +import {AggregatorV3Interface} from "../../src/morpho-chainlink/interfaces/AggregatorV3Interface.sol"; +import {IKelpLRTConfig} from "../../src/interfaces/kelp/IKelpLRTConfig.sol"; +import {IKelpLRTOracle} from "../../src/interfaces/kelp/IKelpLRTOracle.sol"; + +contract RsEthToEthExchangeRateChainlinkAdapterTest is Test { + IKelpLRTConfig public constant KELP_LRT_CONFIG = IKelpLRTConfig(0x947Cb49334e6571ccBFEF1f1f1178d8469D65ec7); + bytes32 public constant LRT_ORACLE = keccak256("LRT_ORACLE"); + + RsEthToEthExchangeRateChainlinkAdapter internal adapter; + MorphoChainlinkOracleV2 internal morphoOracle; + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 20066000); + require(block.chainid == 1, "chain isn't Ethereum"); + adapter = new RsEthToEthExchangeRateChainlinkAdapter(); + morphoOracle = new MorphoChainlinkOracleV2( + vaultZero, 1, AggregatorV3Interface(address(adapter)), feedZero, 18, vaultZero, 1, feedZero, feedZero, 18 + ); + } + + function test_decimals() public { + assertEq(adapter.decimals(), uint8(18)); + } + + function test_description() public { + assertEq(adapter.description(), "rsETH/ETH exchange rate"); + } + + function test_latestRoundData() public { + IKelpLRTOracle lrtOracle = IKelpLRTOracle(KELP_LRT_CONFIG.getContract(LRT_ORACLE)); + uint256 expectedRate = lrtOracle.rsETHPrice(); + + (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = + adapter.latestRoundData(); + assertEq(roundId, 0); + assertEq(uint256(answer), expectedRate); + assertEq(uint256(answer), 1.014115456823606415e18); // Exchange rate queried at block 20066000 + assertEq(startedAt, 0); + assertEq(updatedAt, 0); + assertEq(answeredInRound, 0); + } + + function test_oracleRsEthToEthExchangeRate() public { + (, int256 expectedPrice,,,) = adapter.latestRoundData(); + assertEq(morphoOracle.price(), uint256(expectedPrice) * 10 ** (36 + 18 - 18 - 18)); + } +} diff --git a/test/exchange-rate-adapters/RswEthToEthExchangeRateChainlinkAdapter.t.sol b/test/exchange-rate-adapters/RswEthToEthExchangeRateChainlinkAdapter.t.sol new file mode 100644 index 0000000..0d51c7b --- /dev/null +++ b/test/exchange-rate-adapters/RswEthToEthExchangeRateChainlinkAdapter.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {vaultZero, feedZero} from "../helpers/Constants.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {MorphoChainlinkOracleV2} from "../../src/morpho-chainlink/MorphoChainlinkOracleV2.sol"; +import {RswEthToEthExchangeRateChainlinkAdapter} from "../../src/exchange-rate-adapters/RswEthToEthExchangeRateChainlinkAdapter.sol"; +import {AggregatorV3Interface} from "../../src/morpho-chainlink/interfaces/AggregatorV3Interface.sol"; +import {IRswETH} from "../../src/interfaces/swell/IRswETH.sol"; + +contract RswEthToEthExchangeRateChainlinkAdapterTest is Test { + IRswETH public constant RSWETH = IRswETH(0xFAe103DC9cf190eD75350761e95403b7b8aFa6c0); + + RswEthToEthExchangeRateChainlinkAdapter internal adapter; + MorphoChainlinkOracleV2 internal morphoOracle; + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 20066000); + require(block.chainid == 1, "chain isn't Ethereum"); + adapter = new RswEthToEthExchangeRateChainlinkAdapter(); + morphoOracle = new MorphoChainlinkOracleV2( + vaultZero, 1, AggregatorV3Interface(address(adapter)), feedZero, 18, vaultZero, 1, feedZero, feedZero, 18 + ); + } + + function test_decimals() public { + assertEq(adapter.decimals(), uint8(18)); + } + + function test_description() public { + assertEq(adapter.description(), "rswETH/ETH exchange rate"); + } + + function test_latestRoundData() public { + (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = + adapter.latestRoundData(); + assertEq(roundId, 0); + assertEq(uint256(answer), RSWETH.rswETHToETHRate()); + assertEq(uint256(answer), 1.010001498638344043e18); // Exchange rate queried at block 20066000 + assertEq(startedAt, 0); + assertEq(updatedAt, 0); + assertEq(answeredInRound, 0); + } + + function test_oracleSwEthToEthExchangeRate() public { + (, int256 expectedPrice,,,) = adapter.latestRoundData(); + assertEq(morphoOracle.price(), uint256(expectedPrice) * 10 ** (36 + 18 - 18 - 18)); + } +} diff --git a/test/exchange-rate-adapters/SwEthToEthExchangeRateChainlinkAdapter.t.sol b/test/exchange-rate-adapters/SwEthToEthExchangeRateChainlinkAdapter.t.sol new file mode 100644 index 0000000..20a7f2f --- /dev/null +++ b/test/exchange-rate-adapters/SwEthToEthExchangeRateChainlinkAdapter.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {vaultZero, feedZero} from "../helpers/Constants.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {MorphoChainlinkOracleV2} from "../../src/morpho-chainlink/MorphoChainlinkOracleV2.sol"; +import {SwEthToEthExchangeRateChainlinkAdapter} from "../../src/exchange-rate-adapters/SwEthToEthExchangeRateChainlinkAdapter.sol"; +import {AggregatorV3Interface} from "../../src/morpho-chainlink/interfaces/AggregatorV3Interface.sol"; +import {ISwETH} from "../../src/interfaces/swell/ISwETH.sol"; + +contract SwEthToEthExchangeRateChainlinkAdapterTest is Test { + ISwETH public constant SWETH = ISwETH(0xf951E335afb289353dc249e82926178EaC7DEd78); + + SwEthToEthExchangeRateChainlinkAdapter internal adapter; + MorphoChainlinkOracleV2 internal morphoOracle; + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 20066000); + require(block.chainid == 1, "chain isn't Ethereum"); + adapter = new SwEthToEthExchangeRateChainlinkAdapter(); + morphoOracle = new MorphoChainlinkOracleV2( + vaultZero, 1, AggregatorV3Interface(address(adapter)), feedZero, 18, vaultZero, 1, feedZero, feedZero, 18 + ); + } + + function test_decimals() public { + assertEq(adapter.decimals(), uint8(18)); + } + + function test_description() public { + assertEq(adapter.description(), "swETH/ETH exchange rate"); + } + + function test_latestRoundData() public { + (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = + adapter.latestRoundData(); + assertEq(roundId, 0); + assertEq(uint256(answer), SWETH.swETHToETHRate()); + assertEq(uint256(answer), 1.061942797958435491e18); // Exchange rate queried at block 20066000 + assertEq(startedAt, 0); + assertEq(updatedAt, 0); + assertEq(answeredInRound, 0); + } + + function test_oracleSwEthToEthExchangeRate() public { + (, int256 expectedPrice,,,) = adapter.latestRoundData(); + assertEq(morphoOracle.price(), uint256(expectedPrice) * 10 ** (36 + 18 - 18 - 18)); + } +} diff --git a/test/exchange-rate-adapters/WeEthToEthExchangeRateChainlinkAdapter.t.sol b/test/exchange-rate-adapters/WeEthToEthExchangeRateChainlinkAdapter.t.sol new file mode 100644 index 0000000..c62f0ae --- /dev/null +++ b/test/exchange-rate-adapters/WeEthToEthExchangeRateChainlinkAdapter.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.19; + +import {vaultZero, feedZero} from "../helpers/Constants.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {MorphoChainlinkOracleV2} from "../../src/morpho-chainlink/MorphoChainlinkOracleV2.sol"; +import {WeEthToEthExchangeRateChainlinkAdapter} from "../../src/exchange-rate-adapters/WeEthToEthExchangeRateChainlinkAdapter.sol"; +import {AggregatorV3Interface} from "../../src/morpho-chainlink/interfaces/AggregatorV3Interface.sol"; +import {IWeEth} from "../../src/interfaces/etherfi/IWeEth.sol"; + +contract WeEthToEthExchangeRateChainlinkAdapterTest is Test { + IWeEth public constant WEETH = IWeEth(0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee); + + WeEthToEthExchangeRateChainlinkAdapter internal adapter; + MorphoChainlinkOracleV2 internal morphoOracle; + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 20066000); + require(block.chainid == 1, "chain isn't Ethereum"); + adapter = new WeEthToEthExchangeRateChainlinkAdapter(); + morphoOracle = new MorphoChainlinkOracleV2( + vaultZero, 1, AggregatorV3Interface(address(adapter)), feedZero, 18, vaultZero, 1, feedZero, feedZero, 18 + ); + } + + function test_decimals() public { + assertEq(adapter.decimals(), uint8(18)); + } + + function test_description() public { + assertEq(adapter.description(), "weETH/ETH exchange rate"); + } + + function test_latestRoundData() public { + (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = + adapter.latestRoundData(); + assertEq(roundId, 0); + assertEq(uint256(answer), WEETH.getRate()); + assertEq(uint256(answer), 1.040393022914859755e18); // Exchange rate queried at block 20066000 + assertEq(startedAt, 0); + assertEq(updatedAt, 0); + assertEq(answeredInRound, 0); + } + + function test_oracleWeEthToEthExchangeRate() public { + (, int256 expectedPrice,,,) = adapter.latestRoundData(); + assertEq(morphoOracle.price(), uint256(expectedPrice) * 10 ** (36 + 18 - 18 - 18)); + } +} diff --git a/test/WstEthStEthExchangeRateChainlinkAdapterTest.sol b/test/exchange-rate-adapters/WstEthStEthExchangeRateChainlinkAdapter.t.sol similarity index 74% rename from test/WstEthStEthExchangeRateChainlinkAdapterTest.sol rename to test/exchange-rate-adapters/WstEthStEthExchangeRateChainlinkAdapter.t.sol index a2e43cb..3dad9be 100644 --- a/test/WstEthStEthExchangeRateChainlinkAdapterTest.sol +++ b/test/exchange-rate-adapters/WstEthStEthExchangeRateChainlinkAdapter.t.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import "./helpers/Constants.sol"; -import "../lib/forge-std/src/Test.sol"; -import {MorphoChainlinkOracleV2} from "../src/morpho-chainlink/MorphoChainlinkOracleV2.sol"; -import "../src/wsteth-exchange-rate-adapter/WstEthStEthExchangeRateChainlinkAdapter.sol"; +import {vaultZero, feedZero} from "../helpers/Constants.sol"; +import {Test} from "../../lib/forge-std/src/Test.sol"; +import {MorphoChainlinkOracleV2} from "../../src/morpho-chainlink/MorphoChainlinkOracleV2.sol"; +import {WstEthStEthExchangeRateChainlinkAdapter} from "../../src/exchange-rate-adapters/WstEthStEthExchangeRateChainlinkAdapter.sol"; +import {IStEth} from "../../src/interfaces/lido/IStEth.sol"; +import {AggregatorV3Interface} from "../../src/morpho-chainlink/interfaces/AggregatorV3Interface.sol"; contract WstEthStEthExchangeRateChainlinkAdapterTest is Test { IStEth internal constant ST_ETH = IStEth(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84); @@ -13,7 +15,7 @@ contract WstEthStEthExchangeRateChainlinkAdapterTest is Test { MorphoChainlinkOracleV2 internal morphoOracle; function setUp() public { - vm.createSelectFork(vm.envString("ETH_RPC_URL")); + vm.createSelectFork(vm.envString("ETH_RPC_URL"), 20066000); require(block.chainid == 1, "chain isn't Ethereum"); adapter = new WstEthStEthExchangeRateChainlinkAdapter(); morphoOracle = new MorphoChainlinkOracleV2( @@ -41,7 +43,7 @@ contract WstEthStEthExchangeRateChainlinkAdapterTest is Test { function testLatestRoundDataBounds() public { (, int256 answer,,,) = adapter.latestRoundData(); - assertGe(uint256(answer), 1154690031824824994); // Exchange rate queried at block 19070943 + assertGe(uint256(answer), 1154690031824824994); // Exchange rate queried at block 20066000 assertLe(uint256(answer), 1.5e18); // Max bounds of the exchange rate. Should work for a long enough time. }