Skip to content

Commit

Permalink
Add ezETH/ETH, rsETH/ETH, swETH/ETH, weETH/ETH exchange rate oracles
Browse files Browse the repository at this point in the history
  • Loading branch information
frontier159 committed Jun 11, 2024
1 parent 61ad457 commit 3bf64e7
Show file tree
Hide file tree
Showing 20 changed files with 414 additions and 13 deletions.
3 changes: 2 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/IERC20.sol
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
7 changes: 7 additions & 0 deletions src/interfaces/etherfi/IWeEth.sol
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 6 additions & 0 deletions src/interfaces/kelp/IKelpLRTConfig.sol
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 6 additions & 0 deletions src/interfaces/kelp/IKelpLRTOracle.sol
Original file line number Diff line number Diff line change
@@ -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);
}
File renamed without changes.
10 changes: 10 additions & 0 deletions src/interfaces/renzo/IRenzoOracle.sol
Original file line number Diff line number Diff line change
@@ -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);
}
7 changes: 7 additions & 0 deletions src/interfaces/renzo/IRenzoRestakeManager.sol
Original file line number Diff line number Diff line change
@@ -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);
}
6 changes: 6 additions & 0 deletions src/interfaces/swell/ISwETH.sol
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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"), 19767680);
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.008385750262871563e18); // Exchange rate queried at block 19767680
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));
}
}
Original file line number Diff line number Diff line change
@@ -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"), 19767680);
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.011934221153072470e18); // Exchange rate queried at block 19767680
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));
}
}
Loading

0 comments on commit 3bf64e7

Please sign in to comment.