Summary
When the stETH-USD oracle fails, WSTETHPriceFeed switches to ETHUSDxCanonical mode which returns prices in USD instead of EUR, causing incorrect price calculations.
Vulnerability Detail
The WSTETHPriceFeed has a fallback mechanism when the stETH-USD oracle fails. It calls _shutDownAndSwitchToETHUSDxCanonical() which calculates the price using ETH-USD and the canonical wstETH rate. However, the _fetchPriceETHUSDxCanonical() function in CompositePriceFeed never converts the USD price to EUR before returning.
This means after the stETH-USD oracle failure:
- All prices returned are in USD (~1.05x higher than EUR)
- The
lastGoodPricegets corrupted with a USD value - All subsequent operations use the wrong price denomination
Impact
After stETH-USD oracle failure, all wstETH collateral operations will use incorrect prices. Users could be incorrectly liquidated or be able to borrow too much. The corrupted lastGoodPrice persists even after further fallbacks.
Code Snippet
// If the STETH-USD feed is down, shut down and try to substitute it with the ETH-USD price
if (stEthUsdOracleDown) {
return (_shutDownAndSwitchToETHUSDxCanonical(address(stEthUsdOracle.aggregator), ethUsdPrice), true);
}function _fetchPriceETHUSDxCanonical(uint256 _ethUsdPrice) internal returns (uint256) {
assert(priceSource == PriceSource.ETHUSDxCanonical);
(uint256 lstRate, bool exchangeRateIsDown) = _getCanonicalRate();
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; // @audit - USD, not EUR!
uint256 bestPrice = LiquityMath._min(lstUsdCanonicalPrice, lastGoodPrice);
lastGoodPrice = bestPrice; // @audit - corrupts lastGoodPrice with USD value
return bestPrice; // @audit - returns USD instead of EUR
}Tool Used
Manual Review
Recommendation
Convert the USD price to EUR in the _fetchPriceETHUSDxCanonical() function or override it in WSTETHPriceFeed to include EUR conversion:
function _fetchPriceETHUSDxCanonical(uint256 _ethUsdPrice) internal override returns (uint256) {
(uint256 lstRate, bool exchangeRateIsDown) = _getCanonicalRate();
(uint256 eurUsdPrice, bool eurUsdOracleDown) = _getOracleAnswer(eurUsdOracle);
if (exchangeRateIsDown || eurUsdOracleDown) {
priceSource = PriceSource.lastGoodPrice;
return lastGoodPrice;
}
uint256 lstUsdCanonicalPrice = _ethUsdPrice * lstRate / 1e18;
uint256 lstEurCanonicalPrice = FixedPointMathLib.divWad(lstUsdCanonicalPrice, eurUsdPrice);
uint256 bestPrice = LiquityMath._min(lstEurCanonicalPrice, lastGoodPrice);
lastGoodPrice = bestPrice;
return bestPrice;
}