eth_price_fallback_2 review
mediumExecutive Summary
Logic still does not solve against Redemption Fee being lower than Oracle Drift
solidity// Take the minimum of (market, canonical) in order to mitigate against upward market price manipulation. // NOTE: only needed | /// @audit You should take the max for redemptions uint256 lstUsdPrice = LiquityMath._min(lstUsdMarketPrice, lstUsdCanonicalPrice);
Because rETH and stETH/ETH have a higher deviation threshold than the base redemption fee
It is possible that rETH and stETH will trade at a higher price than what the oracle is reporting
Whenever this happens, more rETH / stETH per BOLD will be redeemed, causing a real loss to Trove owners
Stale price during shutdown will create more damage than necessary
Once the oracle shuts down it will use the ETHXCanonical rate
This should in general be an accurate price (barring an exploit to the LST)
In those scenarios, taking the min(ETHXCanonical, lastGoodPrice) will in the scenario of ETH/USD price raising, open up to massive redemption arbitrages, that could be reduced by instead using the ETHXCanonical price
stETH Price can be off by up to 1%
WSTETHPriceFeed prices the wstETH collateral as follows:
solidityuint256 wstEthUsdPrice = stEthUsdPrice * stEthPerWstEth / 1e18;
This is using stETH/USD feed that has a 1% deviation threshold
This means that in some cases, redeemers will pay less than the deviation theshold, naturally opening up to arbitrage by simply redeeming bold and receiving an outsized amount of stETH
RETH Min Price can cause duration risk arbitrage
While I cannot fully explain how all market dynamics reflect on an LST price
It is fair to look at the pricing of an LST through the lens of:
- Smart Contract Risk
- Underlying ETH
- Duration Risk (Time in the exit queue)
If we assume Smart Contract Risk to be close to zero (incorrect, but somewhat valid) (see stETH|ETH which have a ratio of 0.99936854)
Then we can quantify the price of an LST as the Underlying ETH - the opportunity cost of the time it would take to receive that ETH from the exit queue
If we agree on this valuation model, then we must agree that our rETH/ETH oracle is fundamentally pricing in the Duration Risk more so than anything else
NOTE: This model actually works well even now that rETH is above peg, the opportunity cost of holding rETH is greater than other LSTs because the supply is capped and there is a bunch of yield farming incentives going around
Continuing our analogy, we then have to determine if the Chainlink oracle, with a 2% deviation threshold is able to reliably defend Trove redemptions against attacks to the Duration Risk component of the LST price
To which I believe the answer is no
That goes back to the Redemption Fee arbitrage finding, the oracle is too slow to defend against that component
However, whenever the market does move, due to changes in the Duration Risk component of the LST, Trove holders will be redeemed at a discount, they may not be willing to offer
In other words, I believe that current Oracles are unable to fairly price Duration Risk, and as such they should not be used for Redemptions
I suggest that the Redemption Price of the LST is the ETH/USD * Rate (prevent Skim attacks, minor losses)
And that the Liquidation Price is the Market Price (avoids exploits, serious insolvency risk, etc..)
Oracle Shutdown may magnify redemption premium massively
solidityfunction _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? (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; uint256 bestPrice = LiquityMath._min(lstUsdCanonicalPrice, lastGoodPrice); /// @audit Downward price /// @audit This will keep the lowest possible price forever, it should instead update since the ETH feed is working lastGoodPrice = bestPrice; /// @audit Redemptions may overpay MASSIVELY due to that return bestPrice; }
Recommendation
It is probably best to use the valid eth price * rate instead of taking the minimum, since nobody will be able to borrow anyway, but redemptions may massively overcompensate the redeemer
