Part 2

Zaros
PerpetualsDEXFoundrySolidity
70,000 USDC
View results
Submission Details
Severity: medium
Invalid

Gas Limit Risks in Account Liquidations

Summary

The liquidateAccounts function in LiquidationBranch.sol processes liquidations in a single loop over an unbounded array of account IDs. During extreme market conditions (e.g a crash), liquidating hundreds of accounts in one transaction risks exceed the blockchain’s gas limit, causing the transaction to revert. This leaves undercollateralized positions open, threatening the protocol solvency

Vulnerability Details

https://github.com/Cyfrin/2025-01-zaros-part-2/blob/35deb3e92b2a32cd304bf61d27e6071ef36e446d/src/perpetuals/branches/LiquidationBranch.sol#L115

Exploit Scenario:

A sudden market crash causes 500 accounts to become undercollateralized.

The System Keeper calls liquidateAccounts with all 500 account IDs.

Transaction Execution for example:

uint128[] memory accounts = new uint128[](500);
liquidationBranch.liquidateAccounts(accounts);

Gas Limit Exceeded:

Each liquidation consumes ~70,000 gas for (account checks, position updates, events).

Then the total gas required: 500 * 70,000 = 35,000,000.

The Arbitrum’s block gas limit: ~30,000,000.

Result: Transaction reverts after processing 428 accounts and 72 accounts remain open, accumulating bad debt so the Protocol faces insolvency if collateral value continues to drop.

Impact

Protocol Insolvency as undercollateralized positions remain open, leading to unrecoverable debt.

Tools Used

Manual review

Recommendations

Process accounts in smaller batches to stay within gas limits.

For example:

// In LiquidationBranch.sol
uint256 public constant MAX_BATCH_SIZE = 50; // Based on gas tests
/// @notice Liquidates accounts in batches to avoid gas limits
/// @param accountsIds List of accounts to liquidate
/// @param startIndex Starting index for the batch
/// @param batchSize Maximum accounts to process
function liquidateAccounts(
uint128[] calldata accountsIds,
uint256 startIndex,
uint256 batchSize
) external {
if (batchSize > MAX_BATCH_SIZE) revert Errors.BatchSizeTooLarge(batchSize);
uint256 endIndex = startIndex + batchSize;
if (endIndex > accountsIds.length) endIndex = accountsIds.length;
PerpsEngineConfiguration.Data storage perpsEngineConfiguration = PerpsEngineConfiguration.load();
if (!perpsEngineConfiguration.isLiquidatorEnabled[msg.sender]) {
revert Errors.LiquidatorNotRegistered(msg.sender);
}
LiquidationContext memory ctx;
ctx.liquidationFeeUsdX18 = ud60x18(perpsEngineConfiguration.liquidationFeeUsdX18);
// Process only the current batch
for (uint256 i = startIndex; i < endIndex; i++) {
ctx.tradingAccountId = accountsIds[i];
if (ctx.tradingAccountId == 0) continue;
TradingAccount.Data storage tradingAccount = TradingAccount.loadExisting(ctx.tradingAccountId);
(, ctx.requiredMaintenanceMarginUsdX18, ctx.accountTotalUnrealizedPnlUsdX18) =
tradingAccount.getAccountMarginRequirementUsdAndUnrealizedPnlUsd(0, SD59x18_ZERO);
ctx.marginBalanceUsdX18 = tradingAccount.getMarginBalanceUsd(ctx.accountTotalUnrealizedPnlUsdX18);
if (!TradingAccount.isLiquidatable(
ctx.requiredMaintenanceMarginUsdX18, ctx.marginBalanceUsdX18, ctx.liquidationFeeUsdX18
)) continue;
// ... (rest of liquidation logic remains unchanged)
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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