Summary
Whenever a position is liqudatable that means the margin balance as fallen below the mmr. A position is max profit can reduce their collateral balance up to the point when only liquidation fee is allowed to remain in the users balance but the liquidation profit doesn't correct handle this scenario where PNL is positive, hence instead of increase the users balance by the pnl we decrease the user's collateral balance.
Vulnerability Details
PNL handling in the liquidation function
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
})
);
instead of handling this as it was done during settlement
settlement handling below
tradingAccount.deductAccountMargin(
TradingAccount.DeductAccountMarginParams({
feeRecipients: FeeRecipients.Data({
marginCollateralRecipient: perpsEngineConfiguration.marginCollateralRecipient,
orderFeeRecipient: perpsEngineConfiguration.orderFeeRecipient,
settlementFeeRecipient: perpsEngineConfiguration.settlementFeeRecipient
}),
@audit>> see settlement>> pnlUsdX18: ctx.pnlUsdX18.lt(SD59x18_ZERO) ? ctx.pnlUsdX18.abs().intoUD60x18() : UD60x18_ZERO,
orderFeeUsdX18: ctx.orderFeeUsdX18,
settlementFeeUsdX18: ctx.settlementFeeUsdX18,
marketIds: ctx.marketIds,
accountPositionsNotionalValueX18: new UD60x18[](1)
})
);
We should only return an abs value of the profit when the pnl is less than 0 and not always
function withdrawMargin(uint128 tradingAccountId, address collateralType, uint256 amount) external {
MarginCollateralConfiguration.Data storage marginCollateralConfiguration =
MarginCollateralConfiguration.load(collateralType);
TradingAccount.Data storage tradingAccount =
TradingAccount.loadExistingAccountAndVerifySender(tradingAccountId);
_requireNoActiveMarketOrder(tradingAccountId);
UD60x18 amountX18 = marginCollateralConfiguration.convertTokenAmountToUd60x18(amount);
_requireAmountNotZero(amountX18);
_requireEnoughMarginCollateral(tradingAccount, collateralType, amountX18);
tradingAccount.withdraw(collateralType, amountX18);
(UD60x18 requiredInitialMarginUsdX18,, SD59x18 accountTotalUnrealizedPnlUsdX18) =
tradingAccount.getAccountMarginRequirementUsdAndUnrealizedPnlUsd(0, SD59x18_ZERO);
SD59x18 marginBalanceUsdX18 = tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18);
tradingAccount.validateMarginRequirement(requiredInitialMarginUsdX18, marginBalanceUsdX18, UD60x18_ZERO);
bool userHasOpenPosition = !requiredInitialMarginUsdX18.isZero();
@audit>> if (userHasOpenPosition) {
PerpsEngineConfiguration.Data storage perpsEngineConfiguration = PerpsEngineConfiguration.load();
@audit>> SD59x18 marginBalanceUsdWithoutUnrealizedPnlUsdX18 = tradingAccount.getMarginBalanceUsd(SD59x18_ZERO);
@audit>> if (
marginBalanceUsdWithoutUnrealizedPnlUsdX18.lt(
sd59x18(int128(perpsEngineConfiguration.liquidationFeeUsdX18))
)
) {
revert Errors.NotEnoughCollateralForLiquidationFee(perpsEngineConfiguration.liquidationFeeUsdX18);
}
}
IERC20(collateralType).safeTransfer(msg.sender, amount);
emit LogWithdrawMargin(msg.sender, tradingAccountId, collateralType, amount);
}
Impact
Wrong accounting will cause PNL removal from the balance even though we are in profit. Pnl would be part of MMR already if positive.
Tools Used
manual review
Recommendations
only add pnl as done in the settlement branch when the pnl is less than 0.