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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 1 addition & 4 deletions contracts/src/BorrowerOperations.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1187,16 +1186,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
7 changes: 0 additions & 7 deletions contracts/src/Dependencies/IOsTokenVaultController.sol

This file was deleted.

7 changes: 0 additions & 7 deletions contracts/src/Dependencies/IStaderOracle.sol

This file was deleted.

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);
}
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);
}
106 changes: 61 additions & 45 deletions contracts/src/PriceFeeds/CompositePriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,89 @@ pragma solidity 0.8.18;

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

// import "forge-std/console2.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;

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

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

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

// Otherwise if branch is shut down and already using the lastGoodPrice, continue with it
assert(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);
function _shutDownAndSwitchToETHUSDxCanonical(address _failedOracleAddr, uint256 _ethUsdPrice)
internal
returns (uint256)
{
// Shut down the branch
borrowerOperations.shutdownFromOracleFailure();

// 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);
priceSource = PriceSource.ETHUSDxCanonical;

// Calculate the market LST-USD price: USD_per_LST = USD_per_ETH * ETH_per_LST
uint256 lstUsdMarketPrice = ethUsdPrice * lstEthPrice / 1e18;
emit ShutDownFromOracleFailure(_failedOracleAddr);
return _fetchPriceETHUSDxCanonical(_ethUsdPrice);
}

// Get the ETH_per_LST canonical rate directly from the LST contract
// 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
// TODO: Should we also shutdown if the call to the canonical rate reverts, or returns 0?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, we should fallback to lastGoodPrice.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not done yet, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can remove the TODO?

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 * ETH_per_LST
uint256 lstUsdCanonicalPrice = ethUsdPrice * lstEthRate / 1e18;
// Calculate the canonical LST-USD price: USD_per_LST = USD_per_ETH * underlying_per_LST
uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstRate / 1e18;

// Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation
uint256 lstUsdPrice = LiquityMath._min(lstUsdMarketPrice, lstUsdCanonicalPrice);
uint256 bestPrice = LiquityMath._min(lstUsdCanonicalPrice, lastGoodPrice);

lastGoodPrice = lstUsdPrice;
lastGoodPrice = bestPrice;

return (lstUsdPrice, false);
return bestPrice;
}

// Returns the ETH_per_LST as 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) {}
}
40 changes: 0 additions & 40 deletions contracts/src/PriceFeeds/ETHXPriceFeed.sol

This file was deleted.

43 changes: 24 additions & 19 deletions contracts/src/PriceFeeds/MainnetPriceFeedBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ pragma solidity 0.8.18;

import "../Dependencies/Ownable.sol";
import "../Dependencies/AggregatorV3Interface.sol";
import "../Interfaces/IPriceFeed.sol";
import "../Interfaces/IMainnetPriceFeed.sol";
import "../BorrowerOperations.sol";

// import "forge-std/console2.sol";

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.
bool priceFeedDisabled;

PriceSource public priceSource;

// Last good price tracker for the derived USD price
uint256 public lastGoodPrice;
Expand All @@ -30,9 +31,20 @@ abstract contract MainnetPriceFeedBase is IPriceFeed, Ownable {
bool success;
}

event ShutDownFromOracleFailure(address _failedOracleAddr);

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 {
Expand All @@ -41,19 +53,10 @@ 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:
// 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);
Expand All @@ -70,11 +73,13 @@ 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);
borrowerOperations.shutdownFromOracleFailure();

priceSource = PriceSource.lastGoodPrice;

priceFeedDisabled = true;
emit ShutDownFromOracleFailure(_failedOracleAddr);
return lastGoodPrice;
}

Expand Down
33 changes: 0 additions & 33 deletions contracts/src/PriceFeeds/OSETHPriceFeed.sol

This file was deleted.

Loading