Summary
The is no check to verify if a trade is above the initial margin during trade execution this allows users to open trades below the initial margin though not liquidatable. Increasing a position should always be at or above the initial margin. The present check does not prevent this.
Vulnerability Details
Using the example below
Alice has a position 2000 size worth 10,000 USD
Margin Balance ==> Collateral 3000 USD
PNL- 1100 USD
Example Initial margin is 30%
Initial margin - 3000 USD
If position is increased by 2000 USD . new size 12000 USD
New initial margin ==> 4000 USD
This position will be open under the initial Margin
First check
ctx.shouldUseMaintenanceMargin = !ctx.isNotionalValueIncreasing && !ctx.oldPositionSizeX18.isZero();
@ 1... ctx.requiredMarginUsdX18 =
ctx.shouldUseMaintenanceMargin ? requiredMaintenanceMarginUsdX18 : requiredInitialMarginUsdX18;
@2.. tradingAccount.validateMarginRequirement(
ctx.requiredMarginUsdX18,
tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18),
ctx.orderFeeUsdX18.add(ctx.settlementFeeUsdX18).add(
ud60x18(perpsEngineConfiguration.liquidationFeeUsdX18)
)
);
}
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);
@audit>> UD60x18 adjustedBalanceUsdX18 = marginCollateralConfiguration.getPrice().mul(ud60x18(balance)).mul(
ud60x18(marginCollateralConfiguration.loanToValue)
);
@audit>> marginBalanceUsdX18 = marginBalanceUsdX18.add(adjustedBalanceUsdX18.intoSD59x18());
}
@audit>> marginBalanceUsdX18 = marginBalanceUsdX18.add(activePositionsUnrealizedPnlUsdX18);
}
New margin 4000 USD
@ 2.required 4000 USD.
ltv = 80%.
adjusted Margin Balance 3750 (ltv) ==> 3000 USD.
PNL ==> 1100 USD
New Margin = 4100 USD
function validateMarginRequirement(
Data storage self,
UD60x18 requiredMarginUsdX18,
SD59x18 marginBalanceUsdX18,
UD60x18 totalFeesUsdX18
)
internal
view
{
@3. if (requiredMarginUsdX18.add(totalFeesUsdX18).intoSD59x18().gt(marginBalanceUsdX18)) {
revert Errors.InsufficientMargin(
self.id,
marginBalanceUsdX18.intoInt256(),
requiredMarginUsdX18.intoUint256(),
totalFeesUsdX18.intoUint256()
);
}
Total fee ==>50 USD
@ 3. 4000 USD + 50 USD > 4100 USD
The check passes but after the position has been settled we do not check if the position is STILL above the required margin
we only charge if it is liquidatable.
{
(, UD60x18 requiredMaintenanceMarginUsdX18, SD59x18 accountTotalUnrealizedPnlUsdX18) =
tradingAccount.getAccountMarginRequirementUsdAndUnrealizedPnlUsd(0, SD59x18_ZERO);
SD59x18 marginBalanceUsdX18 = tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18);
if (
TradingAccount.isLiquidatable(
requiredMaintenanceMarginUsdX18,
marginBalanceUsdX18,
ud60x18(perpsEngineConfiguration.liquidationFeeUsdX18)
)
) {
revert Errors.AccountIsLiquidatable(tradingAccountId);
}
}
Present collateral amount after settling
if (ctx.pnlUsdX18.gt(SD59x18_ZERO)) {
IMarketMakingEngine marketMakingEngine = IMarketMakingEngine(perpsEngineConfiguration.marketMakingEngine);
ctx.marginToAddX18 =
marketMakingEngine.getAdjustedProfitForMarketId(marketId, ctx.pnlUsdX18.intoUD60x18().intoUint256());
tradingAccount.deposit(perpsEngineConfiguration.usdToken, ctx.marginToAddX18);
marketMakingEngine.withdrawUsdTokenFromMarket(marketId, ctx.marginToAddX18.intoUint256());
}
New balance = 3750 + 1100 = 4850 - 50 (settlement fee) = 4800.
adjusted New margin balance = 4800 * 80% = 3840 Usd
New initial margin = 4000 USD
4000 USD > 3840 USD position should revert
But since there is no check in place a user can add to their position and open a new trade below the initial margin successfully bypassing the check.
Impact
User can increase their position and bypass the initial margin check successfully opening a position below the initial margin. while there is no negative impact the main trade invariant disallows opening or increasing a trade position below the initial margin as these allows a trader increase his position close to the liquidation point without any buffer.
Tools Used
Manual review
Recommendations
Settle pnl before the check or check again if the position is within required margin after settlement .