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

Multiple Concurrent Orders Break GMX Position Management

Summary

GmxProxy contract allows multiple pending orders to be created simultaneously, violating the requirement that only one order should be in progress at a time. This breaks the order queue management system and could lead to order execution conflicts. When a keeper triggers order creation through the PerpetualVault, the GmxProxy's queue tracking gets out of sync because it doesn't enforce the single pending order constraint.

This is similar to a ticket booking system allowing multiple reservations for the same seat while each booking appears valid in isolation, it creates conflicts at execution time.

The GmxProxy.sol implementation only tracks orders through: GmxProxy.sol#L75

OrderQueue public queue; // Stores the current pending order

The issue is that createOrder() doesn't verify if there's already a pending order before creating a new one. The queue struct gets overwritten rather than enforcing exclusivity.

The trace is like this: PerpetualVault.run()GmxProxy.createOrder() → queue update → No uniqueness check

The current implementation allows a dangerous scenario, multiple orders can be created without checking if there's already one in progress. Each new order simply overwrites the queue, breaking the fundamental assumption that orders are processed sequentially. Consider what happens when a vault tries to adjust positions during volatile market conditions:

  1. Vault A initiates a position increase

  2. Before that order executes, Vault B initiates another order

  3. The queue gets overwritten, losing track of Vault A's order

  4. Both vaults have paid execution fees, but only one order will actually execute

The execution fees (typically 0.002 ETH per order) are lost for any overwritten orders, and position updates become unpredictable. This directly impacts the vault's ability to maintain its target leverage ratio. The protocol's keeper system is explicitly designed for sequential order processing

Vulnerability Details

The GmxProxy contract serves as a critical bridge in the protocol's architecture, managing all interactions with GMX's perpetual trading system. At its peek, it's designed to handle leveraged trading positions up to 3x, with each vault maintaining consistent leverage throughout its lifecycle.

The vulnerability emerges in the order queue management system. When a vault initiates a trade through GmxProxy, the contract should act like a meticulous traffic controller, ensuring only one trade passes through at a time. However, the current implementation lacks this crucial check.

Unfolds in the createOrder() function: GmxProxy#createOrder

function createOrder(
Order.OrderType orderType,
IGmxProxy.OrderData memory orderData
) public returns (bytes32) {
// 🔒 Access control check
require(msg.sender == perpVault, "invalid caller");
// 💰 Calculate and validate execution fee
uint256 positionExecutionFee = getExecutionGasLimit(
orderType,
orderData.callbackGasLimit
) * tx.gasprice;
require(
address(this).balance >= positionExecutionFee,
"insufficient eth balance"
);
// 🚦 Feature flag validation
bytes32 executeOrderFeatureKey = keccak256(
abi.encode(
EXECUTE_ORDER_FEATURE_DISABLED,
orderHandler,
orderType
)
);
require(
dataStore.getBool(executeOrderFeatureKey) == false,
"gmx execution disabled"
);
// 💸 Send execution fee
gExchangeRouter.sendWnt{value: positionExecutionFee}(
orderVault,
positionExecutionFee
);
// 🔄 Handle token transfers for swaps and increases
if (
orderType == Order.OrderType.MarketSwap ||
orderType == Order.OrderType.MarketIncrease
) {
IERC20(orderData.initialCollateralToken).safeApprove(
address(gmxRouter),
orderData.amountIn
);
gExchangeRouter.sendTokens(
orderData.initialCollateralToken,
orderVault,
orderData.amountIn
);
}
// 📝 Construct order parameters
CreateOrderParamsAddresses memory paramsAddresses = CreateOrderParamsAddresses({/*...*/});
CreateOrderParamsNumbers memory paramsNumber = CreateOrderParamsNumbers({/*...*/});
CreateOrderParams memory params = CreateOrderParams({/*...*/});
// 🚨 Critical vulnerability: No check for existing orders
bytes32 requestKey = gExchangeRouter.createOrder(params);
queue.requestKey = requestKey; // 💥 Overwrites any pending order!
return requestKey;
}

Each new order blindly overwrites the queue, breaking the protocol's fundamental assumption of sequential order processing. This isn't just a theoretical concern, it directly impacts the vault's ability to maintain its target leverage ratio, which is a core invariant of the system.

The consequences are precise and measurable:

  • Execution fees of 0.002 ETH are lost for each overwritten order

  • Position updates become unpredictable, potentially deviating from the vault's specified leverage (1x-3x range)

  • The keeper system's asynchronous action processing gets disrupted

Think of it like a vending machine that accepts multiple payments simultaneously but only dispenses one item, the additional payments are lost while the machine's inventory state becomes unclear.

Impact

The assumption that the PerpetualVault's flow control (_gmxLock) would prevent concurrent orders. However, this overlooks:

  1. The keeper system's asynchronous nature (documented in PerpetualVault.sol comments)

  2. GMX's own order execution delays

  3. The possibility of multiple vaults sharing a GmxProxy (though not recommended)

Direct Impact:

  • Lost execution fees (0.002 ETH per overwritten order)

  • Position updates becoming unpredictable

  • Leverage ratios deviating from targets

graph LR
A[Order 1 Created] --> B[Fee Paid]
C[Order 2 Created] --> D[Overwrites Queue]
D --> E[Order 1 Lost]

Recommendations

Consider enforcing the protocol's single-order invariant at the contract level, aligning with the keeper system's design philosophy of sequential, deterministic order processing.

function createOrder(
Order.OrderType orderType,
IGmxProxy.OrderData memory orderData
) public returns (bytes32) {
// 🔒 Access control check
require(msg.sender == perpVault, "invalid caller");
// 🛡️ NEW: Single order constraint protection
require(queue.requestKey == bytes32(0), "Order already pending");
// 💰 Calculate and validate execution fee
uint256 positionExecutionFee = getExecutionGasLimit(
orderType,
orderData.callbackGasLimit
) * tx.gasprice;
require(
address(this).balance >= positionExecutionFee,
"insufficient eth balance"
);
// 🚦 Feature flag validation
bytes32 executeOrderFeatureKey = keccak256(
abi.encode(
EXECUTE_ORDER_FEATURE_DISABLED,
orderHandler,
orderType
)
);
require(
dataStore.getBool(executeOrderFeatureKey) == false,
"gmx execution disabled"
);
// 💸 Send execution fee
gExchangeRouter.sendWnt{value: positionExecutionFee}(
orderVault,
positionExecutionFee
);
// 🔄 Handle token transfers for swaps and increases
if (
orderType == Order.OrderType.MarketSwap ||
orderType == Order.OrderType.MarketIncrease
) {
IERC20(orderData.initialCollateralToken).safeApprove(
address(gmxRouter),
orderData.amountIn
);
gExchangeRouter.sendTokens(
orderData.initialCollateralToken,
orderVault,
orderData.amountIn
);
}
// 📝 Construct order parameters
CreateOrderParamsAddresses memory paramsAddresses = CreateOrderParamsAddresses({/*...*/});
CreateOrderParamsNumbers memory paramsNumber = CreateOrderParamsNumbers({/*...*/});
CreateOrderParams memory params = CreateOrderParams({/*...*/});
// ✅ Create order (now safe with queue check)
bytes32 requestKey = gExchangeRouter.createOrder(params);
queue.requestKey = requestKey; // 🔐 Protected queue update
return requestKey;
}
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!