From d7f800b162a3a5776650acd7569ccd12902af218 Mon Sep 17 00:00:00 2001 From: Horace Pan Date: Tue, 28 May 2024 03:47:33 -0400 Subject: [PATCH 1/2] create sfrxeth-frxeth adapter --- ...xEthFrxEthExchangeRateChainlinkAdapter.sol | 35 +++++++++ .../interfaces/ISfrxEth.sol | 6 ++ ...FrxEthExchangeRateChainlinkAdapterTest.sol | 71 +++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 src/sfrxeth-exchange-rate-adapter/SfrxEthFrxEthExchangeRateChainlinkAdapter.sol create mode 100644 src/sfrxeth-exchange-rate-adapter/interfaces/ISfrxEth.sol create mode 100644 test/SfrxEthFrxEthExchangeRateChainlinkAdapterTest.sol diff --git a/src/sfrxeth-exchange-rate-adapter/SfrxEthFrxEthExchangeRateChainlinkAdapter.sol b/src/sfrxeth-exchange-rate-adapter/SfrxEthFrxEthExchangeRateChainlinkAdapter.sol new file mode 100644 index 0000000..02770f2 --- /dev/null +++ b/src/sfrxeth-exchange-rate-adapter/SfrxEthFrxEthExchangeRateChainlinkAdapter.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.21; + +import {ISfrxEth} from "./interfaces/ISfrxEth.sol"; +import {MinimalAggregatorV3Interface} from "../wsteth-exchange-rate-adapter/interfaces/MinimalAggregatorV3Interface.sol"; + +/// @title SfrxEthFrxEthExchangeRateChainlinkAdapter +/// @notice sfrxETH/frxETH exchange rate price feed +/// @dev The contract should only be deployed on Ethereum and used as a price feed for Morpho oracles. +contract SfrxEthFrxEthExchangeRateChainlinkAdapter is + MinimalAggregatorV3Interface +{ + /// @notice inheritdoc MinimalAggregatorV3Interface + /// @dev The calculated price has 18 decimals precision. + uint8 public constant decimals = 18; + + /// @notice The description of the price feed. + string public constant description = "sfrxETH/frxETH exchange rate"; + + /// @notice The address of sfrxETH on Ethereum. + ISfrxEth public constant SFRX_ETH = + ISfrxEth(0xac3E018457B222d93114458476f3E3416Abbe38F); + + /// @inheritdoc MinimalAggregatorV3Interface + /// @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) + { + // It is assumed that `pricePerShare` returns a price with 18 decimals precision. + return (0, int256(SFRX_ETH.pricePerShare()), 0, 0, 0); + } +} diff --git a/src/sfrxeth-exchange-rate-adapter/interfaces/ISfrxEth.sol b/src/sfrxeth-exchange-rate-adapter/interfaces/ISfrxEth.sol new file mode 100644 index 0000000..051bc2d --- /dev/null +++ b/src/sfrxeth-exchange-rate-adapter/interfaces/ISfrxEth.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.5.0; + +interface ISfrxEth { + function pricePerShare() external view returns (uint256); +} diff --git a/test/SfrxEthFrxEthExchangeRateChainlinkAdapterTest.sol b/test/SfrxEthFrxEthExchangeRateChainlinkAdapterTest.sol new file mode 100644 index 0000000..69180e9 --- /dev/null +++ b/test/SfrxEthFrxEthExchangeRateChainlinkAdapterTest.sol @@ -0,0 +1,71 @@ +// 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/sfrxeth-exchange-rate-adapter/SfrxEthFrxEthExchangeRateChainlinkAdapter.sol"; +import "../src/sfrxeth-exchange-rate-adapter/interfaces/ISfrxEth.sol"; + +contract SfrxEthFrxEthExchangeRateChainlinkAdapterTest is Test { + ISfrxEth internal constant SFRX_ETH = + ISfrxEth(0xac3E018457B222d93114458476f3E3416Abbe38F); + + SfrxEthFrxEthExchangeRateChainlinkAdapter internal adapter; + MorphoChainlinkOracleV2 internal morphoOracle; + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_RPC_URL")); + require(block.chainid == 1, "chain isn't Ethereum"); + adapter = new SfrxEthFrxEthExchangeRateChainlinkAdapter(); + morphoOracle = new MorphoChainlinkOracleV2( + vaultZero, + 1, + AggregatorV3Interface(address(adapter)), + feedZero, + 18, + vaultZero, + 1, + feedZero, + feedZero, + 18 + ); + } + + function testDecimals() public { + assertEq(adapter.decimals(), uint8(18)); + } + + function testDescription() public { + assertEq(adapter.description(), "sfrxETH/frxETH exchange rate"); + } + + function testLatestRoundData() public { + ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) = adapter.latestRoundData(); + assertEq(roundId, 0); + assertEq(uint256(answer), SFRX_ETH.pricePerShare()); + assertEq(startedAt, 0); + assertEq(updatedAt, 0); + assertEq(answeredInRound, 0); + } + + function testLatestRoundDataBounds() public { + (, int256 answer, , , ) = adapter.latestRoundData(); + assertGe(uint256(answer), 1087522005449750632); // Exchange rate queried at block 19966877 + assertLe(uint256(answer), 1.5e18); // Max bounds of the exchange rate. Should work for a long enough time. + } + + function testOracleSfrxEthFrxEthExchangeRate() public { + (, int256 expectedPrice, , , ) = adapter.latestRoundData(); + assertEq( + morphoOracle.price(), + uint256(expectedPrice) * 10 ** (36 + 18 - 18 - 18) + ); + } +} From ffa1052c7aae2b3e732ef7df84333dc003ccc904 Mon Sep 17 00:00:00 2001 From: Horace Pan Date: Tue, 28 May 2024 03:55:46 -0400 Subject: [PATCH 2/2] update doc --- .../SfrxEthFrxEthExchangeRateChainlinkAdapter.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sfrxeth-exchange-rate-adapter/SfrxEthFrxEthExchangeRateChainlinkAdapter.sol b/src/sfrxeth-exchange-rate-adapter/SfrxEthFrxEthExchangeRateChainlinkAdapter.sol index 02770f2..e9160c7 100644 --- a/src/sfrxeth-exchange-rate-adapter/SfrxEthFrxEthExchangeRateChainlinkAdapter.sol +++ b/src/sfrxeth-exchange-rate-adapter/SfrxEthFrxEthExchangeRateChainlinkAdapter.sol @@ -23,7 +23,7 @@ contract SfrxEthFrxEthExchangeRateChainlinkAdapter is /// @inheritdoc MinimalAggregatorV3Interface /// @dev Returns zero for roundId, startedAt, updatedAt and answeredInRound. - /// @dev Silently overflows if `getPooledEthByShares`'s return value is greater than `type(int256).max`. + /// @dev Silently overflows if `pricePerShare`'s return value is greater than `type(int256).max`. function latestRoundData() external view