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

Withdrawal Process State Inconsistency Risk

Summary

A critical vulnerability has been identified in the withdrawal process of the PerpetualVault contract, where improper state management after failed GMX operations could lead to fund misallocation, duplicate withdrawals, or permanent asset locking. This vulnerability stems from incomplete state resets when GMX position reduction requests fail, leaving residual transaction data that may be reused in subsequent operations.


Vulnerability Details

Technical Background

The withdrawal process utilizes state variables (nextAction and flowData) to track multi-step operations. When GMX position reduction fails (via afterOrderCancellation callback), these states aren't fully reset, creating orphaned references to previous requests.

Affected Code

// PerpetualVault.sol (L421-436)
function afterOrderCancellation(...) external {
if (orderResultData.isSettle) {
nextAction.selector = NextActionSelector.SETTLE_ACTION;
} else if (orderType == Order.OrderType.MarketSwap) {
nextAction.selector = NextActionSelector.SWAP_ACTION;
nextAction.data = abi.encode(...);
} else {
// !!! Missing state reset !!!
if (flow == FLOW.DEPOSIT) {
nextAction.selector = NextActionSelector.INCREASE_ACTION;
} else if (flow == FLOW.WITHDRAW) {
nextAction.selector = NextActionSelector.WITHDRAW_ACTION; // Residual state
}
}
}

Impact

  1. Double-Withdrawal Attacks
    Malicious actors could replay failed withdrawal states to drain funds.

  2. Permanent Locking
    Orphaned states may block legitimate withdrawals by creating inconsistent ledger.

Tools Used

Manual Review

Updates

Lead Judging Commences

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

invalid_afterOrderCancellation_do_not_reset_nextAction

Normal behavior, the keeper will retry to increase the position. That’s why there is this condition in `_createIncreasePosition`: ``` if (flow == FLOW.DEPOSIT) { amountIn = depositInfo[counter].amount; flowData = vaultReader.getPositionSizeInTokens(curPositionKey); } else { amountIn = collateralToken.balanceOf(address(this)); } ```

Support

FAQs

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

Give us feedback!