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

Silent Execution Fee Refund Failure with Trapped Funds

Summary

The PerpetualVault contract has a critical issue in its fee refund mechanism, where failed refunds are simply ignored without any proper error handling or recovery mechanism. The withdrawEth function, which only applies to the current owner, does not provide adequate protection or recovery options for user funds trapped due to failed refunds.

Vulnerability Details

function _payExecutionFee(uint256 depositId, bool isDeposit) internal {
uint256 minExecutionFee = getExecutionGasLimit(isDeposit) * tx.gasprice;
if (msg.value < minExecutionFee) {
revert Error.InsufficientAmount();
}
if (msg.value > 0) {
payable(address(gmxProxy)).transfer(msg.value);
depositInfo[depositId].executionFee = msg.value;
}
}

In the _payExecutionFee function the function stores the full message value as the execution fee without taking into account the actual amount used and there is no mechanism to track partial fee usage.

if (refundFee) {
uint256 usedFee = callbackGasLimit * tx.gasprice;
if (depositInfo[counter].executionFee > usedFee) {
try IGmxProxy(gmxProxy).refundExecutionFee(
depositInfo[counter].owner,
depositInfo[counter].executionFee - usedFee
) {} catch {}
}
}

In the _mint function the empty try-catch block silently ignores the failure with no event emitted for failed refunds and no tracking of the failed refund amount which means that if a refund fails, the funds remain stuck in the `GMXProxy` contract.

function withdrawEth() external onlyOwner returns (uint256) {
uint256 balance = address(this).balance;
payable(msg.sender).transfer(balance);
return balance;
}

The withdrawEth function in GMXProxy can only be accessed by the owner, regular users who experience a failed refund have no way to reclaim their funds and the owner does not know which funds are failed refunds and which are valid fees.

There is no way to distinguish between ETH from failed refunds, ETH from unspent execution fees, and ETH from other operations. When an owner withdraws all ETH, there is no way to verify ownership of the funds.

Impact

  • Users lose unclaimed execution fees

  • Users cannot verify refund status

Scenario

  1. User deposits funds with execution fee: vault.deposit{value: 1 ETH}(1000)

  2. Operation completes using only partial fee (e.g., 0.5 ETH)

  3. Refund attempt fails silently due to:

    • Contract out of gas

    • Receiver contract reverts

    • Other network issues

  4. Failed refund (0.5 ETH) remains trapped in GmxProxy

  5. User has no mechanism to recover funds

  6. Owner can withdraw all funds including failed refunds, but:

    • Cannot identify which funds belong to which users

    • No way to verify legitimate claims

    • Users lose their refunds permanently

Tools Used

  • Manual review

Recommendations

Implement user-accessible refund claiming

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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