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();
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).