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

Orders Stuck in Queue Causing Temporary DoS in Perpetual Vault

Summary

The GmxProxy.sol contract contains a vulnerability where an order can become stuck in the queue if GMX Keepers fail to execute or cancel it, preventing the queue.requestKey from being cleared. This blocks subsequent orders initiated by the PerpetualVault, resulting in a temporary Denial of Service (DoS) condition that halts user interactions such as deposits and withdrawals. The issue arises from the absence of a timeout mechanism or robust fallback logic to handle unprocessed orders, representing a significant internal logic flaw exploitable under realistic market conditions.

Vulnerability Details

  • Contract Affected: GmxProxy.sol

  • Functions Involved:

  • Root Cause:

    • The queue struct (OrderQueue { bytes32 requestKey; bool isSettle; }) is populated when an order is created via createOrder, but it is only cleared when GMX triggers a callback (afterOrderExecution or afterOrderCancellation).

    • If an order has an unachievable acceptablePrice (e.g., significantly below market price) and GMX Keepers do not execute or cancel it, no callback occurs, leaving queue.requestKey stuck indefinitely.

    • The PerpetualVault relies on GmxProxy to process orders and a stuck queue causes the vault’s flow to remain in progress (e.g., SIGNAL_CHANGE), blocking new actions due to the _noneFlow check in deposit.

  • Trigger Condition: Submitting an order with an acceptablePrice (e.g., 1000 USD for ETH when the market price is ~3386 USD) that GMX cannot execute immediately, resulting in the order remaining pending without a callback.

    Proof of Code:

    /// @notice Tests that a stuck order in GmxProxy queue causes DoS in PerpetualVault
    function test_QueueStuckDoS() external {
    address alice = makeAddr("alice");
    address keeper = PerpetualVault(vault).keeper();
    IERC20 collateralToken = PerpetualVault(vault).collateralToken();
    uint256 depositAmount = 1e10; // 10 USDC
    uint256 executionFee = 0.01 ether; // Execution fee for deposit
    // Step 1: Alice deposits funds to set up the vault state
    vm.startPrank(alice);
    deal(address(collateralToken), alice, depositAmount);
    deal(alice, 1 ether); // Ensure Alice has ETH for deposit fee
    collateralToken.approve(vault, depositAmount);
    PerpetualVault(vault).deposit{value: executionFee}(depositAmount);
    vm.stopPrank();
    // Step 2: Keeper submits a MarketIncrease order with a low acceptablePrice to stick in queue
    MarketPrices memory prices = mockData.getMarketPrices();
    bytes[] memory data = new bytes[](1);
    address[] memory swapPath = new address[](0); // No swap path needed
    uint256 acceptablePrice = 1000 * 10**30; // 1000 USD, below market price (~3386 USD)
    data[0] = abi.encode(PROTOCOL.GMX, abi.encode(swapPath, depositAmount, acceptablePrice)); // Correct format for GMX
    vm.prank(keeper);
    PerpetualVault(vault).run(true, true, prices, data);
    // Verify that the queue is set in GmxProxy
    address gmxProxy = address(PerpetualVault(vault).gmxProxy());
    (bytes32 requestKey, bool isSettle) = GmxProxy(payable(gmxProxy)).queue();
    assertTrue(requestKey != bytes32(0), "Queue should be set with stuck order");
    assertFalse(isSettle, "Not a settle order");
    // Simulate the order remaining unprocessed (no callback)
    skip(1 hours); // Fast forward 1 hour to mimic a stuck order
    // Step 3: Attempt a new deposit to demonstrate DoS
    uint256 newDepositAmount = 2e10; // 20 USDC
    vm.startPrank(alice);
    deal(address(collateralToken), alice, newDepositAmount);
    collateralToken.approve(vault, newDepositAmount);
    vm.expectRevert(Error.FlowInProgress.selector); // Expect revert due to stuck flow
    PerpetualVault(vault).deposit{value: executionFee}(newDepositAmount);
    vm.stopPrank();
    // Confirm queue remains stuck and flow is blocked
    (bytes32 stuckKey, ) = GmxProxy(payable(gmxProxy)).queue();
    assertEq(stuckKey, requestKey, "Queue remains stuck, causing DoS");
    assertEq(uint8(PerpetualVault(vault).flow()), 2, "Flow stuck at SIGNAL_CHANGE");
    // Log results to verify DoS condition
    emit log_named_bytes32("Stuck Request Key", requestKey);
    emit log_string("DoS confirmed: New deposit cannot proceed due to stuck queue");
    }

[PASS] test_QueueStuckDoS() (gas: 1779771)

Logs:
Stuck Request Key: 0x2e9b56aff7ad76a388482089a48c1ad1e043cb606088141ae8d535ca504b10cb

DoS confirmed: New deposit cannot proceed due to stuck queue

Impact

  • Users are unable to deposit or withdraw funds from the PerpetualVault as new orders are blocked by the stuck queue, leading to a temporary DoS.

  • Funds deposited in the vault remain locked until the stuck order is resolved, potentially causing financial losses if market conditions shift adversely.

  • The vault’s operational reliability is compromised, undermining user confidence in the Gamma Protocol.

  • Affected Parties: All users interacting with the PerpetualVault via GmxProxy, including depositors and withdrawers.

  • Real-world Scenario: During market volatility (e.g., ETH price fluctuates 20% in minutes), an order with a low acceptablePrice may not be executed promptly, stalling the vault for hours until manual intervention or GMX Keeper action occurs.

Tools Used

Manual Review

Foundry

Recommendations

  1. Implement a Timeout Mechanism:

    • Add a timestamp field to the OrderQueue struct and allow PerpetualVault to clear stale orders after a specified duration (e.g., 1 hour).

    • Example:

      struct OrderQueue {
      bytes32 requestKey;
      bool isSettle;
      uint256 timestamp;
      }
      function createOrder(Order.OrderType orderType, IGmxProxy.OrderData memory orderData) public returns (bytes32) {
      require(queue.requestKey == bytes32(0) || block.timestamp > queue.timestamp + 1 hours, "previous order pending");
      queue.timestamp = block.timestamp;
      // ... existing code ...
      return requestKey;
      }
      function cancelStaleOrder() external {
      require(msg.sender == perpVault, "only PerpetualVault");
      require(queue.requestKey != bytes32(0) && block.timestamp > queue.timestamp + 1 hours, "order not stale");
      delete queue;
      }
  2. Enhance Frozen Order Handling:

    • Ensure afterOrderFrozen clears the queue to prevent indefinite blocking:

      function afterOrderFrozen(bytes32 key, Order.Props memory order, EventLogData memory) external override validCallback(key, order) {
      if (key == queue.requestKey) {
      delete queue;
      }
      }
  3. Prevent Queue Overwrite:

    • Add a check to block new orders when queue is occupied:

      function createOrder(Order.OrderType orderType, IGmxProxy.OrderData memory orderData) public returns (bytes32) {
      require(queue.requestKey == bytes32(0), "previous order pending");
      // ... existing code ...
      return requestKey;
      }
Updates

Lead Judging Commences

n0kto Lead Judge 9 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.

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

Support

FAQs

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

Give us feedback!