Part 2

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

Incorrect Funding Updates During LiquidationBranch.liquidateAccounts() Execution with Multiple Accounts or Positions

Summary

LiquidationBranch.liquidateAccounts() can handle multiple account liquidations at once. When liquidating accounts with multiple positions, the mark price should update after each position closes, reflecting the cumulative impact of all liquidations. However, the price only updates for the current position in the loop, ignoring the effect of previously liquidated positions."

Vulnerability Details

Looking at the LiquidationBranch.liquidateAccounts() function, we will see that it iterates through all liquidatable account ids and start closing all account positions if an account is liquidatable.

for (uint256 j; j < ctx.activeMarketsIds.length; j++) {
// load current active market id into working data
ctx.marketId = ctx.activeMarketsIds[j].toUint128();
// fetch storage slot for perp market
PerpMarket.Data storage perpMarket = PerpMarket.load(ctx.marketId);
// load position data for user being liquidated in this market
Position.Data storage position = Position.load(ctx.tradingAccountId, ctx.marketId);
// save open position size
ctx.oldPositionSizeX18 = sd59x18(position.size);
// save inverted sign of open position size to prepare for closing the position
ctx.liquidationSizeX18 = -ctx.oldPositionSizeX18;
// calculate price impact of open position being closed
ctx.markPriceX18 = perpMarket.getMarkPrice(ctx.liquidationSizeX18, perpMarket.getIndexPrice());
// calculate notional value of the position being liquidated and push it to the array
ctx.accountPositionsNotionalValueX18[j] =
ctx.oldPositionSizeX18.abs().intoUD60x18().mul(ctx.markPriceX18);
// get current funding rates
ctx.fundingRateX18 = perpMarket.getCurrentFundingRate();
ctx.fundingFeePerUnitX18 = perpMarket.getNextFundingFeePerUnit(ctx.fundingRateX18, ctx.markPriceX18);
// update funding rates for this perpetual market
perpMarket.updateFunding(ctx.fundingRateX18, ctx.fundingFeePerUnitX18);
// reset the position
position.clear();
// update account's active markets; this calls EnumerableSet::remove which
// is why we are iterating over a memory copy of the trader's active markets
tradingAccount.updateActiveMarkets(ctx.marketId, ctx.oldPositionSizeX18, SD59x18_ZERO);
// we don't check skew during liquidations to protect from DoS
(ctx.newOpenInterestX18, ctx.newSkewX18) = perpMarket.checkOpenInterestLimits(
ctx.liquidationSizeX18, ctx.oldPositionSizeX18, SD59x18_ZERO, false
);
// update perp market's open interest and skew; we don't enforce ipen
// interest and skew caps during liquidations as:
// 1) open interest and skew are both decreased by liquidations
// 2) we don't want liquidation to be DoS'd in case somehow those cap
// checks would fail
perpMarket.updateOpenInterest(ctx.newOpenInterestX18, ctx.newSkewX18);
}

The price impact calculation in the loop above only considers the current position being closed (ctx.liquidationSizeX18 which is -ctx.oldPositionSizeX18), without factoring in how previous liquidations affected the price. The index price remains constant across all position closures, which is not realistic.

Consider a scenario where two liquidatable accounts have long positions of 20e18 and 10e18, with an index price of 100,000e18 (MOCK_BTC_USD_PRICE). When the 20e18 position is liquidated first, the price drops to 999,999e17. The second position's liquidation should then push the price to 999,9995e16, but because each calculation uses the original index price, this cumulative effect isn't captured.

Impact

The miscalculated markPrice affects funding rate calculations. As more liquidations occur, the gap between the actual and expected markPrice grows wider, leading to inaccurate funding rates. This becomes particularly a problem when liquidating positions of varying sizes - if a large position is closed first followed by smaller ones, the funding rate for the smaller positions won't reflect the significant price impact from the earlier large liquidation."

Tools Used

  • Manual Review

Recommendations

Replace ctx.liquidationSizeX18 = -ctx.oldPositionSizeX18 with ctx.liquidationSizeX18 = ctx.liquidationSizeX18.sub(ctx.oldPositionSizeX18)

// save inverted sign of open position size to prepare for closing the position
ctx.liquidationSizeX18 = -ctx.oldPositionSizeX18;
// calculate price impact of open position being closed
ctx.markPriceX18 = perpMarket.getMarkPrice(ctx.liquidationSizeX18, perpMarket.getIndexPrice());
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Support

FAQs

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