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

Infinite Retry Loop for GMX Swaps Could Enable DoS

[M-1] Infinite Retry Loop for GMX Swaps Could Enable DoS

Description:

The Perpetual Vault Protocol uses an all-or-nothing execution for GMX swaps via GmxProxy.createOrder, with no partial fills. If a swap fails (e.g., due to slippage), afterOrderCancellation sets nextAction = SWAP_ACTION, prompting the Protocol Keeper (not GMX Keeper) to retry via runNextAction. Without a retry limit, an attacker can craft a swap with an unachievable minOutputAmount (e.g., 1000 USDC to 1 WETH at $2000/WETH), causing GMX to cancel it repeatedly and the Keeper to retry indefinitely. This leverages the absence of partial fills and the "retry until success" design.

Impact:

  • Denial of Service: The _gmxLock remains true during retries, fully blocking vault actions (e.g., deposits, withdrawals, signal changes) until the swap succeeds or is canceled. Unlike Paraswap’s nonReentrant delay, this halts all operations.

  • Gas Consumption: Each retry (~100k gas, ~$50 at 25 gwei, $2000/ETH) consumes Protocol Keeper ETH, though mitigated by the Known Issues assumption of "more than enough gas" (e.g., 1 ETH ≈ 10,000 retries).

  • Fund Accessibility: Funds stay in the vault or GMX orderVault, locked but not lost during retries.

  • Severity: Medium—significant disruption with full lockout, recoverable via manual cancelOrder, and gas impact softened by funding assumptions.

Proof of Concept:

  1. Setup: Attacker has 1000 USDC, vault deployed with GMX integration.

  2. Malicious Deposit: Attacker calls vault.deposit(1000e6) with 0.01 ETH execution fee, triggering a GMX swap.

  3. Craft Swap: run submits swapData with minOutputAmount = 1e18 (1 WETH), unachievable for 1000 USDC ($1000 vs. $2000).

    • GmxProxy.createOrder submits order.

    • GMX Keeper cancels due to slippage.

  4. Retry Loop:

    • afterOrderCancellation sets nextAction = SWAP_ACTION.

    • Protocol Keeper calls runNextAction, resubmitting the failing swap.

    • _gmxLock = true, blocking new actions (e.g., deposit reverts with "GmxLock").

  5. Outcome: Keeper retries indefinitely (~$50/retry), locking vault until cancelOrder is called.

Recommended Mitigation:

Add a retry limit to cap GMX swap attempts, preventing indefinite loops:

  • Fix:

    // In PerpetualVault.sol
    uint256 public constant MAX_GMX_RETRIES = 5;
    mapping(bytes32 => uint256) public gmxSwapRetries;
    function _doGmxSwap(bytes memory data, bool isCollateralToIndex) internal {
    bytes32 swapKey = keccak256(data);
    if (gmxSwapRetries[swapKey] >= MAX_GMX_RETRIES) {
    delete swapProgressData;
    delete flow;
    _gmxLock = false;
    revert("Max retries exceeded");
    }
    // Existing swap logic...
    gmxSwapRetries[swapKey]++;
    }
    function afterOrderCancellation(bytes32 requestKey, Order.OrderType orderType, IGmxProxy.OrderResultData memory) external {
    bytes32 swapKey = keccak256(nextAction.data);
    if (gmxSwapRetries[swapKey] < MAX_GMX_RETRIES) {
    nextAction.selector = NextActionSelector.SWAP_ACTION;
    nextAction.data = abi.encode(swapProgressData.remaining, swapProgressData.isCollateralToIndex);
    } else {
    delete swapProgressData;
    delete flow;
    _gmxLock = false; // Unlock vault
    }
    // ... rest of function ...
    }
Updates

Lead Judging Commences

n0kto Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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