Skip to content

Commit

Permalink
Merge pull request #393 from liquity/eth_price_fallback_2
Browse files Browse the repository at this point in the history
ETH-USD price fallback
  • Loading branch information
RickGriff authored Nov 6, 2024
2 parents 506e987 + 1bb9c46 commit bf2a5c1
Show file tree
Hide file tree
Showing 25 changed files with 1,933 additions and 471 deletions.
10 changes: 5 additions & 5 deletions contracts/src/BorrowerOperations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1163,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();

Expand All @@ -1174,16 +1176,14 @@ 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
// if the system is already shut down.
if (hasBeenShutDown) return;

_applyShutdown();

emit ShutDownFromOracleFailure(_failedOracleAddr);
}

function _applyShutdown() internal {
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/Interfaces/IBorrowerOperations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import "../Dependencies/AggregatorV3Interface.sol";

pragma solidity ^0.8.0;

interface ICompositePriceFeed is IPriceFeed {
interface IMainnetPriceFeed is IPriceFeed {
enum PriceSource {
primary,
ETHUSDxCanonical,
lastGoodPrice
}

function ethUsdOracle() external view returns (AggregatorV3Interface, uint256, uint8);
function lstEthOracle() external view returns (AggregatorV3Interface, uint256, uint8);
function priceSource() external view returns (PriceSource);
}
1 change: 1 addition & 0 deletions contracts/src/Interfaces/IPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
9 changes: 9 additions & 0 deletions contracts/src/Interfaces/IRETHPriceFeed.sol
Original file line number Diff line number Diff line change
@@ -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);
}
File renamed without changes.
9 changes: 0 additions & 9 deletions contracts/src/Interfaces/IWETHPriceFeed.sol

This file was deleted.

4 changes: 2 additions & 2 deletions contracts/src/Interfaces/IWSTETHPriceFeed.sol
Original file line number Diff line number Diff line change
@@ -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);
}
129 changes: 85 additions & 44 deletions contracts/src/PriceFeeds/CompositePriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,112 @@ pragma solidity 0.8.24;

import "../Dependencies/LiquityMath.sol";
import "./MainnetPriceFeedBase.sol";
import "../Interfaces/ICompositePriceFeed.sol";

// 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).
abstract contract CompositePriceFeed is MainnetPriceFeedBase {
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();
// 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 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(false);

// Check an oracle didn't already fail
assert(priceFeedDisabled == false);
return _fetchPriceDuringShutdown();
}

function _fetchPrice() internal override returns (uint256, bool) {
(uint256 ethUsdPrice, bool ethUsdOracleDown) = _getOracleAnswer(ethUsdOracle);
(uint256 lstEthPrice, bool lstEthOracleDown) = _getOracleAnswer(lstEthOracle);
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();
}

// 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();

// Calculate the market LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST
uint256 lstUsdMarketPrice = ethUsdPrice * lstEthPrice / 1e18;
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();
emit ShutDownFromOracleFailure(_failedOracleAddr);
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);
}

// Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST
uint256 lstUsdCanonicalPrice = ethUsdPrice * lstEthRate / 1e18;
// 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 underlying_per_LST canonical rate directly from the LST contract
(uint256 lstRate, bool exchangeRateIsDown) = _getCanonicalRate();

// Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation
uint256 lstUsdPrice = LiquityMath._min(lstUsdMarketPrice, lstUsdCanonicalPrice);
// If the exchange rate contract is down, switch to (and return) lastGoodPrice.
if (exchangeRateIsDown) {
priceSource = PriceSource.lastGoodPrice;
return lastGoodPrice;
}

lastGoodPrice = lstUsdPrice;
// Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * underlying_per_LST
uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstRate / 1e18;

return (lstUsdPrice, false);
uint256 bestPrice = LiquityMath._min(lstUsdCanonicalPrice, lastGoodPrice);

lastGoodPrice = bestPrice;

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;

return _priceToCheck >= min && _priceToCheck <= max;
}

// Returns the ETH_per_LST as from the LST smart contract. Implementation depends on the specific LST.
function _getCanonicalRate() internal view virtual returns (uint256) {}
// 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) {}
}
38 changes: 0 additions & 38 deletions contracts/src/PriceFeeds/ETHXPriceFeed.sol

This file was deleted.

Loading

0 comments on commit bf2a5c1

Please sign in to comment.