From 54648b8987a9672d8d77a05a628ff3541b6a7117 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Thu, 29 Aug 2024 16:52:30 +0100 Subject: [PATCH 01/19] Add ETH-USD fallback logic and remove OSETH and ETHX contracts --- .../src/Interfaces/ICompositePriceFeed.sol | 10 - .../src/Interfaces/IMainnetPriceFeed.sol | 9 + contracts/src/Interfaces/IRETHPriceFeed.sol | 9 + .../IRETHToken.sol | 0 contracts/src/Interfaces/IWETHPriceFeed.sol | 9 - contracts/src/Interfaces/IWSTETHPriceFeed.sol | 4 +- .../src/PriceFeeds/CompositePriceFeed.sol | 86 +++++---- .../src/PriceFeeds/MainnetPriceFeedBase.sol | 48 +++-- contracts/src/PriceFeeds/RETHPriceFeed.sol | 57 +++++- contracts/src/PriceFeeds/WETHPriceFeed.sol | 41 ++-- contracts/src/PriceFeeds/WSTETHPriceFeed.sol | 46 +++-- contracts/src/test/OracleMainnet.t.sol | 177 ++---------------- .../src/test/TestContracts/Deployment.t.sol | 55 +----- contracts/src/test/zapperLeverage.t.sol | 2 +- 14 files changed, 224 insertions(+), 329 deletions(-) delete mode 100644 contracts/src/Interfaces/ICompositePriceFeed.sol create mode 100644 contracts/src/Interfaces/IMainnetPriceFeed.sol create mode 100644 contracts/src/Interfaces/IRETHPriceFeed.sol rename contracts/src/{Dependencies => Interfaces}/IRETHToken.sol (100%) delete mode 100644 contracts/src/Interfaces/IWETHPriceFeed.sol diff --git a/contracts/src/Interfaces/ICompositePriceFeed.sol b/contracts/src/Interfaces/ICompositePriceFeed.sol deleted file mode 100644 index 2a6143eb2..000000000 --- a/contracts/src/Interfaces/ICompositePriceFeed.sol +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-License-Identifier: MIT -import "../Interfaces/IPriceFeed.sol"; -import "../Dependencies/AggregatorV3Interface.sol"; - -pragma solidity ^0.8.0; - -interface ICompositePriceFeed is IPriceFeed { - function ethUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); - function lstEthOracle() external view returns (AggregatorV3Interface, uint256, uint8); -} diff --git a/contracts/src/Interfaces/IMainnetPriceFeed.sol b/contracts/src/Interfaces/IMainnetPriceFeed.sol new file mode 100644 index 000000000..63c3ee3fd --- /dev/null +++ b/contracts/src/Interfaces/IMainnetPriceFeed.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +import "../Interfaces/IPriceFeed.sol"; +import "../Dependencies/AggregatorV3Interface.sol"; + +pragma solidity ^0.8.0; + +interface IMainnetPriceFeed is IPriceFeed { + function ethUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); +} diff --git a/contracts/src/Interfaces/IRETHPriceFeed.sol b/contracts/src/Interfaces/IRETHPriceFeed.sol new file mode 100644 index 000000000..04e77f754 --- /dev/null +++ b/contracts/src/Interfaces/IRETHPriceFeed.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +import "./IMainnetPriceFeed.sol"; +import "../Dependencies/AggregatorV3Interface.sol"; + +pragma solidity ^0.8.0; + +interface IRETHPriceFeed is IMainnetPriceFeed { + function rEthEthOracle() external view returns (AggregatorV3Interface, uint256, uint8); +} \ No newline at end of file diff --git a/contracts/src/Dependencies/IRETHToken.sol b/contracts/src/Interfaces/IRETHToken.sol similarity index 100% rename from contracts/src/Dependencies/IRETHToken.sol rename to contracts/src/Interfaces/IRETHToken.sol diff --git a/contracts/src/Interfaces/IWETHPriceFeed.sol b/contracts/src/Interfaces/IWETHPriceFeed.sol deleted file mode 100644 index 9739be3c7..000000000 --- a/contracts/src/Interfaces/IWETHPriceFeed.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -import "./IPriceFeed.sol"; -import "../Dependencies/AggregatorV3Interface.sol"; - -pragma solidity ^0.8.0; - -interface IWETHPriceFeed is IPriceFeed { - function ethUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); -} diff --git a/contracts/src/Interfaces/IWSTETHPriceFeed.sol b/contracts/src/Interfaces/IWSTETHPriceFeed.sol index 88e6cd8f7..cfb20934e 100644 --- a/contracts/src/Interfaces/IWSTETHPriceFeed.sol +++ b/contracts/src/Interfaces/IWSTETHPriceFeed.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT -import "./IPriceFeed.sol"; +import "./IMainnetPriceFeed.sol"; import "../Dependencies/AggregatorV3Interface.sol"; pragma solidity ^0.8.0; -interface IWSTETHPriceFeed is IPriceFeed { +interface IWSTETHPriceFeed is IMainnetPriceFeed { function stEthUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); } diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol index 1fc040862..ca0af272b 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -4,71 +4,81 @@ pragma solidity 0.8.24; import "../Dependencies/LiquityMath.sol"; import "./MainnetPriceFeedBase.sol"; -import "../Interfaces/ICompositePriceFeed.sol"; +<<<<<<< HEAD // Composite PriceFeed: outputs an LST-USD price derived from two external price Oracles: LST-ETH, and ETH-USD. // Used where the LST token is non-rebasing (as per rETH, osETH, ETHx, etc). contract CompositePriceFeed is MainnetPriceFeedBase, ICompositePriceFeed { Oracle public lstEthOracle; Oracle public ethUsdOracle; +======= +// import "forge-std/console2.sol"; + +// The CompositePriceFeed is used for feeds that incorporate both a market price oracle (e.g. STETH-USD, or RETH-ETH) +// and an LST canonical rate (e.g. WSTETH:STETH, or RETH:ETH). +contract CompositePriceFeed is MainnetPriceFeedBase { +>>>>>>> 81a1a2aa (Add ETH-USD fallback logic and remove OSETH and ETHX contracts) address public rateProviderAddress; constructor( address _owner, address _ethUsdOracleAddress, - address _lstEthOracleAddress, address _rateProviderAddress, - uint256 _ethUsdStalenessThreshold, - uint256 _lstEthStalenessThreshold - ) MainnetPriceFeedBase(_owner) { - // Store ETH-USD oracle - ethUsdOracle.aggregator = AggregatorV3Interface(_ethUsdOracleAddress); - ethUsdOracle.stalenessThreshold = _ethUsdStalenessThreshold; - ethUsdOracle.decimals = ethUsdOracle.aggregator.decimals(); - assert(ethUsdOracle.decimals == 8); - - // Store LST-ETH oracle - lstEthOracle.aggregator = AggregatorV3Interface(_lstEthOracleAddress); - lstEthOracle.stalenessThreshold = _lstEthStalenessThreshold; - lstEthOracle.decimals = lstEthOracle.aggregator.decimals(); - + uint256 _ethUsdStalenessThreshold + ) MainnetPriceFeedBase( + _owner, + _ethUsdOracleAddress, + _ethUsdStalenessThreshold + ) { // Store rate provider rateProviderAddress = _rateProviderAddress; - - _fetchPrice(); - - // Check an oracle didn't already fail - assert(priceFeedDisabled == false); + } + + function fetchPrice() public returns (uint256, bool) { + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) {return _fetchPrice();} + + // If branch is already shut down and using ETH-USD * canonical_rate, try to use that + if (priceSource == PriceSource.ETHUSDxCanonical) { + (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); + //... but if the ETH-USD oracle *also* fails here, use the lastGoodPrice + if(ethUsdOracleDown) { + // No need to shut down, since branch already is shut down + return (lastGoodPrice, false); + } else { + return (_fetchPriceETHUSDxCanonical(ethUsdPrice), false); + } + } + + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + if (priceSource == PriceSource.lastGoodPrice) {return (lastGoodPrice, false);} } - function _fetchPrice() internal override returns (uint256, bool) { - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - (uint256 lstEthPrice, bool lstEthOracleDown) = _getOracleAnswer(lstEthOracle); - - // If one of Chainlink's responses was invalid in this transaction, disable this PriceFeed and - // return the last good LST-USD price calculated - if (ethUsdOracleDown) return (_disableFeedAndShutDown(address(ethUsdOracle.aggregator)), true); - if (lstEthOracleDown) return (_disableFeedAndShutDown(address(lstEthOracle.aggregator)), true); + function _shutDownAndSwitchToETHUSDxCanonical(address _failedOracleAddr, uint256 _ethUsdPrice) internal returns (uint256) { + // Shut down the branch + borrowerOperations.shutdownFromOracleFailure(_failedOracleAddr); - // Calculate the market LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST - uint256 lstUsdMarketPrice = ethUsdPrice * lstEthPrice / 1e18; + priceSource = PriceSource.ETHUSDxCanonical; + return _fetchPriceETHUSDxCanonical(_ethUsdPrice); + } + // Only called if the primary LST oracle has failed, branch has shut down, + // and we've switched to using: ETH-USD * canonical_rate. + function _fetchPriceETHUSDxCanonical(uint256 _ethUsdPrice) internal returns (uint256) { + assert(priceSource == PriceSource.ETHUSDxCanonical); // Get the ETH_per_LST canonical rate directly from the LST contract // TODO: Should we also shutdown if the call to the canonical rate reverts, or returns 0? uint256 lstEthRate = _getCanonicalRate(); // Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST - uint256 lstUsdCanonicalPrice = ethUsdPrice * lstEthRate / 1e18; - - // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation - uint256 lstUsdPrice = LiquityMath._min(lstUsdMarketPrice, lstUsdCanonicalPrice); + uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstEthRate / 1e18; - lastGoodPrice = lstUsdPrice; + lastGoodPrice = lstUsdCanonicalPrice; - return (lstUsdPrice, false); + return (lstUsdCanonicalPrice); } - // Returns the ETH_per_LST as from the LST smart contract. Implementation depends on the specific LST. + // Returns the LST exchange rate from the LST smart contract. Implementation depends on the specific LST. function _getCanonicalRate() internal view virtual returns (uint256) {} } diff --git a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol index abd03778f..144687e44 100644 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol @@ -4,12 +4,28 @@ pragma solidity 0.8.24; import "../Dependencies/Ownable.sol"; import "../Dependencies/AggregatorV3Interface.sol"; -import "../Interfaces/IPriceFeed.sol"; +import "../Interfaces/IMainnetPriceFeed.sol"; import "../BorrowerOperations.sol"; +<<<<<<< HEAD abstract contract MainnetPriceFeedBase is IPriceFeed, Ownable { // Flag raised when the collateral branch gets shut down. bool priceFeedDisabled; +======= +// import "forge-std/console2.sol"; + +abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { + // Dummy flag raised when the collateral branch gets shut down. + // Should be removed after actual shutdown logic is implemented. + + enum PriceSource { + primary, + ETHUSDxCanonical, + lastGoodPrice + } + + PriceSource public priceSource; +>>>>>>> 81a1a2aa (Add ETH-USD fallback logic and remove OSETH and ETHX contracts) // Last good price tracker for the derived USD price uint256 public lastGoodPrice; @@ -27,9 +43,24 @@ abstract contract MainnetPriceFeedBase is IPriceFeed, Ownable { bool success; } + Oracle public ethUsdOracle; + IBorrowerOperations borrowerOperations; - constructor(address _owner) Ownable(_owner) {} + constructor( + address _owner, + address _ethUsdOracleAddress, + uint256 _ethUsdStalenessThreshold + ) + Ownable(_owner) + { + // Store ETH-USD oracle + ethUsdOracle.aggregator = AggregatorV3Interface(_ethUsdOracleAddress); + ethUsdOracle.stalenessThreshold = _ethUsdStalenessThreshold; + ethUsdOracle.decimals = ethUsdOracle.aggregator.decimals(); + + assert(ethUsdOracle.decimals == 8); + } // TODO: remove this and set address in constructor, since we'll use CREATE2 function setAddresses(address _borrowOperationsAddress) external onlyOwner { @@ -38,15 +69,6 @@ abstract contract MainnetPriceFeedBase is IPriceFeed, Ownable { _renounceOwnership(); } - // fetchPrice returns: - // - The price - // - A bool indicating whether a new oracle failure was detected in the call - function fetchPrice() public returns (uint256, bool) { - if (priceFeedDisabled) return (lastGoodPrice, false); - - return _fetchPrice(); - } - // An individual Pricefeed instance implements _fetchPrice according to the data sources it uses. Returns: // - The price // - A bool indicating whether a new oracle failure was detected in the call @@ -67,11 +89,11 @@ abstract contract MainnetPriceFeedBase is IPriceFeed, Ownable { return (scaledPrice, oracleIsDown); } - function _disableFeedAndShutDown(address _failedOracleAddr) internal returns (uint256) { + function _shutDownAndSwitchToLastGoodPrice(address _failedOracleAddr) internal returns (uint256) { // Shut down the branch borrowerOperations.shutdownFromOracleFailure(_failedOracleAddr); - priceFeedDisabled = true; + priceSource = PriceSource.lastGoodPrice; return lastGoodPrice; } diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol index 2b0d3a169..4637a754b 100644 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/RETHPriceFeed.sol @@ -3,27 +3,64 @@ pragma solidity 0.8.24; import "./CompositePriceFeed.sol"; -import "../Dependencies/IRETHToken.sol"; +import "../Interfaces/IRETHToken.sol"; +import "../Interfaces/IRETHPriceFeed.sol"; -contract RETHPriceFeed is CompositePriceFeed { +// import "forge-std/console2.sol"; + +contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { constructor( address _owner, address _ethUsdOracleAddress, - address _lstEthOracleAddress, - address _rateProviderAddress, + address _rEthEthOracleAddress, + address _rEthTokenAddress, uint256 _ethUsdStalenessThreshold, - uint256 _lstEthStalenessThreshold + uint256 _rEthEthStalenessThreshold ) CompositePriceFeed( _owner, _ethUsdOracleAddress, - _lstEthOracleAddress, - _rateProviderAddress, - _ethUsdStalenessThreshold, - _lstEthStalenessThreshold + _rEthTokenAddress, + _ethUsdStalenessThreshold ) - {} + { + // Store RETH-ETH oracle + rEthEthOracle.aggregator = AggregatorV3Interface(_rEthEthOracleAddress); + rEthEthOracle.stalenessThreshold = _rEthEthStalenessThreshold; + rEthEthOracle.decimals = rEthEthOracle.aggregator.decimals(); + + } + + Oracle public rEthEthOracle; + + function _fetchPrice() internal override returns (uint256, bool) { + assert(priceSource == PriceSource.primary); + (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); + (uint256 rEthEthPrice, bool rEthEthOracleDown) = _getOracleAnswer(rEthEthOracle); + + // If the ETH-USD feed is down, shut down and switch to the last good price seen by the system + // since we always need the + if (ethUsdOracleDown) {return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true);} + // If the ETH-USD feed is live but the RETH-ETH oracle is down, shutdown and substitute RETH-ETH with the canonical rate + if (rEthEthOracleDown) {return (_shutDownAndSwitchToETHUSDxCanonical(address(rEthEthOracle.aggregator), ethUsdPrice), true);} + + // Otherwise, use the primary price calculation: + // Calculate the market LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST + uint256 lstUsdMarketPrice = ethUsdPrice * rEthEthPrice / 1e18; + + // Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST + // TODO: Should we also shutdown if the call to the canonical rate reverts, or returns 0? + uint256 lstUsdCanonicalPrice = ethUsdPrice * _getCanonicalRate() / 1e18; + + // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. + // NOTE: only needed + uint256 lstUsdPrice = LiquityMath._min(lstUsdMarketPrice, lstUsdCanonicalPrice); + + lastGoodPrice = lstUsdPrice; + + return (lstUsdPrice, false); + } function _getCanonicalRate() internal view override returns (uint256) { // RETHToken returns exchange rate with 18 digit decimal precision return IRETHToken(rateProviderAddress).getExchangeRate(); diff --git a/contracts/src/PriceFeeds/WETHPriceFeed.sol b/contracts/src/PriceFeeds/WETHPriceFeed.sol index 3c5955836..efcae1143 100644 --- a/contracts/src/PriceFeeds/WETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WETHPriceFeed.sol @@ -3,32 +3,41 @@ pragma solidity 0.8.24; import "./MainnetPriceFeedBase.sol"; -import "../Interfaces/IWETHPriceFeed.sol"; -contract WETHPriceFeed is MainnetPriceFeedBase, IWETHPriceFeed { - Oracle public ethUsdOracle; - - constructor(address _owner, address _ethUsdOracleAddress, uint256 _ethUsdStalenessThreshold) - MainnetPriceFeedBase(_owner) +// import "forge-std/console2.sol"; + +contract WETHPriceFeed is MainnetPriceFeedBase { + constructor( + address _owner, + address _ethUsdOracleAddress, + uint256 _ethUsdStalenessThreshold + ) + MainnetPriceFeedBase( + _owner, + _ethUsdOracleAddress, + _ethUsdStalenessThreshold + ) { - ethUsdOracle.aggregator = AggregatorV3Interface(_ethUsdOracleAddress); - ethUsdOracle.stalenessThreshold = _ethUsdStalenessThreshold; - ethUsdOracle.decimals = ethUsdOracle.aggregator.decimals(); - - // Check ETH-USD aggregator has the expected 8 decimals - assert(ethUsdOracle.decimals == 8); - _fetchPrice(); // Check the oracle didn't already fail - assert(priceFeedDisabled == false); + assert(priceSource == PriceSource.primary); + } + + function fetchPrice() public returns (uint256, bool) { + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) {return _fetchPrice();} + + // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it + if (priceSource == PriceSource.lastGoodPrice) {return (lastGoodPrice, false);} } function _fetchPrice() internal override returns (uint256, bool) { + assert(priceSource == PriceSource.primary); (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - // If the Chainlink response was invalid in this transaction, return the last good ETH-USD price calculated - if (ethUsdOracleDown) return (_disableFeedAndShutDown(address(ethUsdOracle.aggregator)), true); + // If the ETH-USD Chainlink response was invalid in this transaction, return the last good ETH-USD price calculated + if (ethUsdOracleDown) {return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true);} lastGoodPrice = ethUsdPrice; diff --git a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol index 027920ad8..dbdf81eb2 100644 --- a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol @@ -2,47 +2,65 @@ pragma solidity 0.8.24; -import "./MainnetPriceFeedBase.sol"; +import "./CompositePriceFeed.sol"; import "../Interfaces/IWSTETH.sol"; import "../Interfaces/IWSTETHPriceFeed.sol"; -contract WSTETHPriceFeed is MainnetPriceFeedBase, IWSTETHPriceFeed { +contract WSTETHPriceFeed is CompositePriceFeed, IWSTETHPriceFeed { Oracle public stEthUsdOracle; - IWSTETH public wstETH; constructor( address _owner, + address _ethUsdOracleAddress, address _stEthUsdOracleAddress, - uint256 _stEthUsdStalenessThreshold, - address _wstETHAddress - ) MainnetPriceFeedBase(_owner) { + address _wstEthTokenAddress, + uint256 _ethUsdStalenessThreshold, + uint256 _stEthUsdStalenessThreshold + ) CompositePriceFeed( + _owner, + _ethUsdOracleAddress, + _wstEthTokenAddress, + _ethUsdStalenessThreshold) + { stEthUsdOracle.aggregator = AggregatorV3Interface(_stEthUsdOracleAddress); stEthUsdOracle.stalenessThreshold = _stEthUsdStalenessThreshold; stEthUsdOracle.decimals = stEthUsdOracle.aggregator.decimals(); - wstETH = IWSTETH(_wstETHAddress); - // Check the STETH-USD aggregator has the expected 8 decimals assert(stEthUsdOracle.decimals == 8); _fetchPrice(); // Check the oracle didn't already fail - assert(priceFeedDisabled == false); + assert(priceSource == PriceSource.primary); } function _fetchPrice() internal override returns (uint256, bool) { + assert(priceSource == PriceSource.primary); (uint256 stEthUsdPrice, bool stEthUsdOracleDown) = _getOracleAnswer(stEthUsdOracle); - // If one of Chainlink's responses was invalid in this transaction, disable this PriceFeed and - // return the last good WSTETH-USD price calculated - if (stEthUsdOracleDown) return (_disableFeedAndShutDown(address(stEthUsdOracle.aggregator)), true); + // If the STETH-USD feed is down, shut down and try to substitute it with the ETH-USD price + if (stEthUsdOracleDown) { + (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); + // If the ETH-USD feed is *also* down, shut down and return the last good price + if(ethUsdOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(stEthUsdOracle.aggregator)), true); + } else { + return (_shutDownAndSwitchToETHUSDxCanonical(address(stEthUsdOracle.aggregator), ethUsdPrice), true); + } + } + + // Otherwise, use the primary price calculation: - // Calculate WSTETH-USD price: USD_per_WSTETH = USD_per_STETH * STETH_per_WSTETH - uint256 wstEthUsdPrice = stEthUsdPrice * wstETH.stEthPerToken() / 1e18; + // Calculate WSTETH-USD price USD_per_WSTETH = USD_per_STETH * STETH_per_WSTETH + uint256 wstEthUsdPrice = stEthUsdPrice * _getCanonicalRate() / 1e18; lastGoodPrice = wstEthUsdPrice; return (wstEthUsdPrice, false); } + + function _getCanonicalRate() internal view override returns (uint256) { + return IWSTETH(rateProviderAddress).stEthPerToken(); + } } diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index e7f25eaab..cde9be16f 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -7,13 +7,11 @@ import "./TestContracts/ChainlinkOracleMock.sol"; import "./TestContracts/Deployment.t.sol"; import "../Dependencies/AggregatorV3Interface.sol"; -import "../Interfaces/IWSTETH.sol"; -import "../Interfaces/ICompositePriceFeed.sol"; -import "../Interfaces/IWETHPriceFeed.sol"; +import "../Interfaces/IRETHPriceFeed.sol"; +import "../Interfaces/IWSTETHPriceFeed.sol"; -import "../Dependencies/IRETHToken.sol"; -import "../Dependencies/IOsTokenVaultController.sol"; -import "../Dependencies/IStaderOracle.sol"; +import "../Interfaces/IRETHToken.sol"; +import "../Interfaces/IWSTETH.sol"; import "forge-std/Test.sol"; import "forge-std/console2.sol"; @@ -22,22 +20,15 @@ contract OraclesMainnet is TestAccounts { AggregatorV3Interface ethOracle; AggregatorV3Interface stethOracle; AggregatorV3Interface rethOracle; - AggregatorV3Interface ethXOracle; - AggregatorV3Interface osEthOracle; ChainlinkOracleMock mockOracle; - IWSTETH wstETH; - - IWETHPriceFeed wethPriceFeed; - ICompositePriceFeed rethPriceFeed; + IMainnetPriceFeed wethPriceFeed; + IRETHPriceFeed rethPriceFeed; IWSTETHPriceFeed wstethPriceFeed; - ICompositePriceFeed ethXPriceFeed; - ICompositePriceFeed osEthPriceFeed; IRETHToken rETHToken; - IOsTokenVaultController osTokenVaultController; - IStaderOracle staderOracle; + IWSTETH wstETH; TestDeployer.LiquityContracts[] contractsArray; ICollateralRegistry collateralRegistry; @@ -58,7 +49,7 @@ contract OraclesMainnet is TestAccounts { (A, B, C, D, E, F) = (accountsList[0], accountsList[1], accountsList[2], accountsList[3], accountsList[4], accountsList[5]); - uint256 numCollaterals = 5; + uint256 numCollaterals = 3; TestDeployer.TroveManagerParams memory tmParams = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = @@ -76,14 +67,10 @@ contract OraclesMainnet is TestAccounts { ethOracle = AggregatorV3Interface(result.externalAddresses.ETHOracle); rethOracle = AggregatorV3Interface(result.externalAddresses.RETHOracle); stethOracle = AggregatorV3Interface(result.externalAddresses.STETHOracle); - ethXOracle = AggregatorV3Interface(result.externalAddresses.ETHXOracle); - osEthOracle = AggregatorV3Interface(result.externalAddresses.OSETHOracle); mockOracle = new ChainlinkOracleMock(); rETHToken = IRETHToken(result.externalAddresses.RETHToken); - staderOracle = IStaderOracle(result.externalAddresses.StaderOracle); - osTokenVaultController = IOsTokenVaultController(result.externalAddresses.OsTokenVaultController); wstETH = IWSTETH(result.externalAddresses.WSTETHToken); @@ -108,18 +95,15 @@ contract OraclesMainnet is TestAccounts { vm.startPrank(accountsList[i]); } - wethPriceFeed = IWETHPriceFeed(address(contractsArray[0].priceFeed)); - rethPriceFeed = ICompositePriceFeed(address(contractsArray[1].priceFeed)); + wethPriceFeed = IMainnetPriceFeed(address(contractsArray[0].priceFeed)); + rethPriceFeed = IRETHPriceFeed(address(contractsArray[1].priceFeed)); wstethPriceFeed = IWSTETHPriceFeed(address(contractsArray[2].priceFeed)); - ethXPriceFeed = ICompositePriceFeed(address(contractsArray[3].priceFeed)); - osEthPriceFeed = ICompositePriceFeed(address(contractsArray[4].priceFeed)); // log some current blockchain state // console2.log(block.timestamp, "block.timestamp"); // console2.log(block.number, "block.number"); // console2.log(ethOracle.decimals(), "ETHUSD decimals"); // console2.log(rethOracle.decimals(), "RETHETH decimals"); - // console2.log(ethXOracle.decimals(), "ETHXETH decimals"); // console2.log(stethOracle.decimals(), "STETHETH decimals"); } @@ -162,44 +146,6 @@ contract OraclesMainnet is TestAccounts { assertEq(lastGoodPriceReth, expectedPrice); } - function testSetLastGoodPriceOnDeploymentETHX() public view { - uint256 lastGoodPriceEthX = ethXPriceFeed.lastGoodPrice(); - assertGt(lastGoodPriceEthX, 0); - - uint256 latestAnswerEthXEth = _getLatestAnswerFromOracle(ethXOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 expectedMarketPrice = latestAnswerEthXEth * latestAnswerEthUsd / 1e18; - - (, uint256 ethBalance, uint256 ethXSupply) = staderOracle.exchangeRate(); - uint256 rate = ethBalance * 1e18 / ethXSupply; - assertGt(rate, 1e18); - - uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; - - uint256 expectedPrice = LiquityMath._min(expectedMarketPrice, expectedCanonicalPrice); - - assertEq(lastGoodPriceEthX, expectedPrice); - } - - function testSetLastGoodPriceOnDeploymentOSETH() public view { - uint256 lastGoodPriceOsUsd = osEthPriceFeed.lastGoodPrice(); - assertGt(lastGoodPriceOsUsd, 0); - - uint256 latestAnswerOsEthEth = _getLatestAnswerFromOracle(osEthOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 expectedMarketPrice = latestAnswerOsEthEth * latestAnswerEthUsd / 1e18; - - uint256 rate = osTokenVaultController.convertToAssets(1e18); - assertGt(rate, 1e18); - - uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; - - uint256 expectedPrice = LiquityMath._min(expectedMarketPrice, expectedCanonicalPrice); - - assertEq(lastGoodPriceOsUsd, expectedPrice); - } function testSetLastGoodPriceOnDeploymentWSTETH() public view { uint256 lastGoodPriceWsteth = wstethPriceFeed.lastGoodPrice(); @@ -243,45 +189,6 @@ contract OraclesMainnet is TestAccounts { assertEq(fetchedRethUsdPrice, expectedPrice); } - function testFetchPriceReturnsCorrectPriceETHX() public { - (uint256 fetchedEthXUsdPrice,) = ethXPriceFeed.fetchPrice(); - assertGt(fetchedEthXUsdPrice, 0); - - uint256 latestAnswerEthXEth = _getLatestAnswerFromOracle(ethXOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 expectedMarketPrice = latestAnswerEthXEth * latestAnswerEthUsd / 1e18; - - (, uint256 ethBalance, uint256 ethXSupply) = staderOracle.exchangeRate(); - uint256 rate = ethBalance * 1e18 / ethXSupply; - assertGt(rate, 1e18); - - uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; - - uint256 expectedPrice = LiquityMath._min(expectedMarketPrice, expectedCanonicalPrice); - - assertEq(fetchedEthXUsdPrice, expectedPrice); - } - - function testFetchPriceReturnsCorrectPriceOSETH() public { - (uint256 fetchedOsEthUsdPrice,) = osEthPriceFeed.fetchPrice(); - assertGt(fetchedOsEthUsdPrice, 0); - - uint256 latestAnswerOsEthEth = _getLatestAnswerFromOracle(osEthOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 expectedMarketPrice = latestAnswerOsEthEth * latestAnswerEthUsd / 1e18; - - uint256 rate = osTokenVaultController.convertToAssets(1e18); - assertGt(rate, 1e18); - - uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; - - uint256 expectedPrice = LiquityMath._min(expectedMarketPrice, expectedCanonicalPrice); - - assertEq(fetchedOsEthUsdPrice, expectedPrice); - } - function testFetchPriceReturnsCorrectPriceWSTETH() public { (uint256 fetchedStethUsdPrice,) = wstethPriceFeed.fetchPrice(); assertGt(fetchedStethUsdPrice, 0); @@ -307,30 +214,10 @@ contract OraclesMainnet is TestAccounts { } function testRethEthStalenessThresholdSetRETH() public view { - (, uint256 storedRethEthStaleness,) = rethPriceFeed.lstEthOracle(); + (, uint256 storedRethEthStaleness,) = rethPriceFeed.rEthEthOracle(); assertEq(storedRethEthStaleness, _48_HOURS); } - function testEthUsdStalenessThresholdSetETHX() public view { - (, uint256 storedEthUsdStaleness,) = ethXPriceFeed.ethUsdOracle(); - assertEq(storedEthUsdStaleness, _24_HOURS); - } - - function testEthXEthStalenessThresholdSetETHX() public view { - (, uint256 storedEthXEthStaleness,) = ethXPriceFeed.lstEthOracle(); - assertEq(storedEthXEthStaleness, _48_HOURS); - } - - function testEthUsdStalenessThresholdSetOSETH() public view { - (, uint256 storedEthUsdStaleness,) = osEthPriceFeed.ethUsdOracle(); - assertEq(storedEthUsdStaleness, _24_HOURS); - } - - function testOsEthEthStalenessThresholdSetOSETH() public view { - (, uint256 storedOsEthStaleness,) = osEthPriceFeed.lstEthOracle(); - assertEq(storedOsEthStaleness, _48_HOURS); - } - function testStethUsdStalenessThresholdSetWSTETH() public view { (, uint256 storedStEthUsdStaleness,) = wstethPriceFeed.stEthUsdOracle(); assertEq(storedStEthUsdStaleness, _24_HOURS); @@ -377,48 +264,6 @@ contract OraclesMainnet is TestAccounts { assertEq(trovesCount, 1); } - function testOpenTroveETHX() public { - uint256 latestAnswerEthXEth = _getLatestAnswerFromOracle(ethXOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 calcdEthXUsdPrice = latestAnswerEthXEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdEthXUsdPrice / 2 / 1e18; - - uint256 trovesCount = contractsArray[3].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 0); - - vm.startPrank(A); - contractsArray[3].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - trovesCount = contractsArray[3].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - } - - function testOpenTroveOSETH() public { - uint256 latestAnswerOsEthEth = _getLatestAnswerFromOracle(osEthOracle); - uint256 latestAnswerEthUsd = _getLatestAnswerFromOracle(ethOracle); - - uint256 calcdOsEthUsdPrice = latestAnswerOsEthEth * latestAnswerEthUsd / 1e18; - - uint256 coll = 5 ether; - uint256 debtRequest = coll * calcdOsEthUsdPrice / 2 / 1e18; - - uint256 trovesCount = contractsArray[4].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 0); - - vm.startPrank(A); - contractsArray[4].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) - ); - - trovesCount = contractsArray[4].troveManager.getTroveIdsCount(); - assertEq(trovesCount, 1); - } - function testOpenTroveWSTETH() public { uint256 latestAnswerStethUsd = _getLatestAnswerFromOracle(stethOracle); uint256 wstethStethExchangeRate = wstETH.tokensPerStEth(); diff --git a/contracts/src/test/TestContracts/Deployment.t.sol b/contracts/src/test/TestContracts/Deployment.t.sol index 60a172517..8d28e05d3 100644 --- a/contracts/src/test/TestContracts/Deployment.t.sol +++ b/contracts/src/test/TestContracts/Deployment.t.sol @@ -43,8 +43,6 @@ import {ERC20Faucet} from "./ERC20Faucet.sol"; import "../../PriceFeeds/WETHPriceFeed.sol"; import "../../PriceFeeds/WSTETHPriceFeed.sol"; import "../../PriceFeeds/RETHPriceFeed.sol"; -import "../../PriceFeeds/OSETHPriceFeed.sol"; -import "../../PriceFeeds/ETHXPriceFeed.sol"; import "forge-std/console2.sol"; @@ -186,20 +184,14 @@ contract TestDeployer is MetadataDeployment { address ETHOracle; address STETHOracle; address RETHOracle; - address ETHXOracle; - address OSETHOracle; address WSTETHToken; address RETHToken; - address StaderOracle; // "StaderOracle" is the ETHX contract that manages the canonical exchange rate. Not a market pricacle. - address OsTokenVaultController; } struct OracleParams { uint256 ethUsdStalenessThreshold; uint256 stEthUsdStalenessThreshold; uint256 rEthEthStalenessThreshold; - uint256 ethXEthStalenessThreshold; - uint256 osEthEthStalenessThreshold; } // See: https://solidity-by-example.org/app/create2/ @@ -512,23 +504,16 @@ contract TestDeployer is MetadataDeployment { result.externalAddresses.ETHOracle = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; result.externalAddresses.RETHOracle = 0x536218f9E9Eb48863970252233c8F271f554C2d0; result.externalAddresses.STETHOracle = 0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8; - result.externalAddresses.ETHXOracle = 0xC5f8c4aB091Be1A899214c0C3636ca33DcA0C547; result.externalAddresses.WSTETHToken = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; - // Redstone Oracle with CL interface - // TODO: obtain the Chainlink market price feed and use that, when it's ready - result.externalAddresses.OSETHOracle = 0x66ac817f997Efd114EDFcccdce99F3268557B32C; result.externalAddresses.RETHToken = 0xae78736Cd615f374D3085123A210448E74Fc6393; - result.externalAddresses.StaderOracle = 0xF64bAe65f6f2a5277571143A24FaaFDFC0C2a737; - result.externalAddresses.OsTokenVaultController = 0x2A261e60FB14586B474C208b1B7AC6D0f5000306; vars.oracleParams.ethUsdStalenessThreshold = _24_HOURS; vars.oracleParams.stEthUsdStalenessThreshold = _24_HOURS; vars.oracleParams.rEthEthStalenessThreshold = _48_HOURS; - vars.oracleParams.ethXEthStalenessThreshold = _48_HOURS; - vars.oracleParams.osEthEthStalenessThreshold = _48_HOURS; - vars.numCollaterals = 5; + // Colls: WETH, WSTETH, RETH + vars.numCollaterals = 3; result.contractsArray = new LiquityContracts[](vars.numCollaterals); result.zappersArray = new Zappers[](vars.numCollaterals); vars.priceFeeds = new IPriceFeed[](vars.numCollaterals); @@ -555,30 +540,12 @@ contract TestDeployer is MetadataDeployment { // wstETH vars.priceFeeds[2] = new WSTETHPriceFeed( - address(this), - result.externalAddresses.STETHOracle, - vars.oracleParams.stEthUsdStalenessThreshold, - result.externalAddresses.WSTETHToken - ); - - // ETHx - vars.priceFeeds[3] = new ETHXPriceFeed( address(this), result.externalAddresses.ETHOracle, - result.externalAddresses.ETHXOracle, - result.externalAddresses.StaderOracle, - vars.oracleParams.ethUsdStalenessThreshold, - vars.oracleParams.ethXEthStalenessThreshold - ); - - // osETH - vars.priceFeeds[4] = new OSETHPriceFeed( - address(this), - result.externalAddresses.ETHOracle, - result.externalAddresses.OSETHOracle, - result.externalAddresses.OsTokenVaultController, + result.externalAddresses.STETHOracle, + result.externalAddresses.WSTETHToken, vars.oracleParams.ethUsdStalenessThreshold, - vars.oracleParams.osEthEthStalenessThreshold + vars.oracleParams.stEthUsdStalenessThreshold ); // Deploy Bold @@ -606,18 +573,6 @@ contract TestDeployer is MetadataDeployment { _deployAddressesRegistryMainnet(_troveManagerParamsArray[2]); vars.troveManagers[2] = ITroveManager(troveManagerAddress); - // ETHX - vars.collaterals[3] = IERC20Metadata(0xA35b1B31Ce002FBF2058D22F30f95D405200A15b); - (vars.addressesRegistries[3], troveManagerAddress) = - _deployAddressesRegistryMainnet(_troveManagerParamsArray[3]); - vars.troveManagers[3] = ITroveManager(troveManagerAddress); - - // OSETH - vars.collaterals[4] = IERC20Metadata(0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38); - (vars.addressesRegistries[4], troveManagerAddress) = - _deployAddressesRegistryMainnet(_troveManagerParamsArray[4]); - vars.troveManagers[4] = ITroveManager(troveManagerAddress); - // Deploy registry and register the TMs result.collateralRegistry = new CollateralRegistry(result.boldToken, vars.collaterals, vars.troveManagers); diff --git a/contracts/src/test/zapperLeverage.t.sol b/contracts/src/test/zapperLeverage.t.sol index 863bbe397..3358b5d83 100644 --- a/contracts/src/test/zapperLeverage.t.sol +++ b/contracts/src/test/zapperLeverage.t.sol @@ -37,7 +37,7 @@ contract ZapperLeverageMainnet is DevTestSetup { uint24 constant UNIV3_FEE_USDC_WETH = 500; // 0.05% uint24 constant UNIV3_FEE_WETH_COLL = 100; // 0.01% - uint256 constant NUM_COLLATERALS = 5; + uint256 constant NUM_COLLATERALS = 3; IZapper[] baseZapperArray; ILeverageZapper[] leverageZapperCurveArray; From 63b4db5e0f488488c973a8255168c01af0052dea Mon Sep 17 00:00:00 2001 From: RickGriff Date: Fri, 30 Aug 2024 00:18:09 +0100 Subject: [PATCH 02/19] Add tests for oracle shutdown logic --- contracts/src/PriceFeeds/WETHPriceFeed.sol | 6 +- contracts/src/test/OracleMainnet.t.sol | 344 ++++++++++++++++++++- 2 files changed, 343 insertions(+), 7 deletions(-) diff --git a/contracts/src/PriceFeeds/WETHPriceFeed.sol b/contracts/src/PriceFeeds/WETHPriceFeed.sol index efcae1143..6442b30a9 100644 --- a/contracts/src/PriceFeeds/WETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WETHPriceFeed.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import "./MainnetPriceFeedBase.sol"; -// import "forge-std/console2.sol"; +import "forge-std/console2.sol"; contract WETHPriceFeed is MainnetPriceFeedBase { constructor( @@ -26,7 +26,8 @@ contract WETHPriceFeed is MainnetPriceFeedBase { function fetchPrice() public returns (uint256, bool) { // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) {return _fetchPrice();} + if (priceSource == PriceSource.primary) { + return _fetchPrice();} // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it if (priceSource == PriceSource.lastGoodPrice) {return (lastGoodPrice, false);} @@ -38,7 +39,6 @@ contract WETHPriceFeed is MainnetPriceFeedBase { // If the ETH-USD Chainlink response was invalid in this transaction, return the last good ETH-USD price calculated if (ethUsdOracleDown) {return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true);} - lastGoodPrice = ethUsdPrice; return (ethUsdPrice, false); diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index cde9be16f..480b6c90d 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -491,12 +491,348 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // // Try to adjust Trove - // vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); - // contractsArray[1].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); + vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); + contractsArray[1].borrowerOperations.adjustTrove(troveId, 0, false, 1 wei, true, 1e18); + } + + // --- WETH shutdown --- + + function testWETHPriceFeedShutsDownWhenETHUSDOracleFails() public { + // Fetch price + (uint256 price, bool ethUsdFailed) = wethPriceFeed.fetchPrice(); + assertGt(price, 0); + + // Check oracle call didn't fail + assertFalse(ethUsdFailed); + + // Check branch is live, not shut down + assertEq(contractsArray[0].troveManager.shutdownTime(), 0); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (, ethUsdFailed) = wethPriceFeed.fetchPrice(); + + // Check oracle call failed this time + assertTrue(ethUsdFailed); + + // Confirm the branch is now shutdown + assertEq(contractsArray[0].troveManager.shutdownTime(), block.timestamp); + } + + function testWETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { + // Fetch price + wethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = wethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + assertGt(mockPrice, 0, "mockPrice 0"); + // Confirm the lastGoodPrice is not coincidentally equal to the mock oracle's price + assertNotEq(lastGoodPrice1, uint256(mockPrice)); + + // Fetch price again + (uint256 price, bool ethUsdFailed) = wethPriceFeed.fetchPrice(); + + // Check oracle call failed this time + assertTrue(ethUsdFailed); + + // Confirm the PriceFeed's returned price equals the lastGoodPrice + assertEq(price, lastGoodPrice1, "current price != lastGoodPrice"); + + // Confirm the stored lastGoodPrice has not changed + assertEq(wethPriceFeed.lastGoodPrice(), lastGoodPrice1, "lastGoodPrice not same"); + } + + // --- RETH shutdown --- + + function testRETHPriceFeedShutsDownWhenETHUSDOracleFails() public { + // Fetch price + (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + assertGt(price, 0); + + // Check oracle call didn't fail + assertFalse(anOracleFailed); + + // Check branch is live, not shut down + assertEq(contractsArray[1].troveManager.shutdownTime(), 0); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (, anOracleFailed) = rethPriceFeed.fetchPrice(); + + // Check an oracle call failed this time + assertTrue(anOracleFailed); + + // Confirm the branch is now shutdown + assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); + } + + function testRETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { + // Fetch price + rethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + assertGt(mockPrice, 0, "mockPrice 0"); + + // Fetch price again + (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + + // Check an oracle call failed this time + assertTrue(anOracleFailed); + + // Confirm the PriceFeed's returned price equals the lastGoodPrice + assertEq(price, lastGoodPrice1); + + // Confirm the stored lastGoodPrice has not changed + assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); + } + + function testRETHPriceFeedShutsDownWhenRETHETHOracleFails() public { + // Fetch price + (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + assertGt(price, 0); + + // Check oracle call didn't fail + assertFalse(anOracleFailed); + + // Check branch is live, not shut down + assertEq(contractsArray[1].troveManager.shutdownTime(), 0); + + // Make the RETH-ETH oracle stale + vm.etch(address(rethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (, anOracleFailed) = rethPriceFeed.fetchPrice(); + + // Check an oracle call failed this time + assertTrue(anOracleFailed); + + // Confirm the branch is now shutdown + assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); + } + + function testRETHPriceFeedShutsDownWhenBothOraclesFail() public { + // Fetch price + (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + assertGt(price, 0); + + // Check oracle call didn't fail + assertFalse(anOracleFailed); + + // Check branch is live, not shut down + assertEq(contractsArray[1].troveManager.shutdownTime(), 0); + + // Make the RETH-ETH oracle stale + vm.etch(address(rethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Make the ETH-USD oracle stale too + vm.etch(address(ethOracle), address(mockOracle).code); + (,,,updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (, anOracleFailed) = rethPriceFeed.fetchPrice(); + + // Check an oracle call failed this time + assertTrue(anOracleFailed); + + // Confirm the branch is now shutdown + assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); + } + + function testRETHPriceFeedReturnsLastGoodPriceWhenBothOraclesFail() public { + // Fetch price + rethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Make the RETH-ETH oracle stale too + vm.etch(address(rethOracle), address(mockOracle).code); + (,mockPrice,,updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + + // Check an oracle call failed this time + assertTrue(anOracleFailed); + + // Confirm the PriceFeed's returned price equals the lastGoodPrice + assertEq(price, lastGoodPrice1); + + // Confirm the stored lastGoodPrice has not changed + assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); + } + + // --- WSTETH shutdown --- + + function testWSTETHPriceFeedReturnsPrimaryPriceWhenETHUSDOracleFails() public { + // Fetch price + (uint256 price1, ) = wstethPriceFeed.fetchPrice(); + assertGt(price1, 0, "price is 0"); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + assertGt(mockPrice, 0, "mockPrice 0"); + + // Fetch price again + (uint256 price2, bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + assertFalse(anOracleFailed); + + // Confirm the PriceFeed's returned price equals the previous price + assertEq(price2, price1); + } + + function testWSTETHPriceDoesNotShutdownWhenETHUSDOracleFails() public { + // Fetch price + ( , bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + assertFalse(anOracleFailed); + + // Check branch is live, not shut down + assertEq(contractsArray[2].troveManager.shutdownTime(), 0); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (, anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check that again primary calc oracle did not fail + assertFalse(anOracleFailed); + + // Confirm branch is still live, not shut down + assertEq(contractsArray[2].troveManager.shutdownTime(), 0); + } + + function testWSTETHPriceShutdownWhenSTETHUSDOracleFails() public { + // Fetch price + ( , bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + assertFalse(anOracleFailed); + + // Check branch is live, not shut down + assertEq(contractsArray[2].troveManager.shutdownTime(), 0); + + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (, anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check that this time the primary calc oracle did fail + assertTrue(anOracleFailed); + + // Confirm branch is now shut down + assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); + } + + function testWSTETHPriceShutdownWhenBothOraclesFail() public { + // Fetch price + ( , bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + assertFalse(anOracleFailed); + + // Check branch is live, not shut down + assertEq(contractsArray[2].troveManager.shutdownTime(), 0); + + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,int256 mockPrice,,uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Make the ETH-USD oracle stale too + vm.etch(address(ethOracle), address(mockOracle).code); + (,mockPrice,,updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (, anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check that this time the primary calc oracle did fail + assertTrue(anOracleFailed); + + // Confirm branch is now shut down + assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); + } + + function testWSTETHPriceFeedReturnsLastGoodPriceWhenBothOraclesFail() public { + // Fetch price + wstethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,int256 mockPrice,,uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Make the ETH-USD oracle stale too + vm.etch(address(ethOracle), address(mockOracle).code); + (,mockPrice,,updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (uint256 price, bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check an oracle call failed this time + assertTrue(anOracleFailed); + + // Confirm the PriceFeed's returned price equals the lastGoodPrice + assertEq(price, lastGoodPrice1); + + // Confirm the stored lastGoodPrice has not changed + assertEq(wstethPriceFeed.lastGoodPrice(), lastGoodPrice1); } // TODO: + + // RETHPriceFeed: + // - Switches to ETHUSDxCanonical when RETH-ETH failed + // - When using ETHUSDxCanonical, switches to lastGoodPrice when ETHUSD fails + // - When using ETHUSDxCanonical, remains shutdown when ETHUSD failed + + // WSTETHPriceFeed: + // - switches to ETHUSDxCanonical when WSTETH-STETH failed + // - When using ETHUSDxCanonical, switches to lastGoodPrice when ETHUSD fails + // - When using ETHUSDxCanonical, remains shutdown when ETHUSD failed + // - More basic actions tests (adjust, close, etc) - // - liq tests (manipulate aggregator stored price) - // - conditional shutdown logic tests (manipulate aggregator stored price) + // - liq tests (manipulate aggregator stored price } From 53a33825065db3d03609c9ffd3a2c5bda41f4b35 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Fri, 30 Aug 2024 00:18:33 +0100 Subject: [PATCH 03/19] Remove console.log --- contracts/src/PriceFeeds/WETHPriceFeed.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/src/PriceFeeds/WETHPriceFeed.sol b/contracts/src/PriceFeeds/WETHPriceFeed.sol index 6442b30a9..155d03ada 100644 --- a/contracts/src/PriceFeeds/WETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WETHPriceFeed.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.24; import "./MainnetPriceFeedBase.sol"; -import "forge-std/console2.sol"; +// import "forge-std/console2.sol"; contract WETHPriceFeed is MainnetPriceFeedBase { constructor( From 52756abec30e135ed00a97c689d646a550cffb30 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Fri, 30 Aug 2024 00:50:33 +0100 Subject: [PATCH 04/19] Fix test --- contracts/src/test/OracleMainnet.t.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index 480b6c90d..6437c8df7 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -476,8 +476,7 @@ contract OraclesMainnet is TestAccounts { uint256 debtRequest = coll * calcdRethUsdPrice / 2 / 1e18; vm.startPrank(A); - /* uint256 troveId = */ - contractsArray[1].borrowerOperations.openTrove( + uint256 troveId = contractsArray[1].borrowerOperations.openTrove( A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) ); From b1fb3d1041eb4490956d6d655f7f074e41c86405 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Wed, 4 Sep 2024 10:18:30 +0100 Subject: [PATCH 05/19] Add tests --- contracts/src/test/OracleMainnet.t.sol | 76 +++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index 6437c8df7..e3da43504 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -13,6 +13,8 @@ import "../Interfaces/IWSTETHPriceFeed.sol"; import "../Interfaces/IRETHToken.sol"; import "../Interfaces/IWSTETH.sol"; +import "../MainnetPriceFeedBase.sol"; + import "forge-std/Test.sol"; import "forge-std/console2.sol"; @@ -628,6 +630,29 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); } + function testFetchPriceReturnsETHUSDxCanonicalWhenRETHETHOracleFails() public { + // Make the RETH-ETH oracle stale + vm.etch(address(rethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price + (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + assertGt(price, 0); + + // Check that the primary calc oracle did fail + assertTrue(anOracleFailed); + + // Calc expected price i.e. ETH-USD x canonical + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 exchangeRate = rETHToken.getExchangeRate(); + assertGt(ethUsdPrice, 0); + assertGt(exchangeRate, 0); + uint256 expectedPrice = ethUsdPrice * exchangeRate / 1e18; + + assertEq(price, expectedPrice); + } + function testRETHPriceFeedShutsDownWhenBothOraclesFail() public { // Fetch price (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); @@ -761,7 +786,54 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); } - function testWSTETHPriceShutdownWhenBothOraclesFail() public { + function testFetchPriceReturnsETHUSDxCanonicalWhenSTETHUSDOracleFails() public { + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price + (uint256 price, bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check that the primary calc oracle did fail + assertTrue(anOracleFailed); + + // Calc expected price i.e. ETH-USD x canonical + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 exchangeRate = wstETH.stEthPerToken(); + assertGt(ethUsdPrice, 0); + assertGt(exchangeRate, 0); + uint256 expectedPrice = ethUsdPrice * exchangeRate / 1e18; + + assertEq(price, expectedPrice); + } + + function testWhenUsingETHUSDxCanonicalSwitchesToLastGoodPRiceWhenETHUSDOracleFails() public { + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Check using primary + assertEq(MainnetPriceFeedBase.PriceSource.primary); + + // Fetch price + (uint256 price, bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + + // Check that the primary calc oracle did fail + assertTrue(anOracleFailed); + + // Calc expected price i.e. ETH-USD x canonical + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 exchangeRate = wstETH.stEthPerToken(); + assertGt(ethUsdPrice, 0); + assertGt(exchangeRate, 0); + uint256 expectedPrice = ethUsdPrice * exchangeRate / 1e18; + + assertEq(price, expectedPrice); + } + + function testWSTETHPriceShutdownWhenBothOraclesFail() public { // Fetch price ( , bool anOracleFailed) = wstethPriceFeed.fetchPrice(); @@ -823,12 +895,10 @@ contract OraclesMainnet is TestAccounts { // TODO: // RETHPriceFeed: - // - Switches to ETHUSDxCanonical when RETH-ETH failed // - When using ETHUSDxCanonical, switches to lastGoodPrice when ETHUSD fails // - When using ETHUSDxCanonical, remains shutdown when ETHUSD failed // WSTETHPriceFeed: - // - switches to ETHUSDxCanonical when WSTETH-STETH failed // - When using ETHUSDxCanonical, switches to lastGoodPrice when ETHUSD fails // - When using ETHUSDxCanonical, remains shutdown when ETHUSD failed From 5840a0749ba25acd1bb853c0427c6a1e0954965f Mon Sep 17 00:00:00 2001 From: RickGriff Date: Fri, 6 Sep 2024 12:56:13 +0100 Subject: [PATCH 06/19] Add oracle logic fallback price tests --- .../src/Interfaces/IMainnetPriceFeed.sol | 8 +- .../src/PriceFeeds/CompositePriceFeed.sol | 6 +- .../src/PriceFeeds/MainnetPriceFeedBase.sol | 6 - contracts/src/PriceFeeds/RETHPriceFeed.sol | 2 +- contracts/src/test/OracleMainnet.t.sol | 347 +++++++++++++++--- 5 files changed, 307 insertions(+), 62 deletions(-) diff --git a/contracts/src/Interfaces/IMainnetPriceFeed.sol b/contracts/src/Interfaces/IMainnetPriceFeed.sol index 63c3ee3fd..9626870f5 100644 --- a/contracts/src/Interfaces/IMainnetPriceFeed.sol +++ b/contracts/src/Interfaces/IMainnetPriceFeed.sol @@ -3,7 +3,13 @@ import "../Interfaces/IPriceFeed.sol"; import "../Dependencies/AggregatorV3Interface.sol"; pragma solidity ^0.8.0; - interface IMainnetPriceFeed is IPriceFeed { + enum PriceSource { + primary, + ETHUSDxCanonical, + lastGoodPrice + } + function ethUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); + function priceSource() external view returns (PriceSource); } diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol index ca0af272b..65a912332 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -35,6 +35,9 @@ contract CompositePriceFeed is MainnetPriceFeedBase { rateProviderAddress = _rateProviderAddress; } + // Returns: + // - The price, using the current price calculation + // - A bool that is true if a) the system was not shut down prior to this call and b) an oracle failed during this call. function fetchPrice() public returns (uint256, bool) { // If branch is live and the primary oracle setup has been working, try to use it if (priceSource == PriceSource.primary) {return _fetchPrice();} @@ -42,9 +45,10 @@ contract CompositePriceFeed is MainnetPriceFeedBase { // If branch is already shut down and using ETH-USD * canonical_rate, try to use that if (priceSource == PriceSource.ETHUSDxCanonical) { (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - //... but if the ETH-USD oracle *also* fails here, use the lastGoodPrice + //... but if the ETH-USD oracle *also* fails here, switch to using the lastGoodPrice if(ethUsdOracleDown) { // No need to shut down, since branch already is shut down + priceSource = PriceSource.lastGoodPrice; return (lastGoodPrice, false); } else { return (_fetchPriceETHUSDxCanonical(ethUsdPrice), false); diff --git a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol index 144687e44..2b4a0e6e4 100644 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol @@ -17,12 +17,6 @@ abstract contract MainnetPriceFeedBase is IPriceFeed, Ownable { abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { // Dummy flag raised when the collateral branch gets shut down. // Should be removed after actual shutdown logic is implemented. - - enum PriceSource { - primary, - ETHUSDxCanonical, - lastGoodPrice - } PriceSource public priceSource; >>>>>>> 81a1a2aa (Add ETH-USD fallback logic and remove OSETH and ETHX contracts) diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol index 4637a754b..d35c4dc55 100644 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/RETHPriceFeed.sol @@ -39,7 +39,7 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { (uint256 rEthEthPrice, bool rEthEthOracleDown) = _getOracleAnswer(rEthEthOracle); // If the ETH-USD feed is down, shut down and switch to the last good price seen by the system - // since we always need the + // since we need the ETH-USD price for both calcs: 1) ETH-USD x RETH-ETH, and 2) ETH-USD x canonical if (ethUsdOracleDown) {return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true);} // If the ETH-USD feed is live but the RETH-ETH oracle is down, shutdown and substitute RETH-ETH with the canonical rate if (rEthEthOracleDown) {return (_shutDownAndSwitchToETHUSDxCanonical(address(rEthEthOracle.aggregator), ethUsdPrice), true);} diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index e3da43504..9a71e005f 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -13,8 +13,6 @@ import "../Interfaces/IWSTETHPriceFeed.sol"; import "../Interfaces/IRETHToken.sol"; import "../Interfaces/IWSTETH.sol"; -import "../MainnetPriceFeedBase.sol"; - import "forge-std/Test.sol"; import "forge-std/console2.sol"; @@ -555,11 +553,11 @@ contract OraclesMainnet is TestAccounts { function testRETHPriceFeedShutsDownWhenETHUSDOracleFails() public { // Fetch price - (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); assertGt(price, 0); // Check oracle call didn't fail - assertFalse(anOracleFailed); + assertFalse(oracleFailedWhileBranchLive); // Check branch is live, not shut down assertEq(contractsArray[1].troveManager.shutdownTime(), 0); @@ -570,10 +568,10 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (, anOracleFailed) = rethPriceFeed.fetchPrice(); + (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); // Check an oracle call failed this time - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Confirm the branch is now shutdown assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); @@ -592,10 +590,10 @@ contract OraclesMainnet is TestAccounts { assertGt(mockPrice, 0, "mockPrice 0"); // Fetch price again - (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); // Check an oracle call failed this time - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Confirm the PriceFeed's returned price equals the lastGoodPrice assertEq(price, lastGoodPrice1); @@ -604,13 +602,34 @@ contract OraclesMainnet is TestAccounts { assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); } + function testRETHPriceSourceIsLastGoodPriceWhenETHUSDFails() public { + // Fetch price + rethPriceFeed.fetchPrice(); + + // Check using primary + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + assertTrue(oracleFailedWhileBranchLive); + + // Check using lastGoodPrice + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); + } + function testRETHPriceFeedShutsDownWhenRETHETHOracleFails() public { // Fetch price - (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); assertGt(price, 0); // Check oracle call didn't fail - assertFalse(anOracleFailed); + assertFalse(oracleFailedWhileBranchLive); // Check branch is live, not shut down assertEq(contractsArray[1].troveManager.shutdownTime(), 0); @@ -621,10 +640,10 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (, anOracleFailed) = rethPriceFeed.fetchPrice(); + (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); // Check an oracle call failed this time - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Confirm the branch is now shutdown assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); @@ -637,11 +656,11 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price - (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); assertGt(price, 0); // Check that the primary calc oracle did fail - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Calc expected price i.e. ETH-USD x canonical uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); @@ -653,13 +672,79 @@ contract OraclesMainnet is TestAccounts { assertEq(price, expectedPrice); } + function testRETHPriceSourceIsETHUSDxCanonicalWhenRETHETHFails() public { + // Fetch price + rethPriceFeed.fetchPrice(); + + // Check using primary + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + // Make the RETH-ETH oracle stale + vm.etch(address(rethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + assertTrue(oracleFailedWhileBranchLive); + + // Check using canonical + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); + } + + function testRETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { + // Make the RETH-USD oracle stale + vm.etch(address(rethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Check using primary + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary"); + + // Fetch price + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + // Check that the primary calc oracle did fail + assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); + + // Check using ETHUSDxCanonical + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), "not using ethusdxcanonical"); + + uint256 lastGoodPrice = rethPriceFeed.lastGoodPrice(); + + // Make the ETH-USD oracle stale too + vm.etch(address(ethOracle), address(mockOracle).code); + (,,, updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Calc expected price if didnt fail, i.e. ETH-USD x canonical + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 exchangeRate = rETHToken.getExchangeRate(); + assertGt(ethUsdPrice, 0); + assertGt(exchangeRate, 0); + uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; + + // These should differ since the mock oracle's price should not equal the previous real price + assertNotEq(priceIfDidntFail, lastGoodPrice, "price if didnt fail == lastGoodPrice"); + + // Now fetch the price + (price, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + // This should be false, since the branch is already shutdown and not live + assertFalse(oracleFailedWhileBranchLive); + + // Confirm the returned price is the last good price + assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); + } + function testRETHPriceFeedShutsDownWhenBothOraclesFail() public { // Fetch price - (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); assertGt(price, 0); // Check oracle call didn't fail - assertFalse(anOracleFailed); + assertFalse(oracleFailedWhileBranchLive); // Check branch is live, not shut down assertEq(contractsArray[1].troveManager.shutdownTime(), 0); @@ -675,10 +760,10 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (, anOracleFailed) = rethPriceFeed.fetchPrice(); + (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); // Check an oracle call failed this time - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Confirm the branch is now shutdown assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); @@ -701,10 +786,10 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (uint256 price, bool anOracleFailed) = rethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); // Check an oracle call failed this time - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Confirm the PriceFeed's returned price equals the lastGoodPrice assertEq(price, lastGoodPrice1); @@ -713,13 +798,42 @@ contract OraclesMainnet is TestAccounts { assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); } + function testRETHPriceSourceIsLastGoodPriceWhenBothOraclesFail() public { + // Fetch price + rethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Check using primary + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Make the RETH-ETH oracle stale too + vm.etch(address(rethOracle), address(mockOracle).code); + (,mockPrice,,updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + // Check using lastGoodPrice + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); + } + // --- WSTETH shutdown --- - function testWSTETHPriceFeedReturnsPrimaryPriceWhenETHUSDOracleFails() public { + function testWSTETHPriceSourceIsPrimaryPriceWhenETHUSDOracleFails() public { // Fetch price (uint256 price1, ) = wstethPriceFeed.fetchPrice(); assertGt(price1, 0, "price is 0"); + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); @@ -727,10 +841,31 @@ contract OraclesMainnet is TestAccounts { assertGt(mockPrice, 0, "mockPrice 0"); // Fetch price again - (uint256 price2, bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + (uint256 price2, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(anOracleFailed); + assertFalse(oracleFailedWhileBranchLive); + + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + } + + function testWSTETHPriceFeedReturnsPrimaryPriceWhenETHUSDOracleFails() public { + // Fetch price + (uint256 price1, ) = wstethPriceFeed.fetchPrice(); + assertGt(price1, 0, "price is 0"); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + assertGt(mockPrice, 0, "mockPrice 0"); + + // Fetch price again + (uint256 price2, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + assertFalse(oracleFailedWhileBranchLive); // Confirm the PriceFeed's returned price equals the previous price assertEq(price2, price1); @@ -738,10 +873,10 @@ contract OraclesMainnet is TestAccounts { function testWSTETHPriceDoesNotShutdownWhenETHUSDOracleFails() public { // Fetch price - ( , bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + ( , bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(anOracleFailed); + assertFalse(oracleFailedWhileBranchLive); // Check branch is live, not shut down assertEq(contractsArray[2].troveManager.shutdownTime(), 0); @@ -752,10 +887,10 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (, anOracleFailed) = wstethPriceFeed.fetchPrice(); + (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check that again primary calc oracle did not fail - assertFalse(anOracleFailed); + assertFalse(oracleFailedWhileBranchLive); // Confirm branch is still live, not shut down assertEq(contractsArray[2].troveManager.shutdownTime(), 0); @@ -763,10 +898,10 @@ contract OraclesMainnet is TestAccounts { function testWSTETHPriceShutdownWhenSTETHUSDOracleFails() public { // Fetch price - ( , bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + ( , bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(anOracleFailed); + assertFalse(oracleFailedWhileBranchLive); // Check branch is live, not shut down assertEq(contractsArray[2].troveManager.shutdownTime(), 0); @@ -777,10 +912,10 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (, anOracleFailed) = wstethPriceFeed.fetchPrice(); + (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check that this time the primary calc oracle did fail - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Confirm branch is now shut down assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); @@ -793,10 +928,10 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price - (uint256 price, bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check that the primary calc oracle did fail - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Calc expected price i.e. ETH-USD x canonical uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); @@ -808,37 +943,118 @@ contract OraclesMainnet is TestAccounts { assertEq(price, expectedPrice); } - function testWhenUsingETHUSDxCanonicalSwitchesToLastGoodPRiceWhenETHUSDOracleFails() public { + function testSTETHPriceSourceIsETHUSDxCanonicalWhenSTETHUSDOracleFails() public { + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check that the primary calc oracle did fail + assertTrue(oracleFailedWhileBranchLive); + + // Check using ETHUSDxCanonical + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); + } + + function testSTETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Check using primary - assertEq(MainnetPriceFeedBase.PriceSource.primary); + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); // Fetch price - (uint256 price, bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check that the primary calc oracle did fail - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); - // Calc expected price i.e. ETH-USD x canonical + // Check using ETHUSDxCanonical + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); + + uint256 lastGoodPrice = wstethPriceFeed.lastGoodPrice(); + + // Make the ETH-USD oracle stale too + vm.etch(address(ethOracle), address(mockOracle).code); + (,,, updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Calc expected price if didnt fail, i.e. ETH-USD x canonical uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); uint256 exchangeRate = wstETH.stEthPerToken(); assertGt(ethUsdPrice, 0); assertGt(exchangeRate, 0); - uint256 expectedPrice = ethUsdPrice * exchangeRate / 1e18; + uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; - assertEq(price, expectedPrice); + // These should differ since the mock oracle's price should not equal the previous real price + assertNotEq(priceIfDidntFail, lastGoodPrice, "price if didnt fail == lastGoodPrice"); + + // Now fetch the price + (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check using lastGoodPrice + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); + + // This should be false, since the branch is already shutdown and not live + assertFalse(oracleFailedWhileBranchLive); + + // Confirm the returned price is the last good price + assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); + } + + function testSTETHWhenUsingETHUSDxCanonicalRemainsShutDownWhenETHUSDOracleFails() public { + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + // Check branch is live, not shut down + assertEq(contractsArray[2].troveManager.shutdownTime(), 0); + + // Fetch price + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check that the primary calc oracle did fail + assertTrue(oracleFailedWhileBranchLive); + + // Check using ETHUSDxCanonical + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); + + // Check branch is now shut down + assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); + + // Make the ETH-USD oracle stale too + vm.etch(address(ethOracle), address(mockOracle).code); + (,,, updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Now fetch the price again + (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check using lastGoodPrice + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); + + // Check branch is still down + assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); } function testWSTETHPriceShutdownWhenBothOraclesFail() public { // Fetch price - ( , bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + ( , bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(anOracleFailed); + assertFalse(oracleFailedWhileBranchLive); // Check branch is live, not shut down assertEq(contractsArray[2].troveManager.shutdownTime(), 0); @@ -854,10 +1070,10 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (, anOracleFailed) = wstethPriceFeed.fetchPrice(); + (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check that this time the primary calc oracle did fail - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Confirm branch is now shut down assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); @@ -880,10 +1096,10 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (uint256 price, bool anOracleFailed) = wstethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check an oracle call failed this time - assertTrue(anOracleFailed); + assertTrue(oracleFailedWhileBranchLive); // Confirm the PriceFeed's returned price equals the lastGoodPrice assertEq(price, lastGoodPrice1); @@ -892,16 +1108,41 @@ contract OraclesMainnet is TestAccounts { assertEq(wstethPriceFeed.lastGoodPrice(), lastGoodPrice1); } - // TODO: + function testWSTETHPriceSourceIsLastGoodPriceWhenBothOraclesFail() public { + // Fetch price + wstethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - // RETHPriceFeed: - // - When using ETHUSDxCanonical, switches to lastGoodPrice when ETHUSD fails - // - When using ETHUSDxCanonical, remains shutdown when ETHUSD failed + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - // WSTETHPriceFeed: - // - When using ETHUSDxCanonical, switches to lastGoodPrice when ETHUSD fails - // - When using ETHUSDxCanonical, remains shutdown when ETHUSD failed + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,int256 mockPrice,,uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + // Make the ETH-USD oracle stale too + vm.etch(address(ethOracle), address(mockOracle).code); + (,mockPrice,,updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check using lastGoodPrice + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); + } + + // TODO: + + // WETHPriceFeed: + // - Source when ETH-USD failed: lastGoodPrice + + // RETHPriceFeed: + // - Source when RETH-ETH failed: ETHUSDxCanonical + // - Source when ETH-USD failed: lastGoodPrice + // - More basic actions tests (adjust, close, etc) // - liq tests (manipulate aggregator stored price } From 7058884c9ffea8a273688934a803b66507440aa2 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 17 Sep 2024 10:52:03 +0100 Subject: [PATCH 07/19] Add additional price fallback logic --- contracts/src/BorrowerOperations.sol | 5 +- .../src/Interfaces/IBorrowerOperations.sol | 2 +- .../src/Interfaces/IMainnetPriceFeed.sol | 15 +- contracts/src/Interfaces/IRETHPriceFeed.sol | 2 +- .../src/PriceFeeds/CompositePriceFeed.sol | 75 +-- .../src/PriceFeeds/MainnetPriceFeedBase.sol | 22 +- contracts/src/PriceFeeds/RETHPriceFeed.sol | 58 ++- contracts/src/PriceFeeds/WETHPriceFeed.sol | 30 +- contracts/src/PriceFeeds/WSTETHPriceFeed.sol | 47 +- contracts/src/TroveManager.sol | 2 +- contracts/src/test/OracleMainnet.t.sol | 460 +++++++++++++----- .../src/test/TestContracts/RETHTokenMock.sol | 12 + .../test/TestContracts/WSTETHTokenMock.sol | 15 + 13 files changed, 507 insertions(+), 238 deletions(-) create mode 100644 contracts/src/test/TestContracts/RETHTokenMock.sol create mode 100644 contracts/src/test/TestContracts/WSTETHTokenMock.sol diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index d6fe301d0..41bc7330b 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -157,7 +157,6 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio event BoldTokenAddressChanged(address _boldTokenAddress); event ShutDown(uint256 _tcr); - event ShutDownFromOracleFailure(address _oracleAddress); constructor(IAddressesRegistry _addressesRegistry) AddRemoveManagers(_addressesRegistry) @@ -1174,7 +1173,7 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio } // Not technically a "Borrower op", but seems best placed here given current shutdown logic. - function shutdownFromOracleFailure(address _failedOracleAddr) external { + function shutdownFromOracleFailure() external { _requireCallerIsPriceFeed(); // No-op rather than revert here, so that the outer function call which fetches the price does not revert @@ -1182,8 +1181,6 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio if (hasBeenShutDown) return; _applyShutdown(); - - emit ShutDownFromOracleFailure(_failedOracleAddr); } function _applyShutdown() internal { diff --git a/contracts/src/Interfaces/IBorrowerOperations.sol b/contracts/src/Interfaces/IBorrowerOperations.sol index 96df29199..ffb134904 100644 --- a/contracts/src/Interfaces/IBorrowerOperations.sol +++ b/contracts/src/Interfaces/IBorrowerOperations.sol @@ -94,7 +94,7 @@ interface IBorrowerOperations is ILiquityBase, IAddRemoveManagers { function hasBeenShutDown() external view returns (bool); function shutdown() external; - function shutdownFromOracleFailure(address _failedOracleAddr) external; + function shutdownFromOracleFailure() external; function checkBatchManagerExists(address _batchMananger) external view returns (bool); diff --git a/contracts/src/Interfaces/IMainnetPriceFeed.sol b/contracts/src/Interfaces/IMainnetPriceFeed.sol index 9626870f5..ee8580236 100644 --- a/contracts/src/Interfaces/IMainnetPriceFeed.sol +++ b/contracts/src/Interfaces/IMainnetPriceFeed.sol @@ -3,13 +3,14 @@ import "../Interfaces/IPriceFeed.sol"; import "../Dependencies/AggregatorV3Interface.sol"; pragma solidity ^0.8.0; + interface IMainnetPriceFeed is IPriceFeed { - enum PriceSource { - primary, - ETHUSDxCanonical, - lastGoodPrice - } + enum PriceSource { + primary, + ETHUSDxCanonical, + lastGoodPrice + } - function ethUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); - function priceSource() external view returns (PriceSource); + function ethUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8); + function priceSource() external view returns (PriceSource); } diff --git a/contracts/src/Interfaces/IRETHPriceFeed.sol b/contracts/src/Interfaces/IRETHPriceFeed.sol index 04e77f754..862bf6874 100644 --- a/contracts/src/Interfaces/IRETHPriceFeed.sol +++ b/contracts/src/Interfaces/IRETHPriceFeed.sol @@ -6,4 +6,4 @@ pragma solidity ^0.8.0; interface IRETHPriceFeed is IMainnetPriceFeed { function rEthEthOracle() external view returns (AggregatorV3Interface, uint256, uint8); -} \ No newline at end of file +} diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol index 65a912332..2a52ac50b 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -15,7 +15,7 @@ contract CompositePriceFeed is MainnetPriceFeedBase, ICompositePriceFeed { ======= // import "forge-std/console2.sol"; -// The CompositePriceFeed is used for feeds that incorporate both a market price oracle (e.g. STETH-USD, or RETH-ETH) +// The CompositePriceFeed is used for feeds that incorporate both a market price oracle (e.g. STETH-USD, or RETH-ETH) // and an LST canonical rate (e.g. WSTETH:STETH, or RETH:ETH). contract CompositePriceFeed is MainnetPriceFeedBase { >>>>>>> 81a1a2aa (Add ETH-USD fallback logic and remove OSETH and ETHX contracts) @@ -26,63 +26,76 @@ contract CompositePriceFeed is MainnetPriceFeedBase { address _ethUsdOracleAddress, address _rateProviderAddress, uint256 _ethUsdStalenessThreshold - ) MainnetPriceFeedBase( - _owner, - _ethUsdOracleAddress, - _ethUsdStalenessThreshold - ) { + ) MainnetPriceFeedBase(_owner, _ethUsdOracleAddress, _ethUsdStalenessThreshold) { // Store rate provider rateProviderAddress = _rateProviderAddress; } - + // Returns: // - The price, using the current price calculation - // - A bool that is true if a) the system was not shut down prior to this call and b) an oracle failed during this call. + // - A bool that is true if: + // --- a) the system was not shut down prior to this call, and + // --- b) an oracle or exchange rate contract failed during this call. function fetchPrice() public returns (uint256, bool) { - // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) {return _fetchPrice();} + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); // If branch is already shut down and using ETH-USD * canonical_rate, try to use that if (priceSource == PriceSource.ETHUSDxCanonical) { - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - //... but if the ETH-USD oracle *also* fails here, switch to using the lastGoodPrice - if(ethUsdOracleDown) { - // No need to shut down, since branch already is shut down - priceSource = PriceSource.lastGoodPrice; - return (lastGoodPrice, false); - } else { - return (_fetchPriceETHUSDxCanonical(ethUsdPrice), false); - } + (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); + //... but if the ETH-USD oracle *also* fails here, switch to using the lastGoodPrice + if (ethUsdOracleDown) { + // No need to shut down, since branch already is shut down + priceSource = PriceSource.lastGoodPrice; + return (lastGoodPrice, false); + } else { + return (_fetchPriceETHUSDxCanonical(ethUsdPrice), false); } + } // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it - if (priceSource == PriceSource.lastGoodPrice) {return (lastGoodPrice, false);} + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); } - function _shutDownAndSwitchToETHUSDxCanonical(address _failedOracleAddr, uint256 _ethUsdPrice) internal returns (uint256) { + function _shutDownAndSwitchToETHUSDxCanonical(address _failedOracleAddr, uint256 _ethUsdPrice) + internal + returns (uint256) + { // Shut down the branch - borrowerOperations.shutdownFromOracleFailure(_failedOracleAddr); + borrowerOperations.shutdownFromOracleFailure(); priceSource = PriceSource.ETHUSDxCanonical; + + emit ShutDownFromOracleFailure(_failedOracleAddr); return _fetchPriceETHUSDxCanonical(_ethUsdPrice); } - // Only called if the primary LST oracle has failed, branch has shut down, + // Only called if the primary LST oracle has failed, branch has shut down, // and we've switched to using: ETH-USD * canonical_rate. function _fetchPriceETHUSDxCanonical(uint256 _ethUsdPrice) internal returns (uint256) { assert(priceSource == PriceSource.ETHUSDxCanonical); - // Get the ETH_per_LST canonical rate directly from the LST contract + // Get the underlying_per_LST canonical rate directly from the LST contract // TODO: Should we also shutdown if the call to the canonical rate reverts, or returns 0? - uint256 lstEthRate = _getCanonicalRate(); + (uint256 lstRate, bool exchangeRateIsDown) = _getCanonicalRate(); + + // If the exchange rate contract is down, switch to (and return) lastGoodPrice. + if (exchangeRateIsDown) { + priceSource = PriceSource.lastGoodPrice; + return lastGoodPrice; + } + + // Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * underlying_per_LST + uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstRate / 1e18; - // Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST - uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstEthRate / 1e18; + uint256 bestPrice = LiquityMath._min(lstUsdCanonicalPrice, lastGoodPrice); - lastGoodPrice = lstUsdCanonicalPrice; + lastGoodPrice = bestPrice; - return (lstUsdCanonicalPrice); + return bestPrice; } - // Returns the LST exchange rate from the LST smart contract. Implementation depends on the specific LST. - function _getCanonicalRate() internal view virtual returns (uint256) {} + // Returns the LST exchange rate and a bool indicating whether the exchange rate failed to return a valid rate. + // Implementation depends on the specific LST. + function _getCanonicalRate() internal view virtual returns (uint256, bool) {} } diff --git a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol index 2b4a0e6e4..cec2df73a 100644 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol @@ -37,17 +37,13 @@ abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { bool success; } + event ShutDownFromOracleFailure(address _failedOracleAddr); + Oracle public ethUsdOracle; IBorrowerOperations borrowerOperations; - constructor( - address _owner, - address _ethUsdOracleAddress, - uint256 _ethUsdStalenessThreshold - ) - Ownable(_owner) - { + constructor(address _owner, address _ethUsdOracleAddress, uint256 _ethUsdStalenessThreshold) Ownable(_owner) { // Store ETH-USD oracle ethUsdOracle.aggregator = AggregatorV3Interface(_ethUsdOracleAddress); ethUsdOracle.stalenessThreshold = _ethUsdStalenessThreshold; @@ -63,10 +59,10 @@ abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { _renounceOwnership(); } - // An individual Pricefeed instance implements _fetchPrice according to the data sources it uses. Returns: + // An individual Pricefeed instance implements _fetchPricePrimary according to the data sources it uses. Returns: // - The price - // - A bool indicating whether a new oracle failure was detected in the call - function _fetchPrice() internal virtual returns (uint256, bool) {} + // - A bool indicating whether a new oracle failure or exchange rate failure was detected in the call + function _fetchPricePrimary() internal virtual returns (uint256, bool) {} function _getOracleAnswer(Oracle memory _oracle) internal view returns (uint256, bool) { ChainlinkResponse memory chainlinkResponse = _getCurrentChainlinkResponse(_oracle.aggregator); @@ -85,9 +81,11 @@ abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { function _shutDownAndSwitchToLastGoodPrice(address _failedOracleAddr) internal returns (uint256) { // Shut down the branch - borrowerOperations.shutdownFromOracleFailure(_failedOracleAddr); - + borrowerOperations.shutdownFromOracleFailure(); + priceSource = PriceSource.lastGoodPrice; + + emit ShutDownFromOracleFailure(_failedOracleAddr); return lastGoodPrice; } diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol index d35c4dc55..18f5f325e 100644 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/RETHPriceFeed.sol @@ -6,7 +6,7 @@ import "./CompositePriceFeed.sol"; import "../Interfaces/IRETHToken.sol"; import "../Interfaces/IRETHPriceFeed.sol"; -// import "forge-std/console2.sol"; +import "forge-std/console2.sol"; contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { constructor( @@ -16,53 +16,67 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { address _rEthTokenAddress, uint256 _ethUsdStalenessThreshold, uint256 _rEthEthStalenessThreshold - ) - CompositePriceFeed( - _owner, - _ethUsdOracleAddress, - _rEthTokenAddress, - _ethUsdStalenessThreshold - ) - { + ) CompositePriceFeed(_owner, _ethUsdOracleAddress, _rEthTokenAddress, _ethUsdStalenessThreshold) { // Store RETH-ETH oracle rEthEthOracle.aggregator = AggregatorV3Interface(_rEthEthOracleAddress); rEthEthOracle.stalenessThreshold = _rEthEthStalenessThreshold; rEthEthOracle.decimals = rEthEthOracle.aggregator.decimals(); + _fetchPricePrimary(); + + // Check the oracle didn't already fail + assert(priceSource == PriceSource.primary); } - + Oracle public rEthEthOracle; - function _fetchPrice() internal override returns (uint256, bool) { + function _fetchPricePrimary() internal override returns (uint256, bool) { assert(priceSource == PriceSource.primary); (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); (uint256 rEthEthPrice, bool rEthEthOracleDown) = _getOracleAnswer(rEthEthOracle); + (uint256 rEthPerEth, bool exchangeRateIsDown) = _getCanonicalRate(); + + console2.log(rEthPerEth, "rEthPerEth"); + console2.log(exchangeRateIsDown, "exchangeRateIsDown"); + console2.log(exchangeRateIsDown, "exchangeRateIsDown"); // If the ETH-USD feed is down, shut down and switch to the last good price seen by the system - // since we need the ETH-USD price for both calcs: 1) ETH-USD x RETH-ETH, and 2) ETH-USD x canonical - if (ethUsdOracleDown) {return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true);} + // since we need both ETH-USD and canonical for primary and fallback price calcs + if (ethUsdOracleDown || exchangeRateIsDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); + } // If the ETH-USD feed is live but the RETH-ETH oracle is down, shutdown and substitute RETH-ETH with the canonical rate - if (rEthEthOracleDown) {return (_shutDownAndSwitchToETHUSDxCanonical(address(rEthEthOracle.aggregator), ethUsdPrice), true);} + if (rEthEthOracleDown) { + return (_shutDownAndSwitchToETHUSDxCanonical(address(rEthEthOracle.aggregator), ethUsdPrice), true); + } // Otherwise, use the primary price calculation: - // Calculate the market LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST + // Calculate the market LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH uint256 lstUsdMarketPrice = ethUsdPrice * rEthEthPrice / 1e18; - // Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST + // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH // TODO: Should we also shutdown if the call to the canonical rate reverts, or returns 0? - uint256 lstUsdCanonicalPrice = ethUsdPrice * _getCanonicalRate() / 1e18; + uint256 lstUsdCanonicalPrice = ethUsdPrice * rEthPerEth / 1e18; // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. - // NOTE: only needed + // NOTE: only needed uint256 lstUsdPrice = LiquityMath._min(lstUsdMarketPrice, lstUsdCanonicalPrice); lastGoodPrice = lstUsdPrice; return (lstUsdPrice, false); } - function _getCanonicalRate() internal view override returns (uint256) { - // RETHToken returns exchange rate with 18 digit decimal precision - return IRETHToken(rateProviderAddress).getExchangeRate(); - } + + function _getCanonicalRate() internal view override returns (uint256, bool) { + try IRETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerReth) { + // If rate is 0, return true + if (ethPerReth == 0) return (0, true); + + return (ethPerReth, false); + } catch { + // If call to exchange rate reverts, return true + return (0, true); + } + } } diff --git a/contracts/src/PriceFeeds/WETHPriceFeed.sol b/contracts/src/PriceFeeds/WETHPriceFeed.sol index 155d03ada..a0efebe99 100644 --- a/contracts/src/PriceFeeds/WETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WETHPriceFeed.sol @@ -7,40 +7,32 @@ import "./MainnetPriceFeedBase.sol"; // import "forge-std/console2.sol"; contract WETHPriceFeed is MainnetPriceFeedBase { - constructor( - address _owner, - address _ethUsdOracleAddress, - uint256 _ethUsdStalenessThreshold - ) - MainnetPriceFeedBase( - _owner, - _ethUsdOracleAddress, - _ethUsdStalenessThreshold - ) + constructor(address _owner, address _ethUsdOracleAddress, uint256 _ethUsdStalenessThreshold) + MainnetPriceFeedBase(_owner, _ethUsdOracleAddress, _ethUsdStalenessThreshold) { - _fetchPrice(); + _fetchPricePrimary(); // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); } - function fetchPrice() public returns (uint256, bool) { - // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) { - return _fetchPrice();} + function fetchPrice() public returns (uint256, bool) { + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it - if (priceSource == PriceSource.lastGoodPrice) {return (lastGoodPrice, false);} + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); } - function _fetchPrice() internal override returns (uint256, bool) { + function _fetchPricePrimary() internal override returns (uint256, bool) { assert(priceSource == PriceSource.primary); (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); // If the ETH-USD Chainlink response was invalid in this transaction, return the last good ETH-USD price calculated - if (ethUsdOracleDown) {return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true);} + if (ethUsdOracleDown) return (_shutDownAndSwitchToLastGoodPrice(address(ethUsdOracle.aggregator)), true); + lastGoodPrice = ethUsdPrice; - return (ethUsdPrice, false); } } diff --git a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol index dbdf81eb2..faf095418 100644 --- a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol @@ -16,51 +16,56 @@ contract WSTETHPriceFeed is CompositePriceFeed, IWSTETHPriceFeed { address _wstEthTokenAddress, uint256 _ethUsdStalenessThreshold, uint256 _stEthUsdStalenessThreshold - ) CompositePriceFeed( - _owner, - _ethUsdOracleAddress, - _wstEthTokenAddress, - _ethUsdStalenessThreshold) - { + ) CompositePriceFeed(_owner, _ethUsdOracleAddress, _wstEthTokenAddress, _ethUsdStalenessThreshold) { stEthUsdOracle.aggregator = AggregatorV3Interface(_stEthUsdOracleAddress); stEthUsdOracle.stalenessThreshold = _stEthUsdStalenessThreshold; stEthUsdOracle.decimals = stEthUsdOracle.aggregator.decimals(); - // Check the STETH-USD aggregator has the expected 8 decimals - assert(stEthUsdOracle.decimals == 8); - - _fetchPrice(); + _fetchPricePrimary(); // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); } - function _fetchPrice() internal override returns (uint256, bool) { + function _fetchPricePrimary() internal override returns (uint256, bool) { assert(priceSource == PriceSource.primary); (uint256 stEthUsdPrice, bool stEthUsdOracleDown) = _getOracleAnswer(stEthUsdOracle); + (uint256 stEthPerWstEth, bool exchangeRateIsDown) = _getCanonicalRate(); + + // If exchange rate is down, shut down and switch to last good price - since we need this + // rate for all price calcs + if (exchangeRateIsDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(stEthUsdOracle.aggregator)), true); + } // If the STETH-USD feed is down, shut down and try to substitute it with the ETH-USD price if (stEthUsdOracleDown) { (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - // If the ETH-USD feed is *also* down, shut down and return the last good price - if(ethUsdOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(stEthUsdOracle.aggregator)), true); + // If the ETH-USD feed is *also* down, shut down and return the last good price + if (ethUsdOracleDown) { + return (_shutDownAndSwitchToLastGoodPrice(address(stEthUsdOracle.aggregator)), true); } else { return (_shutDownAndSwitchToETHUSDxCanonical(address(stEthUsdOracle.aggregator), ethUsdPrice), true); } } - - // Otherwise, use the primary price calculation: + // Otherwise, use the primary price calculation. // Calculate WSTETH-USD price USD_per_WSTETH = USD_per_STETH * STETH_per_WSTETH - uint256 wstEthUsdPrice = stEthUsdPrice * _getCanonicalRate() / 1e18; - + uint256 wstEthUsdPrice = stEthUsdPrice * stEthPerWstEth / 1e18; lastGoodPrice = wstEthUsdPrice; return (wstEthUsdPrice, false); } - function _getCanonicalRate() internal view override returns (uint256) { - return IWSTETH(rateProviderAddress).stEthPerToken(); - } + function _getCanonicalRate() internal view override returns (uint256, bool) { + try IWSTETH(rateProviderAddress).stEthPerToken() returns (uint256 stEthPerWstEth) { + // If rate is 0, return true + if (stEthPerWstEth == 0) return (0, true); + + return (stEthPerWstEth, false); + } catch { + // If call to exchange rate reverts, return true + return (0, true); + } + } } diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 86e6b528f..43ca3b126 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -1769,7 +1769,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 _batchDebt, // entire (with interest, batch fee), but without trove change, nor upfront fee nor redist bool _checkBatchSharesRatio // whether we do the check on the resulting ratio inside the func call ) internal { - // Debt + // Debt uint256 currentBatchDebtShares = batches[_batchAddress].totalDebtShares; uint256 batchDebtSharesDelta; uint256 debtIncrease = diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index 9a71e005f..8a6fa9694 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.24; import "./TestContracts/Accounts.sol"; import "./TestContracts/ChainlinkOracleMock.sol"; +import "./TestContracts/RETHTokenMock.sol"; +import "./TestContracts/WSTETHTokenMock.sol"; import "./TestContracts/Deployment.t.sol"; import "../Dependencies/AggregatorV3Interface.sol"; @@ -27,9 +29,12 @@ contract OraclesMainnet is TestAccounts { IRETHPriceFeed rethPriceFeed; IWSTETHPriceFeed wstethPriceFeed; - IRETHToken rETHToken; + IRETHToken rethToken; IWSTETH wstETH; + RETHTokenMock mockRethToken; + WSTETHTokenMock mockWstethToken; + TestDeployer.LiquityContracts[] contractsArray; ICollateralRegistry collateralRegistry; IBoldToken boldToken; @@ -70,10 +75,13 @@ contract OraclesMainnet is TestAccounts { mockOracle = new ChainlinkOracleMock(); - rETHToken = IRETHToken(result.externalAddresses.RETHToken); + rethToken = IRETHToken(result.externalAddresses.RETHToken); wstETH = IWSTETH(result.externalAddresses.WSTETHToken); + mockRethToken = new RETHTokenMock(); + mockWstethToken = new WSTETHTokenMock(); + // Record contracts for (uint256 c = 0; c < numCollaterals; c++) { contractsArray.push(result.contractsArray[c]); @@ -136,7 +144,7 @@ contract OraclesMainnet is TestAccounts { uint256 expectedMarketPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - uint256 rate = rETHToken.getExchangeRate(); + uint256 rate = rethToken.getExchangeRate(); assertGt(rate, 1e18); uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; @@ -146,7 +154,6 @@ contract OraclesMainnet is TestAccounts { assertEq(lastGoodPriceReth, expectedPrice); } - function testSetLastGoodPriceOnDeploymentWSTETH() public view { uint256 lastGoodPriceWsteth = wstethPriceFeed.lastGoodPrice(); assertGt(lastGoodPriceWsteth, 0); @@ -179,7 +186,7 @@ contract OraclesMainnet is TestAccounts { uint256 expectedMarketPrice = latestAnswerREthEth * latestAnswerEthUsd / 1e18; - uint256 rate = rETHToken.getExchangeRate(); + uint256 rate = rethToken.getExchangeRate(); assertGt(rate, 1e18); uint256 expectedCanonicalPrice = rate * latestAnswerEthUsd / 1e18; @@ -509,16 +516,16 @@ contract OraclesMainnet is TestAccounts { // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Fetch price again (, ethUsdFailed) = wethPriceFeed.fetchPrice(); - // Check oracle call failed this time + // Check oracle call failed this time assertTrue(ethUsdFailed); - // Confirm the branch is now shutdown + // Confirm the branch is now shutdown assertEq(contractsArray[0].troveManager.shutdownTime(), block.timestamp); } @@ -530,7 +537,7 @@ contract OraclesMainnet is TestAccounts { // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); assertGt(mockPrice, 0, "mockPrice 0"); // Confirm the lastGoodPrice is not coincidentally equal to the mock oracle's price @@ -538,8 +545,8 @@ contract OraclesMainnet is TestAccounts { // Fetch price again (uint256 price, bool ethUsdFailed) = wethPriceFeed.fetchPrice(); - - // Check oracle call failed this time + + // Check oracle call failed this time assertTrue(ethUsdFailed); // Confirm the PriceFeed's returned price equals the lastGoodPrice @@ -564,19 +571,45 @@ contract OraclesMainnet is TestAccounts { // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Fetch price again (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - // Check an oracle call failed this time + // Check an oracle call failed this time assertTrue(oracleFailedWhileBranchLive); - // Confirm the branch is now shutdown + // Confirm the branch is now shutdown assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); } + function testRETHPriceFeedShutsDownWhenExchangeRateFails() public { + // Fetch price + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + assertGt(price, 0); + + // Check oracle call didn't fail + assertFalse(oracleFailedWhileBranchLive); + + // Check branch is live, not shut down + assertEq(contractsArray[1].troveManager.shutdownTime(), 0); + + // Make the exchange rate 0 + vm.etch(address(rethToken), address(mockRethToken).code); + uint256 rate = rethToken.getExchangeRate(); + assertEq(rate, 0); + + // Fetch price again + (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + // Check a call failed this time + assertTrue(oracleFailedWhileBranchLive); + + // Confirm the branch is now shutdown + assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp, "timestamps not equal"); + } + function testRETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { // Fetch price rethPriceFeed.fetchPrice(); @@ -585,14 +618,38 @@ contract OraclesMainnet is TestAccounts { // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); assertGt(mockPrice, 0, "mockPrice 0"); // Fetch price again (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time + + // Check an oracle call failed this time + assertTrue(oracleFailedWhileBranchLive); + + // Confirm the PriceFeed's returned price equals the lastGoodPrice + assertEq(price, lastGoodPrice1); + + // Confirm the stored lastGoodPrice has not changed + assertEq(rethPriceFeed.lastGoodPrice(), lastGoodPrice1); + } + + function testRETHPriceFeedReturnsLastGoodPriceWhenExchangeRateFails() public { + // Fetch price + rethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Make the exchange rate 0 + vm.etch(address(rethToken), address(mockRethToken).code); + uint256 rate = rethToken.getExchangeRate(); + assertEq(rate, 0); + + // Fetch price again + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + // Check an oracle call failed this time assertTrue(oracleFailedWhileBranchLive); // Confirm the PriceFeed's returned price equals the lastGoodPrice @@ -609,9 +666,9 @@ contract OraclesMainnet is TestAccounts { // Check using primary assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - // Make the ETH-USD oracle stale + // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again @@ -636,35 +693,35 @@ contract OraclesMainnet is TestAccounts { // Make the RETH-ETH oracle stale vm.etch(address(rethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Fetch price again (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - // Check an oracle call failed this time + // Check an oracle call failed this time assertTrue(oracleFailedWhileBranchLive); - // Confirm the branch is now shutdown + // Confirm the branch is now shutdown assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); } function testFetchPriceReturnsETHUSDxCanonicalWhenRETHETHOracleFails() public { // Make the RETH-ETH oracle stale vm.etch(address(rethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price + + // Fetch price (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); assertGt(price, 0); - + // Check that the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive); // Calc expected price i.e. ETH-USD x canonical uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rETHToken.getExchangeRate(); + uint256 exchangeRate = rethToken.getExchangeRate(); assertGt(ethUsdPrice, 0); assertGt(exchangeRate, 0); uint256 expectedPrice = ethUsdPrice * exchangeRate / 1e18; @@ -679,9 +736,9 @@ contract OraclesMainnet is TestAccounts { // Check using primary assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - // Make the RETH-ETH oracle stale + // Make the RETH-ETH oracle stale vm.etch(address(rethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again @@ -696,20 +753,24 @@ contract OraclesMainnet is TestAccounts { function testRETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { // Make the RETH-USD oracle stale vm.etch(address(rethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Check using primary assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary"); - // Fetch price + // Fetch price (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - + // Check that the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); // Check using ETHUSDxCanonical - assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), "not using ethusdxcanonical"); + assertEq( + uint8(rethPriceFeed.priceSource()), + uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), + "not using ethusdxcanonical" + ); uint256 lastGoodPrice = rethPriceFeed.lastGoodPrice(); @@ -717,10 +778,10 @@ contract OraclesMainnet is TestAccounts { vm.etch(address(ethOracle), address(mockOracle).code); (,,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Calc expected price if didnt fail, i.e. ETH-USD x canonical uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = rETHToken.getExchangeRate(); + uint256 exchangeRate = rethToken.getExchangeRate(); assertGt(ethUsdPrice, 0); assertGt(exchangeRate, 0); uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; @@ -730,7 +791,7 @@ contract OraclesMainnet is TestAccounts { // Now fetch the price (price, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - + // This should be false, since the branch is already shutdown and not live assertFalse(oracleFailedWhileBranchLive); @@ -738,6 +799,61 @@ contract OraclesMainnet is TestAccounts { assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); } + function testRETHWhenUsingETHUSDxCanonicalReturnsMinOfLastGoodPriceAndETHUSDxCanonical() public { + // Make the RETH-ETH oracle stale + vm.etch(address(rethOracle), address(mockOracle).code); + (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Check using primary + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + // Fetch price + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + // Check that the primary calc oracle did fail + assertTrue(oracleFailedWhileBranchLive); + + // Check using ETHUSDxCanonical + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); + + // Make lastGoodPrice tiny, and below ETHUSDxCanonical + vm.store( + address(rethPriceFeed), + bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored + bytes32(uint256(1)) // make lastGoodPrice equal to 1 wei + ); + assertEq(rethPriceFeed.lastGoodPrice(), 1); + + + // Fetch the price again + (price, ) = rethPriceFeed.fetchPrice(); + + // Check price was lastGoodPrice + assertEq(price, rethPriceFeed.lastGoodPrice()); + + // Now make lastGoodPrice massive, and greater than ETHUSDxCanonical + vm.store( + address(rethPriceFeed), + bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored + bytes32(uint256(1e27)) // make lastGoodPrice equal to 1e27 i.e. 1 billion (with 18 decimal digits) + ); + assertEq(rethPriceFeed.lastGoodPrice(), 1e27); + + // Fetch the price again + (price, ) = rethPriceFeed.fetchPrice(); + + // Check price is expected ETH-USDxCanonical + // Calc expected price if didnt fail, i.e. + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 exchangeRate = rethToken.getExchangeRate(); + assertGt(ethUsdPrice, 0); + assertGt(exchangeRate, 0); + uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; + + assertEq(price, priceIfDidntFail, "price not equal expected"); + } + function testRETHPriceFeedShutsDownWhenBothOraclesFail() public { // Fetch price (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); @@ -751,21 +867,21 @@ contract OraclesMainnet is TestAccounts { // Make the RETH-ETH oracle stale vm.etch(address(rethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - // Make the ETH-USD oracle stale too + // Make the ETH-USD oracle stale too vm.etch(address(ethOracle), address(mockOracle).code); - (,,,updatedAt,) = ethOracle.latestRoundData(); + (,,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Fetch price again (, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - // Check an oracle call failed this time + // Check an oracle call failed this time assertTrue(oracleFailedWhileBranchLive); - // Confirm the branch is now shutdown + // Confirm the branch is now shutdown assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); } @@ -777,18 +893,18 @@ contract OraclesMainnet is TestAccounts { // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Make the RETH-ETH oracle stale too vm.etch(address(rethOracle), address(mockOracle).code); - (,mockPrice,,updatedAt,) = rethOracle.latestRoundData(); + (, mockPrice,, updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time + + // Check an oracle call failed this time assertTrue(oracleFailedWhileBranchLive); // Confirm the PriceFeed's returned price equals the lastGoodPrice @@ -809,12 +925,12 @@ contract OraclesMainnet is TestAccounts { // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Make the RETH-ETH oracle stale too vm.etch(address(rethOracle), address(mockOracle).code); - (,mockPrice,,updatedAt,) = rethOracle.latestRoundData(); + (, mockPrice,, updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again @@ -826,45 +942,95 @@ contract OraclesMainnet is TestAccounts { // --- WSTETH shutdown --- + function testWSTETHPriceFeedShutsDownWhenExchangeRateFails() public { + // Fetch price + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + assertGt(price, 0); + + // Check oracle call didn't fail + assertFalse(oracleFailedWhileBranchLive); + + // Check branch is live, not shut down + assertEq(contractsArray[1].troveManager.shutdownTime(), 0); + + // Make the exchange rate 0 + vm.etch(address(wstETH), address(mockWstethToken).code); + uint256 rate = wstETH.stEthPerToken(); + assertEq(rate, 0); + + // Fetch price again + (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check a call failed this time + assertTrue(oracleFailedWhileBranchLive); + + // Confirm the branch is now shutdown + assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp, "timestamps not equal"); + } + + function testWSTETHPriceFeedReturnsLastGoodPriceWhenExchangeRateFails() public { + // Fetch price + wstethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Make the exchange rate 0 + vm.etch(address(wstETH), address(mockWstethToken).code); + uint256 rate = wstETH.stEthPerToken(); + assertEq(rate, 0); + + // Fetch price + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check a call failed this time + assertTrue(oracleFailedWhileBranchLive); + + // Confirm the PriceFeed's returned price equals the lastGoodPrice + assertEq(price, lastGoodPrice1); + + // Confirm the stored lastGoodPrice has not changed + assertEq(wstethPriceFeed.lastGoodPrice(), lastGoodPrice1); + } + function testWSTETHPriceSourceIsPrimaryPriceWhenETHUSDOracleFails() public { // Fetch price - (uint256 price1, ) = wstethPriceFeed.fetchPrice(); + (uint256 price1,) = wstethPriceFeed.fetchPrice(); assertGt(price1, 0, "price is 0"); // Check using primary assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - + // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); assertGt(mockPrice, 0, "mockPrice 0"); // Fetch price again (uint256 price2, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc assertFalse(oracleFailedWhileBranchLive); // Check using primary assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); } - function testWSTETHPriceFeedReturnsPrimaryPriceWhenETHUSDOracleFails() public { + function testWSTETHPriceFeedReturnsPrimaryPriceWhenETHUSDOracleFails() public { // Fetch price - (uint256 price1, ) = wstethPriceFeed.fetchPrice(); + (uint256 price1,) = wstethPriceFeed.fetchPrice(); assertGt(price1, 0, "price is 0"); // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,int256 mockPrice,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); assertGt(mockPrice, 0, "mockPrice 0"); // Fetch price again (uint256 price2, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc assertFalse(oracleFailedWhileBranchLive); // Confirm the PriceFeed's returned price equals the previous price @@ -873,9 +1039,9 @@ contract OraclesMainnet is TestAccounts { function testWSTETHPriceDoesNotShutdownWhenETHUSDOracleFails() public { // Fetch price - ( , bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc assertFalse(oracleFailedWhileBranchLive); // Check branch is live, not shut down @@ -883,12 +1049,12 @@ contract OraclesMainnet is TestAccounts { // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Fetch price again (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - + // Check that again primary calc oracle did not fail assertFalse(oracleFailedWhileBranchLive); @@ -898,9 +1064,9 @@ contract OraclesMainnet is TestAccounts { function testWSTETHPriceShutdownWhenSTETHUSDOracleFails() public { // Fetch price - ( , bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc assertFalse(oracleFailedWhileBranchLive); // Check branch is live, not shut down @@ -908,12 +1074,12 @@ contract OraclesMainnet is TestAccounts { // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Fetch price again (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - + // Check that this time the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive); @@ -924,12 +1090,12 @@ contract OraclesMainnet is TestAccounts { function testFetchPriceReturnsETHUSDxCanonicalWhenSTETHUSDOracleFails() public { // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price + + // Fetch price (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - + // Check that the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive); @@ -946,15 +1112,15 @@ contract OraclesMainnet is TestAccounts { function testSTETHPriceSourceIsETHUSDxCanonicalWhenSTETHUSDOracleFails() public { // Check using primary assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - + // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - - // Fetch price + + // Fetch price (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - + // Check that the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive); @@ -965,15 +1131,15 @@ contract OraclesMainnet is TestAccounts { function testSTETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Check using primary assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - // Fetch price + // Fetch price (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - + // Check that the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive); @@ -986,7 +1152,7 @@ contract OraclesMainnet is TestAccounts { vm.etch(address(ethOracle), address(mockOracle).code); (,,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Calc expected price if didnt fail, i.e. ETH-USD x canonical uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); uint256 exchangeRate = wstETH.stEthPerToken(); @@ -1000,9 +1166,9 @@ contract OraclesMainnet is TestAccounts { // Now fetch the price (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - // Check using lastGoodPrice + // Check using lastGoodPrice assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - + // This should be false, since the branch is already shutdown and not live assertFalse(oracleFailedWhileBranchLive); @@ -1013,18 +1179,18 @@ contract OraclesMainnet is TestAccounts { function testSTETHWhenUsingETHUSDxCanonicalRemainsShutDownWhenETHUSDOracleFails() public { // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); - (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Check using primary assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); // Check branch is live, not shut down assertEq(contractsArray[2].troveManager.shutdownTime(), 0); - // Fetch price + // Fetch price (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - + // Check that the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive); @@ -1044,16 +1210,71 @@ contract OraclesMainnet is TestAccounts { // Check using lastGoodPrice assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); - - // Check branch is still down + + // Check branch is still down assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); } - function testWSTETHPriceShutdownWhenBothOraclesFail() public { + function testSTETHWhenUsingETHUSDxCanonicalReturnsMinOfLastGoodPriceAndETHUSDxCanonical() public { + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + // Fetch price - ( , bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check that the primary calc oracle did fail + assertTrue(oracleFailedWhileBranchLive); + + // Check using ETHUSDxCanonical + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical)); + + // Make lastGoodPrice tiny, and below ETHUSDxCanonical + vm.store( + address(wstethPriceFeed), + bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored + bytes32(uint256(1)) // make lastGoodPrice equal to 1 wei + ); + assertEq(wstethPriceFeed.lastGoodPrice(), 1); + + + // Fetch the price again + (price, ) = wstethPriceFeed.fetchPrice(); + + // Check price was lastGoodPrice + assertEq(price, wstethPriceFeed.lastGoodPrice()); + + // Now make lastGoodPrice massive, and greater than ETHUSDxCanonical + vm.store( + address(wstethPriceFeed), + bytes32(uint256(1)), // 1st storage slot where lastGoodPrice is stored + bytes32(uint256(1e27)) // make lastGoodPrice equal to 1e27 i.e. 1 billion (with 18 decimal digits) + ); + assertEq(wstethPriceFeed.lastGoodPrice(), 1e27); - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc + // Fetch the price again + (price, ) = wstethPriceFeed.fetchPrice(); + + // Check price is expected ETH-USDxCanonical + // Calc expected price if didnt fail, i.e. + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 exchangeRate = wstETH.stEthPerToken(); + assertGt(ethUsdPrice, 0); + assertGt(exchangeRate, 0); + uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; + + assertEq(price, priceIfDidntFail); + } + + function testWSTETHPriceShutdownWhenBothOraclesFail() public { + // Fetch price + (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc assertFalse(oracleFailedWhileBranchLive); // Check branch is live, not shut down @@ -1061,17 +1282,17 @@ contract OraclesMainnet is TestAccounts { // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); - (,int256 mockPrice,,uint256 updatedAt,) = stethOracle.latestRoundData(); + (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Make the ETH-USD oracle stale too vm.etch(address(ethOracle), address(mockOracle).code); - (,mockPrice,,updatedAt,) = ethOracle.latestRoundData(); + (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); - + // Fetch price again (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - + // Check that this time the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive); @@ -1085,20 +1306,20 @@ contract OraclesMainnet is TestAccounts { uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); - // Make the STETH-USD oracle stale + // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); - (,int256 mockPrice,,uint256 updatedAt,) = stethOracle.latestRoundData(); + (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Make the ETH-USD oracle stale too vm.etch(address(ethOracle), address(mockOracle).code); - (,mockPrice,,updatedAt,) = ethOracle.latestRoundData(); + (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - - // Check an oracle call failed this time + + // Check an oracle call failed this time assertTrue(oracleFailedWhileBranchLive); // Confirm the PriceFeed's returned price equals the lastGoodPrice @@ -1117,32 +1338,33 @@ contract OraclesMainnet is TestAccounts { // Check using primary assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); - // Make the STETH-USD oracle stale + // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); - (,int256 mockPrice,,uint256 updatedAt,) = stethOracle.latestRoundData(); + (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Make the ETH-USD oracle stale too vm.etch(address(ethOracle), address(mockOracle).code); - (,mockPrice,,updatedAt,) = ethOracle.latestRoundData(); + (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - + // Check using lastGoodPrice assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); } // TODO: - // WETHPriceFeed: - // - Source when ETH-USD failed: lastGoodPrice + // - fix test (make sure altering sotrage correctly) - // RETHPriceFeed: - // - Source when RETH-ETH failed: ETHUSDxCanonical - // - Source when ETH-USD failed: lastGoodPrice + // - RETH: When using primary and exchange rate fails, switch to last good price + // - RETH: When using canonical and exchange rate fails, switch to last good price + // - STETH: When using primary and exchange rate fails, switch to last good price + // - STETH: When using canonical and exchange rate fails, switch to last good price + // - More basic actions tests (adjust, close, etc) // - liq tests (manipulate aggregator stored price } diff --git a/contracts/src/test/TestContracts/RETHTokenMock.sol b/contracts/src/test/TestContracts/RETHTokenMock.sol new file mode 100644 index 000000000..fc7b87015 --- /dev/null +++ b/contracts/src/test/TestContracts/RETHTokenMock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import "../../Interfaces/IRETHToken.sol"; + +contract RETHTokenMock is IRETHToken { + + function getExchangeRate() external view returns (uint256) { + return 0; + } +} \ No newline at end of file diff --git a/contracts/src/test/TestContracts/WSTETHTokenMock.sol b/contracts/src/test/TestContracts/WSTETHTokenMock.sol new file mode 100644 index 000000000..e9b05526c --- /dev/null +++ b/contracts/src/test/TestContracts/WSTETHTokenMock.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.18; + +import "../../Interfaces/IWSTETH.sol"; + +contract WSTETHTokenMock is IWSTETH{ + + function stEthPerToken() external view returns (uint256) {return 0;} + function wrap(uint256 _stETHAmount) external returns (uint256) {return 0;} + function unwrap(uint256 _wstETHAmount) external returns (uint256) {return 0;} + function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256) {return 0;} + function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256) {return 0;} + function tokensPerStEth() external view returns (uint256) {return 0;} +} \ No newline at end of file From d19116d233bb3f00effdfb8949b9ab79636d6ce8 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Wed, 18 Sep 2024 00:33:29 +0100 Subject: [PATCH 08/19] Add test for logic when using ETHUSDxCanonical --- contracts/src/PriceFeeds/RETHPriceFeed.sol | 6 +- contracts/src/test/OracleMainnet.t.sol | 117 +++++++++++++++++++-- 2 files changed, 107 insertions(+), 16 deletions(-) diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol index 18f5f325e..44bd316fa 100644 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/RETHPriceFeed.sol @@ -6,7 +6,7 @@ import "./CompositePriceFeed.sol"; import "../Interfaces/IRETHToken.sol"; import "../Interfaces/IRETHPriceFeed.sol"; -import "forge-std/console2.sol"; +// import "forge-std/console2.sol"; contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { constructor( @@ -36,10 +36,6 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { (uint256 rEthEthPrice, bool rEthEthOracleDown) = _getOracleAnswer(rEthEthOracle); (uint256 rEthPerEth, bool exchangeRateIsDown) = _getCanonicalRate(); - console2.log(rEthPerEth, "rEthPerEth"); - console2.log(exchangeRateIsDown, "exchangeRateIsDown"); - console2.log(exchangeRateIsDown, "exchangeRateIsDown"); - // If the ETH-USD feed is down, shut down and switch to the last good price seen by the system // since we need both ETH-USD and canonical for primary and fallback price calcs if (ethUsdOracleDown || exchangeRateIsDown) { diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index 8a6fa9694..f24d53a66 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -799,6 +799,58 @@ contract OraclesMainnet is TestAccounts { assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); } + function testRETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenExchangeRateFails() public { + // Make the RETH-USD oracle stale + vm.etch(address(rethOracle), address(mockOracle).code); + (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Check using primary + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary"); + + // Fetch price + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + // Check that the primary calc oracle did fail + assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); + + // Check using ETHUSDxCanonical + assertEq( + uint8(rethPriceFeed.priceSource()), + uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), + "not using ethusdxcanonical" + ); + + uint256 lastGoodPrice = rethPriceFeed.lastGoodPrice(); + + // Calc expected price if didnt fail, i.e. ETH-USD x canonical + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 exchangeRate = rethToken.getExchangeRate(); + assertGt(ethUsdPrice, 0); + assertGt(exchangeRate, 0); + uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; + + // Make the exchange rate return 0 + vm.etch(address(rethToken), address(mockRethToken).code); + uint256 rate = rethToken.getExchangeRate(); + assertEq(rate, 0, "mock rate non-zero"); + + // Now fetch the price + (price, oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + + // This should be false, since the branch is already shutdown and not live + assertFalse(oracleFailedWhileBranchLive); + + // Confirm the returned price is the last good price + assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); + // Check we've switched to lastGoodPrice source + assertEq( + uint8(rethPriceFeed.priceSource()), + uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice), + "not using lastGoodPrice" + ); + } + function testRETHWhenUsingETHUSDxCanonicalReturnsMinOfLastGoodPriceAndETHUSDxCanonical() public { // Make the RETH-ETH oracle stale vm.etch(address(rethOracle), address(mockOracle).code); @@ -968,7 +1020,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp, "timestamps not equal"); } - function testWSTETHPriceFeedReturnsLastGoodPriceWhenExchangeRateFails() public { + function testWSTETHPriceFeedReturnsLastGoodPriceWhenExchangeRateFails() public { // Fetch price wstethPriceFeed.fetchPrice(); uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); @@ -1176,6 +1228,58 @@ contract OraclesMainnet is TestAccounts { assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); } + function testSTETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenExchangeRateFails() public { + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary), "not using primary"); + + // Fetch price + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // Check that the primary calc oracle did fail + assertTrue(oracleFailedWhileBranchLive, "primary oracle calc didnt fail"); + + // Check using ETHUSDxCanonical + assertEq( + uint8(wstethPriceFeed.priceSource()), + uint8(IMainnetPriceFeed.PriceSource.ETHUSDxCanonical), + "not using ethusdxcanonical" + ); + + uint256 lastGoodPrice = wstethPriceFeed.lastGoodPrice(); + + // Calc expected price if didnt fail, i.e. ETH-USD x canonical + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 exchangeRate = wstETH.stEthPerToken(); + assertGt(ethUsdPrice, 0); + assertGt(exchangeRate, 0); + uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; + + // Make the exchange rate return 0 + vm.etch(address(wstETH), address(mockWstethToken).code); + uint256 rate = wstETH.stEthPerToken(); + assertEq(rate, 0, "mock rate non-zero"); + + // Now fetch the price + (price, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + + // This should be false, since the branch is already shutdown and not live + assertFalse(oracleFailedWhileBranchLive); + + // Confirm the returned price is the last good price + assertEq(price, lastGoodPrice, "fetched price != lastGoodPrice"); + // Check we've switched to lastGoodPrice source + assertEq( + uint8(wstethPriceFeed.priceSource()), + uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice), + "not using lastGoodPrice" + ); + } + function testSTETHWhenUsingETHUSDxCanonicalRemainsShutDownWhenETHUSDOracleFails() public { // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); @@ -1356,15 +1460,6 @@ contract OraclesMainnet is TestAccounts { } // TODO: - - // - fix test (make sure altering sotrage correctly) - - // - RETH: When using primary and exchange rate fails, switch to last good price - // - RETH: When using canonical and exchange rate fails, switch to last good price - // - STETH: When using primary and exchange rate fails, switch to last good price - // - STETH: When using canonical and exchange rate fails, switch to last good price - - // - More basic actions tests (adjust, close, etc) - // - liq tests (manipulate aggregator stored price + // - liq tests (manipulate aggregator stored price) } From 5727c26df1688c171ac93b84206beb85bf86f9ba Mon Sep 17 00:00:00 2001 From: RickGriff Date: Thu, 19 Sep 2024 13:14:45 +0100 Subject: [PATCH 09/19] Fix tests --- contracts/src/test/OracleMainnet.t.sol | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index f24d53a66..2f4fb5a97 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -706,7 +706,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); } - function testFetchPriceReturnsETHUSDxCanonicalWhenRETHETHOracleFails() public { + function testFetchPriceReturnsMinETHUSDxCanonicalAndLastGoodPriceWhenRETHETHOracleFails() public { // Make the RETH-ETH oracle stale vm.etch(address(rethOracle), address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); @@ -724,9 +724,10 @@ contract OraclesMainnet is TestAccounts { uint256 exchangeRate = rethToken.getExchangeRate(); assertGt(ethUsdPrice, 0); assertGt(exchangeRate, 0); - uint256 expectedPrice = ethUsdPrice * exchangeRate / 1e18; - assertEq(price, expectedPrice); + uint256 expectedPrice = LiquityMath._min(rethPriceFeed.lastGoodPrice(), ethUsdPrice * exchangeRate / 1e18); + + assertEq(price, expectedPrice, "price not expected price"); } function testRETHPriceSourceIsETHUSDxCanonicalWhenRETHETHFails() public { @@ -1139,7 +1140,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); } - function testFetchPriceReturnsETHUSDxCanonicalWhenSTETHUSDOracleFails() public { + function testFetchPriceReturnsMinETHUSDxCanonicalAndLastGoodPriceWhenSTETHUSDOracleFails() public { // Make the STETH-USD oracle stale vm.etch(address(stethOracle), address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); @@ -1151,14 +1152,16 @@ contract OraclesMainnet is TestAccounts { // Check that the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive); + // Calc expected price i.e. ETH-USD x canonical uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); uint256 exchangeRate = wstETH.stEthPerToken(); assertGt(ethUsdPrice, 0); assertGt(exchangeRate, 0); - uint256 expectedPrice = ethUsdPrice * exchangeRate / 1e18; - assertEq(price, expectedPrice); + uint256 expectedPrice = LiquityMath._min(wstethPriceFeed.lastGoodPrice(), ethUsdPrice * exchangeRate / 1e18); + + assertEq(price, expectedPrice, "price not expected price"); } function testSTETHPriceSourceIsETHUSDxCanonicalWhenSTETHUSDOracleFails() public { From 5816899f95718a1751a76ffc011f3dd3c94e8228 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Fri, 27 Sep 2024 18:34:19 +0100 Subject: [PATCH 10/19] Remove TODO comments --- contracts/src/PriceFeeds/CompositePriceFeed.sol | 1 - contracts/src/PriceFeeds/RETHPriceFeed.sol | 1 - 2 files changed, 2 deletions(-) diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol index 2a52ac50b..f7fa37f1f 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -76,7 +76,6 @@ contract CompositePriceFeed is MainnetPriceFeedBase { function _fetchPriceETHUSDxCanonical(uint256 _ethUsdPrice) internal returns (uint256) { assert(priceSource == PriceSource.ETHUSDxCanonical); // Get the underlying_per_LST canonical rate directly from the LST contract - // TODO: Should we also shutdown if the call to the canonical rate reverts, or returns 0? (uint256 lstRate, bool exchangeRateIsDown) = _getCanonicalRate(); // If the exchange rate contract is down, switch to (and return) lastGoodPrice. diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol index 44bd316fa..44fa89456 100644 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/RETHPriceFeed.sol @@ -52,7 +52,6 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { uint256 lstUsdMarketPrice = ethUsdPrice * rEthEthPrice / 1e18; // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - // TODO: Should we also shutdown if the call to the canonical rate reverts, or returns 0? uint256 lstUsdCanonicalPrice = ethUsdPrice * rEthPerEth / 1e18; // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. From 312bd6aec138827386ebdef73d74048ad65ec403 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Sun, 20 Oct 2024 19:48:15 +0100 Subject: [PATCH 11/19] Alter price calc logic for normal redemptions --- contracts/src/Interfaces/IPriceFeed.sol | 1 + .../src/PriceFeeds/CompositePriceFeed.sol | 43 ++- .../src/PriceFeeds/MainnetPriceFeedBase.sol | 2 +- contracts/src/PriceFeeds/RETHPriceFeed.sol | 38 +- contracts/src/PriceFeeds/WETHPriceFeed.sol | 11 +- contracts/src/PriceFeeds/WSTETHPriceFeed.sol | 34 +- contracts/src/TroveManager.sol | 3 +- contracts/src/test/OracleMainnet.t.sol | 350 +++++++++++++++++- .../src/test/TestContracts/PriceFeedMock.sol | 4 + .../test/TestContracts/PriceFeedTestnet.sol | 7 + 10 files changed, 431 insertions(+), 62 deletions(-) diff --git a/contracts/src/Interfaces/IPriceFeed.sol b/contracts/src/Interfaces/IPriceFeed.sol index 2c8133b03..bb95c93c7 100644 --- a/contracts/src/Interfaces/IPriceFeed.sol +++ b/contracts/src/Interfaces/IPriceFeed.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; interface IPriceFeed { function fetchPrice() external returns (uint256, bool); + function fetchRedemptionPrice() external returns (uint256, bool); function lastGoodPrice() external view returns (uint256); function setAddresses(address _borrowerOperationsAddress) external; } diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol index f7fa37f1f..cf63200aa 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -38,24 +38,16 @@ contract CompositePriceFeed is MainnetPriceFeedBase { // --- b) an oracle or exchange rate contract failed during this call. function fetchPrice() public returns (uint256, bool) { // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + if (priceSource == PriceSource.primary) return _fetchPricePrimary(false); - // If branch is already shut down and using ETH-USD * canonical_rate, try to use that - if (priceSource == PriceSource.ETHUSDxCanonical) { - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - //... but if the ETH-USD oracle *also* fails here, switch to using the lastGoodPrice - if (ethUsdOracleDown) { - // No need to shut down, since branch already is shut down - priceSource = PriceSource.lastGoodPrice; - return (lastGoodPrice, false); - } else { - return (_fetchPriceETHUSDxCanonical(ethUsdPrice), false); - } - } + return _fetchPriceDuringShutdown(); + } - // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it - assert(priceSource == PriceSource.lastGoodPrice); - return (lastGoodPrice, false); + function fetchRedemptionPrice() external returns (uint256, bool) { + // If branch is live and the primary oracle setup has been working, try to use it + if (priceSource == PriceSource.primary) return _fetchPricePrimary(true); + + return _fetchPriceDuringShutdown(); } function _shutDownAndSwitchToETHUSDxCanonical(address _failedOracleAddr, uint256 _ethUsdPrice) @@ -71,6 +63,25 @@ contract CompositePriceFeed is MainnetPriceFeedBase { return _fetchPriceETHUSDxCanonical(_ethUsdPrice); } + function _fetchPriceDuringShutdown() internal returns (uint256, bool) { + // When branch is already shut down and using ETH-USD * canonical_rate, try to use that + if (priceSource == PriceSource.ETHUSDxCanonical) { + (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); + //... but if the ETH-USD oracle *also* fails here, switch to using the lastGoodPrice + if (ethUsdOracleDown) { + // No need to shut down, since branch already is shut down + priceSource = PriceSource.lastGoodPrice; + return (lastGoodPrice, false); + } else { + return (_fetchPriceETHUSDxCanonical(ethUsdPrice), false); + } + } + + // Otherwise when branch is shut down and already using the lastGoodPrice, continue with it + assert(priceSource == PriceSource.lastGoodPrice); + return (lastGoodPrice, false); + } + // Only called if the primary LST oracle has failed, branch has shut down, // and we've switched to using: ETH-USD * canonical_rate. function _fetchPriceETHUSDxCanonical(uint256 _ethUsdPrice) internal returns (uint256) { diff --git a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol index cec2df73a..a63ad9881 100644 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol @@ -62,7 +62,7 @@ abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { // An individual Pricefeed instance implements _fetchPricePrimary according to the data sources it uses. Returns: // - The price // - A bool indicating whether a new oracle failure or exchange rate failure was detected in the call - function _fetchPricePrimary() internal virtual returns (uint256, bool) {} + function _fetchPricePrimary(bool _isRedemption) internal virtual returns (uint256, bool) {} function _getOracleAnswer(Oracle memory _oracle) internal view returns (uint256, bool) { ChainlinkResponse memory chainlinkResponse = _getCurrentChainlinkResponse(_oracle.aggregator); diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol index 44fa89456..dab9cc660 100644 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/RETHPriceFeed.sol @@ -22,7 +22,7 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { rEthEthOracle.stalenessThreshold = _rEthEthStalenessThreshold; rEthEthOracle.decimals = rEthEthOracle.aggregator.decimals(); - _fetchPricePrimary(); + _fetchPricePrimary(false); // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); @@ -30,7 +30,9 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { Oracle public rEthEthOracle; - function _fetchPricePrimary() internal override returns (uint256, bool) { + uint256 constant public _2_PERCENT = 2e16; + + function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { assert(priceSource == PriceSource.primary); (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); (uint256 rEthEthPrice, bool rEthEthOracleDown) = _getOracleAnswer(rEthEthOracle); @@ -48,21 +50,37 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { // Otherwise, use the primary price calculation: - // Calculate the market LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 lstUsdMarketPrice = ethUsdPrice * rEthEthPrice / 1e18; + // Calculate the market RETH-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH + uint256 rEthUsdMarketPrice = ethUsdPrice * rEthEthPrice / 1e18; // Calculate the canonical LST-USD price: USD_per_RETH = USD_per_ETH * ETH_per_RETH - uint256 lstUsdCanonicalPrice = ethUsdPrice * rEthPerEth / 1e18; + uint256 rEthUsdCanonicalPrice = ethUsdPrice * rEthPerEth / 1e18; + + uint256 rEthUsdPrice; - // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. - // NOTE: only needed - uint256 lstUsdPrice = LiquityMath._min(lstUsdMarketPrice, lstUsdCanonicalPrice); + // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb + if (_isRedemption && _within2pct(rEthUsdMarketPrice, rEthUsdCanonicalPrice)) { + rEthUsdPrice = LiquityMath._max(rEthUsdMarketPrice, rEthUsdCanonicalPrice); + } else { + // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. + // Assumes a deviation between market <> canonical of >2% represents a legitemate market price difference. + rEthUsdPrice = LiquityMath._min(rEthUsdMarketPrice, rEthUsdCanonicalPrice); + } - lastGoodPrice = lstUsdPrice; + lastGoodPrice = rEthUsdPrice; - return (lstUsdPrice, false); + return (rEthUsdPrice, false); } + function _within2pct(uint256 _rEthUsdMarketPrice, uint256 _rEthUsdCanonicalPrice) internal pure returns (bool) { + // Calculate the price deviation of the oracle market price relative to the canonical price + uint256 max = _rEthUsdCanonicalPrice * (DECIMAL_PRECISION + _2_PERCENT) / 1e18; + uint256 min = _rEthUsdCanonicalPrice * (DECIMAL_PRECISION - _2_PERCENT) / 1e18; + + return _rEthUsdMarketPrice >= min && _rEthUsdCanonicalPrice <= max; + } + + function _getCanonicalRate() internal view override returns (uint256, bool) { try IRETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerReth) { // If rate is 0, return true diff --git a/contracts/src/PriceFeeds/WETHPriceFeed.sol b/contracts/src/PriceFeeds/WETHPriceFeed.sol index a0efebe99..c09d394b9 100644 --- a/contracts/src/PriceFeeds/WETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WETHPriceFeed.sol @@ -10,7 +10,7 @@ contract WETHPriceFeed is MainnetPriceFeedBase { constructor(address _owner, address _ethUsdOracleAddress, uint256 _ethUsdStalenessThreshold) MainnetPriceFeedBase(_owner, _ethUsdOracleAddress, _ethUsdStalenessThreshold) { - _fetchPricePrimary(); + _fetchPricePrimary(false); // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); @@ -18,14 +18,19 @@ contract WETHPriceFeed is MainnetPriceFeedBase { function fetchPrice() public returns (uint256, bool) { // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) return _fetchPricePrimary(); + if (priceSource == PriceSource.primary) return _fetchPricePrimary(false); // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it assert(priceSource == PriceSource.lastGoodPrice); return (lastGoodPrice, false); } - function _fetchPricePrimary() internal override returns (uint256, bool) { + function fetchRedemptionPrice() external returns (uint256, bool) { + // Use same price for redemption as all other ops in WETH branch + return fetchPrice(); + } + + function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { assert(priceSource == PriceSource.primary); (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); diff --git a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol index faf095418..78b684b4a 100644 --- a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol @@ -21,37 +21,41 @@ contract WSTETHPriceFeed is CompositePriceFeed, IWSTETHPriceFeed { stEthUsdOracle.stalenessThreshold = _stEthUsdStalenessThreshold; stEthUsdOracle.decimals = stEthUsdOracle.aggregator.decimals(); - _fetchPricePrimary(); + _fetchPricePrimary(false); // Check the oracle didn't already fail assert(priceSource == PriceSource.primary); } - function _fetchPricePrimary() internal override returns (uint256, bool) { + function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { assert(priceSource == PriceSource.primary); (uint256 stEthUsdPrice, bool stEthUsdOracleDown) = _getOracleAnswer(stEthUsdOracle); (uint256 stEthPerWstEth, bool exchangeRateIsDown) = _getCanonicalRate(); + (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - // If exchange rate is down, shut down and switch to last good price - since we need this - // rate for all price calcs - if (exchangeRateIsDown) { + // - If exchange rate or ETH-USD is down, shut down and switch to last good price. Reasoning: + // - Exchange rate is used in all price calcs + // - ETH-USD is used in the fallback calc, and for redemptions in the primary price calc + if (exchangeRateIsDown || ethUsdOracleDown) { return (_shutDownAndSwitchToLastGoodPrice(address(stEthUsdOracle.aggregator)), true); } // If the STETH-USD feed is down, shut down and try to substitute it with the ETH-USD price if (stEthUsdOracleDown) { - (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); - // If the ETH-USD feed is *also* down, shut down and return the last good price - if (ethUsdOracleDown) { - return (_shutDownAndSwitchToLastGoodPrice(address(stEthUsdOracle.aggregator)), true); - } else { - return (_shutDownAndSwitchToETHUSDxCanonical(address(stEthUsdOracle.aggregator), ethUsdPrice), true); - } + return (_shutDownAndSwitchToETHUSDxCanonical(address(stEthUsdOracle.aggregator), ethUsdPrice), true); + } + + // Otherwise, use the primary price calculation: + uint256 wstEthUsdPrice; + + if (_isRedemption) { + // If it's a redemption, take the max of (STETH-USD, ETH-USD) and convert to WSTETH-USD + wstEthUsdPrice = LiquityMath._max(stEthUsdPrice, ethUsdPrice) * stEthPerWstEth / 1e18; + } else { + // Otherwise, just calculate WSTETH-USD price: USD_per_WSTETH = USD_per_STETH * STETH_per_WSTETH + wstEthUsdPrice = stEthUsdPrice * stEthPerWstEth / 1e18; } - // Otherwise, use the primary price calculation. - // Calculate WSTETH-USD price USD_per_WSTETH = USD_per_STETH * STETH_per_WSTETH - uint256 wstEthUsdPrice = stEthUsdPrice * stEthPerWstEth / 1e18; lastGoodPrice = wstEthUsdPrice; return (wstEthUsdPrice, false); diff --git a/contracts/src/TroveManager.sol b/contracts/src/TroveManager.sol index 43ca3b126..66a61ecee 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -846,6 +846,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { IActivePool activePoolCached = activePool; TroveChange memory totalsTroveChange; + // Use the standard fetchPrice here, since if branch has shut down we don't worry about small redemption arbs (uint256 price,) = priceFeed.fetchPrice(); uint256 remainingBold = _boldAmount; @@ -1179,7 +1180,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 spSize = stabilityPool.getTotalBoldDeposits(); uint256 unbackedPortion = totalDebt > spSize ? totalDebt - spSize : 0; - (uint256 price,) = priceFeed.fetchPrice(); + (uint256 price,) = priceFeed.fetchRedemptionPrice(); // It's redeemable if the TCR is above the shutdown threshold, and branch has not been shut down bool redeemable = _getTCR(price) >= SCR && shutdownTime == 0; diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index 2f4fb5a97..406ad5085 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -124,6 +124,12 @@ contract OraclesMainnet is TestAccounts { return uint256(answer) * 10 ** (18 - decimals); } + function redeem(address _from, uint256 _boldAmount) public { + vm.startPrank(_from); + collateralRegistry.redeemCollateral(_boldAmount, MAX_UINT256, 1e18); + vm.stopPrank(); + } + // --- lastGoodPrice set on deployment --- function testSetLastGoodPriceOnDeploymentWETH() public view { @@ -273,7 +279,7 @@ contract OraclesMainnet is TestAccounts { function testOpenTroveWSTETH() public { uint256 latestAnswerStethUsd = _getLatestAnswerFromOracle(stethOracle); - uint256 wstethStethExchangeRate = wstETH.tokensPerStEth(); + uint256 wstethStethExchangeRate = wstETH.stEthPerToken(); uint256 calcdWstethUsdPrice = latestAnswerStethUsd * wstethStethExchangeRate / 1e18; @@ -1045,7 +1051,7 @@ contract OraclesMainnet is TestAccounts { assertEq(wstethPriceFeed.lastGoodPrice(), lastGoodPrice1); } - function testWSTETHPriceSourceIsPrimaryPriceWhenETHUSDOracleFails() public { + function testWSTETHPriceSourceIsLastGoodPricePriceWhenETHUSDOracleFails() public { // Fetch price (uint256 price1,) = wstethPriceFeed.fetchPrice(); assertGt(price1, 0, "price is 0"); @@ -1062,18 +1068,20 @@ contract OraclesMainnet is TestAccounts { // Fetch price again (uint256 price2, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(oracleFailedWhileBranchLive); + // Check ncall failed + assertTrue(oracleFailedWhileBranchLive); - // Check using primary - assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + // Check using lastGoodPrice + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); } - function testWSTETHPriceFeedReturnsPrimaryPriceWhenETHUSDOracleFails() public { + function testWSTETHPriceFeedReturnsLastGoodPriceWhenETHUSDOracleFails() public { // Fetch price (uint256 price1,) = wstethPriceFeed.fetchPrice(); assertGt(price1, 0, "price is 0"); + uint256 lastGoodPriceBeforeFail = wstethPriceFeed.lastGoodPrice(); + // Make the ETH-USD oracle stale vm.etch(address(ethOracle), address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); @@ -1083,14 +1091,16 @@ contract OraclesMainnet is TestAccounts { // Fetch price again (uint256 price2, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - // Check no oracle failed in this call, since it uses only STETH-USD oracle in the primary calc - assertFalse(oracleFailedWhileBranchLive); + // Check oracle failed in this call + assertTrue(oracleFailedWhileBranchLive); - // Confirm the PriceFeed's returned price equals the previous price - assertEq(price2, price1); + // Confirm the PriceFeed's returned price equals the stored lastGoodPrice + assertEq(price2, lastGoodPriceBeforeFail); + // Confirm the stored last good price didn't change + assertEq(lastGoodPriceBeforeFail, wstethPriceFeed.lastGoodPrice()); } - function testWSTETHPriceDoesNotShutdownWhenETHUSDOracleFails() public { + function testWSTETHPriceDoesShutsDownWhenETHUSDOracleFails() public { // Fetch price (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); @@ -1108,11 +1118,11 @@ contract OraclesMainnet is TestAccounts { // Fetch price again (, oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); - // Check that again primary calc oracle did not fail - assertFalse(oracleFailedWhileBranchLive); + // Check that the primary calc did fail + assertTrue(oracleFailedWhileBranchLive); - // Confirm branch is still live, not shut down - assertEq(contractsArray[2].troveManager.shutdownTime(), 0); + // Confirm branch is shut down + assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); } function testWSTETHPriceShutdownWhenSTETHUSDOracleFails() public { @@ -1462,7 +1472,315 @@ contract OraclesMainnet is TestAccounts { assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); } + // --- redemptions --- + + function testNormalWETHRedemptionDoesNotHitShutdownBranch() public { + // Fetch price + wethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = wethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Check using primary + assertEq(uint8(wethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + uint256 coll = 100 ether; + uint256 debtRequest = 3000e18; + + vm.startPrank(A); + uint256 troveId = contractsArray[0].borrowerOperations.openTrove( + A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) + ); + + // Make the ETH-USD oracle stale + vm.etch(address(ethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (uint256 price, bool oracleFailedWhileBranchLive) = wethPriceFeed.fetchPrice(); + assertTrue(oracleFailedWhileBranchLive); + // Confirm branch shutdown + assertEq(contractsArray[0].troveManager.shutdownTime(), block.timestamp); + + uint256 totalBoldRedeemAmount = 100e18; + uint256 branch0DebtBefore = contractsArray[0].activePool.getBoldDebt(); + assertGt(branch0DebtBefore, 0); + + uint256 boldBalBefore_A = boldToken.balanceOf(A); + + // Redeem + redeem(A, totalBoldRedeemAmount); + + // Confirm A lost no BOLD + assertEq( boldToken.balanceOf(A), boldBalBefore_A); + + // Confirm WETH branch did not get redeemed from + assertEq(contractsArray[0].activePool.getBoldDebt(), branch0DebtBefore); + } + + function testNormalRETHRedemptionDoesNotHitShutdownBranch() public { + // Fetch price + rethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Check using primary + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + uint256 coll = 100 ether; + uint256 debtRequest = 3000e18; + + vm.startPrank(A); + uint256 troveId = contractsArray[1].borrowerOperations.openTrove( + A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) + ); + + // Make the RETH-ETH oracle stale + vm.etch(address(rethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + assertTrue(oracleFailedWhileBranchLive); + // Confirm RETH branch shutdown + assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); + + uint256 totalBoldRedeemAmount = 100e18; + uint256 branch1DebtBefore = contractsArray[1].activePool.getBoldDebt(); + assertGt(branch1DebtBefore, 0); + + uint256 boldBalBefore_A = boldToken.balanceOf(A); + + // Redeem + redeem(A, totalBoldRedeemAmount); + + // Confirm A lost no BOLD + assertEq( boldToken.balanceOf(A), boldBalBefore_A); + + // Confirm RETH branch did not get redeemed from + assertEq(contractsArray[1].activePool.getBoldDebt(), branch1DebtBefore); + } + + function testNormalWSTETHRedemptionDoesNotHitShutdownBranch() public { + // Fetch price + wstethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + uint256 coll = 100 ether; + uint256 debtRequest = 3000e18; + + vm.startPrank(A); + uint256 troveId = contractsArray[2].borrowerOperations.openTrove( + A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) + ); + + // Make the STETH-USD oracle stale + vm.etch(address(stethOracle), address(mockOracle).code); + (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); + assertEq(updatedAt, block.timestamp - 7 days); + + // Fetch price again + (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + assertTrue(oracleFailedWhileBranchLive); + // Confirm RETH branch shutdown + assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); + + uint256 totalBoldRedeemAmount = 100e18; + uint256 branch2DebtBefore = contractsArray[2].activePool.getBoldDebt(); + assertGt(branch2DebtBefore, 0); + + uint256 boldBalBefore_A = boldToken.balanceOf(A); + + // Redeem + redeem(A, totalBoldRedeemAmount); + + // Confirm A lost no BOLD + assertEq( boldToken.balanceOf(A), boldBalBefore_A); + + // Confirm RETH branch did not get redeemed from + assertEq(contractsArray[2].activePool.getBoldDebt(), branch2DebtBefore); + } + + function testRedemptionOfWETHUsesETHUSDMarketforPrimaryPrice() public { + // Fetch price + wethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = wethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Check using primary + assertEq(uint8(wethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + uint256 coll = 100 ether; + uint256 debtRequest = 3000e18; + + vm.startPrank(A); + uint256 troveId = contractsArray[0].borrowerOperations.openTrove( + A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) + ); + + // Expected price used for primary calc: ETH-USD market price + uint256 expectedPrice = _getLatestAnswerFromOracle(ethOracle); + assertGt(expectedPrice, 0); + + // Calc expected fee based on price + uint256 totalBoldRedeemAmount = 100e18; + uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; + assertGt(totalCorrespondingColl, 0); + + uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) + * DECIMAL_PRECISION / totalBoldRedeemAmount; + assertGt(redemptionFeePct, 0); + + uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; + + uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; + assertGt(expectedCollDelta, 0); + + uint256 branch0DebtBefore = contractsArray[0].activePool.getBoldDebt(); + assertGt(branch0DebtBefore, 0); + uint256 A_collBefore = contractsArray[0].collToken.balanceOf(A); + assertGt(A_collBefore, 0); + // Redeem + redeem(A, totalBoldRedeemAmount); + + // Confirm WETH branch got redeemed from + assertEq(contractsArray[0].activePool.getBoldDebt(), branch0DebtBefore - totalBoldRedeemAmount); + + // Confirm the received amount coll is the expected amount (i.e. used the expected price) + assertEq(contractsArray[0].collToken.balanceOf(A), A_collBefore + expectedCollDelta); + } + + function testRedemptionOfWSTETHUsesMaxETHUSDMarketandWSTETHUSDMarketForPrimaryPrice() public { + // Fetch price + wstethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + uint256 coll = 100 ether; + uint256 debtRequest = 3000e18; + + vm.startPrank(A); + uint256 troveId = contractsArray[2].borrowerOperations.openTrove( + A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) + ); + + // Expected price used for primary calc: ETH-USD market price + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 stethUsdPrice = _getLatestAnswerFromOracle(stethOracle); + assertNotEq(ethUsdPrice, stethUsdPrice, "raw prices equal"); + + // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH + uint256 expectedPrice = LiquityMath._max(ethUsdPrice, stethUsdPrice) * wstETH.stEthPerToken() / 1e18; + assertGt(expectedPrice, 0, "expected price not 0"); + + // Calc expected fee based on price + uint256 totalBoldRedeemAmount = 100e18; + uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; + assertGt(totalCorrespondingColl, 0, "coll not 0"); + + uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) + * DECIMAL_PRECISION / totalBoldRedeemAmount; + assertGt(redemptionFeePct, 0, "fee not 0"); + + uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; + + uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; + assertGt(expectedCollDelta, 0, "delta not 0"); + + uint256 branch2DebtBefore = contractsArray[2].activePool.getBoldDebt(); + assertGt(branch2DebtBefore, 0); + uint256 A_collBefore = contractsArray[2].collToken.balanceOf(A); + assertGt(A_collBefore, 0); + + // Redeem + redeem(A, totalBoldRedeemAmount); + + // Confirm WSTETH branch got redeemed from + assertEq(contractsArray[2].activePool.getBoldDebt(), branch2DebtBefore - totalBoldRedeemAmount); + + // Confirm the received amount coll is the expected amount (i.e. used the expected price) + assertEq(contractsArray[2].collToken.balanceOf(A), A_collBefore + expectedCollDelta); + } + + function testRedemptionOfRETHUsesMaxCanonicalAndMarketforPrimaryPriceWhenWithin2pct() public { + // Fetch price + rethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Check using primary + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + uint256 coll = 100 ether; + uint256 debtRequest = 3000e18; + + vm.startPrank(A); + uint256 troveId = contractsArray[1].borrowerOperations.openTrove( + A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) + ); + + // Expected price used for primary calc: ETH-USD market price + uint256 canonicalRethRate = rethToken.getExchangeRate(); + uint256 marketRethPrice = _getLatestAnswerFromOracle(rethOracle); + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + assertNotEq(canonicalRethRate, marketRethPrice, "raw price and rate equal"); + + // Check market is within 2pct of max; + uint256 max = (1e18 + 2e16) * canonicalRethRate / 1e18; + uint256 min = (1e18 - 2e16) * canonicalRethRate / 1e18; + assertGe(marketRethPrice, min); + assertLe(marketRethPrice, max); + + // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH + uint256 expectedPrice = LiquityMath._max(canonicalRethRate, marketRethPrice) * ethUsdPrice / 1e18; + assertGt(expectedPrice, 0, "expected price not 0"); + + // Calc expected fee based on price + uint256 totalBoldRedeemAmount = 100e18; + uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; + assertGt(totalCorrespondingColl, 0, "coll not 0"); + + uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) + * DECIMAL_PRECISION / totalBoldRedeemAmount; + assertGt(redemptionFeePct, 0, "fee not 0"); + + uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; + + uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; + assertGt(expectedCollDelta, 0, "delta not 0"); + + uint256 branch1DebtBefore = contractsArray[1].activePool.getBoldDebt(); + assertGt(branch1DebtBefore, 0); + uint256 A_collBefore = contractsArray[1].collToken.balanceOf(A); + assertGt(A_collBefore, 0); + + // Redeem + redeem(A, totalBoldRedeemAmount); + + // Confirm RETH branch got redeemed from + assertEq(contractsArray[1].activePool.getBoldDebt(), branch1DebtBefore - totalBoldRedeemAmount); + + // Confirm the received amount coll is the expected amount (i.e. used the expected price) + assertEq(contractsArray[1].collToken.balanceOf(A), A_collBefore + expectedCollDelta); + } + // TODO: + + // Tests: + // , should we just block normal redemptions in shutdown mode? + // --- redemptions when branch shutdown use same price as nornmal ops --- + + // - redemptions of WETH use lastGoodPrice when price source switched to lastGoodPrice + // - redemptions of ... + // - More basic actions tests (adjust, close, etc) // - liq tests (manipulate aggregator stored price) } diff --git a/contracts/src/test/TestContracts/PriceFeedMock.sol b/contracts/src/test/TestContracts/PriceFeedMock.sol index 96a9371b8..b1b6f6de3 100644 --- a/contracts/src/test/TestContracts/PriceFeedMock.sol +++ b/contracts/src/test/TestContracts/PriceFeedMock.sol @@ -19,6 +19,10 @@ contract PriceFeedMock is IPriceFeedMock { return (PRICE, false); } + function fetchRedemptionPrice() external view returns (uint256, bool) { + return (PRICE, false); + } + function lastGoodPrice() external view returns (uint256) { return PRICE; } diff --git a/contracts/src/test/TestContracts/PriceFeedTestnet.sol b/contracts/src/test/TestContracts/PriceFeedTestnet.sol index 4e72a4968..cb61b3ddc 100644 --- a/contracts/src/test/TestContracts/PriceFeedTestnet.sol +++ b/contracts/src/test/TestContracts/PriceFeedTestnet.sol @@ -31,6 +31,13 @@ contract PriceFeedTestnet is IPriceFeedTestnet { return (_price, false); } + function fetchRedemptionPrice() external override returns (uint256, bool) { + // Fire an event just like the mainnet version would. + // This lets the subgraph rely on events to get the latest price even when developing locally. + emit LastGoodPriceUpdated(_price); + return (_price, false); + } + // Manual external price setter. function setPrice(uint256 price) external returns (bool) { _price = price; From a437ec8590d1c393fa08fc40f571eb6a8799cb0f Mon Sep 17 00:00:00 2001 From: RickGriff Date: Sun, 20 Oct 2024 20:11:17 +0100 Subject: [PATCH 12/19] Prevent two shutdown events when oracle fails in TCR shutdown call --- contracts/src/BorrowerOperations.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/contracts/src/BorrowerOperations.sol b/contracts/src/BorrowerOperations.sol index 41bc7330b..e4f5073a3 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -1162,8 +1162,11 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio uint256 totalColl = getEntireSystemColl(); uint256 totalDebt = getEntireSystemDebt(); - (uint256 price,) = priceFeed.fetchPrice(); + (uint256 price, bool newOracleFailureDetected) = priceFeed.fetchPrice(); + // If the oracle failed, the above call to PriceFeed will have shut this branch down + if (newOracleFailureDetected) {return;} + // Otherwise, proceed with the TCR check: uint256 TCR = LiquityMath._computeCR(totalColl, totalDebt, price); if (TCR >= SCR) revert TCRNotBelowSCR(); From d45f664371af6200e42cdc618e579db0b0508692 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Thu, 24 Oct 2024 01:41:35 +0100 Subject: [PATCH 13/19] Fix bug in 2pct comparison --- .../src/PriceFeeds/CompositePriceFeed.sol | 7 +- .../src/PriceFeeds/MainnetPriceFeedBase.sol | 4 - contracts/src/PriceFeeds/RETHPriceFeed.sol | 4 +- contracts/src/PriceFeeds/WETHPriceFeed.sol | 8 +- contracts/src/test/OracleMainnet.t.sol | 75 +++++++++++++++++-- .../TestContracts/ChainlinkOracleMock.sol | 16 +++- contracts/utils/assets/test_output/uris.html | 3 + 7 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 contracts/utils/assets/test_output/uris.html diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol index cf63200aa..b715bd431 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -105,7 +105,12 @@ contract CompositePriceFeed is MainnetPriceFeedBase { return bestPrice; } + // An individual Pricefeed instance implements _fetchPricePrimary according to the data sources it uses. Returns: + // - The price + // - A bool indicating whether a new oracle failure or exchange rate failure was detected in the call + function _fetchPricePrimary(bool _isRedemption) internal virtual returns (uint256, bool) {} + // Returns the LST exchange rate and a bool indicating whether the exchange rate failed to return a valid rate. // Implementation depends on the specific LST. - function _getCanonicalRate() internal view virtual returns (uint256, bool) {} + function _getCanonicalRate() internal view virtual returns (uint256, bool) {} } diff --git a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol index a63ad9881..dda11d58c 100644 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol @@ -59,10 +59,6 @@ abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { _renounceOwnership(); } - // An individual Pricefeed instance implements _fetchPricePrimary according to the data sources it uses. Returns: - // - The price - // - A bool indicating whether a new oracle failure or exchange rate failure was detected in the call - function _fetchPricePrimary(bool _isRedemption) internal virtual returns (uint256, bool) {} function _getOracleAnswer(Oracle memory _oracle) internal view returns (uint256, bool) { ChainlinkResponse memory chainlinkResponse = _getCurrentChainlinkResponse(_oracle.aggregator); diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol index dab9cc660..44d0fa329 100644 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/RETHPriceFeed.sol @@ -63,7 +63,7 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { rEthUsdPrice = LiquityMath._max(rEthUsdMarketPrice, rEthUsdCanonicalPrice); } else { // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. - // Assumes a deviation between market <> canonical of >2% represents a legitemate market price difference. + // Assumes a deviation between market <> canonical of >2% represents a legitimate market price difference. rEthUsdPrice = LiquityMath._min(rEthUsdMarketPrice, rEthUsdCanonicalPrice); } @@ -77,7 +77,7 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { uint256 max = _rEthUsdCanonicalPrice * (DECIMAL_PRECISION + _2_PERCENT) / 1e18; uint256 min = _rEthUsdCanonicalPrice * (DECIMAL_PRECISION - _2_PERCENT) / 1e18; - return _rEthUsdMarketPrice >= min && _rEthUsdCanonicalPrice <= max; + return _rEthUsdMarketPrice >= min && _rEthUsdMarketPrice <= max; } diff --git a/contracts/src/PriceFeeds/WETHPriceFeed.sol b/contracts/src/PriceFeeds/WETHPriceFeed.sol index c09d394b9..8576cc5d8 100644 --- a/contracts/src/PriceFeeds/WETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WETHPriceFeed.sol @@ -18,7 +18,7 @@ contract WETHPriceFeed is MainnetPriceFeedBase { function fetchPrice() public returns (uint256, bool) { // If branch is live and the primary oracle setup has been working, try to use it - if (priceSource == PriceSource.primary) return _fetchPricePrimary(false); + if (priceSource == PriceSource.primary) return _fetchPricePrimary(); // Otherwise if branch is shut down and already using the lastGoodPrice, continue with it assert(priceSource == PriceSource.lastGoodPrice); @@ -30,7 +30,11 @@ contract WETHPriceFeed is MainnetPriceFeedBase { return fetchPrice(); } - function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { + // _fetchPricePrimary returns: + // - The price + // - A bool indicating whether a new oracle failure was detected in the call + function _fetchPricePrimary(bool _isRedemption) internal virtual returns (uint256, bool) {} + function _fetchPricePrimary() internal returns (uint256, bool) { assert(priceSource == PriceSource.primary); (uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle); diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index 406ad5085..7f04c9573 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -1772,14 +1772,77 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[1].collToken.balanceOf(A), A_collBefore + expectedCollDelta); } - // TODO: + function testRedemptionOfRETHUsesMinCanonicalAndMarketforPrimaryPriceWhenDeviationGreaterThan2pct() public { + // Fetch price + rethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = rethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Check using primary + assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + uint256 coll = 100 ether; + uint256 debtRequest = 3000e18; + + vm.startPrank(A); + uint256 troveId = contractsArray[1].borrowerOperations.openTrove( + A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) + ); + vm.stopPrank(); + + // Replace the RETH Oracle's code with the mock oracle's code + vm.etch(address(rethOracle), address(mockOracle).code); + // Wrap so we can use the mock's price setter to manipulate the reth-eth price + ChainlinkOracleMock mock = ChainlinkOracleMock(address(rethOracle)); + // Set ETH_per_RETH market price to 0.5, and make it fresh + mock.setPriceAndStaleness(5e7, false); + + (,int256 price,,,) = rethOracle.latestRoundData(); + // Confirm that RETH oracle now returns the artificial low price + assertEq(price, 5e7, "reth-eth price not 0.5"); + + // // Expected price used for primary calc: ETH-USD market price + uint256 canonicalRethRate = rethToken.getExchangeRate(); + uint256 marketRethPrice = _getLatestAnswerFromOracle(rethOracle); + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + assertNotEq(canonicalRethRate, marketRethPrice, "raw price and rate equal"); + + // Check market is not within 2pct of canonical + uint256 min = (1e18 - 2e16) * canonicalRethRate / 1e18; + assertLe(marketRethPrice, min, "market reth-eth price not < min"); - // Tests: - // , should we just block normal redemptions in shutdown mode? - // --- redemptions when branch shutdown use same price as nornmal ops --- + // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH + uint256 expectedPrice = LiquityMath._min(canonicalRethRate, marketRethPrice) * ethUsdPrice / 1e18; + assertGt(expectedPrice, 0, "expected price not 0"); + + // Calc expected fee based on price, i.e. the minimum + uint256 totalBoldRedeemAmount = 1e18; + uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; + assertGt(totalCorrespondingColl, 0, "coll not 0"); + + uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) + * DECIMAL_PRECISION / totalBoldRedeemAmount; + assertGt(redemptionFeePct, 0, "fee not 0"); + + uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; + + uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; + assertGt(expectedCollDelta, 0, "delta not 0"); - // - redemptions of WETH use lastGoodPrice when price source switched to lastGoodPrice - // - redemptions of ... + uint256 branch1DebtBefore = contractsArray[1].activePool.getBoldDebt(); + assertGt(branch1DebtBefore, 0); + uint256 A_collBefore = contractsArray[1].collToken.balanceOf(A); + assertGt(A_collBefore, 0); + + // Redeem + redeem(A, totalBoldRedeemAmount); + + // Confirm RETH branch got redeemed from + assertEq(contractsArray[1].activePool.getBoldDebt(), branch1DebtBefore - totalBoldRedeemAmount, "active debt != branch - redeemed"); + + // Confirm the received amount coll is the expected amount (i.e. used the expected price) + assertEq(contractsArray[1].collToken.balanceOf(A), A_collBefore + expectedCollDelta, "A's coll didn't change" ); + } // - More basic actions tests (adjust, close, etc) // - liq tests (manipulate aggregator stored price) diff --git a/contracts/src/test/TestContracts/ChainlinkOracleMock.sol b/contracts/src/test/TestContracts/ChainlinkOracleMock.sol index 8242abca2..da715dbe5 100644 --- a/contracts/src/test/TestContracts/ChainlinkOracleMock.sol +++ b/contracts/src/test/TestContracts/ChainlinkOracleMock.sol @@ -6,6 +6,12 @@ import "../../Dependencies/AggregatorV3Interface.sol"; // Mock Chainlink oracle that returns a stale price answer contract ChainlinkOracleMock is AggregatorV3Interface { + // Default price of 2000 + int256 price = 2000e8; + + // Price is stale by default + bool stale = true; + function decimals() external pure returns (uint8) { return 8; } @@ -15,10 +21,14 @@ contract ChainlinkOracleMock is AggregatorV3Interface { view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { - // returns a 2000 USD price, but it's stale - int256 price = 2000e8; - uint256 lastUpdateTime = block.timestamp - 7 days; + // 7 days old if stale, otherwise current time + uint256 lastUpdateTime = stale ? block.timestamp - 7 days : block.timestamp; return (0, price, 0, lastUpdateTime, 0); } + + function setPriceAndStaleness(int256 _price, bool _isStale) external { + price = _price; + stale = _isStale; + } } diff --git a/contracts/utils/assets/test_output/uris.html b/contracts/utils/assets/test_output/uris.html new file mode 100644 index 000000000..10ddae5a1 --- /dev/null +++ b/contracts/utils/assets/test_output/uris.html @@ -0,0 +1,3 @@ +Test Uri From 87d848a8cf47c06d69b0e17de0601a10811af492 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Mon, 28 Oct 2024 16:30:46 +0000 Subject: [PATCH 14/19] Fix oracle redemption test --- contracts/src/test/OracleMainnet.t.sol | 184 +++++++++++------- .../TestContracts/ChainlinkOracleMock.sol | 34 ++-- 2 files changed, 136 insertions(+), 82 deletions(-) diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index 7f04c9573..25936beea 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -16,7 +16,7 @@ import "../Interfaces/IRETHToken.sol"; import "../Interfaces/IWSTETH.sol"; import "forge-std/Test.sol"; -import "forge-std/console2.sol"; +import "lib/forge-std/src/console2.sol"; contract OraclesMainnet is TestAccounts { AggregatorV3Interface ethOracle; @@ -45,8 +45,17 @@ contract OraclesMainnet is TestAccounts { uint256 decimals; } + struct Vars { + uint256 numCollaterals; + uint256 initialColl; + uint256 price; + uint256 coll; + uint256 debtRequest; + } + function setUp() public { vm.createSelectFork(vm.rpcUrl("mainnet")); + Vars memory vars; accounts = new Accounts(); createAccounts(); @@ -54,11 +63,11 @@ contract OraclesMainnet is TestAccounts { (A, B, C, D, E, F) = (accountsList[0], accountsList[1], accountsList[2], accountsList[3], accountsList[4], accountsList[5]); - uint256 numCollaterals = 3; + vars.numCollaterals = 3; TestDeployer.TroveManagerParams memory tmParams = TestDeployer.TroveManagerParams(150e16, 110e16, 110e16, 5e16, 10e16); TestDeployer.TroveManagerParams[] memory troveManagerParamsArray = - new TestDeployer.TroveManagerParams[](numCollaterals); + new TestDeployer.TroveManagerParams[](vars.numCollaterals); for (uint256 i = 0; i < troveManagerParamsArray.length; i++) { troveManagerParamsArray[i] = tmParams; } @@ -83,15 +92,15 @@ contract OraclesMainnet is TestAccounts { mockWstethToken = new WSTETHTokenMock(); // Record contracts - for (uint256 c = 0; c < numCollaterals; c++) { + for (uint256 c = 0; c < vars.numCollaterals; c++) { contractsArray.push(result.contractsArray[c]); } // Give all users all collaterals - uint256 initialColl = 1000_000e18; + vars.initialColl = 1000_000e18; for (uint256 i = 0; i < 6; i++) { - for (uint256 j = 0; j < numCollaterals; j++) { - deal(address(contractsArray[j].collToken), accountsList[i], initialColl); + for (uint256 j = 0; j < vars.numCollaterals; j++) { + deal(address(contractsArray[j].collToken), accountsList[i], vars.initialColl); vm.startPrank(accountsList[i]); // Approve all Borrower Ops to use the user's WETH funds contractsArray[0].collToken.approve(address(contractsArray[j].borrowerOperations), type(uint256).max); @@ -130,6 +139,41 @@ contract OraclesMainnet is TestAccounts { vm.stopPrank(); } + function etchStaleMockToEthOracle(bytes memory _mockOracleCode) internal { + // Etch the mock code to the ETH-USD oracle address + vm.etch(address(ethOracle), _mockOracleCode); + ChainlinkOracleMock mock = ChainlinkOracleMock(address(ethOracle)); + mock.setDecimals(8); + // Fake ETH-USD price of 2000 USD + mock.setPrice(2000e8); + // Make it stale + mock.setUpdatedAt(block.timestamp - 7 days); + } + + function etchStaleMockToRethOracle(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 + ChainlinkOracleMock mock = ChainlinkOracleMock(address(rethOracle)); + mock.setDecimals(18); + // Set 1 RETH = 1 ETH + mock.setPrice(1e18); + // Make it stale + mock.setUpdatedAt(block.timestamp - 7 days); + } + + function etchStaleMockToStethOracle(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 + ChainlinkOracleMock mock = ChainlinkOracleMock(address(stethOracle)); + mock.setDecimals(8); + // Set 1 STETH = 2000 USD + mock.setPrice(2000e8); + // Make it stale + mock.setUpdatedAt(block.timestamp - 7 days); + } + // --- lastGoodPrice set on deployment --- function testSetLastGoodPriceOnDeploymentWETH() public view { @@ -302,41 +346,38 @@ contract OraclesMainnet is TestAccounts { function testManipulatedChainlinkReturnsStalePrice() public { // Replace the ETH Oracle's code with the mock oracle's code that returns a stale price - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); - console2.log(updatedAt); - console2.log(block.timestamp); - // Confirm it's stale assertEq(updatedAt, block.timestamp - 7 days); } function testManipulatedChainlinkReturns2kUsdPrice() public { // Replace the ETH Oracle's code with the mock oracle's code that returns a stale price - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); uint256 price = _getLatestAnswerFromOracle(ethOracle); assertEq(price, 2000e18); } function testOpenTroveWETHWithStalePriceReverts() public { - vm.etch(address(ethOracle), address(mockOracle).code); + Vars memory vars; + etchStaleMockToEthOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); assertFalse(contractsArray[0].borrowerOperations.hasBeenShutDown()); - uint256 price = _getLatestAnswerFromOracle(ethOracle); - - uint256 coll = 5 ether; - uint256 debtRequest = coll * price / 2 / 1e18; + vars.price = _getLatestAnswerFromOracle(ethOracle); + vars.coll = 5 ether; + vars.debtRequest = vars.coll * vars.price / 2 / 1e18; vm.startPrank(A); vm.expectRevert(BorrowerOperations.NewOracleFailureDetected.selector); contractsArray[0].borrowerOperations.openTrove( - A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) + A, 0, vars.coll, vars.debtRequest, 0, 0, 5e16, vars.debtRequest, address(0), address(0), address(0) ); } @@ -356,7 +397,7 @@ contract OraclesMainnet is TestAccounts { assertEq(trovesCount, 1); // Replace oracle with a stale oracle - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -366,7 +407,7 @@ contract OraclesMainnet is TestAccounts { } function testOpenTroveWSTETHWithStalePriceReverts() public { - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -400,7 +441,7 @@ contract OraclesMainnet is TestAccounts { assertEq(trovesCount, 1); // Replace oracle with a stale oracle - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -411,7 +452,7 @@ contract OraclesMainnet is TestAccounts { function testOpenTroveRETHWithStaleRETHPriceReverts() public { // Make only RETH oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -449,7 +490,7 @@ contract OraclesMainnet is TestAccounts { assertEq(trovesCount, 1); // Make only RETH oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -460,7 +501,7 @@ contract OraclesMainnet is TestAccounts { function testOpenTroveRETHWithStaleETHPriceReverts() public { // Make only ETH oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -498,7 +539,7 @@ contract OraclesMainnet is TestAccounts { assertEq(trovesCount, 1); // Make only ETH oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -521,7 +562,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[0].troveManager.shutdownTime(), 0); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -542,7 +583,7 @@ contract OraclesMainnet is TestAccounts { assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); assertGt(mockPrice, 0, "mockPrice 0"); @@ -576,7 +617,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[1].troveManager.shutdownTime(), 0); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -623,7 +664,7 @@ contract OraclesMainnet is TestAccounts { assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); assertGt(mockPrice, 0, "mockPrice 0"); @@ -673,7 +714,7 @@ contract OraclesMainnet is TestAccounts { assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -698,7 +739,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[1].troveManager.shutdownTime(), 0); // Make the RETH-ETH oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -714,7 +755,7 @@ contract OraclesMainnet is TestAccounts { function testFetchPriceReturnsMinETHUSDxCanonicalAndLastGoodPriceWhenRETHETHOracleFails() public { // Make the RETH-ETH oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -744,7 +785,7 @@ contract OraclesMainnet is TestAccounts { assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); // Make the RETH-ETH oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -759,7 +800,7 @@ contract OraclesMainnet is TestAccounts { function testRETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { // Make the RETH-USD oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -782,7 +823,7 @@ contract OraclesMainnet is TestAccounts { uint256 lastGoodPrice = rethPriceFeed.lastGoodPrice(); // Make the ETH-USD oracle stale too - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -808,7 +849,7 @@ contract OraclesMainnet is TestAccounts { function testRETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenExchangeRateFails() public { // Make the RETH-USD oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -860,7 +901,7 @@ contract OraclesMainnet is TestAccounts { function testRETHWhenUsingETHUSDxCanonicalReturnsMinOfLastGoodPriceAndETHUSDxCanonical() public { // Make the RETH-ETH oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -925,12 +966,12 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[1].troveManager.shutdownTime(), 0); // Make the RETH-ETH oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Make the ETH-USD oracle stale too - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -951,12 +992,12 @@ contract OraclesMainnet is TestAccounts { assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Make the RETH-ETH oracle stale too - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (, mockPrice,, updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -983,12 +1024,12 @@ contract OraclesMainnet is TestAccounts { assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Make the RETH-ETH oracle stale too - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (, mockPrice,, updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1060,7 +1101,7 @@ contract OraclesMainnet is TestAccounts { assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); assertGt(mockPrice, 0, "mockPrice 0"); @@ -1083,7 +1124,7 @@ contract OraclesMainnet is TestAccounts { uint256 lastGoodPriceBeforeFail = wstethPriceFeed.lastGoodPrice(); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); assertGt(mockPrice, 0, "mockPrice 0"); @@ -1111,7 +1152,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[2].troveManager.shutdownTime(), 0); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1136,7 +1177,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[2].troveManager.shutdownTime(), 0); // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1152,7 +1193,7 @@ contract OraclesMainnet is TestAccounts { function testFetchPriceReturnsMinETHUSDxCanonicalAndLastGoodPriceWhenSTETHUSDOracleFails() public { // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1179,7 +1220,7 @@ contract OraclesMainnet is TestAccounts { assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1195,7 +1236,7 @@ contract OraclesMainnet is TestAccounts { function testSTETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenETHUSDOracleFails() public { // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1214,7 +1255,7 @@ contract OraclesMainnet is TestAccounts { uint256 lastGoodPrice = wstethPriceFeed.lastGoodPrice(); // Make the ETH-USD oracle stale too - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1243,7 +1284,7 @@ contract OraclesMainnet is TestAccounts { function testSTETHWhenUsingETHUSDxCanonicalSwitchesToLastGoodPriceWhenExchangeRateFails() public { // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1295,7 +1336,7 @@ contract OraclesMainnet is TestAccounts { function testSTETHWhenUsingETHUSDxCanonicalRemainsShutDownWhenETHUSDOracleFails() public { // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1318,7 +1359,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); // Make the ETH-USD oracle stale too - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1334,7 +1375,7 @@ contract OraclesMainnet is TestAccounts { function testSTETHWhenUsingETHUSDxCanonicalReturnsMinOfLastGoodPriceAndETHUSDxCanonical() public { // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1398,12 +1439,12 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[2].troveManager.shutdownTime(), 0); // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Make the ETH-USD oracle stale too - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1424,12 +1465,12 @@ contract OraclesMainnet is TestAccounts { assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Make the ETH-USD oracle stale too - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1456,12 +1497,12 @@ contract OraclesMainnet is TestAccounts { assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (, int256 mockPrice,, uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); // Make the ETH-USD oracle stale too - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (, mockPrice,, updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1492,7 +1533,7 @@ contract OraclesMainnet is TestAccounts { ); // Make the ETH-USD oracle stale - vm.etch(address(ethOracle), address(mockOracle).code); + etchStaleMockToEthOracle(address(mockOracle).code); (,,,uint256 updatedAt,) = ethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1536,7 +1577,7 @@ contract OraclesMainnet is TestAccounts { ); // Make the RETH-ETH oracle stale - vm.etch(address(rethOracle), address(mockOracle).code); + etchStaleMockToRethOracle(address(mockOracle).code); (,,,uint256 updatedAt,) = rethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1580,7 +1621,7 @@ contract OraclesMainnet is TestAccounts { ); // Make the STETH-USD oracle stale - vm.etch(address(stethOracle), address(mockOracle).code); + etchStaleMockToStethOracle(address(mockOracle).code); (,,,uint256 updatedAt,) = stethOracle.latestRoundData(); assertEq(updatedAt, block.timestamp - 7 days); @@ -1791,15 +1832,18 @@ contract OraclesMainnet is TestAccounts { vm.stopPrank(); // Replace the RETH Oracle's code with the mock oracle's code - vm.etch(address(rethOracle), address(mockOracle).code); - // Wrap so we can use the mock's price setter to manipulate the reth-eth price + etchStaleMockToRethOracle(address(mockOracle).code); ChainlinkOracleMock mock = ChainlinkOracleMock(address(rethOracle)); - // Set ETH_per_RETH market price to 0.5, and make it fresh - mock.setPriceAndStaleness(5e7, false); + // Set ETH_per_RETH market price to 0.95 + mock.setPrice(95e16); + // Make it fresh + mock.setUpdatedAt(block.timestamp); + // RETH-ETH price has 18 decimals + mock.setDecimals(18); (,int256 price,,,) = rethOracle.latestRoundData(); // Confirm that RETH oracle now returns the artificial low price - assertEq(price, 5e7, "reth-eth price not 0.5"); + assertEq(price, 95e16, "reth-eth price not 0.95"); // // Expected price used for primary calc: ETH-USD market price uint256 canonicalRethRate = rethToken.getExchangeRate(); @@ -1816,7 +1860,7 @@ contract OraclesMainnet is TestAccounts { assertGt(expectedPrice, 0, "expected price not 0"); // Calc expected fee based on price, i.e. the minimum - uint256 totalBoldRedeemAmount = 1e18; + uint256 totalBoldRedeemAmount = 100e18; uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; assertGt(totalCorrespondingColl, 0, "coll not 0"); diff --git a/contracts/src/test/TestContracts/ChainlinkOracleMock.sol b/contracts/src/test/TestContracts/ChainlinkOracleMock.sol index da715dbe5..b859318e5 100644 --- a/contracts/src/test/TestContracts/ChainlinkOracleMock.sol +++ b/contracts/src/test/TestContracts/ChainlinkOracleMock.sol @@ -4,16 +4,19 @@ pragma solidity 0.8.24; import "../../Dependencies/AggregatorV3Interface.sol"; -// Mock Chainlink oracle that returns a stale price answer +// 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 { - // Default price of 2000 - int256 price = 2000e8; + uint8 decimal; + + int256 price; - // Price is stale by default - bool stale = true; + uint256 lastUpdateTime; - function decimals() external pure returns (uint8) { - return 8; + // We use 8 decimals unless set to 18 + function decimals() external view returns (uint8) { + return decimal; } function latestRoundData() @@ -21,14 +24,21 @@ contract ChainlinkOracleMock is AggregatorV3Interface { view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) { - // 7 days old if stale, otherwise current time - uint256 lastUpdateTime = stale ? block.timestamp - 7 days : block.timestamp; - + // console2.log(lastUpdateTime, "lastUpdateTime"); + // console2.log(block.timestamp, "block.timestamp"); + // console2.log(price, "price returned by oracle"); return (0, price, 0, lastUpdateTime, 0); } - function setPriceAndStaleness(int256 _price, bool _isStale) external { + function setDecimals(uint8 _decimals) external { + decimal = _decimals; + } + + function setPrice(int256 _price) external { price = _price; - stale = _isStale; + } + + function setUpdatedAt(uint256 _updatedAt) external { + lastUpdateTime = _updatedAt; } } From 79cab00eeaa44d0671ca08f620a12ce3f60165ed Mon Sep 17 00:00:00 2001 From: RickGriff Date: Mon, 4 Nov 2024 22:49:39 +0000 Subject: [PATCH 15/19] Add threshold condition to WSTETH redemption pricing --- .../src/PriceFeeds/CompositePriceFeed.sol | 11 +++ contracts/src/PriceFeeds/RETHPriceFeed.sol | 13 +-- contracts/src/PriceFeeds/WSTETHPriceFeed.sol | 8 +- contracts/src/test/OracleMainnet.t.sol | 86 ++++++++++++++++++- 4 files changed, 104 insertions(+), 14 deletions(-) diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol index b715bd431..654f6476e 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -105,6 +105,17 @@ contract CompositePriceFeed is MainnetPriceFeedBase { return bestPrice; } + function _withinDeviationThreshold(uint256 _priceToCheck, uint256 _referencePrice, uint256 _deviationThreshold) internal pure returns (bool) { + // Calculate the price deviation of the oracle market price relative to the canonical price + uint256 max = _referencePrice * (DECIMAL_PRECISION + _deviationThreshold) / 1e18; + uint256 min = _referencePrice * (DECIMAL_PRECISION - _deviationThreshold) / 1e18; + + console2.log(_priceToCheck, "_priceToCheck"); + console2.log(_referencePrice, "_referencePrice"); + console2.log(_priceToCheck >= min && _priceToCheck <= max, "is within threshold"); + return _priceToCheck >= min && _priceToCheck <= max; + } + // An individual Pricefeed instance implements _fetchPricePrimary according to the data sources it uses. Returns: // - The price // - A bool indicating whether a new oracle failure or exchange rate failure was detected in the call diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol index 44d0fa329..3a949f29f 100644 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/RETHPriceFeed.sol @@ -30,7 +30,7 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { Oracle public rEthEthOracle; - uint256 constant public _2_PERCENT = 2e16; + uint256 constant public RETH_ETH_DEVIATION_THRESHOLD = 2e16; // 2% function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) { assert(priceSource == PriceSource.primary); @@ -59,7 +59,7 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { uint256 rEthUsdPrice; // If it's a redemption and canonical is within 2% of market, use the max to mitigate unwanted redemption oracle arb - if (_isRedemption && _within2pct(rEthUsdMarketPrice, rEthUsdCanonicalPrice)) { + if (_isRedemption && _withinDeviationThreshold(rEthUsdMarketPrice, rEthUsdCanonicalPrice, RETH_ETH_DEVIATION_THRESHOLD)) { rEthUsdPrice = LiquityMath._max(rEthUsdMarketPrice, rEthUsdCanonicalPrice); } else { // Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. @@ -72,15 +72,6 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { return (rEthUsdPrice, false); } - function _within2pct(uint256 _rEthUsdMarketPrice, uint256 _rEthUsdCanonicalPrice) internal pure returns (bool) { - // Calculate the price deviation of the oracle market price relative to the canonical price - uint256 max = _rEthUsdCanonicalPrice * (DECIMAL_PRECISION + _2_PERCENT) / 1e18; - uint256 min = _rEthUsdCanonicalPrice * (DECIMAL_PRECISION - _2_PERCENT) / 1e18; - - return _rEthUsdMarketPrice >= min && _rEthUsdMarketPrice <= max; - } - - function _getCanonicalRate() internal view override returns (uint256, bool) { try IRETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerReth) { // If rate is 0, return true diff --git a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol index 78b684b4a..b215b5fa7 100644 --- a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol @@ -6,9 +6,13 @@ import "./CompositePriceFeed.sol"; import "../Interfaces/IWSTETH.sol"; import "../Interfaces/IWSTETHPriceFeed.sol"; +// import "forge-std/console2.sol"; + contract WSTETHPriceFeed is CompositePriceFeed, IWSTETHPriceFeed { Oracle public stEthUsdOracle; + uint256 constant public STETH_USD_DEVIATION_THRESHOLD = 1e16; // 1% + constructor( address _owner, address _ethUsdOracleAddress, @@ -48,8 +52,8 @@ contract WSTETHPriceFeed is CompositePriceFeed, IWSTETHPriceFeed { // Otherwise, use the primary price calculation: uint256 wstEthUsdPrice; - if (_isRedemption) { - // If it's a redemption, take the max of (STETH-USD, ETH-USD) and convert to WSTETH-USD + if (_isRedemption && _withinDeviationThreshold(stEthUsdPrice, ethUsdPrice, STETH_USD_DEVIATION_THRESHOLD)) { + // If it's a redemption and within 1%, take the max of (STETH-USD, ETH-USD) to mitigate unwanted redemption arb and convert to WSTETH-USD wstEthUsdPrice = LiquityMath._max(stEthUsdPrice, ethUsdPrice) * stEthPerWstEth / 1e18; } else { // Otherwise, just calculate WSTETH-USD price: USD_per_WSTETH = USD_per_STETH * STETH_per_WSTETH diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index 25936beea..90ae022fa 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -1696,7 +1696,7 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[0].collToken.balanceOf(A), A_collBefore + expectedCollDelta); } - function testRedemptionOfWSTETHUsesMaxETHUSDMarketandWSTETHUSDMarketForPrimaryPrice() public { + function testRedemptionOfWSTETHUsesMaxETHUSDMarketandWSTETHUSDMarketForPrimaryPriceWhenWithin1pct() public { // Fetch price wstethPriceFeed.fetchPrice(); uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); @@ -1717,6 +1717,11 @@ contract OraclesMainnet is TestAccounts { uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); uint256 stethUsdPrice = _getLatestAnswerFromOracle(stethOracle); assertNotEq(ethUsdPrice, stethUsdPrice, "raw prices equal"); + // Check STETH-USD is within 1ct of ETH-USD + uint256 max = (1e18 + 1e16) * ethUsdPrice / 1e18; + uint256 min = (1e18 - 1e16) * ethUsdPrice / 1e18; + assertGe(stethUsdPrice, min); + assertLe(stethUsdPrice, max); // USD_per_WSTETH = USD_per_STETH(or_per_ETH) * STETH_per_WSTETH uint256 expectedPrice = LiquityMath._max(ethUsdPrice, stethUsdPrice) * wstETH.stEthPerToken() / 1e18; @@ -1751,6 +1756,85 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[2].collToken.balanceOf(A), A_collBefore + expectedCollDelta); } + function testRedemptionOfWSTETHUsesMinETHUSDMarketandWSTETHUSDMarketForPrimaryPriceWhenNotWithin1pct() public { + // Fetch price + console.log("test::first wsteth pricefeed call"); + wstethPriceFeed.fetchPrice(); + uint256 lastGoodPrice1 = wstethPriceFeed.lastGoodPrice(); + assertGt(lastGoodPrice1, 0, "lastGoodPrice 0"); + + // Check using primary + assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.primary)); + + uint256 coll = 100 ether; + uint256 debtRequest = 3000e18; + + vm.startPrank(A); + uint256 troveId = contractsArray[2].borrowerOperations.openTrove( + A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) + ); + + // Get the raw ETH-USD price (at 8 decimals) for comparison + (,int256 rawEthUsdPrice,,,) = ethOracle.latestRoundData(); + assertGt(rawEthUsdPrice, 0, "eth-usd price not 0"); + + // Replace the STETH-USD Oracle's code with the mock oracle's code + etchStaleMockToStethOracle(address(mockOracle).code); + ChainlinkOracleMock mock = ChainlinkOracleMock(address(stethOracle)); + // Reduce STETH-USD price to 90% of ETH-USD price. Use 8 decimal precision on the oracle. + mock.setPrice(int256(rawEthUsdPrice * 90e6 / 1e8)); + // Make it fresh + mock.setUpdatedAt(block.timestamp); + // STETH-USD price has 8 decimals + mock.setDecimals(8); + + assertEq(contractsArray[2].troveManager.shutdownTime(), 0, "is shutdown"); + + uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); + uint256 stethUsdPrice = _getLatestAnswerFromOracle(stethOracle); + console.log(stethUsdPrice, "test stehUsdPrice after replacement"); + console.log(ethUsdPrice, "test ethUsdPrice after replacement"); + console.log(ethUsdPrice * 90e16 / 1e18, "test ethUsdPrice * 90e16 / 1e18"); + + // Confirm that STETH-USD is lower than ETH-USD + assertLt(stethUsdPrice, ethUsdPrice, "steth-usd not < eth-usd"); + + // USD_per_STETH = USD_per_STETH * STETH_per_WSTETH + // Use STETH-USD as expected price since it is out of range of ETH-USD + uint256 expectedPrice = stethUsdPrice * wstETH.stEthPerToken() / 1e18; + assertGt(expectedPrice, 0, "expected price not 0"); + + // Calc expected fee based on price + uint256 totalBoldRedeemAmount = 100e18; + uint256 totalCorrespondingColl = totalBoldRedeemAmount * DECIMAL_PRECISION / expectedPrice; + assertGt(totalCorrespondingColl, 0, "coll not 0"); + + uint256 redemptionFeePct = collateralRegistry.getEffectiveRedemptionFeeInBold(totalBoldRedeemAmount) + * DECIMAL_PRECISION / totalBoldRedeemAmount; + assertGt(redemptionFeePct, 0, "fee not 0"); + + uint256 totalCollFee = totalCorrespondingColl * redemptionFeePct / DECIMAL_PRECISION; + + uint256 expectedCollDelta = totalCorrespondingColl - totalCollFee; + assertGt(expectedCollDelta, 0, "delta not 0"); + + uint256 branch2DebtBefore = contractsArray[2].activePool.getBoldDebt(); + assertGt(branch2DebtBefore, 0); + uint256 A_collBefore = contractsArray[2].collToken.balanceOf(A); + assertGt(A_collBefore, 0); + + // Redeem + redeem(A, totalBoldRedeemAmount); + + assertEq(contractsArray[2].troveManager.shutdownTime(), 0, "is shutdown"); + + // Confirm WSTETH branch got redeemed from + assertEq(contractsArray[2].activePool.getBoldDebt(), branch2DebtBefore - totalBoldRedeemAmount, "remaining branch debt wrong"); + + // Confirm the received amount coll is the expected amount (i.e. used the expected price) + assertEq(contractsArray[2].collToken.balanceOf(A), A_collBefore + expectedCollDelta, "remaining branch coll wrong"); + } + function testRedemptionOfRETHUsesMaxCanonicalAndMarketforPrimaryPriceWhenWithin2pct() public { // Fetch price rethPriceFeed.fetchPrice(); From 68486579f6e10876e0120aa931279390afdd0d87 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 5 Nov 2024 01:06:57 +0000 Subject: [PATCH 16/19] Add gas checks for external calls --- .../src/PriceFeeds/CompositePriceFeed.sol | 3 --- .../src/PriceFeeds/MainnetPriceFeedBase.sol | 10 +++++++- contracts/src/PriceFeeds/RETHPriceFeed.sol | 7 ++++++ contracts/src/PriceFeeds/WSTETHPriceFeed.sol | 10 +++++++- contracts/src/test/OracleMainnet.t.sol | 23 +++++++++++++++++++ 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol index 654f6476e..443dc23d6 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -110,9 +110,6 @@ contract CompositePriceFeed is MainnetPriceFeedBase { uint256 max = _referencePrice * (DECIMAL_PRECISION + _deviationThreshold) / 1e18; uint256 min = _referencePrice * (DECIMAL_PRECISION - _deviationThreshold) / 1e18; - console2.log(_priceToCheck, "_priceToCheck"); - console2.log(_referencePrice, "_referencePrice"); - console2.log(_priceToCheck >= min && _priceToCheck <= max, "is within threshold"); return _priceToCheck >= min && _priceToCheck <= max; } diff --git a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol index dda11d58c..8d03694b4 100644 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol @@ -37,6 +37,7 @@ abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { bool success; } + error InsufficientGasForExternalCall(); event ShutDownFromOracleFailure(address _failedOracleAddr); Oracle public ethUsdOracle; @@ -90,7 +91,9 @@ abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { view returns (ChainlinkResponse memory chainlinkResponse) { - // Secondly, try to get latest price data: + uint256 gasBefore = gasleft(); + + // Try to get latest price data: try _aggregator.latestRoundData() returns ( uint80 roundId, int256 answer, uint256, /* startedAt */ uint256 updatedAt, uint80 /* answeredInRound */ ) { @@ -102,6 +105,11 @@ abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { return chainlinkResponse; } catch { + // Require that enough gas was provided to prevent an OOG revert in the call to Chainlink + // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used + // in the check itself. + if (gasleft() <= gasBefore / 64) {revert InsufficientGasForExternalCall();} + // If call to Chainlink aggregator reverts, return a zero response with success = false return chainlinkResponse; } diff --git a/contracts/src/PriceFeeds/RETHPriceFeed.sol b/contracts/src/PriceFeeds/RETHPriceFeed.sol index 3a949f29f..9fe803829 100644 --- a/contracts/src/PriceFeeds/RETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/RETHPriceFeed.sol @@ -73,12 +73,19 @@ contract RETHPriceFeed is CompositePriceFeed, IRETHPriceFeed { } function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + try IRETHToken(rateProviderAddress).getExchangeRate() returns (uint256 ethPerReth) { // If rate is 0, return true if (ethPerReth == 0) return (0, true); return (ethPerReth, false); } catch { + // Require that enough gas was provided to prevent an OOG revert in the external call + // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used + // in the check itself. + if (gasleft() <= gasBefore / 64) {revert InsufficientGasForExternalCall();} + // If call to exchange rate reverts, return true return (0, true); } diff --git a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol index b215b5fa7..2e08ba847 100644 --- a/contracts/src/PriceFeeds/WSTETHPriceFeed.sol +++ b/contracts/src/PriceFeeds/WSTETHPriceFeed.sol @@ -66,14 +66,22 @@ contract WSTETHPriceFeed is CompositePriceFeed, IWSTETHPriceFeed { } function _getCanonicalRate() internal view override returns (uint256, bool) { + uint256 gasBefore = gasleft(); + try IWSTETH(rateProviderAddress).stEthPerToken() returns (uint256 stEthPerWstEth) { // If rate is 0, return true if (stEthPerWstEth == 0) return (0, true); return (stEthPerWstEth, false); } catch { - // If call to exchange rate reverts, return true + // Require that enough gas was provided to prevent an OOG revert in the external call + // causing a shutdown. Instead, just revert. Slightly conservative, as it includes gas used + // in the check itself. + if (gasleft() <= gasBefore / 64) {revert InsufficientGasForExternalCall();} + + // If call to exchange rate reverted for another reason, return true return (0, true); } + } } diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index 90ae022fa..b8264a386 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -2,6 +2,11 @@ pragma solidity 0.8.24; +import "../PriceFeeds/WSTETHPriceFeed.sol"; +import "../PriceFeeds/MainnetPriceFeedBase.sol"; +import "../PriceFeeds/RETHPriceFeed.sol"; +import "../PriceFeeds/WETHPriceFeed.sol"; + import "./TestContracts/Accounts.sol"; import "./TestContracts/ChainlinkOracleMock.sol"; import "./TestContracts/RETHTokenMock.sol"; @@ -1972,6 +1977,24 @@ contract OraclesMainnet is TestAccounts { assertEq(contractsArray[1].collToken.balanceOf(A), A_collBefore + expectedCollDelta, "A's coll didn't change" ); } + // --- Low gas reverts --- + +// --- Call these functions with 10k gas - i.e. enough to run out of gas in the Chainlink calls --- +function testRevertLowGasWSTETH() public { + vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); + address(wstethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); +} + +function testRevertLowGasRETH() public { + vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); + address(rethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); +} + +function testRevertLowGasWETH() public { + vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); + address(wethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); +} + // - More basic actions tests (adjust, close, etc) // - liq tests (manipulate aggregator stored price) } From eb41c9c8beff31edf33d1c077dc305100554f9c0 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 5 Nov 2024 01:16:36 +0000 Subject: [PATCH 17/19] Make composite feed abstract --- contracts/src/PriceFeeds/CompositePriceFeed.sol | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/contracts/src/PriceFeeds/CompositePriceFeed.sol b/contracts/src/PriceFeeds/CompositePriceFeed.sol index 443dc23d6..1278a0f24 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -5,20 +5,11 @@ pragma solidity 0.8.24; import "../Dependencies/LiquityMath.sol"; import "./MainnetPriceFeedBase.sol"; -<<<<<<< HEAD -// Composite PriceFeed: outputs an LST-USD price derived from two external price Oracles: LST-ETH, and ETH-USD. -// Used where the LST token is non-rebasing (as per rETH, osETH, ETHx, etc). -contract CompositePriceFeed is MainnetPriceFeedBase, ICompositePriceFeed { - Oracle public lstEthOracle; - Oracle public ethUsdOracle; - -======= // import "forge-std/console2.sol"; // The CompositePriceFeed is used for feeds that incorporate both a market price oracle (e.g. STETH-USD, or RETH-ETH) // and an LST canonical rate (e.g. WSTETH:STETH, or RETH:ETH). -contract CompositePriceFeed is MainnetPriceFeedBase { ->>>>>>> 81a1a2aa (Add ETH-USD fallback logic and remove OSETH and ETHX contracts) +abstract contract CompositePriceFeed is MainnetPriceFeedBase { address public rateProviderAddress; constructor( From bdb6492edd2d9ec10b492ecfe246be021b4d8710 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 5 Nov 2024 23:44:10 +0000 Subject: [PATCH 18/19] Update sol versions and cleanup --- contracts/src/PriceFeeds/ETHXPriceFeed.sol | 38 ------------------- .../src/PriceFeeds/MainnetPriceFeedBase.sol | 14 +++---- contracts/src/PriceFeeds/OSETHPriceFeed.sol | 31 --------------- .../src/test/TestContracts/RETHTokenMock.sol | 2 +- .../test/TestContracts/WSTETHTokenMock.sol | 2 +- contracts/utils/assets/test_output/uris.html | 2 +- 6 files changed, 8 insertions(+), 81 deletions(-) delete mode 100644 contracts/src/PriceFeeds/ETHXPriceFeed.sol delete mode 100644 contracts/src/PriceFeeds/OSETHPriceFeed.sol diff --git a/contracts/src/PriceFeeds/ETHXPriceFeed.sol b/contracts/src/PriceFeeds/ETHXPriceFeed.sol deleted file mode 100644 index 891b048c5..000000000 --- a/contracts/src/PriceFeeds/ETHXPriceFeed.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./CompositePriceFeed.sol"; -import "../Dependencies/IStaderOracle.sol"; - -contract ETHXPriceFeed is CompositePriceFeed { - constructor( - address _owner, - address _ethUsdOracleAddress, - address _lstEthOracleAddress, - address _rateProviderAddress, - uint256 _ethUsdStalenessThreshold, - uint256 _lstEthStalenessThreshold - ) - CompositePriceFeed( - _owner, - _ethUsdOracleAddress, - _lstEthOracleAddress, - _rateProviderAddress, - _ethUsdStalenessThreshold, - _lstEthStalenessThreshold - ) - {} - - function _getCanonicalRate() internal view override returns (uint256) { - // StaderOracle returns ETH balance and ETHX supply each with 18 digit decimal precision - - ( - , // uint256 reportingBlockNumber - uint256 ethBalance, - uint256 ethXSupply - ) = IStaderOracle(rateProviderAddress).exchangeRate(); - - return ethBalance * 1e18 / ethXSupply; - } -} diff --git a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol index 8d03694b4..fcb8beee8 100644 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol @@ -7,19 +7,15 @@ import "../Dependencies/AggregatorV3Interface.sol"; import "../Interfaces/IMainnetPriceFeed.sol"; import "../BorrowerOperations.sol"; -<<<<<<< HEAD -abstract contract MainnetPriceFeedBase is IPriceFeed, Ownable { - // Flag raised when the collateral branch gets shut down. - bool priceFeedDisabled; -======= // import "forge-std/console2.sol"; abstract contract MainnetPriceFeedBase is IMainnetPriceFeed, Ownable { - // Dummy flag raised when the collateral branch gets shut down. - // Should be removed after actual shutdown logic is implemented. - + + // Determines where the PriceFeed sources data from. Possible states: + // - primary: Uses the primary price calcuation, which depends on the specific feed + // - ETHUSDxCanonical: Uses Chainlink's ETH-USD multiplied by the LST' canonical rate + // - lastGoodPrice: the last good price recorded by this PriceFeed. PriceSource public priceSource; ->>>>>>> 81a1a2aa (Add ETH-USD fallback logic and remove OSETH and ETHX contracts) // Last good price tracker for the derived USD price uint256 public lastGoodPrice; diff --git a/contracts/src/PriceFeeds/OSETHPriceFeed.sol b/contracts/src/PriceFeeds/OSETHPriceFeed.sol deleted file mode 100644 index ef3fb2a7b..000000000 --- a/contracts/src/PriceFeeds/OSETHPriceFeed.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.24; - -import "./CompositePriceFeed.sol"; -import "../Dependencies/IOsTokenVaultController.sol"; - -contract OSETHPriceFeed is CompositePriceFeed { - constructor( - address _owner, - address _ethUsdOracleAddress, - address _lstEthOracleAddress, - address _rateProviderAddress, - uint256 _ethUsdStalenessThreshold, - uint256 _lstEthStalenessThreshold - ) - CompositePriceFeed( - _owner, - _ethUsdOracleAddress, - _lstEthOracleAddress, - _rateProviderAddress, - _ethUsdStalenessThreshold, - _lstEthStalenessThreshold - ) - {} - - function _getCanonicalRate() internal view override returns (uint256) { - // OsTokenVaultController returns rate with 18 digit decimal precision - return IOsTokenVaultController(rateProviderAddress).convertToAssets(1e18); - } -} diff --git a/contracts/src/test/TestContracts/RETHTokenMock.sol b/contracts/src/test/TestContracts/RETHTokenMock.sol index fc7b87015..0df7491bb 100644 --- a/contracts/src/test/TestContracts/RETHTokenMock.sol +++ b/contracts/src/test/TestContracts/RETHTokenMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity 0.8.24; import "../../Interfaces/IRETHToken.sol"; diff --git a/contracts/src/test/TestContracts/WSTETHTokenMock.sol b/contracts/src/test/TestContracts/WSTETHTokenMock.sol index e9b05526c..7d27cdf97 100644 --- a/contracts/src/test/TestContracts/WSTETHTokenMock.sol +++ b/contracts/src/test/TestContracts/WSTETHTokenMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.18; +pragma solidity 0.8.24; import "../../Interfaces/IWSTETH.sol"; diff --git a/contracts/utils/assets/test_output/uris.html b/contracts/utils/assets/test_output/uris.html index 10ddae5a1..14d106a1e 100644 --- a/contracts/utils/assets/test_output/uris.html +++ b/contracts/utils/assets/test_output/uris.html @@ -1,3 +1,3 @@ Test Uri From 1bb9c4692ef4f73570e65f8d40c8f1590d676331 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Wed, 6 Nov 2024 00:17:59 +0000 Subject: [PATCH 19/19] Remove warnings from tests and mocks --- contracts/src/test/OracleMainnet.t.sol | 54 +++++++++---------- .../src/test/TestContracts/RETHTokenMock.sol | 2 +- .../test/TestContracts/WSTETHTokenMock.sol | 12 ++--- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/contracts/src/test/OracleMainnet.t.sol b/contracts/src/test/OracleMainnet.t.sol index b8264a386..3a62dd1e0 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -724,7 +724,7 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + (, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); assertTrue(oracleFailedWhileBranchLive); @@ -795,7 +795,7 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + (, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); assertTrue(oracleFailedWhileBranchLive); @@ -881,7 +881,6 @@ contract OraclesMainnet is TestAccounts { uint256 exchangeRate = rethToken.getExchangeRate(); assertGt(ethUsdPrice, 0); assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; // Make the exchange rate return 0 vm.etch(address(rethToken), address(mockRethToken).code); @@ -1039,7 +1038,7 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + rethPriceFeed.fetchPrice(); // Check using lastGoodPrice assertEq(uint8(rethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); @@ -1112,7 +1111,7 @@ contract OraclesMainnet is TestAccounts { assertGt(mockPrice, 0, "mockPrice 0"); // Fetch price again - (uint256 price2, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check ncall failed assertTrue(oracleFailedWhileBranchLive); @@ -1230,7 +1229,7 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); // Check that the primary calc oracle did fail assertTrue(oracleFailedWhileBranchLive); @@ -1310,13 +1309,6 @@ contract OraclesMainnet is TestAccounts { ); uint256 lastGoodPrice = wstethPriceFeed.lastGoodPrice(); - - // Calc expected price if didnt fail, i.e. ETH-USD x canonical - uint256 ethUsdPrice = _getLatestAnswerFromOracle(ethOracle); - uint256 exchangeRate = wstETH.stEthPerToken(); - assertGt(ethUsdPrice, 0); - assertGt(exchangeRate, 0); - uint256 priceIfDidntFail = ethUsdPrice * exchangeRate / 1e18; // Make the exchange rate return 0 vm.etch(address(wstETH), address(mockWstethToken).code); @@ -1512,7 +1504,7 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + wstethPriceFeed.fetchPrice(); // Check using lastGoodPrice assertEq(uint8(wstethPriceFeed.priceSource()), uint8(IMainnetPriceFeed.PriceSource.lastGoodPrice)); @@ -1533,7 +1525,7 @@ contract OraclesMainnet is TestAccounts { uint256 debtRequest = 3000e18; vm.startPrank(A); - uint256 troveId = contractsArray[0].borrowerOperations.openTrove( + contractsArray[0].borrowerOperations.openTrove( A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) ); @@ -1543,7 +1535,7 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = wethPriceFeed.fetchPrice(); + (, bool oracleFailedWhileBranchLive) = wethPriceFeed.fetchPrice(); assertTrue(oracleFailedWhileBranchLive); // Confirm branch shutdown assertEq(contractsArray[0].troveManager.shutdownTime(), block.timestamp); @@ -1577,7 +1569,7 @@ contract OraclesMainnet is TestAccounts { uint256 debtRequest = 3000e18; vm.startPrank(A); - uint256 troveId = contractsArray[1].borrowerOperations.openTrove( + contractsArray[1].borrowerOperations.openTrove( A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) ); @@ -1587,7 +1579,7 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); + (, bool oracleFailedWhileBranchLive) = rethPriceFeed.fetchPrice(); assertTrue(oracleFailedWhileBranchLive); // Confirm RETH branch shutdown assertEq(contractsArray[1].troveManager.shutdownTime(), block.timestamp); @@ -1621,7 +1613,7 @@ contract OraclesMainnet is TestAccounts { uint256 debtRequest = 3000e18; vm.startPrank(A); - uint256 troveId = contractsArray[2].borrowerOperations.openTrove( + contractsArray[2].borrowerOperations.openTrove( A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) ); @@ -1631,7 +1623,7 @@ contract OraclesMainnet is TestAccounts { assertEq(updatedAt, block.timestamp - 7 days); // Fetch price again - (uint256 price, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); + (, bool oracleFailedWhileBranchLive) = wstethPriceFeed.fetchPrice(); assertTrue(oracleFailedWhileBranchLive); // Confirm RETH branch shutdown assertEq(contractsArray[2].troveManager.shutdownTime(), block.timestamp); @@ -1665,7 +1657,7 @@ contract OraclesMainnet is TestAccounts { uint256 debtRequest = 3000e18; vm.startPrank(A); - uint256 troveId = contractsArray[0].borrowerOperations.openTrove( + contractsArray[0].borrowerOperations.openTrove( A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) ); @@ -1714,7 +1706,7 @@ contract OraclesMainnet is TestAccounts { uint256 debtRequest = 3000e18; vm.startPrank(A); - uint256 troveId = contractsArray[2].borrowerOperations.openTrove( + contractsArray[2].borrowerOperations.openTrove( A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) ); @@ -1775,7 +1767,7 @@ contract OraclesMainnet is TestAccounts { uint256 debtRequest = 3000e18; vm.startPrank(A); - uint256 troveId = contractsArray[2].borrowerOperations.openTrove( + contractsArray[2].borrowerOperations.openTrove( A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) ); @@ -1853,7 +1845,7 @@ contract OraclesMainnet is TestAccounts { uint256 debtRequest = 3000e18; vm.startPrank(A); - uint256 troveId = contractsArray[1].borrowerOperations.openTrove( + contractsArray[1].borrowerOperations.openTrove( A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) ); @@ -1915,7 +1907,7 @@ contract OraclesMainnet is TestAccounts { uint256 debtRequest = 3000e18; vm.startPrank(A); - uint256 troveId = contractsArray[1].borrowerOperations.openTrove( + contractsArray[1].borrowerOperations.openTrove( A, 0, coll, debtRequest, 0, 0, 5e16, debtRequest, address(0), address(0), address(0) ); vm.stopPrank(); @@ -1982,17 +1974,23 @@ contract OraclesMainnet is TestAccounts { // --- Call these functions with 10k gas - i.e. enough to run out of gas in the Chainlink calls --- function testRevertLowGasWSTETH() public { vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - address(wstethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); + // just catch return val to suppress warning + (bool success, ) = address(wstethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); + assertFalse(success); } function testRevertLowGasRETH() public { vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - address(rethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); + // just catch return val to suppress warning + (bool success, ) = address(rethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); + assertFalse(success); } function testRevertLowGasWETH() public { vm.expectRevert(MainnetPriceFeedBase.InsufficientGasForExternalCall.selector); - address(wethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); + // just catch return val to suppress warning + (bool success, ) = address(wethPriceFeed).call{gas: 10000}(abi.encodeWithSignature("fetchPrice()")); + assertFalse(success); } // - More basic actions tests (adjust, close, etc) diff --git a/contracts/src/test/TestContracts/RETHTokenMock.sol b/contracts/src/test/TestContracts/RETHTokenMock.sol index 0df7491bb..436841ede 100644 --- a/contracts/src/test/TestContracts/RETHTokenMock.sol +++ b/contracts/src/test/TestContracts/RETHTokenMock.sol @@ -6,7 +6,7 @@ import "../../Interfaces/IRETHToken.sol"; contract RETHTokenMock is IRETHToken { - function getExchangeRate() external view returns (uint256) { + function getExchangeRate() external pure returns (uint256) { return 0; } } \ No newline at end of file diff --git a/contracts/src/test/TestContracts/WSTETHTokenMock.sol b/contracts/src/test/TestContracts/WSTETHTokenMock.sol index 7d27cdf97..1c4be0e57 100644 --- a/contracts/src/test/TestContracts/WSTETHTokenMock.sol +++ b/contracts/src/test/TestContracts/WSTETHTokenMock.sol @@ -6,10 +6,10 @@ import "../../Interfaces/IWSTETH.sol"; contract WSTETHTokenMock is IWSTETH{ - function stEthPerToken() external view returns (uint256) {return 0;} - function wrap(uint256 _stETHAmount) external returns (uint256) {return 0;} - function unwrap(uint256 _wstETHAmount) external returns (uint256) {return 0;} - function getWstETHByStETH(uint256 _stETHAmount) external view returns (uint256) {return 0;} - function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256) {return 0;} - function tokensPerStEth() external view returns (uint256) {return 0;} + function stEthPerToken() external pure returns (uint256) {return 0;} + function wrap(uint256 _stETHAmount) external pure returns (uint256) {return _stETHAmount;} + function unwrap(uint256 _wstETHAmount) external pure returns (uint256) {return _wstETHAmount;} + function getWstETHByStETH(uint256 _stETHAmount) external pure returns (uint256) {return _stETHAmount;} + function getStETHByWstETH(uint256 _wstETHAmount) external pure returns (uint256) {return _wstETHAmount;} + function tokensPerStEth() external pure returns (uint256) {return 0;} } \ No newline at end of file