The issue in short is that when a user has requested a withdraw and the vault gets liquidated with sizeInTokens as 0 , then there would not be a decrease order made on GMX (because of liquidation) and the user would be paid from whatever was received after liquidation and the user's shares , in this case the user should be refunded for the fees since there was no GMX order made , but we will see how there was no refund made to the user.
Consider the following ->
1.) There is an active perp vault position on GMX with leverage > 1x.
2.) A user has requested a withdraw using withdraw()
and pays the execution fee ->
[https://github.com/CodeHawks-Contests/2025-02-gamma/blob/main/contracts/PerpetualVault.sol#L272]
the flow is assigned as WITHDRAW at L255 , and since if (curPositionKey != bytes32(0))
(cause a position is open on GMX with leverage > 1x) ->
Therefore , next action is WITHDRAW_ACTION and _settle()
is called.
3.) Inside _settle()
a settle order is created (routed through GmxProxy.sol) ->
4.) After a successful settle order , afterOrderExecution()
would be invoked by GmxProxy and nextAction would be assigned as WITHDRAW_ACTION
->
5.) Now lets say the position in GMX got fully liquidated , therefore afterLiquidationExecution()
would be invoked (L563) ->
and since sizeInTokens
would be 0 (fully liquidated ) then curPositionKey
would be deleted (would be 0 now) 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)->
Therefore _withdraw()
is invoked.
7.) And inside withdraw , this branch would be invoked since curPositionKey = 0 (L1102) , ->
8.) In the above _handleReturn
call refundFee
parameter has been set to false (3rd parameter) , but this is incorrect , since in this case no decrease orders were opened in GMX (which happen in the last else case in _withdraw
) and therefore the user should have been refunded the fee.
9.) Therefore after the collateral is transferred in _handleReturn() there was no refund which is wrong as explained above.
In case the position got liquidated , there was no decrease order made on GMX , hence the user should not have been charged the fee and should have been given a refund , but in the above flow we see user will not receive a refund when vault is liquidated.
Manual analysis
Instead do
No fee needed in _payExecutionFee when position is closed. Make a PoC if you disagree.
Likelihood: Low. A decrease order has to be executed just after a liquidation. Impact: Medium. Execution fees are not refunded, even if not used.
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.