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

Infinite Retry Loop for Paraswap Swaps Could Enable Minor DoS

[L-1] Infinite Retry Loop for Paraswap Swaps Could Enable Minor DoS

Description:

The Perpetual Vault Protocol enforces an all-or-nothing execution for Paraswap swaps via ParaSwapUtils.swap, reverting if the full amount cannot be swapped (no partial fills). The "retry until success" design assumes the Protocol Keeper retries failed swaps via runNextAction (e.g., due to insufficient liquidity). Without a retry limit, an attacker could craft a swap with unachievable parameters (e.g., 1000 USDC to 1 WETH with low liquidity), causing the Keeper to retry indefinitely. This is executed by the Protocol Keeper, not GMX Keepers, and leverages the absence of partial fill support.

Impact:

  • Minor Denial of Service: The nonReentrant modifier delays subsequent vault actions (e.g., deposits, withdrawals) while the Keeper retries, but doesn’t fully lock the vault like GMX’s _gmxLock. Other actions remain processable after retries conclude or are canceled.

  • Gas Consumption: Each retry (~100k gas, ~$50 at 25 gwei, $2000/ETH) consumes Protocol Keeper ETH, though the Known Issues section assumes "more than enough gas" mitigates exhaustion (e.g., 1 ETH covers ~10,000 retries).

  • Fund Accessibility: Funds stay in the vault, accessible once retries stop or are canceled—temporary inconvenience, not loss.

  • Severity: Low—temporary delays with low impact, mitigated by assumed gas funding and recoverable via manual cancelFlow.

Proof of Concept:

Assume a test setup with PerpetualVault, KeeperProxy, and ParaSwapUtils. The attacker deposits 1000 USDC and triggers a failing Paraswap swap:

  1. Attacker Deposits: Deposits 1000 USDC via vault.deposit.

  2. Malicious Swap: Keeper calls run with swapData (1000 USDC → 1 WETH, unachievable due to liquidity).

  3. Failure and Retry:

    • ParaSwapUtils.swap reverts.

    • Keeper retries via runNextAction.

  4. Loop: Repeats (e.g., 10 times in test), each call delayed by nonReentrant, consuming ~$50/retry.

  5. Impact: New deposits delayed until retries stop or cancelFlow is called.

Recommended Mitigation:

Add a retry limit to cap Paraswap swap attempts, preventing excessive retries while maintaining intent:

  • Fix:

    // In PerpetualVault.sol
    uint256 public constant MAX_PARASWAP_RETRIES = 3;
    mapping(bytes32 => uint256) public paraswapRetries;
    function _doDexSwap(bytes memory data, bool isCollateralToIndex) internal returns (uint256 outputAmount) {
    bytes32 swapKey = keccak256(data);
    if (paraswapRetries[swapKey] >= MAX_PARASWAP_RETRIES) {
    delete swapProgressData;
    delete flow;
    return 0;
    }
    // Existing swap logic...
    paraswapRetries[swapKey]++;
    }
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.