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

Overwriting of the `queue.isSettle` Flag in GMXProxy

GMXProxy

Summary

Design flaw exists in its management of the global order queue. Specifically, the contract uses a single OrderQueue struct to store both the order’s unique request key and an isSettle flag that indicates whether the order is a settlement order. Because new orders simply overwrite this global queue without verifying that a previous order has been fully processed, concurrent or rapidly submitted orders can cause the isSettle flag to be overwritten. This leads to a situation where the callback for an earlier order may be processed with the wrong flag, causing the PerpetualVault to misinterpret the order type and update its state incorrectly.

Where the Issue Is

  • Order Creation in GMXProxy:
    When a regular order is created via createOrder, the global queue is set with:

    queue.requestKey = keyA; // for Order A
    // Implicitly, queue.isSettle remains false (default)

    Later, when a settlement order is created via settle, the same global queue is overwritten:

    queue.requestKey = keyB; // for Order B
    queue.isSettle = true;

    No check is made to ensure that an active order (Order A) has been completed before overwriting the global queue.

  • Callback Handling:
    In afterOrderExecution (and similarly in afterOrderCancellation), the contract reads the current value of queue.isSettle and includes it in the order result data sent to PerpetualVault. If Order A’s callback is executed after Order B has been submitted, the callback for Order A will erroneously use isSettle = true (from Order B), misrepresenting Order A as a settlement order.

Root Cause

The root cause of the vulnerability is that the GMXProxy contract relies on a single, global OrderQueue struct for all order submissions without binding the isSettle flag to a unique order identifier. In effect, if multiple orders are initiated in close succession—either accidentally or through attacker‐induced race conditions—the global state is overwritten. There is no on‑chain mechanism (e.g., a mapping from request keys to order metadata) to ensure that callbacks are processed against the vault state snapshot corresponding to the order’s creation.

Clear Attack Path / Scenario

  1. Order A (Regular Order) Submission:

    • PerpetualVault calls createOrder, setting:
      queue.requestKey = keyA
      queue.isSettle = false

    • Order A is recorded as a regular (non-settlement) order.

  2. Order B (Settlement Order) Submission:

    • Before Order A’s callback is processed, PerpetualVault calls settle, which overwrites the global queue with:
      queue.requestKey = keyB
      queue.isSettle = true

  3. Callback for Order A:

    • GMX later triggers the callback for Order A.

    • The callback function in GMXProxy uses the current global queue, finding requestKey = keyB and isSettle = true.

    • As a result, the PerpetualVault receives data indicating that Order A was a settlement order, leading to incorrect state updates (for example, marking a position as settled when it is not) or misallocation of fees.

Impact

  • State Inconsistency: The PerpetualVault may misinterpret the type of order it is processing. This can result in incorrect collateral adjustments, fee misallocation, or errors in share minting.

  • Financial Loss: Persistent misinterpretation of order types could allow an attacker—or even result from a coding error—to “game” the vault state, leading to extraction or diversion of funds.

  • Invariant Violation: The protocol’s core invariants regarding fair and consistent fee accounting and state management are violated.

Recommendation

To mitigate this vulnerability, the order tracking should be redesigned so that each order’s isSettle flag is stored independently:

  • Mapping Approach:
    Replace the single global queue with a mapping keyed by the unique order request key:

    mapping(bytes32 => bool) public orderIsSettle;
    bytes32 public latestRequestKey; // (Optional: if tracking the latest order is needed)

    Then, in createOrder and settle, store:

    // In createOrder:
    orderIsSettle[requestKey] = false;
    latestRequestKey = requestKey; // Optional
    // In settle:
    orderIsSettle[requestKey] = true;
    latestRequestKey = requestKey; // Optional

    Finally, in callback functions (e.g., afterOrderExecution), retrieve the order’s flag using the unique request key:

    bool isSettleForOrder = orderIsSettle[requestKey];
    delete orderIsSettle[requestKey]; // Clean up after callback
  • Alternative Approach:
    Enforce single-order processing by checking that no order is currently active before allowing a new one. However, this may restrict concurrent order handling, so the mapping approach is preferable.

Severity

High – This vulnerability directly impacts the integrity of order processing, which may lead to misallocation of funds or incorrect state transitions in the protocol.


Proof-of-Concept (Foundry PoC)

Below is a Foundry PoC that simulates the vulnerability by using simplified versions of the GMXProxy and PerpetualVault components. The PoC demonstrates that when a regular order is submitted (with isSettle = false) and then a settlement order is submitted (with isSettle = true), the global queue is overwritten. Consequently, a callback for the first order incorrectly interprets the order type.

File: src/FakeGmxProxy.sol

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;
/**
* @title FakeGmxProxy
* @dev A simplified version of the GMXProxy contract that demonstrates the vulnerability.
* It uses a single global OrderQueue struct to store the current order's request key and isSettle flag.
*/
contract FakeGmxProxy {
struct OrderQueue {
bytes32 requestKey;
bool isSettle;
}
// Global order queue
OrderQueue public queue;
/**
* @notice Simulates creating a regular order.
* @param requestKey The unique key for the order.
*/
function createOrder(bytes32 requestKey) external {
// For a regular order, isSettle remains false (default)
queue.requestKey = requestKey;
// isSettle is false by default (no explicit assignment needed)
}
/**
* @notice Simulates creating a settlement order.
* @param requestKey The unique key for the settlement order.
*/
function settle(bytes32 requestKey) external {
// For settlement orders, set isSettle to true.
queue.requestKey = requestKey;
queue.isSettle = true;
}
/**
* @notice Simulates processing a callback.
* @param expectedRequestKey The request key that the vault expects to be processed.
* @return returnedIsSettle The isSettle flag from the global queue.
*/
function processCallback(bytes32 expectedRequestKey) external view returns (bool returnedIsSettle) {
// In a real callback, the orderResultData would be constructed using queue.isSettle.
// Here, we simply return the current isSettle flag.
return queue.isSettle;
}
/**
* @notice Clears the global queue.
*/
function clearQueue() external {
delete queue;
}
}

File: src/FakeVault.sol

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;
import "./FakeGmxProxy.sol";
/**
* @title FakeVault
* @dev A simplified version of a vault that submits orders to the FakeGmxProxy.
* It simulates the scenario where a regular order is submitted followed by a settlement order.
*/
contract FakeVault {
FakeGmxProxy public proxy;
// For demonstration, we store the expected order type for the first order.
bool public expectedIsSettleForOrderA;
constructor(FakeGmxProxy _proxy) {
proxy = _proxy;
}
/**
* @notice Submits a regular order (Order A) to the proxy.
* @param orderKey The unique key for Order A.
*/
function submitOrderA(bytes32 orderKey) external {
expectedIsSettleForOrderA = false; // Regular order expects isSettle to be false.
proxy.createOrder(orderKey);
}
/**
* @notice Submits a settlement order (Order B) to the proxy.
* @param orderKey The unique key for Order B.
*/
function submitOrderB(bytes32 orderKey) external {
// For settlement order, isSettle should be true.
proxy.settle(orderKey);
}
/**
* @notice Simulates processing the callback for Order A.
* @param orderKey The expected order key for Order A.
* @return misinterpretedIsSettle The value of the isSettle flag read during the callback.
*/
function processCallbackForOrderA(bytes32 orderKey) external view returns (bool misinterpretedIsSettle) {
// In an ideal scenario, the callback should use isSettle = false (expected for Order A).
// However, if Order B overwrote the queue, the value may be true.
misinterpretedIsSettle = proxy.processCallback(orderKey);
}
}

File: test/FakeVaultTest.t.sol

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;
import "forge-std/Test.sol";
import "../src/FakeGmxProxy.sol";
import "../src/FakeVault.sol";
/**
* @title FakeVaultTest
* @dev This test demonstrates the vulnerability by simulating the submission of two orders:
* Order A (regular order) followed by Order B (settlement order) before Order A's callback.
* The test verifies that the callback for Order A incorrectly interprets the isSettle flag.
*/
contract FakeVaultTest is Test {
FakeGmxProxy proxy;
FakeVault vault;
function setUp() public {
proxy = new FakeGmxProxy();
vault = new FakeVault(proxy);
}
function testQueueIsSettleOverwritten() public {
// Step 1: Submit Order A (regular order) with requestKey "orderA".
bytes32 orderKeyA = keccak256(abi.encodePacked("orderA"));
vault.submitOrderA(orderKeyA);
// At this point, the proxy.queue is set with orderKeyA and isSettle should be false.
bool isSettleAfterOrderA = proxy.processCallback(orderKeyA);
assertEq(isSettleAfterOrderA, false, "Order A should have isSettle == false");
// Step 2: Submit Order B (settlement order) with requestKey "orderB".
bytes32 orderKeyB = keccak256(abi.encodePacked("orderB"));
vault.submitOrderB(orderKeyB);
// Now, the proxy.queue is overwritten with orderKeyB and isSettle == true.
bool isSettleAfterOrderB = proxy.processCallback(orderKeyB);
assertEq(isSettleAfterOrderB, true, "Order B should have isSettle == true");
// Step 3: Process callback for Order A.
// Although Order A was a regular order, the global queue now holds isSettle == true.
bool misinterpretedIsSettle = vault.processCallbackForOrderA(orderKeyA);
// The expected behavior for Order A is false, but due to the overwrite, it is true.
assertTrue(misinterpretedIsSettle, "Callback for Order A incorrectly indicates a settlement order");
}
}


From the project root, run:

forge test --match-path test/FakeVaultTest.t.sol

The test should pass—demonstrating that when Order B (settlement order) is submitted before Order A’s callback, the global queue’s isSettle flag is overwritten. As a result, the callback for Order A misinterprets the order as a settlement order.

Updates

Lead Judging Commences

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

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.

invalid_queue_requestKey_overwrite

Order is proceed one by one and requestKey is only used to cancelOrder. I didn’t see any real scenario where it will cause a problem. Flow and gmxLock will prevent that to happen.

Support

FAQs

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

Give us feedback!