The function PerpetualVault:cancelFlow:L419-422, can be called by the keeper to cancel the current flow. It will call the internal PerpetualVault:_cancelFlow:L1220-1242 function, which refunds the execution fee paid by the user who initialized the (latest) flow, now being canceled. The refund is being done by calling refundExecutionFee on the GmxProxy contract, using depositInfo[counter].owner and depositInfo[counter].executionFee as arguments. This will correctly identify the user in the case of a deposit, as the counter state will always be the index of the latest depositor. However, it will most likely be incorrect if the flow being canceled is a withdrawal, since the withdrawer’s index in the depositInfo mapping will not equal the current counter value unless they were also the latest depositor. If the withdrawal being canceled does not belong to the latest depositor, the user who was the last depositor will have their execution fee refunded instead. This will continue to happen each time a flow is canceled, meaning the lastest depositor can have their fee refunded many times over, while the withdrawers will not receive any refund.
The state variable depositInfo is used to track deposits.
The state variable counter in PerpetualVault:L94 increments each time a deposit is done, and when used to index the depositInfo mapping it will always reference the latest entry in the mapping/the last depositor.
_cancelFlow function contains the flawed code for handling the cancellation of a withdrawal flow:
The same arguments for refundExecutionFee are used both for canceling deposits and if the flow is a withdrawal.
This will be correct for the deposit case, and if the user currently withdrawing also made the last deposit.
If this is not the case, the data at index counter will belong to another user, and the refundExecutionFee function will refund them, and the withdrawer will get no refund.
Add this code to test/PerpetualVault.t.sol as it uses fixtures from that file.
Paid executionFees will be refunded to the wrong user. This is likely to happen frequently if cancelFlow is called regularly. While the NatSpec for cancelFlow states this should only be done for GMX positions, there is nothing stopping this from being done to vaults that have used DEX swaps (ParaSwap). If DEX swaps are used frequently by the keeper,
it could incentivize depositors to pay very large executionFees. This is because the fee will be repaid on deposit if only a DEX swap is used. And being the latest depositor with a large depositInfo[counter].executionFee would mean
getting repaid that amount every time a withdrawal is canceled.
Manual review
In withdraw function at the start of a withdrawal flow the state variable flowData is set to the depositId of the withdrawal.
This can be used in PerpetualVault:_cancelFlow:L1220-1242 to get the correct index for depositInfo mapping.
Likelihood: Low, contract has to call cancelFlow after a withdraw, and the settle action is already executed by GMX. Impact: High, the fees will be distributed to the last depositor and not the withdrawer.
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.