Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exchange rate oracles for ezETH, rsETH, swETH, rswETH, weETH #100

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@morpho-org/morpho-blue-oracles",
"description": "Morpho Blue Oracles",
"license": "GPL-2.0-or-later",
"version": "1.0.0",
"files": [
"src",
"README.md",
"LICENSE"
],
"scripts": {
"lint:forge": "forge fmt --check",
"lint:fix": "yarn lint:forge:fix && yarn lint:hardhat:fix",
"lint:forge:fix": "forge fmt"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 Origami Finance
/// @custom:contact team@origami.finance
/// @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 constant override 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 view override 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 Origami Finance
/// @custom:contact team@origami.finance
/// @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 constant override 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 view override 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 {IRswETH} from "../interfaces/swell/IRswETH.sol";
import {IMinimalAggregatorV3Interface} from "../interfaces/IMinimalAggregatorV3Interface.sol";

/// @title RswEthToEthExchangeRateChainlinkAdapter
/// @author Origami Finance
/// @custom:contact team@origami.finance
/// @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 constant override 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 view override returns (uint80, int256, uint256, uint256, uint80) {
return (0, int256(RSWETH.rswETHToETHRate()), 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 Origami Finance
/// @custom:contact team@origami.finance
/// @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 constant override 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 view override 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 Origami Finance
/// @custom:contact team@origami.finance
/// @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 constant override 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 view override 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 {
frontier159 marked this conversation as resolved.
Show resolved Hide resolved
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);
}
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/IRswETH.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 IRswETH {
function rswETHToETHRate() external view returns (uint256);
}
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"), 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));
}
}
Loading