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

New Position Created Automatically Post-Liquidation

Summary

After a liquidation event returns collateralToken (e.g., USDC), the runNextAction function with INCREASE_ACTION processes a pending deposit and automatically opens a new short position with the deposited funds, even though the previous position was liquidated. This exposes user funds to unintended risk without reassessment or explicit consent, violating user expectations and potentially leading to further losses.

Vulnerability Details

  • Location: PerpetualVault.sol, runNextAction function, INCREASE_ACTION branch.

  • Condition: Occurs when flow = DEPOSIT and nextAction.selector = INCREASE_ACTION persists post-liquidation, with indexToken balance below ONE_USD.

  • Behavior: The contract transfers the pending deposit (e.g., 10 USDC) to gmxProxy and opens a new 2x short position via _createIncreasePosition, without pausing or re-evaluating the strategy after a liquidation event.

Impact

  • Likelihood: High – Common when deposits are pending during liquidation (normal vault usage).

  • Severity: Medium – Funds are not locked but are immediately put at risk of further loss in a potentially failing strategy.

  • User Risk: Users lose control over their funds, which are reinvested into a short position post-liquidation without consent, potentially compounding losses (e.g., if ETH price continues rising).

  • Trust: Undermines user confidence in the vault’s risk management.

Proof of Concept

Steps to Exploit:

  1. Deploy PerpetualVault with 2x leverage (vault2x), initialized with ETH/USDC market.

  2. User deposits 10 USDC (depositFixtureInto2x).

  3. Keeper opens a 2x short position (runShort2x), transferring 10 USDC to gmxProxy.

  4. User deposits another 10 USDC (deposit), setting flow = DEPOSIT and nextAction = INCREASE_ACTION.

  5. Simulate full liquidation: mock vaultReader.getPositionSizeInTokens to return 0, return 5 USDC to vault2x.

  6. Keeper calls runNextAction, which processes the second deposit and opens a new 2x short position with the 10 USDC.

Add these 2 functions to PerpetualVaul.t.sol to use the same setup.

function runShort2x(address keeper) internal {
MarketPrices memory prices = mockData.getMarketPrices();
bytes[] memory data = new bytes[](1);
data[0] = abi.encode(0);
vm.prank(keeper);
PerpetualVault(vault2x).run(true, false, prices, data);
PerpetualVault.FLOW flow = PerpetualVault(vault2x).flow();
assertEq(uint8(flow), 2);
assertEq(PerpetualVault(vault2x).positionIsClosed(), true);
(PerpetualVault.NextActionSelector selector, ) = PerpetualVault(vault2x)
.nextAction();
assertEq(uint8(selector), 0);
GmxOrderExecuted2x(true);
bytes32 curPositionKey = PerpetualVault(vault2x).curPositionKey();
assertTrue(curPositionKey != bytes32(0));
assertEq(PerpetualVault(vault2x).beenLong(), false);
vm.prank(keeper);
PerpetualVault(vault2x).runNextAction(prices, data);
flow = PerpetualVault(vault2x).flow();
assertEq(uint8(flow), 0);
}
function test_Scenario1_PostLiquidationNewPosition() external {
address keeper = PerpetualVault(vault2x).keeper();
address alice = makeAddr("alice");
address gmxProxy = address(PerpetualVault(vault2x).gmxProxy());
IERC20 collateralToken = PerpetualVault(vault2x).collateralToken();
depositFixtureInto2x(alice, 1e10);
runShort2x(keeper);
uint256 secondDepositAmount = 1e10;
deal(address(collateralToken), alice, secondDepositAmount);
deal(alice, 100 ether);
vm.startPrank(alice);
collateralToken.approve(vault2x, secondDepositAmount);
PerpetualVault(vault2x).deposit{
value: PerpetualVault(vault2x).getExecutionGasLimit(true) *
tx.gasprice
}(secondDepositAmount);
vm.stopPrank();
vm.mockCall(
address(reader),
abi.encodeWithSelector(
VaultReader.getPositionSizeInTokens.selector,
PerpetualVault(vault2x).curPositionKey()
),
abi.encode(0)
);
deal(
address(collateralToken),
vault2x,
collateralToken.balanceOf(vault2x) + 5e6
);
vm.prank(gmxProxy);
PerpetualVault(vault2x).afterLiquidationExecution();
// Remaining from liquidation + 1e10 from the second deposit
assertEq(collateralToken.balanceOf(vault2x), 1e10 + 5e6);
assertEq(collateralToken.balanceOf(gmxProxy), 0e10);
uint256 indexTokenBalance = IERC20(PerpetualVault(vault2x).indexToken())
.balanceOf(vault2x);
MarketPrices memory prices = mockData.getMarketPrices();
bytes[] memory swapData = new bytes[](2);
swapData[0] = abi.encode(3380000000000000);
bytes memory gmxSwapData = abi.encode(
address(alice), // Swap path (simplified)
indexTokenBalance,
0 // Min output amount
);
swapData[1] = abi.encode(PROTOCOL.GMX, gmxSwapData);
vm.prank(keeper);
PerpetualVault(vault2x).runNextAction(prices, swapData);
// Remaining from liquidation but the 1e10 from second deposit is missing
// That means in position has been opened
assertEq(collateralToken.balanceOf(vault2x), 5e6);
assertEq(collateralToken.balanceOf(gmxProxy), 0e10);
}

Tools Used

  • Manual review

  • Foundry (Forge) for testing and simulation.

  • Solidity compiler for contract analysis.

Recommendations

  • Pause Post-Liquidation: In afterLiquidationExecution, reset nextAction to NO_ACTION when sizeInTokens = 0, requiring keeper to reassess strategy before using new deposits.

  • User Consent: Add a mechanism (e.g., user approval flag) to confirm intent for new positions post-liquidation.

  • Strategy Check: Implement a keeper validation step to prevent automatic position creation after liquidation events.

Updates

Lead Judging Commences

n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Users mistake, only impacting themselves.

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Appeal created

joesepherus Submitter
8 months ago
n0kto Lead Judge
8 months ago
n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Users mistake, only impacting themselves.

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Support

FAQs

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