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 rseth ezeth oracles #29

Merged
merged 6 commits into from
Feb 12, 2025
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
- name: Filter directories
run: |
sudo apt update && sudo apt install -y lcov
lcov --remove lcov.info 'test/*' 'script/*' --output-file lcov.info --rc lcov_branch_coverage=1
lcov --remove lcov.info 'test/*' --output-file lcov.info --rc lcov_branch_coverage=1

# This step posts a detailed coverage report as a comment and deletes previous comments on
# each push. The below step is used to fail coverage if the specified coverage threshold is
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Please note all these oracles are designed for consumption by `AaveOracle` which

[WEETHExchangeRateOracle](https://github.com/marsfoundation/sparklend-advanced/blob/master/src/WEETHExchangeRateOracle.sol): Provides weETH/USD by multiplying the weETH exchange rate by ETH/USD. Used for: weETH market

[RSETHExchangeRateOracle](https://github.com/marsfoundation/sparklend-advanced/blob/master/src/RSETHExchangeRateOracle.sol): Provides rsETH/USD by multiplying the rsETH exchange rate by ETH/USD. Used for: rsETH market

[EZETHExchangeRateOracle](https://github.com/marsfoundation/sparklend-advanced/blob/master/src/EZETHExchangeRateOracle.sol): Provides ezETH/USD by multiplying the ezETH exchange rate by ETH/USD. Used for: ezETH market

[MorphoUpgradableOracle](https://github.com/marsfoundation/sparklend-advanced/blob/master/src/MorphoUpgradableOracle.sol): Allows Spark Governance to change an oracle for Morpho Blue markets. Planned to be used in Morpho Blue.

### Custom Interest Rate Strategies
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ libs = ["lib"]
solc_version = "0.8.25"
optimizer = true
optimizer_runs = 200
evm_version = "paris"
evm_version = "cancun"
remappings = [
"ds-test/=lib/forge-std/lib/ds-test/src/",
"erc20-helpers/=lib/erc20-helpers/src/",
Expand Down
56 changes: 56 additions & 0 deletions src/EZETHExchangeRateOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.0;

import { IPriceSource } from "./interfaces/IPriceSource.sol";

interface IEZETHExchangeRateOracle {
function calculateTVLs() external view returns (uint256[][] memory, uint256[] memory, uint256);
function ezETH() external view returns (address);
}

interface IEZETH {
function totalSupply() external view returns (uint256);
}

/**
* @title EZETHExchangeRateOracle
* @dev Provides ezETH / USD by multiplying the ezETH exchange rate by ETH / USD.
* This provides a "non-market" price. Any depeg event will be ignored.
*/
contract EZETHExchangeRateOracle {

/// @notice Renzo restaked eth rate source oracle contract.
IEZETHExchangeRateOracle public immutable oracle;

/// @notice Renzo restaked eth token contract.
IEZETH public immutable ezETH;

/// @notice The price source for ETH / USD.
IPriceSource public immutable ethSource;

constructor(address _oracle, address _ethSource) {
// 8 decimals required as AaveOracle assumes this
require(IPriceSource(_ethSource).decimals() == 8, "EZETHExchangeRateOracle/invalid-decimals");

oracle = IEZETHExchangeRateOracle(_oracle);
ezETH = IEZETH(oracle.ezETH());
ethSource = IPriceSource(_ethSource);
}

function latestAnswer() external view returns (int256) {
int256 ethUsd = ethSource.latestAnswer();
( ,, uint256 tvl ) = oracle.calculateTVLs();
int256 exchangeRate = int256(tvl * 1e18 / ezETH.totalSupply());

if (ethUsd <= 0 || exchangeRate <= 0) {
return 0;
}

return exchangeRate * ethUsd / 1e18;
}

function decimals() external pure returns (uint8) {
return 8;
}

}
46 changes: 46 additions & 0 deletions src/RSETHExchangeRateOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.0;

import { IPriceSource } from "./interfaces/IPriceSource.sol";

interface IKelpDAORestakedEthOracle {
function rsETHPrice() external view returns (uint256);
}

/**
* @title RSETHExchangeRateOracle
* @dev Provides rsETH / USD by multiplying the rsETH exchange rate by ETH / USD.
* This provides a "non-market" price. Any depeg event will be ignored.
*/
contract RSETHExchangeRateOracle {

/// @notice KelpDAO restaked eth rate source oracle contract.
IKelpDAORestakedEthOracle public immutable oracle;

/// @notice The price source for ETH / USD.
IPriceSource public immutable ethSource;

constructor(address _oracle, address _ethSource) {
// 8 decimals required as AaveOracle assumes this
require(IPriceSource(_ethSource).decimals() == 8, "RSETHExchangeRateOracle/invalid-decimals");

oracle = IKelpDAORestakedEthOracle(_oracle);
ethSource = IPriceSource(_ethSource);
}

function latestAnswer() external view returns (int256) {
int256 ethUsd = ethSource.latestAnswer();
int256 exchangeRate = int256(oracle.rsETHPrice());

if (ethUsd <= 0 || exchangeRate <= 0) {
return 0;
}

return exchangeRate * ethUsd / 1e18;
}

function decimals() external pure returns (uint8) {
return 8;
}

}
92 changes: 92 additions & 0 deletions test/EZETHExchangeRateOracle.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.0;

import "forge-std/Test.sol";

import { PriceSourceMock } from "./mocks/PriceSourceMock.sol";

import { EZETHExchangeRateOracle } from "../src/EZETHExchangeRateOracle.sol";

// This can be both the oracle and the ezETH token for the purposes of this unit testing contract
contract EZETHOracleMock {

uint256 exchangeRate;

constructor(uint256 _exchangeRate) {
exchangeRate = _exchangeRate;
}

function calculateTVLs() external view returns (uint256[][] memory, uint256[] memory, uint256 _exchangeRate) {
_exchangeRate = exchangeRate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_exchangeRate = exchangeRate;
function calculateTVLs() external view returns (uint256[][] memory, uint256[] memory, uint256 _exchangeRate) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you want changed here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahah damn just uint256[][] memory , => uint256[][] memory, for first return param

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}

function totalSupply() external pure returns (uint256) {
return 1e18;
}

function ezETH() external view returns (address) {
return address(this);
}

function setExchangeRate(uint256 _exchangeRate) external {
exchangeRate = _exchangeRate;
}

}

contract EZETHExchangeRateOracleTest is Test {

EZETHOracleMock ezethOracle;
PriceSourceMock ethSource;

EZETHExchangeRateOracle oracle;

function setUp() public {
ezethOracle = new EZETHOracleMock(1.2e18);
ethSource = new PriceSourceMock(2000e8, 8);
oracle = new EZETHExchangeRateOracle(address(ezethOracle), address(ethSource));
}

function test_constructor() public {
assertEq(address(oracle.oracle()), address(ezethOracle));
assertEq(address(oracle.ethSource()), address(ethSource));
assertEq(oracle.decimals(), 8);
}

function test_invalid_decimals() public {
ethSource.setLatestAnswer(2000e18);
ethSource.setDecimals(18);
vm.expectRevert("EZETHExchangeRateOracle/invalid-decimals");
new EZETHExchangeRateOracle(address(ezethOracle), address(ethSource));
}

function test_latestAnswer_zeroEthUsd() public {
ethSource.setLatestAnswer(0);
assertEq(oracle.latestAnswer(), 0);
}

function test_latestAnswer_negativeEthUsd() public {
ethSource.setLatestAnswer(-1);
assertEq(oracle.latestAnswer(), 0);
}

function test_latestAnswer_zeroExchangeRate() public {
ezethOracle.setExchangeRate(0);
assertEq(oracle.latestAnswer(), 0);
}

function test_latestAnswer() public {
// 1.2 * 2000 = 2400
assertEq(oracle.latestAnswer(), 2400e8);

// 1 * 2000 = 2000
ezethOracle.setExchangeRate(1e18);
assertEq(oracle.latestAnswer(), 2000e8);

// 0.5 * 1200 = 600
ezethOracle.setExchangeRate(0.5e18);
ethSource.setLatestAnswer(1200e8);
assertEq(oracle.latestAnswer(), 600e8);
}

}
90 changes: 90 additions & 0 deletions test/RSETHExchangeRateOracle.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity ^0.8.0;

import "forge-std/Test.sol";

import { PriceSourceMock } from "./mocks/PriceSourceMock.sol";

import { RSETHExchangeRateOracle } from "../src/RSETHExchangeRateOracle.sol";

contract RSETHOracleMock {

uint256 exchangeRate;

constructor(uint256 _exchangeRate) {
exchangeRate = _exchangeRate;
}

function rsETHPrice() external view returns (uint256) {
return exchangeRate;
}

function setExchangeRate(uint256 _exchangeRate) external {
exchangeRate = _exchangeRate;
}

}

contract RSETHExchangeRateOracleTest is Test {

RSETHOracleMock rsethOracle;
PriceSourceMock ethSource;

RSETHExchangeRateOracle oracle;

function setUp() public {
rsethOracle = new RSETHOracleMock(1.2e18);
ethSource = new PriceSourceMock(2000e8, 8);
oracle = new RSETHExchangeRateOracle(address(rsethOracle), address(ethSource));
}

function test_constructor() public {
assertEq(address(oracle.oracle()), address(rsethOracle));
assertEq(address(oracle.ethSource()), address(ethSource));
assertEq(oracle.decimals(), 8);
}

function test_invalid_decimals() public {
ethSource.setLatestAnswer(2000e18);
ethSource.setDecimals(18);
vm.expectRevert("RSETHExchangeRateOracle/invalid-decimals");
new RSETHExchangeRateOracle(address(rsethOracle), address(ethSource));
}

function test_latestAnswer_zeroEthUsd() public {
ethSource.setLatestAnswer(0);
assertEq(oracle.latestAnswer(), 0);
}

function test_latestAnswer_negativeEthUsd() public {
ethSource.setLatestAnswer(-1);
assertEq(oracle.latestAnswer(), 0);
}

function test_latestAnswer_zeroExchangeRate() public {
rsethOracle.setExchangeRate(0);
assertEq(oracle.latestAnswer(), 0);
}

function test_latestAnswer_negativeExchangeRate() public {
// RETH ER can't go negative, but it can have a silent overflow
assertLt(int256(uint256(int256(-1))), 0);
rsethOracle.setExchangeRate(uint256(int256(-1)));
assertEq(oracle.latestAnswer(), 0);
}

function test_latestAnswer() public {
// 1.2 * 2000 = 2400
assertEq(oracle.latestAnswer(), 2400e8);

// 1 * 2000 = 2000
rsethOracle.setExchangeRate(1e18);
assertEq(oracle.latestAnswer(), 2000e8);

// 0.5 * 1200 = 600
rsethOracle.setExchangeRate(0.5e18);
ethSource.setLatestAnswer(1200e8);
assertEq(oracle.latestAnswer(), 600e8);
}

}
Loading
Loading