diff --git a/contracts/test/OracleMainnet.t.sol b/contracts/test/OracleMainnet.t.sol index e87b05cc..6a8a36a6 100644 --- a/contracts/test/OracleMainnet.t.sol +++ b/contracts/test/OracleMainnet.t.sol @@ -9,6 +9,7 @@ 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"; @@ -31,6 +32,7 @@ contract OraclesMainnet is TestAccounts { ChainlinkOracleMock mockOracle; GasGuzzlerToken gasGuzzlerToken; + GasGuzzlerOracle gasGuzzlerOracle; IMainnetPriceFeed wethPriceFeed; IRETHPriceFeed rethPriceFeed; @@ -96,6 +98,7 @@ contract OraclesMainnet is TestAccounts { mockOracle = new ChainlinkOracleMock(); gasGuzzlerToken = new GasGuzzlerToken(); + gasGuzzlerOracle = new GasGuzzlerOracle(); rethToken = IRETHToken(result.externalAddresses.RETHToken); @@ -192,18 +195,47 @@ 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); - // // Wrap so we can use the mock's functions - // GasGuzzlerToken mockReth = GasGuzzlerToken(address(rethToken)); } function etchGasGuzzlerMockToWstethToken(bytes memory _mockTokenCode) internal { // Etch the mock code to the RETH token address vm.etch(address(wstETH), _mockTokenCode); - // // Wrap so we can use the mock's functions - // GasGuzzlerToken mockWsteth = GasGuzzlerToken(address(wstETH)); } // --- lastGoodPrice set on deployment --- @@ -2042,33 +2074,53 @@ contract OraclesMainnet is TestAccounts { // --- Low gas market oracle reverts --- - // --- Call these functions with 10k gas - i.e. enough to run out of gas in the Chainlink calls --- 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); + + // 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); } 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); - // just catch return val to suppress warning - (bool success,) = address(rethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); - assertFalse(success); + (bool revertAsExpected,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); + assertTrue(revertAsExpected); } - function testRevertLowGasETHOracle() public { + 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(wethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); - assertFalse(success); + (bool revertAsExpected,) = address(wethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); + assertTrue(revertAsExpected); } // --- 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(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); + (bool success,) = address(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); assertTrue(success); // Etch gas guzzler to the LST @@ -2076,14 +2128,13 @@ contract OraclesMainnet is TestAccounts { // 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 - (success,) = address(wstethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); - assertFalse(success); + (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(wstethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); + (bool success,) = address(rethPriceFeed).call{gas: 500000}(abi.encodeWithSignature("fetchPrice()")); assertTrue(success); // Etch gas guzzler to the LST @@ -2091,9 +2142,8 @@ contract OraclesMainnet is TestAccounts { // 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 - (success,) = address(rethPriceFeed).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) diff --git a/contracts/test/TestContracts/GasGuzzlerOracle.sol b/contracts/test/TestContracts/GasGuzzlerOracle.sol new file mode 100644 index 00000000..016ce94b --- /dev/null +++ b/contracts/test/TestContracts/GasGuzzlerOracle.sol @@ -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; + } +}