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

Inadequate Synchronization between Order Creation and Vault State Updates

Summary:
The PerpetualVault and GmxProxy contracts coordinate order creation and execution callbacks using global state variables—such as a single global OrderQueue in GmxProxy and the vault’s “flow” state in PerpetualVault—without binding each order to a unique vault state snapshot. As a result, if a new order is submitted before the callback for a previous order is processed, the global order data is overwritten. This misalignment causes the callback to operate on stale or mismatched state, potentially resulting in incorrect fee assessments, erroneous share minting, or collateral misallocations that harm depositor balances.


Issue Is in:

  • GmxProxy:
    Uses a single global variable (an OrderQueue) to store the active order’s request key. Each call to createOrder or settle simply overwrites this value.

  • PerpetualVault:
    Maintains its own “flow” state but does not bind order information (e.g. the request key) to a snapshot of the vault’s state. If two orders are submitted back‑to‑back, the vault’s expectation for a callback corresponding to the first order is lost.


Root Cause:
There is no on‑chain mechanism (such as unique order IDs linked with the vault’s state) to ensure that an order callback is processed against the exact state that existed when the order was created. As a result, if a new order is initiated before the callback for a previous order is processed—or if network delays cause callbacks to arrive out of order—the vault may update its state based on mismatched or stale order data.


Proof-of-Concept Scenario: (Foundry test below)

  1. Scenario Setup:

    • The vault submits an order (Order1) with a unique key (orderKey1), and the proxy stores this key in its global queue.

    • Before the callback for Order1 is processed, the vault submits a second order (Order2) with a different key (orderKey2), which overwrites the proxy’s global queue.

  2. Callback Mismatch:

    • When a callback for Order1 is later processed, the vault (or callback function) would expect the global queue to hold orderKey1.

    • However, the proxy’s queue now contains orderKey2. This mismatch demonstrates that the vault state is not correctly synchronized with the order data.

  3. Outcome:

    • The callback processing uses the wrong order data, leading to incorrect fee accounting or collateral adjustments. This can be exploited or can result in inadvertent misprocessing, harming depositor balances.


Impact:

  • Invariant Violation: The core invariant of consistent share and fee accounting is broken.

  • Financial Risk: Incorrect processing of order callbacks can lead to misallocated funds or dilution of depositor positions.

  • Exploitation: An attacker or a misconfigured vault could trigger back‑to‑back orders to deliberately force a state mismatch.


Recommendation:

  • Unique Order Tracking:
    Modify the design so that each order is recorded with a unique identifier and associated with a snapshot of the vault’s state.

  • State-Order Binding in Callbacks:
    In the callback functions, verify that the order being processed matches the vault’s stored state (for example, by comparing the unique order ID stored in the vault with the one provided in the callback).

  • Sequential Order Enforcement:
    Alternatively, enforce a strict sequential order submission policy such that a new order cannot be initiated until the callback for the previous order has been fully processed.


Severity:
High by likelyhood & impact


Proof-of-Concept (Foundry PoC)

Below is a simplified simulation using Foundry that demonstrates the vulnerability. In this PoC, a simplified version of a proxy (simulating GmxProxy) and a vault (simulating PerpetualVault) are implemented. The vault submits an order and later submits a second order before processing the callback for the first order. A callback simulated for Order1 then detects a mismatch in the stored order key.

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 uses a single global variable to store the active order's key.
*/
contract FakeGmxProxy {
// Global order queue (for simplicity, only storing the order request key).
bytes32 public orderQueue;
/**
* @notice Simulates order creation by storing the provided order key.
* @param orderKey The unique key for the order.
*/
function createOrder(bytes32 orderKey) external {
orderQueue = orderKey;
}
/**
* @notice Retrieves the current order key stored in the queue.
* @return The stored order key.
*/
function getOrderQueue() external view returns (bytes32) {
return orderQueue;
}
}

File: src/FakeVault.sol

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.4;
import "./FakeGmxProxy.sol";
/**
* @title FakeVault
* @dev A simplified vault contract that submits orders to the FakeGmxProxy.
* It maintains a pending order key which is expected to correspond to the proxy's stored order key when processing a callback.
*/
contract FakeVault {
FakeGmxProxy public proxy;
// Stores the pending order key that the vault expects to be processed.
bytes32 public pendingOrderKey;
constructor(FakeGmxProxy _proxy) {
proxy = _proxy;
}
/**
* @notice Submits an order with a given order key.
* @param orderKey The unique order key.
*/
function submitOrder(bytes32 orderKey) external {
pendingOrderKey = orderKey;
proxy.createOrder(orderKey);
}
/**
* @notice Simulates callback processing by comparing an expected order key with the proxy's current order key.
* @param expectedOrderKey The order key expected to be processed.
* @return True if the expected order key matches the proxy's order key, false otherwise.
*/
function processCallbackWithExpected(bytes32 expectedOrderKey) external view returns (bool) {
return (expectedOrderKey == proxy.getOrderQueue());
}
}

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 that if two orders are submitted in quick succession,
* the second order overwrites the global order key in the proxy. Consequently, when
* processing a callback expecting the first order's key, the check fails.
*/
contract FakeVaultTest is Test {
FakeGmxProxy proxy;
FakeVault vault;
function setUp() public {
proxy = new FakeGmxProxy();
vault = new FakeVault(proxy);
}
function testOrderSynchronizationMismatch() public {
// Simulate submission of the first order.
bytes32 orderKey1 = keccak256(abi.encodePacked("order1"));
vault.submitOrder(orderKey1);
// Save the expected order key for the first order.
bytes32 expectedOrderForOrder1 = orderKey1;
// Simulate submission of a second order before processing callback for order1.
bytes32 orderKey2 = keccak256(abi.encodePacked("order2"));
vault.submitOrder(orderKey2);
// At this point, the proxy's global order queue now holds orderKey2.
bytes32 currentQueue = proxy.getOrderQueue();
// The expected order for the callback of the first order (orderKey1) does not match.
bool callbackMatches = vault.processCallbackWithExpected(expectedOrderForOrder1);
// Assert that the callback for the first order would fail (i.e. expected order does not match current queue).
assertTrue(!callbackMatches, "Callback for order1 should not match after order2 overwrites the order queue");
}
}

Run the Test:
From the project root, execute:

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

confirming that when two orders are submitted consecutively, the global order key is overwritten. Consequently, a callback expecting the first order's key fails the check—demonstrating the vulnerability.

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!