Summary
When a position is being liquidated the margin should be deducted from all the tokens that he deposited, but currently, TradingAccount::deductAccountMargin
uses collateralLiquidationPriority
:
function deductAccountMargin(
Data storage self,
FeeRecipients.Data memory feeRecipients,
UD60x18 pnlUsdX18,
UD60x18 settlementFeeUsdX18,
UD60x18 orderFeeUsdX18
)
internal
returns (UD60x18 marginDeductedUsdX18)
{
DeductAccountMarginContext memory ctx;
GlobalConfiguration.Data storage globalConfiguration = GlobalConfiguration.load();
uint256 cachedCollateralLiquidationPriorityLength = globalConfiguration.collateralLiquidationPriority.length();
...MORE CODE
}
Which is part of the global configuration and is not guaranteed to contain all the collateral types that the user has, especially when valid collaterals types are being removed from GlobalConfiguration
:
function removeCollateralFromLiquidationPriority(Data storage self, address collateralType) internal {
bool isInCollateralLiquidationPriority = self.collateralLiquidationPriority.contains(collateralType);
if (!isInCollateralLiquidationPriority) revert Errors.MarginCollateralTypeNotInPriority(collateralType);
address[] memory copyCollateralLiquidationPriority = self.collateralLiquidationPriority.values();
uint256 copyCollateralLiquidationPriorityLength = copyCollateralLiquidationPriority.length;
address[] memory newCollateralLiquidationPriority = new address[](copyCollateralLiquidationPriorityLength - 1);
uint256 indexCollateral;
for (uint256 i; i < copyCollateralLiquidationPriorityLength; i++) {
address collateral = copyCollateralLiquidationPriority[i];
self.collateralLiquidationPriority.remove(collateral);
if (collateral == collateralType) {
continue;
}
newCollateralLiquidationPriority[indexCollateral] = collateral;
indexCollateral++;
}
for (uint256 i; i < copyCollateralLiquidationPriorityLength - 1; i++) {
self.collateralLiquidationPriority.add(newCollateralLiquidationPriority[i]);
}
}
On the other hand, when the margin balance of trader is being calculated TradingAccount::marginCollateralBalanceX18
is used and this is the right set that contains all the collateral of the account:
function getMarginBalanceUsd(
Data storage self,
SD59x18 activePositionsUnrealizedPnlUsdX18
)
internal
view
returns (SD59x18 marginBalanceUsdX18)
{
uint256 cachedMarginCollateralBalanceLength = self.marginCollateralBalanceX18.length();
}
Vulnerability Details
Knowing that information, we can see that in case one of the collateral types is being removed from the collateralLiquidationPriority
but it will remain in the trading account’s marginCollateralBalanceX18
, since admins don’t have control over it. Then when position is being checked for liquidation the margin collateral will contain the removed collateral types as well, furthermore if position becomes liquidatable, margin falls below the maintenance margin, protocol will be at permanent loss, because deductAccountMargin
will not withdraw from the removed collateral types.
function liquidateAccounts(uint128[] calldata accountsIds) external {
...MORE CODE
ctx.marginBalanceUsdX18 = tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18);
if (!TradingAccount.isLiquidatable(requiredMaintenanceMarginUsdX18, ctx.marginBalanceUsdX18)) {
continue;
}
ctx.liquidatedCollateralUsdX18 = tradingAccount.deductAccountMargin({
feeRecipients: FeeRecipients.Data({
marginCollateralRecipient: globalConfiguration.marginCollateralRecipient,
orderFeeRecipient: address(0),
settlementFeeRecipient: globalConfiguration.liquidationFeeRecipient
}),
pnlUsdX18: requiredMaintenanceMarginUsdX18,
orderFeeUsdX18: UD60x18_ZERO,
settlementFeeUsdX18: ctx.liquidationFeeUsdX18
});
}
So not the entire requiredMaintenanceMarginUsdX18
will be withdrawn but it will depend on the amount of removed collateral that the trader has deposited initially.
Important note: this attack can be performed both maliciously or not depending on the intentions of the trader.
Although frontrunning in Arbitrum is not possible addition/removal of collateral types will be under Zaros governance and there will be a proposal that the trader can monitor. The moment that this proposal passes he can simply swap all of his collateral for the token that will be removed and wait until his position becomes liquidatable.
After his position is liquidated and LPs have taken the loss (not part of this audit, but it’s standard in the PnL based systems) trader can freely withdraw this collateral type, as there is no check in the TradingAccount::withdrawMargin whether this type persists in the collateralLiquidationPriority
.
function withdrawMargin(uint128 tradingAccountId, address collateralType, uint256 amount) external {
MarginCollateralConfiguration.Data storage marginCollateralConfiguration =
MarginCollateralConfiguration.load(collateralType);
TradingAccount.Data storage tradingAccount =
TradingAccount.loadExistingAccountAndVerifySender(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);
IERC20(collateralType).safeTransfer(msg.sender, amount);
emit LogWithdrawMargin(msg.sender, tradingAccountId, collateralType, amount);
}
Impact
When collateral type is removed from collateralLiquidationPriority
, liquidations will issue less collateral and will allow the trader to take all of his initially deposited collateral back.
Tools Used
Manual Review
Recommendations
Update the code to validate the margin requirements in TradingAccount::getMarginBalanceUsd
from collateralLiquidationPriority
instead of the marginCollateralBalanceX18
. That way when type is removed it is automatically being removed from the collateral margin of the user as well, but this can lead to situation where this removal makes the whole account subject of liquidation if the trader have significant balance.