Skip to content

Commit

Permalink
Add additional price fallback logic
Browse files Browse the repository at this point in the history
  • Loading branch information
RickGriff committed Sep 17, 2024
1 parent bbf0e6f commit 41c1ba2
Show file tree
Hide file tree
Showing 13 changed files with 507 additions and 238 deletions.
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?
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?
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

0 comments on commit 41c1ba2

Please sign in to comment.