From b1e188dff360e32dd057b62f6119f41f6b60b675 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 21 Jan 2025 19:10:13 +0400 Subject: [PATCH] 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; + } +}