DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: high
Invalid

Liquidations can revert

Summary

Liquidations can revert

Vulnerability Details

Upon liquidations, we use Chainlink to fetch prices in 3 instances:

UD60x18 markPrice = perpMarket.getMarkPrice(sizeDeltaX18, perpMarket.getIndexPrice());
ctx.markPriceX18 = perpMarket.getMarkPrice(ctx.liquidationSizeX18, perpMarket.getIndexPrice());
UD60x18 adjustedBalanceUsdX18 = marginCollateralConfiguration.getPrice().mul(ud60x18(balance)).mul(ud60x18(marginCollateralConfiguration.loanToValue));

This is the code that eventually gets called in ChainlinkUtil.sol

function getPrice(
IAggregatorV3 priceFeed,
uint32 priceFeedHeartbeatSeconds,
IAggregatorV3 sequencerUptimeFeed
)
internal
view
returns (UD60x18 price)
{
uint8 priceDecimals = priceFeed.decimals();
// should revert if priceDecimals > 18
if (priceDecimals > Constants.SYSTEM_DECIMALS) {
revert Errors.InvalidOracleReturn();
}
if (address(sequencerUptimeFeed) != address(0)) {
try sequencerUptimeFeed.latestRoundData() returns (
uint80, int256 answer, uint256 startedAt, uint256, uint80
) {
bool isSequencerUp = answer == 0;
if (!isSequencerUp) {
revert Errors.OracleSequencerUptimeFeedIsDown(address(sequencerUptimeFeed));
}
uint256 timeSinceUp = block.timestamp - startedAt;
if (timeSinceUp <= Constants.SEQUENCER_GRACE_PERIOD_TIME) {
revert Errors.GracePeriodNotOver();
}
} catch {
revert Errors.InvalidSequencerUptimeFeedReturn();
}
}
try priceFeed.latestRoundData() returns (uint80, int256 answer, uint256, uint256 updatedAt, uint80) {
if (block.timestamp - updatedAt > priceFeedHeartbeatSeconds) {
revert Errors.OraclePriceFeedHeartbeat(address(priceFeed));
}
IOffchainAggregator aggregator = IOffchainAggregator(priceFeed.aggregator());
int192 minAnswer = aggregator.minAnswer();
int192 maxAnswer = aggregator.maxAnswer();
if (answer <= minAnswer || answer >= maxAnswer) {
revert Errors.OraclePriceFeedOutOfRange(address(priceFeed));
}
price = ud60x18(answer.toUint256() * 10 ** (Constants.SYSTEM_DECIMALS - priceDecimals));
} catch {
revert Errors.InvalidOracleReturn();
}
}

We can see that the code can revert in many different places such as invalid round, grace period not being over, stale data, price below or above the min/max answer. Any of those reverts will make the whole liquidation to revert.

Even worse, every user can increase the chances of the liquidation failing due to the reason above. Since we go over every market the user has a position in, if he simply creates a position in every market with the minimum amount, the chances of one of them reverting is a lot higher. We also go over every single collateral the user owns thereby also increasing the chances.

Impact

Liquidations can revert

Tools Used

Manual Review

Recommendations

Implement a fallback oracle, possibly Uniswap TWAP.

Another option is to handle the reverts and emit an event that you can track off-chain. Then, upon that event, a keeper calls the function with a price he has fetched from somewhere making the liquidation pass (of course, you have to refactor the code to allow the keeper to pass a price in those cases).

Updates

Lead Judging Commences

inallhonesty Lead Judge
over 1 year ago
inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!