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

Incomplete GMX Order Refunds

Summary

The GmxProxy contract fails to automatically refund execution fees to users when GMX orders are canceled. Execution fees sent to GMX during order creation remain trapped in the contract, causing financial loss for users.

https://github.com/CodeHawks-Contests/2025-02-gamma/blame/e5b98627a4c965e203dbb616a5f43ec194e7631a/contracts/GmxProxy.sol#L292

https://github.com/CodeHawks-Contests/2025-02-gamma/blob/e5b98627a4c965e203dbb616a5f43ec194e7631a/contracts/GmxProxy.sol#L371

Vulnerability Details

Faulty Code Snippet:

function afterOrderCancellation(
bytes32 key,
Order.Props memory order,
EventLogData memory /* eventData */
) external override validCallback(key, order) {
// ❌ Missing refund logic
IGmxProxy.OrderResultData memory orderResultData = ...;
IPerpetualVault(perpVault).afterOrderCancellation(...);
delete queue;
}
function createOrder(...) public returns (bytes32) {
// Sends execution fee to GMX
gExchangeRouter.sendWnt{value: positionExecutionFee}(orderVault, positionExecutionFee);
// ❌ No tracking of fees per order
}

No Automatic Refunds: When orders are canceled, the execution fee is not returned to the user.

No Fee Tracking: The contract does not track which user paid the fee for each order, making refunds impossible.

Example Scenario

  • User A deposits 1 ETH as an execution fee to open a position.

  • The order is canceled (e.g., due to timeout or market conditions).

Result: The 1 ETH remains stuck in the GmxProxy contract, and User A cannot recover it.

Impact

  1. User Losses: Execution fees are permanently lost if orders fail or are canceled.

  2. Contract ETH Buildup: Excess ETH accumulates in the contract, creating management overhead.

Tools Used

Maunal Review

Recommendations

Track Execution Fees: Store the fee amount and depositor’s address for each order.

Auto-Refund on Cancellation: Return fees to the user when orders are canceled.

// GmxProxy.sol
struct OrderQueue {
bytes32 requestKey;
bool isSettle;
address depositor; // ✅ Track depositor
uint256 executionFee; // ✅ Track fee
}
mapping(bytes32 => OrderQueue) public orderQueue; // Track by requestKey
function createOrder(...) public returns (bytes32) {
// ...
orderQueue[requestKey] = OrderQueue({
requestKey: requestKey,
isSettle: false,
depositor: msg.sender, // Track user
executionFee: positionExecutionFee // Track fee
});
}
function afterOrderCancellation(...) external override validCallback(...) {
OrderQueue memory queueEntry = orderQueue[key];
// Refund fee to the depositor
if (queueEntry.executionFee > 0) {
payable(queueEntry.depositor).transfer(queueEntry.executionFee);
}
delete orderQueue[key];

After Fixes:

Added depositor and executionFee to OrderQueue to track who paid the fee.

Upon cancellation, refund the fee to the original depositor.

Verification

Test Case 1 (Successful Refund):

User deposits 1 ETH for an order.

Order is canceled.

Result: 1 ETH is returned to the user ✅.

Test Case 2 (No Double-Spending):

Multiple orders are canceled.

Result: Each refund is correctly mapped to its depositor ✅.

Updates

Lead Judging Commences

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

invalid_no_refund_during_cancellation

Order is not executed, those fees can be used for the next retry.

Support

FAQs

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