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

case in PerpetualVault.sol#_runSwap() function makes the contract unusable

Summary

A logic flaw in the _runSwap() function of PerpetualVault.sol can cause the protocol to become unusable when a user attempts to deposit funds. The issue prevents further deposits, withdrawals, and automated execution by the keeper, effectively denying service to the protocol.

Vulnerability Details

The protocol supports two trading execution methods:

1. Trades executed on both GMX and ParaSwap

2. Trades executed on either GMX or ParaSwap exclusively

Scenario Leading to Failure

The vulnerability manifests when the vault operates at 1x leverage long and exclusively executes trades on ParaSwap. The following sequence leads to a protocol-wide failure:

Vault Setup:

• The protocol administrators deploy a vault.

• They deposit funds and open a 1x long position on ParaSwap.

User Deposit Initiates an Increase Action:

• A user deposits funds into the vault.

• Since a position is already open (positionIsClosed == false), the execution enters the else block in deposit().

• Execution fees are paid, and the next action is set to INCREASE_ACTION, with the data encoding the long position.

if (positionIsClosed) {
MarketPrices memory prices;
_mint(counter, amount, false, prices);
_finalize(hex'');
} else {
_payExecutionFee(counter, true);
// Mint shares in the NextAction phase to use off-chain price data for security
nextAction.selector = NextActionSelector.INCREASE_ACTION;
nextAction.data = abi.encode(beenLong);
}

Keeper Executes runNextAction():

• The keeper calls runNextAction(), which deletes the nextAction state variable after caching its value.

• Since nextAction.selector == INCREASE_ACTION, the execution proceeds to _runSwap().

function runNextAction(MarketPrices memory prices, bytes[] memory metadata)
external nonReentrant gmxLock
{
_onlyKeeper();
Action memory _nextAction = nextAction;
delete nextAction;
if (_nextAction.selector == NextActionSelector.INCREASE_ACTION) {
(bool _isLong) = abi.decode(_nextAction.data, (bool));
if (_isLongOneLeverage(_isLong)) {
_runSwap(metadata, true, prices);
}
}
}

Issue in _runSwap() Leaves Protocol in a Stuck State:

• The vault executes the trade on ParaSwap.

• The function enters the if (flow == FLOW.DEPOSIT) branch and mints shares to the depositor.

• However, the flow state is never reset, it remains stuck on DEPOSIT.

if (metadata.length != 1) {
revert Error.InvalidData();
}
(PROTOCOL _protocol, bytes memory data) = abi.decode(metadata[0], (PROTOCOL, bytes));
if (_protocol == PROTOCOL.DEX) {
uint256 outputAmount = _doDexSwap(data, isCollateralToIndex);
// update global state
if (flow == FLOW.DEPOSIT) {
_mint(counter, outputAmount + swapProgressData.swapped, true, prices);
} else if (flow == FLOW.WITHDRAW) {
_handleReturn(outputAmount + swapProgressData.swapped, false, true);
} else {
_updateState(!isCollateralToIndex, isCollateralToIndex);
}
}

Resulting Failure

• flow is stuck on DEPOSIT: This blocks all further deposit(), withdraw(), and run() calls, as they require flow == NO_FLOW.

• nextAction is empty (selector = NO_ACTION): This means runNextAction() immediately reverts, making it impossible for the keeper to proceed.

Impact

This issue halts protocol operations because:

1. New deposits cannot be processed (deposit() fails due to ongoing DEPOSIT flow).

2. Withdrawals cannot be executed (withdraw() fails for the same reason).

3. The keeper cannot continue executing automated actions (runNextAction() always reverts wasting keeper gas).

Tools Used

manual review

Recommendations

Properly reset flow after swap completion

Updates

Lead Judging Commences

n0kto Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

finding_deposit_1x_long_dex_positionIsOpened_DoS_Flow

Likelihood: Medium/High, - Leverage = 1x - beenLong = True - positionIsClosed = False - Metadata → 1 length and Dex Swap Impact: Medium/High, DoS on any new action before the admin uses setVaultState Since this seems to be the most probable path for a 1x PerpVault, this one deserves a High.

Support

FAQs

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