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

Execution Fee Bypass Enables Dead Orders in GMX Queue

Summary

GmxProxy contract allows order creation and settlement without sufficient execution fees, potentially leading to failed transactions on GMX. This breaks the requirement that all GMX interactions must have adequate execution fees to compensate keepers. Occurs when the GmxProxy contract processes orders with insufficient ETH balance for execution fees. The contract's minEth check exists but can be bypassed, similar to trying to send a package without paying sufficient postage, it gets accepted initially but fails at delivery.

The core functionality revolves around a vault system where users deposit USDC as collateral. This collateral is then used to manage leveraged positions on GMX perpetual markets, with each vault dedicated to a specific market and leverage ratio (ranging from 1x to 3x). For example, separate vaults exist for 1x ETH, 2x ETH, and 3x ETH positions.

Vulnerability Details

When a user initiates a position through PerpetualVault, the request flows to GmxProxy which should ensure sufficient ETH exists for GMX keeper execution fees. The contract checks if its balance exceeds minEth (0.002 ETH), but here's where things get interesting:

The execution fee validation in createOrder() looks promising at first glance: GmxProxy#createOrder

function createOrder(Order.OrderType orderType, IGmxProxy.OrderData memory orderData) public returns (bytes32) {
// 🔒 Access control check
require(msg.sender == perpVault, "invalid caller");
// 💰 Calculate required execution fee
uint256 positionExecutionFee = getExecutionGasLimit(
orderType,
orderData.callbackGasLimit
) * tx.gasprice;
// 🚨 VULNERABILITY: Only checks contract balance, not msg.value
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"
);
// 💸 Transfer execution fee to GMX
gExchangeRouter.sendWnt{value: positionExecutionFee}(
orderVault,
positionExecutionFee
);
// 🔄 Handle token transfers for swaps and increases
if (orderType == Order.OrderType.MarketSwap ||
orderType == Order.OrderType.MarketIncrease) {
// 👍 Token approvals and transfers
IERC20(orderData.initialCollateralToken).safeApprove(...);
gExchangeRouter.sendTokens(...);
}
// 📝 Construct order parameters
CreateOrderParamsAddresses memory paramsAddresses = ...;
CreateOrderParamsNumbers memory paramsNumber = ...;
// 🚀 Submit order to GMX
bytes32 requestKey = gExchangeRouter.createOrder(params);
// ✍️ Track order in queue
queue.requestKey = requestKey;
return requestKey;
}

However, the contract allows orders to proceed even when msg.value is lower than minEth. This creates a scenario where orders enter the GMX system but lack the necessary fees for keeper execution.

When an order lacks sufficient execution fees:

  1. The GMX keeper network won't process it

  2. The position remains in limbo

  3. Users lose their partial execution fee payment

  4. The vault's position management capabilities become blocked

It disrupts the core automated position management that makes the Perpetual Vault system valuable to users seeking hands-off leveraged trading.

Impact

The protocol's key innovation is its automated position management through a Keeper system. When users deposit funds, they're essentially delegating trading execution to the protocol's automated strategies. For 1x long positions, the system can execute trades through either GMX spot markets or Paraswap for optimal pricing. For leveraged positions (>1x) or shorts, the system interacts directly with GMX's perpetual markets.

Imagine a busy highway where cars can enter without paying the toll, only to be permanently stuck at a checkpoint. The GmxProxy contract creates this exact scenario by allowing position orders to enter GMX's system without proper execution fees, leading to permanently stranded positions.

When users interact with the Perpetual Vault to manage leveraged positions, their requests flow through GmxProxy which acts as the gatekeeper to GMX's perpetual trading system. The contract attempts to ensure sufficient ETH exists for keeper execution fees, but its validation has a critical oversight.

When an order enters without proper execution fees (less than 0.002 ETH), it becomes permanently stuck in GMX's queue. The keepers, who require proper compensation to execute orders, will perpetually skip these underfunded positions. This breaks the vault's core promise of automated position management.

Recommendations

Consider this validation to ensure proper execution fee handling while maintaining compatibility with the broader system architecture including PerpetualVault's automated position management and GMX integration.

function createOrder(Order.OrderType orderType, IGmxProxy.OrderData memory orderData) public returns (bytes32) {
// 🔒 Only PerpetualVault can call this
require(msg.sender == perpVault, "invalid caller");
// 💰 Calculate keeper execution fee
uint256 positionExecutionFee = getExecutionGasLimit(
orderType,
orderData.callbackGasLimit
) * tx.gasprice;
// ✨ THE VALIDATION
require(msg.value >= positionExecutionFee, "Insufficient execution fee");
require(address(this).balance >= positionExecutionFee, "Insufficient balance");
// 🛡️ GMX system check
bytes32 executeOrderFeatureKey = keccak256(
abi.encode(EXECUTE_ORDER_FEATURE_DISABLED, orderHandler, orderType)
);
require(dataStore.getBool(executeOrderFeatureKey) == false, "gmx execution disabled");
// 💸 Forward execution fee to GMX
gExchangeRouter.sendWnt{value: positionExecutionFee}(orderVault, positionExecutionFee);
// 🔄 Setup token transfers for position changes
if (orderType == Order.OrderType.MarketSwap || orderType == Order.OrderType.MarketIncrease) {
IERC20(orderData.initialCollateralToken).safeApprove(address(gmxRouter), orderData.amountIn);
gExchangeRouter.sendTokens(orderData.initialCollateralToken, orderVault, orderData.amountIn);
}
// 📋 Build order parameters
CreateOrderParamsAddresses memory paramsAddresses = CreateOrderParamsAddresses({...});
CreateOrderParamsNumbers memory paramsNumbers = CreateOrderParamsNumbers({...});
// 🚀 Submit to GMX
bytes32 requestKey = gExchangeRouter.createOrder(params);
queue.requestKey = requestKey;
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.

Support

FAQs

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

Give us feedback!