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

Liquidations may be delayed by monitoring LiquidationKeeper

Summary

Liquidations within LiquidationBranch.sol are conducted by the liquidation chainlink autonomous keeper in batch. If there are not enough accounts waiting to be liquidated, then the keeper will withhold liquidation. Users can see they will be liquidation soon, and can choose to deposit collateral. This will in turn make them no longer liquidatable.

Vulnerability Details

Within LiquidationKeeper.sol, there is a check on the number of liquidatable accounts. Due to this, some liquidations will remain in buffer under performData. This data is public on the blockchain and can be monitored.

function checkUpkeep(bytes calldata checkData)
external
view
returns (bool upkeepNeeded, bytes memory performData)
{
// ....
if (liquidatableAccountsIds.length == 0 || liquidatableAccountsIds.length <= performLowerBound) {
performData = abi.encode(accountsToBeLiquidated);
return (upkeepNeeded, performData);
}
// ....
}

The market order keeper within MarketOrderKeeper.sol has no such check and thus will fill orders more frequently than liquidations. This will effectively notify users ahead of time that their positions are subject to liquidation. To delay liquidation, they may simply deposit more collateral.

Impact

Depending on how widespread the use of this exploit is, it could potentially result in an ecosystem wherein liquidations are unreliably filled, enabling users to be very risky with their positions free from reprisal. This will also diminish the expected return of assets from liquidizations on users on behalf of the protocol.

Proof of Concept

Within LiquidiationBranch.sol, we check to see that the requiredMaintenanceMarginUsdX18 is less than ctx.marginBalanceUsdX18.

function liquidateAccounts(uint128[] calldata accountsIds) external {
// ....
if (!TradingAccount.isLiquidatable(requiredMaintenanceMarginUsdX18, ctx.marginBalanceUsdX18)) {
continue;
}
// ....
}

The maintenance margin is calculated within TradingBranch.sol:

function getAccountMarginRequirementUsdAndUnrealizedPnlUsd(
Data storage self,
uint128 targetMarketId,
SD59x18 sizeDeltaX18
)
internal
view
returns (
UD60x18 requiredInitialMarginUsdX18,
UD60x18 requiredMaintenanceMarginUsdX18,
SD59x18 accountTotalUnrealizedPnlUsdX18
)
{
// ....
// iterate over account's active markets
uint256 cachedActiveMarketsIdsLength = self.activeMarketsIds.length();
// Going through users investments
for (uint256 i; i < cachedActiveMarketsIdsLength; i++) {
// ...
requiredMaintenanceMarginUsdX18 = requiredMaintenanceMarginUsdX18.add(positionMaintenanceMarginUsdX18);
// ...
}
// ....
}

Clearly, requiredMaintenanceMarginUsdX18 will evaluate to the sum of all positionMaintenanceMarginUsdX18 values, or the total maintenance margin across all positions. To check if an account is liquidatable, the below must evaluate to true under TradingAccount.sol::isLiquidatable

return requiredMaintenanceMarginUsdX18.intoSD59x18().gt(marginBalanceUsdX18);

Therefore, to avoid liquidation on subsequent calls to checkUpkeep, all a user would have to do is to deposit collateral into their account using tradingAccount.sol::depositMargin untill the above will evaluate to False. Furthermore, users may now exit any existing losing positions to diminish future liquidation chance.

Tools Used

Manual Review

Recommended Mitigation Steps

When accounts are considered liquidatable, we should handle liquidations as they come as opposed to batch processing. This will eliminate the possibility of liquidation monitoring

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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