Severity: High
The getAccountMarginRequirementUsdAndUnrealizedPnlUsd function incorrectly uses current order's fill price as the mark price for computing margin requirements the account needs to satisfy after the trade is processed.
The TradingAccount::getAccountMarginRequrementUsdAndUnrealizedPnlUsd function is used to compute margin requirements of an account. The function performs additional computations to calculate the margin requirements of the account after the trade was executed.
The function calculates uses incorrect markPrice for computing after trade margin requirements. The incorrect price is used to calculate the intialMargin and maintenanceMargin. As a result, the protocol might process invalid orders which do not meet the actual margin requirements and might reject valid orders.
The vulnerable uses of this function is in SettlementBranch::_fillOrder and OrderBranch::createMarketOrder. Both the _fillOrder and createMarketOrder functions try to ensure that account meets the margin requirements after the requsted order is processed.
consider following definitions:
usedInitialMargin, usedMaintenanceMargin: These are values returned by the current incorrect implementation and are used by the _fillOrder, createMarketOrder functions. These are margin requirements the protocol considers the account needs to satisfy after the trade.
correctInitialMargin, correctMaintenanceMargin: These are correct margin requirement values after the trade. If the liquidation function is called immediately after processing the trade, the liquidation function uses these values to check if the account is liquidatable.
The incorrect code is in the if (targetMarketId != 0) branch of the getAccountMarginRequrementUsdAndUnrealizedPnlUsd function. The branch calculates markPrice for filling the current order. The function uses this markPrice as the price liquidation code would use if called immediately after filling the order. These two prices will be different leading to incorrect calculation of margin requirements.
Consider skew and positionSize as the market skew and trader's position size in the target market.
markPrice for filling the order as calculated by the current implementation:
markPriceCurrentOrder = perpMarket.getMarkPrice(sizeDelta, indexPrice)
The markPrice depends on the market skew and skewScale. The markPriceCurrentOrder is the filling price for the current order.
Consider that the current order is filled and the liquidation code is executed in the next transaction. The markPrice will be different because of change in skew and the position size.
afterTradeSkew = skew + sizeDelta
afterTradePositionSize = positionSize + sizeDelta
markPriceUsedByLiquidation = perpMarket.getMarkPrice(-afterTradePositionSize, indexPrice)
The markPriceCurrentOrder, markPriceUsedByLiquidation are different because of changes in skew and the change in position size. Because the markPriceCurrentOrder is used for computing after the trade margin requirements (i.e as markPriceUsedByLiquidation) and margin requirements change depending on the mark price, the function returns incorrect margin requirement values.
Let
correctMarkPrice = markPriceUsedByLiquidation
usedMarkPrice = markPriceCurrentOrder
positionSize = position.size before the order.
Then
correctMarkPrice - usedMarkPrice = -1/2 * indexPrice * positionSize / skewScale
As a result,
if positionSize > 0:
correctMarkPrice < usedMarkPrice =>
correctInitialMargin < usedInitialMargin
correctMaintenanceMargin < usedMaintenanceMargin
As a result, protocol uses a larger value than the actual for the margin requirement values.
if correctInitialMargin < traderMarginBalance < usedInitialMargin
protocol rejects a size increasing trade even though user has required initialMargin.
if correctMaintenanceMargin < traderMarginBalance < usedMaintenanceMargin
protocol considers the account to be liquidatable even though its not and rejects orders which reduce the position size.
The trader cannot save their account from becoming liquidatable.
if positionSize < 0:
correctMarkPrice > usedMarkPrice =>
correctInitialMargin > usedInitialMargin
correctMaintenanceMargin > usedMaintenanceMargin
As a result, protocol uses a smaller value than the actual for the margin requirement values.
if correctInitialMargin > traderMarginBalance > usedInitialMargin
protocol accepts a size increasing trade when the trader is under initialMarginRequirement.
if correctMaintenanceMargin > traderMarginBalance > usedMaintenanceMargin
Protocol allows a liquidatable account to perform trades and close their positions.
Use of getAccountMarginRequirementUsdAndUnrealizedPnlUsd to compute after the trade margin values: SettlementBranch.sol#L410-L414
Use of markPriceCurrentOrder for calculating after the tradenotionalValue: TradingAccount.sol#L232-L240
Computation of markPrice by the liquidation code: TradingAccount.sol#L278-L285
Use of skew in computation of markPrice: PerpMarket.sol#L110-L121
The following POC can be used to verify the issue. The test case only considers one active market and the test case shows the difference in the correct and used values. The mock functions are a copy of the original implementation without any storage variables. The arthimetic operations are equivalent.
The pnl and fundingFee are not considered in the calculations as these values only influence the trader's marginBalance.
forge test --match-contract AfterTradeMarginTest -vv --via-ir
if positionSize > 0:
if correctInitialMargin < traderMarginBalance < usedInitialMargin
protocol rejects a size increasing trade even though user has required initialMargin.
if correctMaintenanceMargin < traderMarginBalance < usedMaintenanceMargin
protocol considers the account to be liquidatable even though its not and rejects orders which reduce the position size.
The trader cannot save their account from becoming liquidatable.
if positionSize < 0:
if correctInitialMargin > traderMarginBalance > usedInitialMargin
protocol accepts a size increasing trade when the trader is under initialMarginRequirement.
if correctMaintenanceMargin > traderMarginBalance > usedMaintenanceMargin
Protocol allows a liquidatable account to perform trades and close their positions.
Manual Review
Add a function to PerpMarket library which takes skew as an argument and computes the markPrice. In the getAccountMarginRequirementUsdAndUnrealizedPnlUsd function use the added function with after the trade skew and correct position size to compute the markPriceAfterTheTrade. Use this markPrice to compute the notional value and margin requirements.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.