From 41c1ba2dfa021cc3b84c5561c7611a5f294a3578 Mon Sep 17 00:00:00 2001 From: RickGriff Date: Tue, 17 Sep 2024 10:52:03 +0100 Subject: [PATCH] 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 c4e3520a..21cc8a23 100644 --- a/contracts/src/BorrowerOperations.sol +++ b/contracts/src/BorrowerOperations.sol @@ -160,7 +160,6 @@ contract BorrowerOperations is LiquityBase, AddRemoveManagers, IBorrowerOperatio event BoldTokenAddressChanged(address _boldTokenAddress); event ShutDown(uint256 _tcr); - event ShutDownFromOracleFailure(address _oracleAddress); constructor(IAddressesRegistry _addressesRegistry) AddRemoveManagers(_addressesRegistry) @@ -1187,7 +1186,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 @@ -1195,8 +1194,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 d4eb9c61..1d8d6a7b 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 9626870f..ee858023 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 04e77f75..862bf687 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 a6d4bebe..55efffdb 100644 --- a/contracts/src/PriceFeeds/CompositePriceFeed.sol +++ b/contracts/src/PriceFeeds/CompositePriceFeed.sol @@ -7,7 +7,7 @@ import "./MainnetPriceFeedBase.sol"; // 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 { address public rateProviderAddress; @@ -17,63 +17,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 000197ff..a5357ce5 100644 --- a/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol +++ b/contracts/src/PriceFeeds/MainnetPriceFeedBase.sol @@ -31,17 +31,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; @@ -57,10 +53,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); @@ -79,9 +75,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 fe90feef..53de59be 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 03197075..b8aeb1c9 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 f1d4594f..15af9447 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 8e968eaf..4772d6a1 100644 --- a/contracts/src/TroveManager.sol +++ b/contracts/src/TroveManager.sol @@ -1724,7 +1724,7 @@ contract TroveManager is LiquityBase, ITroveManager, ITroveEvents { uint256 _batchColl, // without trove change uint256 _batchDebt // entire (with interest, batch fee), but without trove change, nor upfront fee nor redist ) 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 e4f8ce89..28f6bb89 100644 --- a/contracts/src/test/OracleMainnet.t.sol +++ b/contracts/src/test/OracleMainnet.t.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.18; 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 00000000..fc7b8701 --- /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 00000000..e9b05526 --- /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