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

ETH-USD price fallback #393

merged 19 commits into from
Nov 6, 2024

Conversation

RickGriff
Copy link
Collaborator

@RickGriff RickGriff commented Aug 29, 2024

Addresses the issue of a stale price during shutdown after oracle failure. For WSTETH and RETH branches, we now do our best to use a live price based on ETH-USD x canonical_rate if the primary price calculation has failed.
https://github.com/liquity/bold?tab=readme-ov-file#4---oracle-failure-and-urgent-redemptions-with-the-frozen-last-good-price

Fixes #474

@RickGriff RickGriff changed the title Eth price fallback 2 Eth price fallback Aug 29, 2024
@RickGriff RickGriff changed the title Eth price fallback ETH-USD price fallback Aug 29, 2024
@RickGriff RickGriff marked this pull request as ready for review September 3, 2024 09:10
@RickGriff RickGriff marked this pull request as draft September 3, 2024 09:10
@bingen bingen marked this pull request as ready for review September 4, 2024 17:15
Copy link
Collaborator

@bingen bingen left a comment

Choose a reason for hiding this comment

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

Looks good in general. I left some questions inline.

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();}
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 pass forge fmt here (I was confused until I saw the ending } was at the end of the line).

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? No big deal, we can merge and do forge fmt later.

return _fetchPrice();}

// Otherwise if branch is shut down and already using the lastGoodPrice, continue with it
if (priceSource == PriceSource.lastGoodPrice) {return (lastGoodPrice, false);}
Copy link
Collaborator

Choose a reason for hiding this comment

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

As far is I can see, this one will never have ETHUSDxCanonical. So I guess we can get rid of the if clause (and adapt the comment above)

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can even have an assert for testing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point.

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?

@@ -70,11 +83,11 @@ 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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

It’s not related to this PR, but I wonder if we could emit the ShutDownFromOracleFailure here, so we don’t need to pass _failedOracleAddr as param to BorrowerOperations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, makes sense

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?

}

// Otherwise if branch is shut down and already using the lastGoodPrice, continue with it
if (priceSource == PriceSource.lastGoodPrice) {return (lastGoodPrice, false);}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we can get rid of the if, as there’s no other possibility, and note it in a comment.
It doesn’t matter too much, but right now it feels like the execution flow could not enter in any of the if clauses and return (0, false) (which is not the case, fortunately).

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?

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


return (lstUsdPrice, false);
return (lstUsdCanonicalPrice);
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 don’t parenthesis anymore.

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?

// 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
// TODO: Should we also shutdown if the call to the canonical rate reverts, or returns 0?
uint256 lstEthRate = _getCanonicalRate();
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the case of wstETH this is not LST -> ETH, but LST -> stETH. I guess we are assuming 1 stETH = 1 ETH, but historically there have been deviations.
https://www.coingecko.com/en/coins/lido-staked-ether/eth
Maybe we should at least note it as a comment or in the README.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it's the best we can do when the STETH-USD source has failed. Sure, I'll make a note in the Readme.

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?


// Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation
uint256 lstUsdPrice = LiquityMath._min(lstUsdMarketPrice, lstUsdCanonicalPrice);
uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstEthRate / 1e18;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if it would still make sense to use the min against lastGoodPrice.
If the branch was shutdown, it may be due to a bug in the LST, so the canonical price may be wrong.
This way we would at least make sure we protect against the worst case of urgent redemptions not being profitable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Right, yes this makes sense. It could still be that market_price < min(lastGoodPrice, canonical_price) (depends on price movement) - but it increases the chances that we can fully clear the bad debt. Good idea!

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?

@@ -158,20 +156,14 @@ contract TestDeployer is MetadataDeployment {
address ETHOracle;
address STETHOracle;
address RETHOracle;
address ETHXOracle;
address OSETHOracle;
Copy link
Collaborator

Choose a reason for hiding this comment

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

😢

Copy link
Collaborator

@bingen bingen left a comment

Choose a reason for hiding this comment

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

Looks good to me!

// 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
// 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 guess we can remove the TODO?

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

rEthUsdPrice = LiquityMath._max(rEthUsdMarketPrice, rEthUsdCanonicalPrice);
} else {
// Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation.
// Assumes a deviation between market <> canonical of >2% represents a legitemate market price difference.
Copy link
Collaborator

Choose a reason for hiding this comment

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

There’s a typo here, right? (legitimate)

uint256 max = _rEthUsdCanonicalPrice * (DECIMAL_PRECISION + _2_PERCENT) / 1e18;
uint256 min = _rEthUsdCanonicalPrice * (DECIMAL_PRECISION - _2_PERCENT) / 1e18;

return _rEthUsdMarketPrice >= min && _rEthUsdCanonicalPrice <= max;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn’t both be _rEthUsdMarketPrice here?

return fetchPrice();
}

function _fetchPricePrimary(bool _isRedemption) internal override returns (uint256, bool) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

_isRedemption isn’t used here? Doesn’t it throw a warning?

@RickGriff RickGriff merged commit bf2a5c1 into main Nov 6, 2024
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

CS-BOLD-010 Low 5.13: Shutdown Can Be Triggered Twice
2 participants