Part 2

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

Users can open a position below the intial margin and bypass the check

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

  1. Alice has a position 2000 size worth 10,000 USD

  2. Margin Balance ==> Collateral 3000 USD

  3. PNL- 1100 USD

  4. Example Initial margin is 30%

  5. Initial margin - 3000 USD

  6. If position is increased by 2000 USD . new size 12000 USD

  7. New initial margin ==> 4000 USD

  8. 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;
// reverts if the trader can't satisfy the appropriate margin requirement
@2.. tradingAccount.validateMarginRequirement(
ctx.requiredMarginUsdX18,
tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18), // BUG we can open positions below the required
ctx.orderFeeUsdX18.add(ctx.settlementFeeUsdX18).add(
ud60x18(perpsEngineConfiguration.liquidationFeeUsdX18) //BUG @audit what is liquidation fee doing here
)
);
}
/// @notice Returns the margin balance of the account in usd.
/// @dev The margin balance takes uPnL and each collateral type's LTV into account.
/// @param self The trading account storage pointer.
/// @param activePositionsUnrealizedPnlUsdX18 The total unrealized PnL of the account's active positions.
function getMarginBalanceUsd(
Data storage self,
SD59x18 activePositionsUnrealizedPnlUsdX18
)
internal
view
returns (SD59x18 marginBalanceUsdX18)
{
// cache colllateral length
uint256 cachedMarginCollateralBalanceLength = self.marginCollateralBalanceX18.length();
// iterate over every collateral account has deposited
for (uint256 i; i < cachedMarginCollateralBalanceLength; i++) {
// read key/value from storage for current iteration
(address collateralType, uint256 balance) = self.marginCollateralBalanceX18.at(i);
// load collateral margin config for this collateral type
MarginCollateralConfiguration.Data storage marginCollateralConfiguration =
MarginCollateralConfiguration.load(collateralType);
// calculate the collateral's "effective" balance as:
// collateral_price * deposited_balance * collateral_loan_to_value_ratio
@audit>> UD60x18 adjustedBalanceUsdX18 = marginCollateralConfiguration.getPrice().mul(ud60x18(balance)).mul(
ud60x18(marginCollateralConfiguration.loanToValue)
);
// add this account's "effective" collateral balance to cumulative output
@audit>> marginBalanceUsdX18 = marginBalanceUsdX18.add(adjustedBalanceUsdX18.intoSD59x18());
}
// finally add the unrealized PNL to the cumulative output
@audit>> marginBalanceUsdX18 = marginBalanceUsdX18.add(activePositionsUnrealizedPnlUsdX18);
}
  1. 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)) { //Note
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.

{
// // get account's required maintenance margin & unrealized PNL
(, UD60x18 requiredMaintenanceMarginUsdX18, SD59x18 accountTotalUnrealizedPnlUsdX18) =
tradingAccount.getAccountMarginRequirementUsdAndUnrealizedPnlUsd(0, SD59x18_ZERO);
// use unrealized PNL to calculate & output account's margin balance
SD59x18 marginBalanceUsdX18 = tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18);
// prevent liquidatable accounts from trading
if (
TradingAccount.isLiquidatable(
requiredMaintenanceMarginUsdX18,
marginBalanceUsdX18,
ud60x18(perpsEngineConfiguration.liquidationFeeUsdX18)
)
) {
revert Errors.AccountIsLiquidatable(tradingAccountId);
}
}

Present collateral amount after settling

// if trader's old position had positive pnl then credit that to the trader
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);
// mint settlement tokens credited to trader; tokens are minted to
// address(this) since they have been credited to the trader's margin
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 .

Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope

Appeal created

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

Support

FAQs

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