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

Flow does not finalize for single ParaSwap deposit in `_runSwap()` function.

Summary

Flow does not finalize for single ParaSwap deposit in _runSwap() function. This will lead to DoS of the entire flow.

Vulnerability Details

When the vault is an 1xLong, it uses spot swap to handle position increase/decrease. When users deposit, it will set the nextAction.selector to INCREASE_ACTION, then the keeper will call runNextAction() to trigger the swap.

If the swap is a single Paraswap, it only mints the shares, but does not finalize the flow. This means the flow variable is not cleared, and the entire workflow is stuck.

https://github.com/CodeHawks-Contests/2025-02-gamma/blob/main/contracts/PerpetualVault.sol#L534-L535

function _runSwap(bytes[] memory metadata, bool isCollateralToIndex, MarketPrices memory prices) internal returns (bool completed) {
...
if (_protocol == PROTOCOL.DEX) {
uint256 outputAmount = _doDexSwap(data, isCollateralToIndex);
// update global state
if (flow == FLOW.DEPOSIT) {
// last `depositId` equals with `counter` because another deposit is not allowed before previous deposit is completely processed
@> _mint(counter, outputAmount + swapProgressData.swapped, true, prices);
} else if (flow == FLOW.WITHDRAW) {
_handleReturn(outputAmount + swapProgressData.swapped, false, true);
} else {
// in the flow of SIGNAL_CHANGE, if `isCollateralToIndex` is true, it is opening position, or closing position
_updateState(!isCollateralToIndex, isCollateralToIndex);
}
return true;
}
}

An easy PoC for this is modifying the test_RefundGas_When_Using_Paraswap_Only. After user deposits, the flow variable is not cleared.

function test_RefundGas_When_Using_Paraswap_Only() external {
address keeper = PerpetualVault(vault).keeper();
address alice = makeAddr("alice");
depositFixture(alice, 1e10);
MarketPrices memory prices = mockData.getMarketPrices();
bytes memory paraSwapData = mockData.getParaSwapData(vault);
bytes[] memory swapData = new bytes[](1);
swapData[0] = abi.encode(PROTOCOL.DEX, paraSwapData);
vm.prank(keeper);
PerpetualVault(vault).run(true, true, prices, swapData);
IERC20 collateralToken = PerpetualVault(vault).collateralToken();
uint256 amount = 1e10;
deal(address(collateralToken), alice, amount);
deal(alice, 1e18);
uint256 executionFee = PerpetualVault(vault).getExecutionGasLimit(true);
vm.startPrank(alice);
collateralToken.approve(vault, amount);
PerpetualVault(vault).deposit{value: executionFee * tx.gasprice}(amount);
vm.stopPrank();
prices = mockData.getMarketPrices();
uint256 ethBalBefore = alice.balance;
vm.prank(keeper);
PerpetualVault(vault).runNextAction(prices, swapData);
assertTrue(ethBalBefore < alice.balance);
// @audit-note: Add this line and it will fail.
@> assertEq(uint8(PerpetualVault(vault).flow()), 0);
}

Impact

Entire workflow is stuck.

Tools Used

N/A

Recommendations

Call _finalize() function after minting.

Updates

Lead Judging Commences

n0kto Lead Judge 9 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.