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

GMX Order Execution Failure Leading to Stranded Collateral (Improper Collateral Recovery on Order Failure)

Summary

Collateral remains locked in GMX's OrderVault when orders fail execution, causing permanent loss of user funds.

Vulnerability Details

Description:
When GMX orders fail execution (due to slippage, liquidity issues, etc.), the protocol fails to recover collateral from GMX's OrderVault. The afterOrderCancellation callback resets internal state but doesn't handle collateral retrieval.

Root Cause:
The root cause of this issue is the lack of proper handling for failed orders in the createOrder function. The function does not include logic to recover collateral from GMX's OrderVault when an order fails.

// GmxProxy.sol
function afterOrderCancellation(...) {
delete queue; // Resets state
// No collateral recovery ← Critical issue
}

Proof of Concept: Copy and paste in the PerpetualVault.t.sol

function test_OrderFailure_StrandedCollateral() public {
// Setup deposit
address alice = makeAddr("alice");
uint256 depositAmount = 1e10;
depositFixture(alice, depositAmount);
// Get collateral token
IERC20 collateralToken = PerpetualVault(vault).collateralToken();
// Verify initial vault balance
uint256 initialVaultBalance = collateralToken.balanceOf(vault);
// Create order that will fail execution
MarketPrices memory prices = mockData.getMarketPrices();
bytes[] memory swapData = new bytes[](1);
address[] memory gmxPath = new address[](1);
gmxPath[0] = address(0x70d95587d40A2caf56bd97485aB3Eec10Bee6336);
uint256 minOutputAmount = depositAmount * 2; // Impossible to hit
swapData[0] = abi.encode(PROTOCOL.GMX, abi.encode(gmxPath, depositAmount, minOutputAmount));
// Execute failing order
address keeper = PerpetualVault(vault).keeper();
vm.prank(keeper);
PerpetualVault(vault).run(true, true, prices, swapData);
// Simulate failed execution
GmxOrderExecuted(false);
// Verify collateral stuck in GMX
address gmxOrderVault = address(0x31eF83a530Fde1B38EE9A18093A333D8Bbbc40D5);
uint256 strandedCollateral = IERC20(collateralToken).balanceOf(gmxOrderVault);
assertGt(strandedCollateral, 0, "Collateral not recovered");
// Verify user state
uint256[] memory depositIds = PerpetualVault(vault).getUserDeposits(alice);
(
uint256 amount,
uint256 shares,
address owner,
uint256 executionFee,
uint256 timestamp,
address recipient
) = PerpetualVault(vault).depositInfo(depositIds[0]);
assertEq(depositIds.length, 1, "Deposit not recorded");
assertEq(shares, 0, "Invalid shares minted");
// Verify vault balance unchanged
assertEq(
collateralToken.balanceOf(vault),
initialVaultBalance,
"Vault balance changed unexpectedly"
);
}

Impact

  • Funds Stuck in GMX's OrderVault: Collateral remains in GMX's OrderVault indefinitely if the order fails.

  • Protocol State Inconsistency: The PerpetualVault records the deposit as completed and mints shares, even though no GMX position is opened.

  • User Shares: Users may receive shares for deposits not backed by actual collateral, leading to potential financial discrepancies.

Tools Used

Manual

Recommendations

To mitigate this issue, implement logic to recover collateral from GMX's OrderVault when an order fails. Here are some recommendations:

  1. Recover Collateral on Order Cancellation: Add logic to the afterOrderCancellation function to recover collateral from GMX's OrderVault.

  2. Update Protocol State: Ensure that the protocol state is updated correctly when an order fails, including adjusting user shares and deposit records.

Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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