DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: low
Invalid

Users Might Incur Losses In Withdrawals In Cases Of Liquidation Due To Lack Of Control/Slippage Flags

Summary

In case where the perp vault position gets liquidated within a user's withdrawal flow , the user would unintentionally incur losses , no choice is given to the user if he wants to cancel his withdrawal in cases of liquidation and wait for a new healthier position to be open on GMX.

Vulnerability Details

Consider the following ->

1.) There is an active perp vault position on GMX with leverage > 1x.

2.) A user has requested a withdraw via withdraw() ->

the flow is assigned as WITHDRAW at L255 , and since if (curPositionKey != bytes32(0)) (cause a position is open on GMX with leverage > 1x) ->

if (curPositionKey != bytes32(0)) {
nextAction.selector = NextActionSelector.WITHDRAW_ACTION;
_settle(); // Settles any outstanding fees and updates state before processing withdrawal
}

The withdrawing user (for example) expects around ~1000 USDC (just for example , can be way higher) amount back after his withdrawal i.e. when he submitted his withdrawal via the withdraw function.

And , next action is WITHDRAW_ACTION and _settle() is called.

3.) Inside _settle() a settle order is created (routed through GmxProxy.sol) ->

[https://github.com/CodeHawks-Contests/2025-02-gamma/blob/main/contracts/PerpetualVault.sol#L964]

4.) After a successful settle order , afterOrderExecution() would be invoked by GmxProxy and nextAction would be assigned as WITHDRAW_ACTION ->

if (orderResultData.isSettle) {
nextAction.selector = NextActionSelector.WITHDRAW_ACTION;
emit GmxPositionCallbackCalled(requestKey, true);
return;
}

5.) Now lets say the position in GMX got liquidated , therefore afterLiquidationExecution() would be invoked (L563) ->

function afterLiquidationExecution() external {
if (msg.sender != address(gmxProxy)) {
revert Error.InvalidCall();
}
depositPaused = true;
uint256 sizeInTokens = vaultReader.getPositionSizeInTokens(curPositionKey);
if (sizeInTokens == 0) {
delete curPositionKey;
}
if (flow == FLOW.NONE) {
flow = FLOW.LIQUIDATION;
nextAction.selector = NextActionSelector.FINALIZE;
} else if (flow == FLOW.DEPOSIT) {
flowData = sizeInTokens;
} else if (flow == FLOW.WITHDRAW) {
// restart the withdraw flow even though current step is FINALIZE.
nextAction.selector = NextActionSelector.WITHDRAW_ACTION;
}

and since sizeInTokens would be 0 (fully liquidated ) then curPositionKey would be deleted and since flow was WITHDRAW , nextAction.selector would be assigned WITHDRAW_ACTION

6.) Then keeper would invoke runNextAction() and since nextAction is WITHDRAW_ACTION , this branch would be invoked (L371-L381)->

else if (_nextAction.selector == NextActionSelector.WITHDRAW_ACTION) {
// swap indexToken that could be generated from settle action or liquidation/ADL into collateralToken
// use only DexSwap
if (
IERC20(indexToken).balanceOf(address(this)) * prices.indexTokenPrice.min >= ONE_USD
) {
(, bytes memory data) = abi.decode(metadata[1], (PROTOCOL, bytes));
_doDexSwap(data, false);
}
uint256 depositId = flowData;
_withdraw(depositId, metadata[0], prices);

Therefore _withdraw() is invoked.

7.) And inside withdraw , this branch would be invoked since curPositionKey = 0 (L1102) , ->

} else if (curPositionKey == bytes32(0)) { // vault liquidated
_handleReturn(0, true, false);
}

this means that in the _handleReturnFunction we would enter this branch (L1133) ->

if (positionClosed) {
amount = collateralToken.balanceOf(address(this)) * shares / totalShares;
}

8.) Say , the current balance of the collateral token in the perp vault after liquidation is 500 USDC (just for example) , and user owns 50% of the shares (just for example) , therefore the amount calculated would be 500/2 = 250 and these 250 tokens are then transferred with _transferToken(depositId, amount);

9.) Therefore , the user has unintentionally suffered a loss of 750 , when he initiated the withdrawal he calculated his risks according to the position size open in GMX but withing the flow of this the position got liquidated.

In the design of the perp vault if the position gets liquidated a new position would be opened on GMX by gamma keepers , a user who was withdrawing might want that if position got liquidated then cancel the withdraw flow cause he might want to withdraw later now when a new position would be opened and be bullish that it makes profit (using the returned amount after liquidation a new position can be opened) , in short he would never want to withdraw if he would be incurring losses .

Impact

The user has unintentionally suffered a loss of 750 , when he initiated the withdrawal he calculated his risks according to the position size open in GMX but withing the flow of this the position got liquidated.

Tools Used

Manual analysis

Recommendations

A user can mention a flag where if set to true then the withdrawal flow would be cancelled if liquidated or threshold hit and if set to false (emergency kind of wothdrawal) then even if liquidated the user would receive the withdrawal amount (even in losses).

Updates

Lead Judging Commences

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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