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 1 commit
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
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
15 changes: 8 additions & 7 deletions contracts/src/Interfaces/IMainnetPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion contracts/src/Interfaces/IRETHPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ pragma solidity ^0.8.0;

interface IRETHPriceFeed is IMainnetPriceFeed {
function rEthEthOracle() external view returns (AggregatorV3Interface, uint256, uint8);
}
}
75 changes: 44 additions & 31 deletions contracts/src/PriceFeeds/CompositePriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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?
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 * 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) {}
}
22 changes: 10 additions & 12 deletions contracts/src/PriceFeeds/MainnetPriceFeedBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down
58 changes: 36 additions & 22 deletions contracts/src/PriceFeeds/RETHPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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?
Copy link
Collaborator

Choose a reason for hiding this comment

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

We are doing this above, right? So we can remove the TODO

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);
}
}
}
30 changes: 11 additions & 19 deletions contracts/src/PriceFeeds/WETHPriceFeed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading
Loading