diff --git a/contracts/test/OracleMainnet.t.sol b/contracts/test/OracleMainnet.t.sol index e9076f35c..6a8a36a66 100644 --- a/contracts/test/OracleMainnet.t.sol +++ b/contracts/test/OracleMainnet.t.sol @@ -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"; @@ -29,6 +31,8 @@ contract OraclesMainnet is TestAccounts { AggregatorV3Interface rethOracle; ChainlinkOracleMock mockOracle; + GasGuzzlerToken gasGuzzlerToken; + GasGuzzlerOracle gasGuzzlerOracle; IMainnetPriceFeed wethPriceFeed; IRETHPriceFeed rethPriceFeed; @@ -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); @@ -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 { @@ -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 { @@ -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); } - 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) diff --git a/contracts/test/TestContracts/ChainlinkOracleMock.sol b/contracts/test/TestContracts/ChainlinkOracleMock.sol index df13d22f4..a01817d49 100644 --- a/contracts/test/TestContracts/ChainlinkOracleMock.sol +++ b/contracts/test/TestContracts/ChainlinkOracleMock.sol @@ -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; diff --git a/contracts/test/TestContracts/GasGuzzlerOracle.sol b/contracts/test/TestContracts/GasGuzzlerOracle.sol new file mode 100644 index 000000000..016ce94b1 --- /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; + } +} diff --git a/contracts/test/TestContracts/GasGuzzlerToken.sol b/contracts/test/TestContracts/GasGuzzlerToken.sol new file mode 100644 index 000000000..ef1cbd60f --- /dev/null +++ b/contracts/test/TestContracts/GasGuzzlerToken.sol @@ -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) { + // Expensive SLOAD loop that hits the block gas limit before completing + for (uint256 i = 0; i < 1000000; i++) { + uint256 unusedVar = pointlessStorageVar + i; + } + return 11e17; + } +}