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

Open interests and skew are set to 0 everytime someone is liquidated, leading to bypass limits set by the protocol

Description

LiquidationBranch::liquidateAccounts allows whitelisted liquidators to liquidate an account that lacks sufficient collateral to maintain its positions. This function utilizes a struct named LiquidationContext.

The issue arises as the function fails to initialize two members of the struct:

  • newOpenInterestX18

  • newSkewX18

Consequently, these members are reset to 0 each time this function is called. They are used to update the open interest and skew of all markets when an account ID is liquidated.

This implies that for a single account liquidation, open interests and skew are reset to 0 instead of merely deducting the liquidated positions. These variables are crucial for maintaining limits in the protocol to prevent speculation from excessively inflating the real price. Without these limits, the price, skew, and speculation can grow indefinitely, leading to significant price fluctuations on Zarkos.

function liquidateAccounts(uint128[] calldata accountsIds) external {
...
@> LiquidationContext memory ctx;
...
for (uint256 i; i < accountsIds.length; i++) {
...
for (uint256 j; j < ctx.activeMarketsIds.length; j++) {
...
@> perpMarket.updateOpenInterest(ctx.newOpenInterestX18, ctx.newSkewX18);
}
...
}
}

Risk

Likelyhood: High

  • Every liquidation resets open interests and skew to 0.

Impact: High

  • PerpMarket::checkOpenInterestLimits will not function correctly, bypassing the skew limit and the open interests limit.

  • The protocol cannot prevent market from becoming unstable, deviating from the real price, leading to significant price fluctuations.

Proof of Concept

Foundry PoC to add in test/integration/perpetuals/liquidation-branch/liquidateAccounts/liquidateAccounts.t.sol

The PoC is a copy of the fuzz test at the end of the file, with a non-liquidatable position added. The result will always be 0 for the open-interest, which is a clear bug.

function testFuzz_LiquidationEraseOpenInterest(
uint256 marketId,
uint256 secondMarketId,
bool isLong,
uint256 amountOfTradingAccounts,
uint256 timeDelta
)
external
givenTheSenderIsARegisteredLiquidator
whenTheAccountsIdsArrayIsNotEmpty
givenAllAccountsExist
{
// CODE COPIED FROM testFuzz_GivenThereAreLiquidatableAccountsInTheArray with a non-liquidable position added //
TestFuzz_GivenThereAreLiquidatableAccountsInTheArray_Context memory ctx;
ctx.fuzzMarketConfig = getFuzzMarketConfig(marketId);
ctx.secondMarketConfig = getFuzzMarketConfig(secondMarketId);
vm.assume(ctx.fuzzMarketConfig.marketId != ctx.secondMarketConfig.marketId);
amountOfTradingAccounts = bound({ x: amountOfTradingAccounts, min: 1, max: 10 });
timeDelta = bound({ x: timeDelta, min: 1 seconds, max: 1 days });
ctx.marginValueUsd = 10_000e18 / amountOfTradingAccounts;
ctx.initialMarginRate = ctx.fuzzMarketConfig.imr;
deal({ token: address(usdz), to: users.naruto.account, give: ctx.marginValueUsd });
ctx.accountsIds = new uint128[](amountOfTradingAccounts + 2);
ctx.accountMarginValueUsd = ctx.marginValueUsd / (amountOfTradingAccounts + 1);
for (uint256 i; i < amountOfTradingAccounts; i++) {
ctx.tradingAccountId = createAccountAndDeposit(ctx.accountMarginValueUsd, address(usdz));
openPosition(
ctx.fuzzMarketConfig,
ctx.tradingAccountId,
ctx.initialMarginRate,
ctx.accountMarginValueUsd / 2,
isLong
);
openPosition(
ctx.secondMarketConfig,
ctx.tradingAccountId,
ctx.secondMarketConfig.imr,
ctx.accountMarginValueUsd / 2,
isLong
);
ctx.accountsIds[i] = ctx.tradingAccountId;
deal({ token: address(usdz), to: users.naruto.account, give: ctx.marginValueUsd });
}
setAccountsAsLiquidatable(ctx.fuzzMarketConfig, isLong);
setAccountsAsLiquidatable(ctx.secondMarketConfig, isLong);
ctx.nonLiquidatableTradingAccountId = createAccountAndDeposit(ctx.accountMarginValueUsd, address(usdz));
ctx.accountsIds[amountOfTradingAccounts] = ctx.nonLiquidatableTradingAccountId;
// NON-LIQUIDABLE POSITION ADDED for nonLiquidatableTradingAccountId
openPosition(
ctx.fuzzMarketConfig,
ctx.nonLiquidatableTradingAccountId,
ctx.fuzzMarketConfig.imr,
ctx.accountMarginValueUsd / 2,
isLong
);
changePrank({ msgSender: liquidationKeeper });
for (uint256 i; i < ctx.accountsIds.length; i++) {
if (ctx.accountsIds[i] == ctx.nonLiquidatableTradingAccountId || ctx.accountsIds[i] == 0) {
continue;
}
// it should emit a {LogLiquidateAccount} event
vm.expectEmit({
checkTopic1: true,
checkTopic2: true,
checkTopic3: false,
checkData: false,
emitter: address(perpsEngine)
});
emit LiquidationBranch.LogLiquidateAccount({
keeper: liquidationKeeper,
tradingAccountId: ctx.accountsIds[i],
amountOfOpenPositions: 0,
requiredMaintenanceMarginUsd: 0,
marginBalanceUsd: 0,
liquidatedCollateralUsd: 0,
liquidationFeeUsd: 0
});
}
skip(timeDelta);
ctx.expectedLastFundingRate = perpsEngine.getFundingRate(ctx.fuzzMarketConfig.marketId).intoInt256();
ctx.expectedLastFundingTime = block.timestamp;
perpsEngine.liquidateAccounts(ctx.accountsIds);
// CODE COPIED FROM testFuzz_GivenThereAreLiquidatableAccountsInTheArray with a non-liquidable position added //
// it should update open interest value
(,, ctx.openInterestX18) = perpsEngine.getOpenInterest(ctx.fuzzMarketConfig.marketId);
ctx.expectedOpenInterest = sd59x18(
PositionHarness(address(perpsEngine)).exposed_Position_load(
ctx.nonLiquidatableTradingAccountId, ctx.fuzzMarketConfig.marketId
).size
).abs().intoUD60x18().intoUint256();
// WILL ALWAYS BE 0 ! Test do not fail even with an open position for a non liquidable account !
assertEq(0, ctx.openInterestX18.intoUint256(), "open interest");
}

Recommended Mitigation

For each liquidation, the open interest should be set to the previous value minus the liquidated position. The same applies to the skew.

Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

`liquidateAccounts` calls `updateOpenInterest` with uninitialized OI and skew)

Support

FAQs

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