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.