DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: high
Valid

Incorrect Calculation of `collateralDeltaAmount` in `PerpetualVault::_withdraw` Results in Users Receiving Less Funds Than Expected During Withdrawals

Summary

A vulnerability exists in the PerpetualVault::_withdraw function, specifically in the calculation of the Profit and Loss (PnL) deduction. The issue arises due to the use of gmxReader.getPositionInfo with an incorrect parameter usePositionSizeAsSizeDeltaUsd set to true that results in the PnL being calculated for the entire position rather than just for the user's share. This leads to an excessive deduction from the user's withdrawal amount, reducing their funds unfairly.

Vulnerability Details

The affected code is in the PerpetualVault::_withdraw function, specifically the part:

int256 pnl = vaultReader.getPnl(curPositionKey, prices, sizeDeltaInUsd);
if (pnl < 0) {
collateralDeltaAmount = collateralDeltaAmount - feeAmount - uint256(-pnl) / prices.shortTokenPrice.max;
}
  • The function vaultReader.getPnl:

function getPnl(
bytes32 key,
MarketPrices memory prices,
uint256 sizeDeltaUsd
) external view returns (int256) {
uint256 sizeInTokens = getPositionSizeInUsd(key);
if (sizeInTokens == 0) return 0;
PositionInfo memory positionInfo = gmxReader.getPositionInfo(
address(dataStore),
referralStorage,
key,
prices,
sizeDeltaUsd,
address(0),
true
);
return positionInfo.pnlAfterPriceImpactUsd;
}

internally calls gmxReader.getPositionInfo with the last parameter usePositionSizeAsSizeDeltaUsd set to true.

  • This setting computes the PnL for the entire position rather than just for sizeDeltaUsd.

  • As a result, pnl in PerpetualVault::_withdraw represents the total loss of the entire position instead of only the user's portion.

  • This incorrect calculation leads to an unfair reduction in the user's withdrawal amount (collateralDeltaAmount is less than it should be).

Impact

  • Users may receive significantly less funds than they actually hold when withdrawing.

  • A key invariant of the protocol, Depositor Share Value Preservation, is broken. Losses may not be distributed proportionally based on share ownership.

  • This issue can cause financial losses and unfair withdrawals.

Example:

Assume:

  • User A has 20 shares

  • User B has 20 shares (same as user A)

  • totalShares = 100

  • collateralAmount = 20,000

  • pnl = -$500 (the total PnL for the position)

  • shortTokenPrice.max = 1

User A Withdraws First (While Position is Still Open)

Using the calculation in PerpetualVault::_withdraw, User A’s collateralDeltaAmount is (ignoring feeAmount):

collateralDeltaAmount = 20,000 * 20 / 100 - 500 / 1 = 3,500

What they should actually get (when pnl is scaled properly to their share proportion):

collateralDeltaAmount = 20,000 * 20 / 100 - 100/ 1 = 3,900

Discrepancy: 400 loss for User A

User B Withdraws After the Position Closes

After User A withdraws:

  • collateralAmount updates to (Subtracting User A’s collateral and pnl ):

    20,000 - 3,500 - 500 = 16,000

Now User B withdraws their share of the remaining collateral following PerpetualVault::_withdraw for a closed position:

amount = 16,000 * 20 / 80 = 4,000

Final Discrepancy

  • **User A received: 3,500 **

  • **User B received: 4,000 **

  • Difference: 500

Because the loss was not distributed proportionally, User A lost 400 more than they should have, while User B benefited by receiving a higher amount than they should.

  • In addition, since the whole position pnl is subtracted in

collateralDeltaAmount = collateralDeltaAmount - feeAmount - uint256(-pnl) / prices.shortTokenPrice.max;

collateralDeltaAmount may become negative causing the function to revert.

Tools Used

Manual Review

Recommendations

Two possible ways of mitigating the vulnerability are:

  1. Modify gmxReader.getPositionInfo Call

    • Set the last parameter to false when calling gmxReader.getPositionInfo inside vaultReader.getPnl.

    PositionInfo memory positionInfo = gmxReader.getPositionInfo(
    address(dataStore),
    referralStorage,
    key,
    prices,
    sizeDeltaUsd,
    address(0),
    - true
    + false // Ensure PnL is calculated for the specific withdrawal amount
    );
  2. Scale PnL Deduction in _withdraw Function

    • Adjust pnl by multiplying it with shares / totalShares before deducting from collateralDeltaAmount.

    int256 pnl = vaultReader.getPnl(curPositionKey, prices, sizeDeltaInUsd);
    + int256 deltaPnl = (pnl * uint256(shares)) / uint256(totalShares);
    if (pnl < 0) {
    - collateralDeltaAmount = collateralDeltaAmount - feeAmount - uint256(-pnl) / prices.shortTokenPrice.max;}
    + collateralDeltaAmount = collateralDeltaAmount - feeAmount - uint256(-deltaPnl) / prices.shortTokenPrice.max;}
Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_getPnL_use_usePositionSizeAsSizeDeltaUsd_and_substract_it

Likelihood: Medium/High, every withdrawal from a short or leveraged vault that is not liquidated and has a negative PnL. Impact: High, subtract collateralDeltaAmount from the entire PnL of the position instead of the delta amount PnL. DoS or loss of funds

Support

FAQs

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

Give us feedback!