Part 2

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

Users Cannot Be Liquidated When Market is Paused on `marketMakingEngine`

Summary

If a market is paused on marketMakingEngine, liquidation attempts for accounts within that market will revert, but if it is paused on PrepEngine it will not revert.

Vulnerability Details

Consider a scenario where a user opens a leveraged positions in an active market. Their account becomes liquidatable due to insufficient margin. The market is then paused or disabled by calling marketMakingEngine::pauseMarket function.

/src/market-making/branches/MarketMakingEngineConfigurationBranch.sol:578
578: function pauseMarket(uint128 marketId) external onlyOwner returns (bool success) {
579: success = LiveMarkets.load().removeMarket(marketId);
580: if (success) emit LogMarketPaused(marketId);
581: }

The liquidationKeeper calls perpsEngine::liquidateAccounts(), which internally calls CreditDelegationBranch::depositCreditForMarket.

/src/perpetuals/leaves/TradingAccount.sol:621
621: marketMakingEngine.depositCreditForMarket(
622: uint128(params.marketIds[j]),
623: collateralType,
624: marginCollateralConfiguration.convertUd60x18ToTokenAmount(collateralAmountX18)
625: );

depositCreditForMarket function checks if the market is live ? by calling Market::loadLive.

/src/market-making/branches/CreditDelegationBranch.sol:198
198: Market.Data storage market = Market.loadLive(marketId);

The transaction reverts instead of allowing liquidation.

/src/market-making/leaves/Market.sol:120
120: function loadLive(uint128 marketId) internal view returns (Data storage market) {
121: market = loadExisting(marketId);
122:
123: if (!LiveMarkets.load().containsMarket(marketId)) {
124: revert Errors.MarketIsDisabled(marketId);
125: }
126: }

Consider below proof of code:

POC

/2025-01-zaros-part-2/test/integration/perpetuals/liquidation-branch/liquidateAccounts/liquidateAccounts.t.sol:100
100: function test_revert_liquidation_call() // @audit POC
101: external
102: givenTheSenderIsARegisteredLiquidator
103: whenTheAccountsIdsArrayIsNotEmpty
104: givenAllAccountsExist
105: {
106: // fuzz args=[31177684502158619379073888611697022634825149689041911218646198327836032260383, 14618 , false, 2992, 19068]
107: uint256 marketId = 3.117e76;
108: uint256 secondMarketId=14618;
109: bool isLong=false;
110: uint256 amountOfTradingAccounts =2992;
111: uint256 timeDelta = 19068;
112: TestFuzz_GivenThereAreLiquidatableAccountsInTheArray_Context memory ctx;
113:
114: ctx.fuzzMarketConfig = getFuzzMarketConfig(marketId);
115: ctx.secondMarketConfig = getFuzzMarketConfig(secondMarketId);
116:
117: vm.assume(ctx.fuzzMarketConfig.marketId != ctx.secondMarketConfig.marketId);
118:
119: amountOfTradingAccounts = bound({ x: amountOfTradingAccounts, min: 1, max: 10 });
120: timeDelta = bound({ x: timeDelta, min: 1 seconds, max: 1 days });
121:
122: ctx.marginValueUsd = 10_000e18 / amountOfTradingAccounts;
123: ctx.initialMarginRate = ctx.fuzzMarketConfig.imr;
124:
125: deal({ token: address(usdToken), to: users.naruto.account, give: ctx.marginValueUsd });
126:
127: // last account id == 0
128: ctx.accountsIds = new uint128[](amountOfTradingAccounts + 2);
129: ctx.accountsUnrealizedPnl = new SD59x18[](amountOfTradingAccounts + 2);
130: ctx.accountsMarginBalanceInitial = new SD59x18[](amountOfTradingAccounts + 2);
131:
132: ctx.accountMarginValueUsd = ctx.marginValueUsd / (amountOfTradingAccounts + 1);
133:
134: for (uint256 i; i < amountOfTradingAccounts; i++) {
135: ctx.tradingAccountId = createAccountAndDeposit(ctx.accountMarginValueUsd, address(usdToken));
136:
137: openPosition(
138: ctx.fuzzMarketConfig,
139: ctx.tradingAccountId,
140: ctx.initialMarginRate,
141: ctx.accountMarginValueUsd / 2,
142: isLong
143: );
144:
145: openPosition(
146: ctx.secondMarketConfig,
147: ctx.tradingAccountId,
148: ctx.secondMarketConfig.imr,
149: ctx.accountMarginValueUsd / 2,
150: isLong
151: );
152:
153: ctx.accountsIds[i] = ctx.tradingAccountId;
154: ctx.accountsMarginBalanceInitial[i] =
155: perpsEngine.exposed_getMarginBalanceUsd(ctx.accountsIds[i], sd59x18(0));
156:
157: deal({ token: address(usdToken), to: users.naruto.account, give: ctx.marginValueUsd });
158: }
159:
160: setAccountsAsLiquidatable(ctx.fuzzMarketConfig, isLong);
161: setAccountsAsLiquidatable(ctx.secondMarketConfig, isLong);
162:
163: ctx.nonLiquidatableTradingAccountId = createAccountAndDeposit(ctx.accountMarginValueUsd, address(usdToken));
164: openPosition(
165: ctx.fuzzMarketConfig,
166: ctx.nonLiquidatableTradingAccountId,
167: ctx.fuzzMarketConfig.imr,
168: ctx.accountMarginValueUsd / 2,
169: isLong
170: );
171:
172: changePrank({ msgSender: liquidationKeeper });
173:
174: skip(timeDelta);
175:
176: for (uint256 i; i < ctx.accountsIds.length; i++) {
177: (,, ctx.accountsUnrealizedPnl[i]) = perpsEngine.exposed_getAccountMarginRequirementUsdAndUnrealizedPnlUsd(
178: ctx.accountsIds[i], 0, sd59x18(0)
179: );
180:
181: if (ctx.accountsIds[i] == ctx.nonLiquidatableTradingAccountId || ctx.accountsIds[i] == 0) {
182: continue;
183: }
184: }
185:
186: ctx.expectedLastFundingRate = perpsEngine.getFundingRate(ctx.fuzzMarketConfig.marketId).intoInt256();
187: ctx.expectedLastFundingTime = block.timestamp;
188:
189: changePrank({ msgSender: users.owner.account });
190: marketMakingEngine.pauseMarket(ctx.fuzzMarketConfig.marketId); // pause/disable the market
191:
192: changePrank({ msgSender: liquidationKeeper });
193: perpsEngine.liquidateAccounts(ctx.accountsIds); // will revert here
194: }

Impact

Unliquidated accounts continue holding bad debt, increasing risk for the protocol. In case market is not Live on MarketMakingEngine .

Tools Used

Manual Review, Unit Testing

Recommendations

Consider allowing liquidation even if the market is paused, ensuring risk is managed.

Updates

Lead Judging Commences

inallhonesty Lead Judge
10 months ago
inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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

Give us feedback!