Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ETH-USD price fallback #393

Merged
merged 19 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
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
Loading