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

Infinite Retry Loop in afterOrderCancellation Function

Summary

The afterOrderCancellation function in the PerpetualVault.sol does not enforce a limit on the number of retry attempts for failed actions. If an order (such as a swap) consistently fails due to poor liquidity or price impact, the contract will keep retrying indefinitely, leading to a denial-of-service (DoS) condition where funds are locked, and users are unable to withdraw.

Vulnerability Details

Location:

  • Function: afterOrderCancellation

  • Affected Code:

else if (orderType == Order.OrderType.MarketSwap) {
// If GMX swap fails, retry in the next action.
nextAction.selector = NextActionSelector.SWAP_ACTION;
// abi.encode(swapAmount, swapDirection): if swap direction is true, swap collateralToken to indexToken
nextAction.data = abi.encode(swapProgressData.remaining, swapProgressData.isCollateralToIndex);
}

Description:

  • When a GMX swap operation fails, the contract automatically sets the next action to retry the swap.

  • If the failure persists indefinitely due to external factors (e.g., low liquidity, incorrect price impact settings, or oracle failures), the contract will continuously retry without any termination condition.

  • This locks up user funds, preventing withdrawals and other actions dependent on contract state progression.

Impact

  • Financial Impact: Users may permanently lose access to their funds.

  • Operational Risk: The contract could become non-functional if repeated failures occur.

  • Systemic Risk: A widespread issue across multiple positions, leading to vault-wide liquidation risks.

  1. Denial-of-Service (DoS): The vault remains in a perpetual retry state, blocking all depositors from accessing their funds.

  2. Gas Drain: Since retries are initiated upon every call to afterOrderCancellation, attackers or external actors could exploit this by triggering failed transactions to consume contract gas.

  3. Keeper Exploitation: Malicious or misconfigured keepers could intentionally allow swaps to fail, causing an endless loop and disrupting contract operations.

Tools Used

Manuel Review

Recommendations

Fix: Implement Retry Limit

Add a retry counter (retryCount) and enforce a maximum retry threshold (MAX_RETRIES) to prevent infinite loops:

uint256 public retryCount;
uint256 public constant MAX_RETRIES = 3;
if (retryCount >= MAX_RETRIES) {
revert Error.TooManyRetries();
}
retryCount++;

Fix: Implement Timeout Mechanism

Introduce a time-based cooldown before retrying failed operations:

uint256 public lastRetryTime;
uint256 public constant RETRY_COOLDOWN = 1 hours;
if (block.timestamp < lastRetryTime + RETRY_COOLDOWN) {
revert Error.RetryTooSoon();
}
lastRetryTime = block.timestamp;

Fix: Allow Manual Recovery

Enable an emergency withdrawal if retries exceed a threshold:

if (retryCount >= MAX_RETRIES) {
emergencyWithdrawEnabled = true;
emit EmergencyWithdrawActivated();
}
Updates

Lead Judging Commences

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

Support

FAQs

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