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

Insufficient Execution Fee Causing Temporary Lock in PerpetualVault

Summary

A vulnerability in PerpetualVault.sol allows an insufficient executionFee to cause GMX orders to stall temporarily if gas prices spike after submission. This locks the vault (_gmxLock = true) until the GMX Keeper cancels the order, resulting in a temporary Denial of Service (DoS). While funds remain safe and are returned upon cancellation, the vault is inaccessible during this period, impacting user operations.

Vulnerability Details

  • Location: PerpetualVault.sol, _payExecutionFee(https://github.com/CodeHawks-Contests/2025-02-gamma/blob/84b9da452fc84762378481fa39b4087b10bab5e0/contracts/PerpetualVault.sol#L804)

    function _payExecutionFee(uint256 depositId, bool isDeposit) internal {
    uint256 minExecutionFee = getExecutionGasLimit(isDeposit) * tx.gasprice;
    if (msg.value < minExecutionFee) {
    revert Error.InsufficientAmount();
    }
    if (msg.value > 0) {
    payable(address(gmxProxy)).transfer(msg.value);
    depositInfo[depositId].executionFee = msg.value;
    }
    }
  • Description: The executionFee is calculated as getExecutionGasLimit(isDeposit) * tx.gasprice without a buffer for gas price fluctuations. If gas prices increase significantly post-submission (e.g., from 0.1 Gwei to 1 Gwei), the fee may become insufficient for the GMX Keeper to execute the order. The Keeper eventually cancels the order via afterOrderCancellation, resetting _gmxLock and flow, but the vault remains locked until cancellation occurs (e.g., up to ~1 hour on Arbitrum mainnet fork).

  • Preconditions:

    • Gas price spikes after order submission.

    • Submitted msg.value is insufficient for the Keeper at the new gas price.

Impact

  • Temporary DoS: Vault operations (e.g., deposits, withdrawals) are halted from order submission until the GMX Keeper cancels the order, potentially lasting up to an hour or more depending on Keeper behavior.

  • Funds Safe: Tokens are returned to the vault or user upon cancellation, preventing permanent loss.

  • User Experience: Temporary inaccessibility during volatile market conditions may frustrate users, potentially affecting trust and protocol usability.

Tools Used

  • Manual Review

  • Foundry

POC

Add the test to PerpetualVaultTest (test/PerpetualVaultTest.t.sol)

contract PerpetualVaultTest is Test, ArbitrumTest {
enum PROTOCOL {
DEX,
GMX
}
address payable vault;
address payable vault2x;
VaultReader reader;
MockData mockData;
IERC20 usdc = IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831); // USDC on Arbitrum
//// .....
function test_InsufficientExecutionFeeLock() external {
address alice = makeAddr("alice");
uint256 depositAmount = 1000 * 10**6; // 1000 USDC
// Step 1: Set initial low gas price (e.g., 0.1 Gwei)
vm.txGasPrice(0.1 gwei);
// Step 2: Alice deposits with just enough execution fee based on low gas price
vm.startPrank(alice);
deal(address(usdc), alice, depositAmount); // Fund USDC alice
deal(alice, 1 ether); // Fund ETH
usdc.approve(vault, depositAmount);
uint256 executionFee = PerpetualVault(vault).getExecutionGasLimit(true);
if (executionFee == 0) executionFee = 2_000_000;
executionFee = executionFee * tx.gasprice;
PerpetualVault(vault).deposit{value: executionFee}(depositAmount);
vm.stopPrank();
// Step 3: Simulate gas price spike (e.g., to 1 Gwei)
vm.txGasPrice(1 gwei);
// Step 4: Wait for GMX Keeper to process (or not process) on fork
skip(1 hours); // Fast forward time
// Step 5: Verify vault state (check if locked or canceled by Keeper)
bool isLocked = PerpetualVault(vault).isLock();
console.log("Vault lock state after 1 hour:", isLocked ? "Locked" : "Unlocked");
uint8 flowState = uint8(PerpetualVault(vault).flow());
console.log("Flow state:", flowState);
// If vault sitll locked, check DoS
if (isLocked) {
vm.startPrank(alice);
deal(address(usdc), alice, depositAmount);
usdc.approve(vault, depositAmount);
vm.expectRevert(Error.GmxLock.selector);
PerpetualVault(vault).deposit{value: executionFee}(depositAmount);
vm.stopPrank();
address gmxProxy = address(PerpetualVault(vault).gmxProxy());
assertEq(usdc.balanceOf(gmxProxy), depositAmount, "Funds should be stuck in GmxProxy");
console.log("DoS confirmed: Vault locked due to insufficient execution fee");
} else {
console.log("Vault unlocked: GMX Keeper likely canceled the order");
}
}
}

[PASS] test_InsufficientExecutionFeeLock() (gas: 658956)

Logs:
Vault lock state after 1 hour: Unlocked

Flow state: 0

Vault unlocked: GMX Keeper likely canceled the order

Recommendations

  1. Add Gas Price Buffer:

    • Increase the executionFee calculation with a 50% buffer to reduce the likelihood of orders stalling due to gas price spikes.

    uint256 minExecutionFee = getExecutionGasLimit(isDeposit) * tx.gasprice * 15 / 10; // 50% buffer
  2. Implement Explicit Timeout Mechanism:

    • Add a manual cancellation option to recover the vault faster if the GMX Keeper delays beyond an acceptable period (e.g., 1 hour).

    mapping(bytes32 => uint256) public orderTimestamps;
    function cancelOrder() external nonReentrant {
    require(block.timestamp > orderTimestamps[queue.requestKey] + 1 hours, "Order still active");
    gmxProxy.cancelOrder();
    _gmxLock = false;
    }
  3. Monitor Gas Prices Off-Chain:

    • Use an off-chain script to dynamically adjust msg.value based on recent gas price trends, though this is supplementary to on-chain fixes.

Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

invalid_tx-gasprice_instable

The frontrunner won’t trigger "congestion" without a huge amount of transactions, and it will cost a lot. Moreover, the execution gas limit is overestimated to prevent such cases: ``` executionGasLimit = baseGasLimit + ((estimatedGasLimit + _callbackGasLimit) * multiplierFactor) / PRECISION; ``` The keeper won’t wait long to execute the order; otherwise, GMX would not be competitive.

Support

FAQs

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

Give us feedback!