-
Notifications
You must be signed in to change notification settings - Fork 132
ETH-USD price fallback #393
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
Conversation
bingen
left a comment
There was a problem hiding this 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();} |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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);} |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point.
There was a problem hiding this comment.
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?
| function _disableFeedAndShutDown(address _failedOracleAddr) internal returns (uint256) { | ||
| function _shutDownAndSwitchToLastGoodPrice(address _failedOracleAddr) internal returns (uint256) { | ||
| // Shut down the branch | ||
| borrowerOperations.shutdownFromOracleFailure(_failedOracleAddr); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, makes sense
There was a problem hiding this comment.
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);} |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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?
| 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? |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
| lastGoodPrice = lstUsdCanonicalPrice; | ||
|
|
||
| return (lstUsdPrice, false); | ||
| return (lstUsdCanonicalPrice); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
| 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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?
| address STETHOracle; | ||
| address RETHOracle; | ||
| address ETHXOracle; | ||
| address OSETHOracle; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😢
9cdf066 to
bbf0e6f
Compare
bingen
left a comment
There was a problem hiding this 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!
| 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? |
There was a problem hiding this comment.
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?
|
|
||
| // 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? |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
6a05645 to
bdb6492
Compare
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