Summary
User can escape the liquidation fee if his collateral only has USDC and USDz.
Vulnerability Details
In LiquidationBranch.sol
, the check of liquidatable account is as follows:
(, UD60x18 requiredMaintenanceMarginUsdX18, SD59x18 accountTotalUnrealizedPnlUsdX18) =
tradingAccount.getAccountMarginRequirementUsdAndUnrealizedPnlUsd(0, SD59x18_ZERO);
ctx.marginBalanceUsdX18 = tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18);
if (!TradingAccount.isLiquidatable(requiredMaintenanceMarginUsdX18, ctx.marginBalanceUsdX18)) {
continue;
}
getMarginBalanceUsd
function is as follows:
function getMarginBalanceUsd(
Data storage self,
SD59x18 activePositionsUnrealizedPnlUsdX18
)
internal
view
returns (SD59x18 marginBalanceUsdX18)
{
uint256 cachedMarginCollateralBalanceLength = self.marginCollateralBalanceX18.length();
for (uint256 i; i < cachedMarginCollateralBalanceLength; i++) {
(address collateralType, uint256 balance) = self.marginCollateralBalanceX18.at(i);
MarginCollateralConfiguration.Data storage marginCollateralConfiguration =
MarginCollateralConfiguration.load(collateralType);
UD60x18 adjustedBalanceUsdX18 = marginCollateralConfiguration.getPrice().mul(ud60x18(balance)).mul(
ud60x18(marginCollateralConfiguration.loanToValue)
);
marginBalanceUsdX18 = marginBalanceUsdX18.add(adjustedBalanceUsdX18.intoSD59x18());
}
marginBalanceUsdX18 = marginBalanceUsdX18.add(activePositionsUnrealizedPnlUsdX18);
}
We can conclude that collateral margin balance = collateral_price * deposited_balance * collateral_loan_to_value_ratio.
The collateral_loan_to_value_ratio is as follows for different collaterals.
1 - USDz- 1e18 LTV
2 - USDC- 1e18 LTV
3 - WETH- 0.8e18 LTV
4 - WBTC- 0.8e18 LTV
5 - wstETH - 0.7e18 LTV
6 - weETH- 0.7e18 LTV
So if users only have collateral like USDC and USDz, marginBalanceUsdX18(effective collateral balance) is equal to actual collateral balance.
ctx.liquidatedCollateralUsdX18 = tradingAccount.deductAccountMargin({
feeRecipients: FeeRecipients.Data({
marginCollateralRecipient: globalConfiguration.marginCollateralRecipient,
orderFeeRecipient: address(0),
settlementFeeRecipient: globalConfiguration.liquidationFeeRecipient
}),
pnlUsdX18: requiredMaintenanceMarginUsdX18,
orderFeeUsdX18: UD60x18_ZERO,
settlementFeeUsdX18: ctx.liquidationFeeUsdX18
});
In deductAccountMargin
, requiredMaintenanceMarginUsdX18
is equal or less than marginBalanceUsdX18
(effective collateral balance/actual collateral balance for only USDC/USDz collateral). In most case, if liquidatio keeper works well, requiredMaintenanceMarginUsdX18
is equal or slightly less than margin balance.
The margin balance is only enough to pay requiredMaintenanceMarginUsdX18
. The liquidation fee is missed to be paid without enough collateral.
Impact
User can escape the liquidation fee if his collateral only has USDC and USDz.
Tools Used
manual
Recommendations
LiquidationBranch::checkLiquidatableAccounts
+ GlobalConfiguration.Data storage globalConfiguration = GlobalConfiguration.load();
+ SD59x18 liquidationFeeUsdX18 = sd59x18(globalConfiguration.liquidationFeeUsdX18);
+ if (TradingAccount.isLiquidatable(requiredMaintenanceMarginUsdX18 + liquidationFeeUsdX18, marginBalanceUsdX18)) {
- if (TradingAccount.isLiquidatable(requiredMaintenanceMarginUsdX18, marginBalanceUsdX18)) {
liquidatableAccountsIds[i] = tradingAccountId;
}
LiquidationBranch::liquidateAccounts
// if account is not liquidatable, skip to next account
// account is liquidatable if requiredMaintenanceMarginUsdX18 > ctx.marginBalanceUsdX18
- if (!TradingAccount.isLiquidatable(requiredMaintenanceMarginUsdX18, ctx.marginBalanceUsdX18)) {
+ if (!TradingAccount.isLiquidatable(requiredMaintenanceMarginUsdX18 + sd59x18(ctx.liquidationFeeUsdX18), ctx.marginBalanceUsdX18)) {
continue;
}