Skip to content

Commit

Permalink
Merge pull request #734 from liquity/extra_oracle_tests
Browse files Browse the repository at this point in the history
Add extra oracle tests
  • Loading branch information
RickGriff authored Jan 23, 2025
2 parents 02b98e9 + 6ba339e commit 9ec8311
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 15 deletions.
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);
}

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) {
// Expensive SLOAD loop that hits the block gas limit before completing
for (uint256 i = 0; i < 1000000; i++) {
uint256 unusedVar = pointlessStorageVar + i;
}
return 11e17;
}
}

0 comments on commit 9ec8311

Please sign in to comment.