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

Locked Funds Due to Incomplete Flow Reset After Full Withdrawal.

Description: Users can deposit and withdraw funds using the deposit() and withdraw() functions. An off-chain keeper manages the opening and closing of positions on GMX via the run() and runNextAction() functions. However, when users fully withdraw their funds, the protocol fails to clear the state variable positionIsClosed. As a result, even though the user's position is fully closed, the vault remains in an "open" state, preventing subsequent deposits from being processed and potentially locking user funds.

Impact: New deposits trigger an increase flow that never completes, locking funds in the vault and preventing users from opening new positions or withdrawing funds.

Proof-Of-Concept:

  1. Alice deposits funds when the vault is idle.

  2. The vault immediately mints shares via _mint(), and then the off-chain keeper calls run() and runNextAction() to open a position on GMX. now, positionIsClosed is false.

  3. After that Alice withdraws all her funds from protocol, fully closing her position but the vault does not reset positionIsClosed back to true, so the vault remains in an “open position” state.

  4. When Alice later attempts to deposit again in protocol. the deposit function falls into the branch that handles active positions because positionIsClosed is false. This branch queues an increase action rather than minting shares immediately.

  5. The keeper calls the run() but the calls revert and Alice's funds are stuck in the protocol

  6. When the keeper calls run(), the call reverts with a FlowInProgress() error because the vault’s flow state was not properly reset. If the keeper then calls runNextAction(), the flow remains stuck in the DEPOSIT state instead of being cleared to NONE.

  7. As a result, if Alice subsequently attempts to withdraw, the transaction reverts with a custom error indicating that a flow is in progress, leaving her funds locked in the vault.

Paste this code in PrepetualVAult.t.sol

function test_userCanNotDepositAgain() external {
address alice = makeAddr("alice");
payable(alice).transfer(1 ether);
IERC20 collateralToken = PerpetualVault(vault).collateralToken();
assertEq(collateralToken.balanceOf(address(PerpetualVault(vault))), 0);
// alice deposit some amount
vm.startPrank(alice);
deal(address(collateralToken), alice, 1e10);
uint256 executionFee = PerpetualVault(vault).getExecutionGasLimit(true);
collateralToken.approve(vault, 1e10);
PerpetualVault(vault).deposit{value: executionFee * tx.gasprice}(1e10);
vm.stopPrank();
assertEq(uint8(PerpetualVault(vault).flow()), 0); // 1 == NONE
assertEq(collateralToken.balanceOf(address(PerpetualVault(vault))), 1e10);
assertEq(PerpetualVault(vault).positionIsClosed(), true);
// keeper calls
MarketPrices memory prices = mockData.getMarketPrices();
bytes[] memory data = new bytes[](2);
data[0] = abi.encode(3380000000000000);
address keeper = PerpetualVault(vault).keeper();
vm.prank(keeper);
PerpetualVault(vault).run(true, false, prices, data);
assertEq(PerpetualVault(vault).positionIsClosed(), true);
GmxOrderExecuted(true);
bytes[] memory metadata = new bytes[](0);
vm.prank(keeper);
PerpetualVault(vault).runNextAction(prices, metadata);
// position is opend now
assertEq(PerpetualVault(vault).positionIsClosed(), false);
// alice fully close his position
uint256[] memory depositIds = PerpetualVault(vault).getUserDeposits(alice);
executionFee = PerpetualVault(vault).getExecutionGasLimit(false);
uint256 lockTime = 1;
PerpetualVault(vault).setLockTime(lockTime);
vm.warp(block.timestamp + lockTime + 1);
payable(alice).transfer(1 ether);
vm.prank(alice);
PerpetualVault(vault).withdraw{value: executionFee * tx.gasprice}(alice, depositIds[0]);
GmxOrderExecuted(true);
bytes[] memory swapData = new bytes[](2);
swapData[0] = abi.encode(3390000000000000);
vm.prank(keeper);
PerpetualVault(vault).runNextAction(prices, swapData);
GmxOrderExecuted(true);
bytes[] memory metadata2 = new bytes[](0);
vm.prank(keeper);
PerpetualVault(vault).runNextAction(prices, metadata2);
uint256 balance = IERC20(collateralToken).balanceOf(alice);
emit log_named_decimal_uint("Withdrawn Amount", balance, 6);
// but position remains open
assertEq(PerpetualVault(vault).positionIsClosed(), false);
assertEq(collateralToken.balanceOf(address(PerpetualVault(vault))), 0);
assertEq(uint8(PerpetualVault(vault).flow()), 0); // 0 == NONE
vm.warp(block.timestamp + 1 weeks + 1);
// alice wants to invest through gamma in gmx again
vm.startPrank(alice);
deal(address(collateralToken), alice, 1e10);
executionFee = PerpetualVault(vault).getExecutionGasLimit(true);
collateralToken.approve(vault, 1e10);
PerpetualVault(vault).deposit{value: executionFee * tx.gasprice}(1e10);
vm.stopPrank();
assertEq(uint8(PerpetualVault(vault).flow()), 1); // 0 == DEPOSIT
// keeper calls
prices = mockData.getMarketPrices();
data = new bytes[](2);
data[0] = abi.encode(3380000000000000);
// kepper fails to execute transaction because flow is not none
vm.expectRevert(Error.FlowInProgress.selector);
vm.prank(keeper);
PerpetualVault(vault).run(true, false, prices, data);
swapData = new bytes[](2);
swapData[0] = abi.encode(3380000000000000);
vm.prank(keeper);
PerpetualVault(vault).runNextAction(prices, swapData);
assertEq(uint8(PerpetualVault(vault).flow()), 1); // 0 == DEPOSIT
lockTime = 1;
PerpetualVault(vault).setLockTime(lockTime);
vm.warp(block.timestamp + lockTime + 1);
payable(alice).transfer(1 ether);
// Alice is unable to withdraw because of flow in prograss error
vm.prank(alice);
vm.expectRevert(Error.FlowInProgress.selector);
PerpetualVault(vault).withdraw{value: executionFee * tx.gasprice}(alice, depositIds[0]);
}

Recommended Mitigation Steps: Implement a state reset logic that correctly updates flow and the positionIsClosed

Updates

Lead Judging Commences

n0kto Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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