From b1e188dff360e32dd057b62f6119f41f6b60b675 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 21 Jan 2025 19:10:13 +0400 Subject: [PATCH 1/3] Add extra oracle tests --- contracts/test/OracleMainnet.t.sol | 99 ++++++++++++++++++- .../TestContracts/ChainlinkOracleMock.sol | 1 - .../test/TestContracts/GasGuzzlerToken.sol | 30 ++++++ 3 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 contracts/test/TestContracts/GasGuzzlerToken.sol diff --git a/contracts/test/OracleMainnet.t.sol b/contracts/test/OracleMainnet.t.sol index e9076f35c..3d24700eb 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/GasGuzzlerToken.sol"; import "./TestContracts/RETHTokenMock.sol"; import "./TestContracts/WSTETHTokenMock.sol"; import "./TestContracts/Deployment.t.sol"; @@ -29,6 +30,7 @@ contract OraclesMainnet is TestAccounts { AggregatorV3Interface rethOracle; ChainlinkOracleMock mockOracle; + GasGuzzlerToken gasGuzzlerToken; IMainnetPriceFeed wethPriceFeed; IRETHPriceFeed rethPriceFeed; @@ -93,6 +95,7 @@ contract OraclesMainnet is TestAccounts { stethOracle = AggregatorV3Interface(result.externalAddresses.STETHOracle); mockOracle = new ChainlinkOracleMock(); + gasGuzzlerToken = new GasGuzzlerToken(); rethToken = IRETHToken(result.externalAddresses.RETHToken); @@ -101,6 +104,8 @@ contract OraclesMainnet is TestAccounts { mockRethToken = new RETHTokenMock(); mockWstethToken = new WSTETHTokenMock(); + + // Record contracts for (uint256 c = 0; c < vars.numCollaterals; c++) { contractsArray.push(result.contractsArray[c]); @@ -189,6 +194,20 @@ contract OraclesMainnet is TestAccounts { mock.setUpdatedAt(block.timestamp - 7 days); } + 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 --- function testSetLastGoodPriceOnDeploymentWETH() public view { @@ -295,6 +314,43 @@ 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,30 +2044,65 @@ 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 --- // --- Call these functions with 10k gas - i.e. enough to run out of gas in the Chainlink calls --- - function testRevertLowGasWSTETH() public { + function testRevertLowGasSTETHOracle() public { vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); // just catch return val to suppress warning (bool success,) = address(wstethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); assertFalse(success); } - function testRevertLowGasRETH() public { + function testRevertLowGasRETHOracle() public { vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); // just catch return val to suppress warning (bool success,) = address(rethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); assertFalse(success); } - function testRevertLowGasWETH() public { + function testRevertLowGasETHOracle() public { vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); // just catch return val to suppress warning (bool success,) = address(wethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); assertFalse(success); } + // --- 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()")); + 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); + // just catch return val to suppress warning + (success,) = address(wstethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); + assertFalse(success); + } + + 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()")); + 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 + (success,) = address(rethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); + assertFalse(success); + } + + + + // - More basic actions tests (adjust, close, etc) // - liq tests (manipulate aggregator stored price) } 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/GasGuzzlerToken.sol b/contracts/test/TestContracts/GasGuzzlerToken.sol new file mode 100644 index 000000000..18ee3e56e --- /dev/null +++ b/contracts/test/TestContracts/GasGuzzlerToken.sol @@ -0,0 +1,30 @@ +// 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; + } +} From c2bb40109be45bd1eebacbb19a1ee64977abcacc Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 21 Jan 2025 19:12:18 +0400 Subject: [PATCH 2/3] forge fmt --- contracts/test/OracleMainnet.t.sol | 9 +-------- contracts/test/TestContracts/GasGuzzlerToken.sol | 4 +--- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/contracts/test/OracleMainnet.t.sol b/contracts/test/OracleMainnet.t.sol index 3d24700eb..e87b05cc1 100644 --- a/contracts/test/OracleMainnet.t.sol +++ b/contracts/test/OracleMainnet.t.sol @@ -104,8 +104,6 @@ contract OraclesMainnet is TestAccounts { mockRethToken = new RETHTokenMock(); mockWstethToken = new WSTETHTokenMock(); - - // Record contracts for (uint256 c = 0; c < vars.numCollaterals; c++) { contractsArray.push(result.contractsArray[c]); @@ -349,8 +347,6 @@ contract OraclesMainnet is TestAccounts { assertLt(relativeDelta, 1e16); } - - // // --- Basic actions --- function testOpenTroveWETH() public { @@ -2093,16 +2089,13 @@ contract OraclesMainnet is TestAccounts { // 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 + // 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); } - - - // - More basic actions tests (adjust, close, etc) // - liq tests (manipulate aggregator stored price) } diff --git a/contracts/test/TestContracts/GasGuzzlerToken.sol b/contracts/test/TestContracts/GasGuzzlerToken.sol index 18ee3e56e..ef1cbd60f 100644 --- a/contracts/test/TestContracts/GasGuzzlerToken.sol +++ b/contracts/test/TestContracts/GasGuzzlerToken.sol @@ -2,14 +2,12 @@ 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 From 6ba339e550c6922a997aca24e2cc42aa4ce3d279 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Wed, 22 Jan 2025 16:58:54 +0400 Subject: [PATCH 3/3] Fix gas guzzler tests --- contracts/test/OracleMainnet.t.sol | 96 ++++++++++++++----- .../test/TestContracts/GasGuzzlerOracle.sol | 47 +++++++++ 2 files changed, 120 insertions(+), 23 deletions(-) create mode 100644 contracts/test/TestContracts/GasGuzzlerOracle.sol diff --git a/contracts/test/OracleMainnet.t.sol b/contracts/test/OracleMainnet.t.sol index e87b05cc1..6a8a36a66 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 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; + } +}