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

Single-Order `queue` Overwritten by Consecutive Calls to `createOrder()`

In the GmxProxy contract

Summary:

Because GmxProxy uses a single global queue.requestKey and overwrites it on every createOrder() or settle(), the contract fails to track older pending orders correctly if multiple are created consecutively. Properly storing each order’s unique requestKey is crucial to ensuring GMX callbacks and user cancellations reference the correct order, preventing logic confusion and potential partial breaks in the order flow.

RootCause & Where It Happens

In the contract, createOrder() stores every newly created order’s requestKey into the global:

queue.requestKey = requestKey;

similarly in settle():

bytes32 requestKey = gExchangeRouter.createOrder(params);
queue.requestKey = requestKey;
queue.isSettle = true;

The contract’s queue struct only holds one requestKey and an isSettle boolean. There is no logic preventing the user (the perpVault) from calling createOrder() (or settle()) multiple times in rapid succession. If two orders are created before the first one is executed by GMX, the second order’s requestKey overwrites the first order’s stored key in queue. Consequently, if the GMX keeper executes or cancels the first order later, that callback will produce a mismatch or the contract will no longer have the correct requestKey in memory for the first order.

Impact

  1. Loss of Reference to Older Orders

    • The first order’s requestKey is overwritten by a second call to createOrder(), so the contract’s queue no longer references the original. Any callback or cancellation for that first order will not match the stored requestKey, potentially leading to incorrect or missing finalization in the perpetual vault.

  2. Potential Order-Flow Breakdowns

    • The contract depends on queue.requestKey when finalizing or canceling orders. If multiple orders are stacked, the second one’s creation effectively “forgets” the older order. The older order may then execute or cancel without the appropriate “afterOrderExecution” or “afterOrderCancellation” logic triggered in perpVault (or it triggers with an empty queue or a mismatched key).

  3. Possible DoS or Confusion

    • If the keeper or user attempts to cancel the first order, cancelOrder() only calls gExchangeRouter.cancelOrder(queue.requestKey), referencing the newest overwritten key, not the one for the older order. This either reverts (queue.requestKey != olderKey) or cancels the wrong order.

The ISSUE ->

  • The contract’s design allows multiple calls to createOrder() from perpVault with no mutual exclusion or queue system.

  • The code never guards against overwriting an active requestKey.

  • The queue concept is singly dimensioned, storing exactly one key. Storing multiple, unexecuted orders is not properly supported.

  • This leads to well-defined breakage if two or more orders remain in flight at the same time.

Foundry PoC

PoC test that demonstrates the “Single-Order Queue Overwritten by Consecutive Calls to createOrder()” vulnerability. In this PoC, we simulate two consecutive order creations on a test version of the GmxProxy. The test shows that the first order’s request key is overwritten by the second order, and then when a callback is simulated using the first key, the call fails (reverts). This proves that the proxy cannot reliably track multiple pending orders.


// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "../src/interfaces/IGmxProxy.sol";
import "../src/libraries/Order.sol";
import "../src/GmxProxy.sol";
import "../src/libraries/StructData.sol";
/**
* @dev TestGmxProxy is a minimal test harness version of GmxProxy that overrides
* createOrder() to simulate order creation and returns a pseudo-random requestKey.
*/
contract TestGmxProxy is GmxProxy {
uint256 public orderCounter;
// Override createOrder to simply generate a new requestKey and store it in the queue.
function createOrder(
Order.OrderType /*orderType*/,
IGmxProxy.OrderData memory /*orderData*/
) public override returns (bytes32) {
orderCounter++;
// Generate a pseudo-unique key.
bytes32 key = keccak256(abi.encodePacked(orderCounter, block.timestamp, msg.sender));
queue.requestKey = key;
// For simplicity, we do not set queue.isSettle here.
return key;
}
// Expose queue for testing.
function getQueue() external view returns (bytes32, bool) {
return (queue.requestKey, queue.isSettle);
}
}
/**
* @title MultipleOrdersOverwriteTest
* @dev This test demonstrates that if two orders are created in succession, the first order’s key is overwritten.
*/
contract MultipleOrdersOverwriteTest is Test {
TestGmxProxy testProxy;
// Dummy order data (we don't need real values for this PoC)
IGmxProxy.OrderData dummyOrderData;
function setUp() public {
testProxy = new TestGmxProxy();
// Set dummy order data fields (only amountIn is needed for our simulation)
dummyOrderData.amountIn = 1;
dummyOrderData.callbackGasLimit = 300000;
dummyOrderData.sizeDeltaUsd = 1000;
// Other fields can remain zero or default
}
function testQueueOverwriting() public {
// Simulate first order creation.
bytes32 firstKey = testProxy.createOrder(Order.OrderType.MarketIncrease, dummyOrderData);
// Capture the queue after first order.
(bytes32 keyAfterFirst, ) = testProxy.getQueue();
assertEq(firstKey, keyAfterFirst, "First order key should be stored in queue");
// Simulate a second order creation.
bytes32 secondKey = testProxy.createOrder(Order.OrderType.MarketSwap, dummyOrderData);
// Capture the queue after second order.
(bytes32 keyAfterSecond, ) = testProxy.getQueue();
assertEq(secondKey, keyAfterSecond, "Second order key should overwrite queue");
// Verify that the second key is different from the first key.
assertTrue(firstKey != secondKey, "Keys should differ");
// Now, simulate a GMX callback using the first order key.
// We expect that because the queue now holds the second key, a callback with the first key should fail.
// For our PoC, we simulate the callback by calling afterOrderExecution() and expecting a revert.
// Prepare dummy OrderResultData and MarketPrices (values don't matter for this PoC).
IGmxProxy.OrderResultData memory resultData;
resultData.orderType = Order.OrderType.MarketIncrease;
resultData.isLong = true;
resultData.sizeDeltaUsd = 1000;
resultData.isSettle = false;
MarketPrices memory prices;
prices.indexTokenPrice = PriceRange({min: 1, max: 1});
prices.longTokenPrice = PriceRange({min: 1, max: 1});
prices.shortTokenPrice = PriceRange({min: 1, max: 1});
// Expect the callback to revert because the queue no longer contains the first key.
vm.expectRevert();
testProxy.afterOrderExecution(firstKey, bytes32(0), resultData, prices);
}
}

Recommended Remediation

  1. Support Multiple Pending Orders

    • Use a mapping of orderId or array instead of a single queue.requestKey. Each newly created order is appended, and each callback references the correct key.

    • The contract can store requestKey -> bool isSettle so that each order’s metadata is not lost.

  2. Lock the Proxy for One Order at a Time

    • If the design truly wants only one order in flight, enforce a check that no requestKey is set prior to creating a new order. For example:

      require(queue.requestKey == bytes32(0), "Finish or cancel the current order first");
    • Then the user cannot create a second order while the first is pending.

  3. Handle Overwrites

    • If multiple orders must exist concurrently, the contract must unify how queue is used or discard the single-queue approach, migrating to a multi-key approach.

Updates

Lead Judging Commences

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