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 extra oracle tests #734

Merged
merged 3 commits into from
Jan 23, 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
162 changes: 148 additions & 14 deletions contracts/test/OracleMainnet.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import "src/PriceFeeds/WETHPriceFeed.sol";

import "./TestContracts/Accounts.sol";
import "./TestContracts/ChainlinkOracleMock.sol";
import "./TestContracts/GasGuzzlerOracle.sol";
import "./TestContracts/GasGuzzlerToken.sol";
import "./TestContracts/RETHTokenMock.sol";
import "./TestContracts/WSTETHTokenMock.sol";
import "./TestContracts/Deployment.t.sol";
Expand All @@ -29,6 +31,8 @@ contract OraclesMainnet is TestAccounts {
AggregatorV3Interface rethOracle;

ChainlinkOracleMock mockOracle;
GasGuzzlerToken gasGuzzlerToken;
GasGuzzlerOracle gasGuzzlerOracle;

IMainnetPriceFeed wethPriceFeed;
IRETHPriceFeed rethPriceFeed;
Expand Down Expand Up @@ -93,6 +97,8 @@ contract OraclesMainnet is TestAccounts {
stethOracle = AggregatorV3Interface(result.externalAddresses.STETHOracle);

mockOracle = new ChainlinkOracleMock();
gasGuzzlerToken = new GasGuzzlerToken();
gasGuzzlerOracle = new GasGuzzlerOracle();

rethToken = IRETHToken(result.externalAddresses.RETHToken);

Expand Down Expand Up @@ -189,6 +195,49 @@ contract OraclesMainnet is TestAccounts {
mock.setUpdatedAt(block.timestamp - 7 days);
}

function etchGasGuzzlerToEthOracle(bytes memory _mockOracleCode) internal {
// Etch the mock code to the ETH-USD oracle address
vm.etch(address(ethOracle), _mockOracleCode);
GasGuzzlerOracle mock = GasGuzzlerOracle(address(ethOracle));
mock.setDecimals(8);
// Fake ETH-USD price of 2000 USD
mock.setPrice(2000e8);
mock.setUpdatedAt(block.timestamp);
}


function etchGasGuzzlerToRethOracle(bytes memory _mockOracleCode) internal {
// Etch the mock code to the RETH-ETH oracle address
vm.etch(address(rethOracle), _mockOracleCode);
// Wrap so we can use the mock's setters
GasGuzzlerOracle mock = GasGuzzlerOracle(address(rethOracle));
mock.setDecimals(18);
// Set 1 RETH = 1.1 ETH
mock.setPrice(11e17);
mock.setUpdatedAt(block.timestamp);
}

function etchGasGuzzlerToStethOracle(bytes memory _mockOracleCode) internal {
// Etch the mock code to the STETH-USD oracle address
vm.etch(address(stethOracle), _mockOracleCode);
// Wrap so we can use the mock's setters
GasGuzzlerOracle mock = GasGuzzlerOracle(address(stethOracle));
mock.setDecimals(8);
// Set 1 STETH = 2000 USD
mock.setPrice(2000e8);
mock.setUpdatedAt(block.timestamp);
}

function etchGasGuzzlerMockToRethToken(bytes memory _mockTokenCode) internal {
// Etch the mock code to the RETH token address
vm.etch(address(rethToken), _mockTokenCode);
}

function etchGasGuzzlerMockToWstethToken(bytes memory _mockTokenCode) internal {
// Etch the mock code to the RETH token address
vm.etch(address(wstETH), _mockTokenCode);
}

// --- lastGoodPrice set on deployment ---

function testSetLastGoodPriceOnDeploymentWETH() public view {
Expand Down Expand Up @@ -295,6 +344,41 @@ contract OraclesMainnet is TestAccounts {
assertEq(storedStEthUsdStaleness, _24_HOURS);
}

// --- LST exchange rates and market price oracle sanity checks ---

function testRETHExchangeRateBetween1And2() public {
uint256 rate = rethToken.getExchangeRate();
assertGt(rate, 1e18);
assertLt(rate, 2e18);
}

function testWSTETHExchangeRateBetween1And2() public {
uint256 rate = wstETH.stEthPerToken();
assertGt(rate, 1e18);
assertLt(rate, 2e18);
}

function testRETHOracleAnswerBetween1And2() public {
uint256 answer = _getLatestAnswerFromOracle(rethOracle);
assertGt(answer, 1e18);
assertLt(answer, 2e18);
}

function testSTETHOracleAnswerWithin1PctOfETHOracleAnswer() public {
uint256 stethUsd = _getLatestAnswerFromOracle(stethOracle);
uint256 ethUsd = _getLatestAnswerFromOracle(ethOracle);

uint256 relativeDelta;

if (stethUsd > ethUsd) {
relativeDelta = (stethUsd - ethUsd) * 1e18 / ethUsd;
} else {
relativeDelta = (ethUsd - stethUsd) * 1e18 / stethUsd;
}

assertLt(relativeDelta, 1e16);
}

// // --- Basic actions ---

function testOpenTroveWETH() public {
Expand Down Expand Up @@ -1988,28 +2072,78 @@ contract OraclesMainnet is TestAccounts {
assertEq(contractsArray[1].collToken.balanceOf(A), A_collBefore + expectedCollDelta, "A's coll didn't change");
}

// --- Low gas reverts ---
// --- Low gas market oracle reverts ---

function testRevertLowGasSTETHOracle() public {
// Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k
(bool success,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(success);

// Etch gas guzzler to the oracle
etchGasGuzzlerToStethOracle(address(gasGuzzlerOracle).code);

// --- Call these functions with 10k gas - i.e. enough to run out of gas in the Chainlink calls ---
function testRevertLowGasWSTETH() public {
// After etching the gas guzzler to the oracle, confirm the same call with 500k gas now reverts due to OOG
vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector);
// just catch return val to suppress warning
(bool success,) = address(wstethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()"));
assertFalse(success);
(bool revertAsExpected,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(revertAsExpected);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Interesting, so when the failure is catched by vm.expecRevert then bool return value is true?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, strange design choice in Foundry IMO. See the "Gotcha: low level calls" section here:
https://book.getfoundry.sh/cheatcodes/expect-revert

}

function testRevertLowGasRETH() public {
function testRevertLowGasRETHOracle() public {
// Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k
(bool success,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(success);

// Etch gas guzzler to the oracle
etchGasGuzzlerToRethOracle(address(gasGuzzlerOracle).code);

// After etching the gas guzzler to the oracle, confirm the same call with 500k gas now reverts due to OOG
vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector);
(bool revertAsExpected,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(revertAsExpected);
}

function testRevertLowGasETHOracle() public {
// Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k
(bool success,) = address(wethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(success);

// Etch gas guzzler to the oracle
etchGasGuzzlerToEthOracle(address(gasGuzzlerOracle).code);

// After etching the gas guzzler to the oracle, confirm the same call with 500k gas now reverts due to OOG
vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector);
// just catch return val to suppress warning
(bool success,) = address(rethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()"));
assertFalse(success);
(bool revertAsExpected,) = address(wethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(revertAsExpected);
}

function testRevertLowGasWETH() public {
// --- Test with a gas guzzler token, and confirm revert ---

function testRevertLowGasWSTETHToken() public {
// Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k
(bool success,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(success);

// Etch gas guzzler to the LST
etchGasGuzzlerMockToWstethToken(address(gasGuzzlerToken).code);

// After etching the gas guzzler to the LST, confirm the same call with 500k gas now reverts due to OOG
vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector);
(bool revertsAsExpected,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(revertsAsExpected);
}

function testRevertLowGasRETHToken() public {
// Confirm call to the real external contracts succeeds with sufficient gas i.e. 500k
(bool success,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(success);

// Etch gas guzzler to the LST
etchGasGuzzlerMockToRethToken(address(gasGuzzlerToken).code);

// After etching the gas guzzler to the LST, confirm the same call with 500k gas now reverts due to OOG
vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector);
// just catch return val to suppress warning
(bool success,) = address(wethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()"));
assertFalse(success);
(bool revertsAsExpected,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()"));
assertTrue(revertsAsExpected);
}

// - More basic actions tests (adjust, close, etc)
Expand Down
1 change: 0 additions & 1 deletion contracts/test/TestContracts/ChainlinkOracleMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import "src/Dependencies/AggregatorV3Interface.sol";

// Mock Chainlink oracle that returns a stale price answer.
// this contract code is etched over mainnet oracle addresses in mainnet fork tests.
// As such, we use bools for staleness and decimals to save us having to set some contract state each time after etching.
contract ChainlinkOracleMock is AggregatorV3Interface {
uint8 decimal;

Expand Down
47 changes: 47 additions & 0 deletions contracts/test/TestContracts/GasGuzzlerOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.24;

import "src/Dependencies/AggregatorV3Interface.sol";

// Mock oracle that consumes all gas in the price getter.
// this contract code is etched over mainnet oracle addresses in mainnet fork tests.
contract GasGuzzlerOracle is AggregatorV3Interface {
uint8 decimal;

int256 price;

uint256 lastUpdateTime;

uint256 pointlessStorageVar = 42;

// We use 8 decimals unless set to 18
function decimals() external view returns (uint8) {
return decimal;
}

function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
// Expensive SLOAD loop that hits the block gas limit before completing
for (uint256 i = 0; i < 1000000; i++) {
uint256 unusedVar = pointlessStorageVar + i;
}

return (0, price, 0, lastUpdateTime, 0);
}

function setDecimals(uint8 _decimals) external {
decimal = _decimals;
}

function setPrice(int256 _price) external {
price = _price;
}

function setUpdatedAt(uint256 _updatedAt) external {
lastUpdateTime = _updatedAt;
}
}
28 changes: 28 additions & 0 deletions contracts/test/TestContracts/GasGuzzlerToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.24;

// Mock token that uses all available gas on exchange rate calls.
// This contract code is etched over LST token addresses in mainnet fork tests.
// Has exchange rate functions for WSTETH and RETH.
contract GasGuzzlerToken {
uint256 pointlessStorageVar = 42;

// RETH exchange rate getter
function getExchangeRate() external view returns (uint256) {
// Expensive SLOAD loop that hits the block gas limit before completing
for (uint256 i = 0; i < 1000000; i++) {
uint256 unusedVar = pointlessStorageVar + i;
}
return 11e17;
}

// WSTETH exchange rate getter
function stEthPerToken() external view returns (uint256) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don’t we need one for RETH?

// Expensive SLOAD loop that hits the block gas limit before completing
for (uint256 i = 0; i < 1000000; i++) {
uint256 unusedVar = pointlessStorageVar + i;
}
return 11e17;
}
}
Loading