Part 2

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

Liquidation is a sensitive function and should not revert under any condition but the new implementation introduces points of failure and reversals.

Summary

The perpetual Engine has a liquidation function in scope of this audit this was initially handled and no reverts will occur in the system but presently the new integration introduces some new risk that will cause liquidation to revert and the contract to remain insolvent.

The liquidation process is done in batches depending on the amount of liquidatable positions , DOS in one will cause a DOS in all, the new implementation introduces and enforces some checks that can revert the liquidation call.

Vulnerability Details

After every liquidation call the contract tries to settle the positions losses (PNL) and calls the trading library to properly settles this action this will also make calls to the Market Egine but this call is overall exposed and will cause liquidation to revert in some conditions this should never happen under any condition ideally.

  1. Margin deductions

// deduct maintenance margin from the account's collateral
// settlementFee = liquidationFee
@audit>> ctx.liquidatedCollateralUsdX18 = tradingAccount.deductAccountMargin(
TradingAccount.DeductAccountMarginParams({
feeRecipients: FeeRecipients.Data({
marginCollateralRecipient: perpsEngineConfiguration.marginCollateralRecipient,
orderFeeRecipient: address(0),
settlementFeeRecipient: perpsEngineConfiguration.liquidationFeeRecipient
}),
@audit>> pnlUsdX18: ctx.accountTotalUnrealizedPnlUsdX18.abs().intoUD60x18().add(
ctx.requiredMaintenanceMarginUsdX18
),
orderFeeUsdX18: UD60x18_ZERO,
settlementFeeUsdX18: ctx.liquidationFeeUsdX18,
marketIds: ctx.activeMarketsIds,
accountPositionsNotionalValueX18: ctx.accountPositionsNotionalValueX18
})
);

2.During settlement we call

// pnl logic same as settlement & order fee above
if (params.pnlUsdX18.gt(UD60x18_ZERO) && ctx.pnlDeductedUsdX18.lt(params.pnlUsdX18)) {
(ctx.withdrawnMarginUsdX18, ctx.isMissingMargin, ctx.assetAmount) = withdrawMarginUsd(
self,
collateralType,
ctx.marginCollateralPriceUsdX18,
params.pnlUsdX18.sub(ctx.pnlDeductedUsdX18),
address(this)
);
// verify if we have the asset to send to the market making engine
if (ctx.assetAmount > 0) {
// create variable to cache the sum of all position usd
UD60x18 sumOfAllPositionsUsdX18;
// cache market ids length
uint256 cachedMarketIdsLength = params.marketIds.length;
// we need to sum the notional value of all active positions only if we're handling more than one
// market id
if (cachedMarketIdsLength > 1) {
for (uint256 j; j < params.accountPositionsNotionalValueX18.length; j++) {
sumOfAllPositionsUsdX18 =
sumOfAllPositionsUsdX18.add(params.accountPositionsNotionalValueX18[j]);
}
}
// loop into market ids
for (uint256 j; j < cachedMarketIdsLength; j++) {
// collateral native precision -> collateral zaros precision
UD60x18 collateralAmountX18 =
marginCollateralConfiguration.convertTokenAmountToUd60x18(ctx.assetAmount);
// if we have more than one market id, we need to calculate the percentage that will be
// deposited for this market
if (cachedMarketIdsLength > 1) {
// calculate the percentage to deposit to this market
UD60x18 percentDeductForThisMarketX18 =
params.accountPositionsNotionalValueX18[j].div(sumOfAllPositionsUsdX18);
collateralAmountX18 = collateralAmountX18.mul(percentDeductForThisMarketX18);
}
// deposit the collateral in the market making engine
@audit>> marketMakingEngine.depositCreditForMarket(
uint128(params.marketIds[j]),
collateralType,
marginCollateralConfiguration.convertUd60x18ToTokenAmount(collateralAmountX18)
);
}
}
ctx.pnlDeductedUsdX18 = ctx.pnlDeductedUsdX18.add(ctx.withdrawnMarginUsdX18);
}

3.The call then goes to the MarketMakingEngine

we revert when

  1. when Marketid is pause this should not affect liquidation in any circumstance.

  2. totaldelegationcreditusd==0

function depositCreditForMarket(
uint128 marketId,
address collateralAddr,
uint256 amount
)
external
onlyRegisteredEngine(marketId)
{
if (amount == 0) revert Errors.ZeroInput("amount");
// loads the collateral's data storage pointer, must be enabled
Collateral.Data storage collateral = Collateral.load(collateralAddr);
@audit>> liquidation can revert>> collateral.verifyIsEnabled();
// loads the market's data storage pointer, must have delegated credit so
// engine is not depositing credit to an empty distribution (with 0 total shares)
// although this should never happen if the system functions properly.
@audit>> load market not load live, see comment. liquidation WILL revert>> Market.Data storage market = Market.loadLive(marketId);
@audit>> liquidation can revert>> if (market.getTotalDelegatedCreditUsd().isZero()) {
revert Errors.NoDelegatedCredit(marketId);
}
// uint256 -> UD60x18 scaling decimals to zaros internal precision
UD60x18 amountX18 = collateral.convertTokenAmountToUd60x18(amount);
// caches the usdToken address
address usdToken = MarketMakingEngineConfiguration.load().usdTokenOfEngine[msg.sender];
// caches the usdc
address usdc = MarketMakingEngineConfiguration.load().usdc;
// note: storage updates must occur using zaros internal precision
if (collateralAddr == usdToken) {
// if the deposited collateral is USD Token, it reduces the market's realized debt
market.updateNetUsdTokenIssuance(unary(amountX18.intoSD59x18()));
} else {
if (collateralAddr == usdc) {
market.settleCreditDeposit(address(0), amountX18);
} else {
// deposits the received collateral to the market to be distributed to vaults
// to be settled in the future
market.depositCredit(collateralAddr, amountX18);
}
}
// transfers the margin collateral asset from the registered engine to the market making engine
// NOTE: The engine must approve the market making engine to transfer the margin collateral asset, see
// PerpsEngineConfigurationBranch::setMarketMakingEngineAllowance
// note: transfers must occur using token native precision
IERC20(collateralAddr).safeTransferFrom(msg.sender, address(this), amount);
// emit an event
emit LogDepositCreditForMarket(msg.sender, marketId, collateralAddr, amount);
}

see

/// @notice Unpauses a specific market by adding its ID from the list of live markets.
/// @param marketId The ID of the market to be unpaused.
/// @return success A boolean indicating whether the operation was successful.
function unpauseMarket(uint128 marketId) external onlyOwner returns (bool success) {
success = LiveMarkets.load().addMarket(marketId);
if (success) emit LogMarketUnpaused(marketId);
}
/// @notice Pauses a specific market by removing its ID from the list of live markets.
/// @param marketId The ID of the market to be paused.
/// @return success A boolean indicating whether the operation was successful.
function pauseMarket(uint128 marketId) external onlyOwner returns (bool success) {
success = LiveMarkets.load().removeMarket(marketId);
if (success) emit LogMarketPaused(marketId);
}

Impact

A paused market is one way of how liquidation can be DOSed among other reasons according to the initial implementation liquidation should never revert and based on the comment we should load the market and not check markets that are paused or unpaused. We should be able to liquidate a market paused/unpaused to prevent bad debt in the system. Other points of failure should also not occur to prevent bad debt formation.

Tools Used

Manual Review

Recommendations

  1. for the paused market scenario, load the market and allow all market ids if they exist to be able to liquidate an account

  2. review other new possible points of failure and ensure that liquidation takes place first and vault balancing should occur later.

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

[INVALID] `depositCreditForMarket` reverting would prevent liquidations

Appeal created

bigsam Submitter
4 months ago
inallhonesty Lead Judge
4 months ago
inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

[INVALID] `depositCreditForMarket` reverting would prevent liquidations

Support

FAQs

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