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

Funds Locked When Liquidation Returns indexToken

Summary

If liquidation returns indexToken (e.g., ETH) instead of collateralToken (e.g., USDC), calling runNextAction with INCREASE_ACTION fails due to a mismatch between GMX swap data and the ParaSwap-specific _doDexSwap function, resulting in a memory allocation error (0x41). This locks all funds in the vault (collateralToken and indexToken), rendering them unrecoverable without owner intervention.

Vulnerability Details

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

  • Condition: indexToken balance * price ≥ ONE_USD (e.g., 0.005 ETH * 3386 > 1 USD), triggering _doDexSwap with GMX-formatted data (PROTOCOL.GMX, (address[], uint256, uint256)).

  • Behavior: _doDexSwap expects (address, uint256, bytes), fails to decode, and reverts, stalling flows like DEPOSIT and locking funds in vault2x.

Impact

  • Likelihood: Low to Medium – GMX typically returns USDC for shorts, but indexToken return is possible in edge cases (e.g., pool imbalances).

  • Severity: High – All vault funds during the flow (e.g., 10 USDC + 0.005 ETH) are locked, requiring owner action via setVaultState, introducing centralization and delay risks.

  • User Risk: Complete loss of access to funds until manual recovery, potentially indefinite if owner is unresponsive.

Proof of Concept

Steps to Exploit:

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

  2. User deposits 10 USDC (depositFixtureInto2x).

  3. Keeper opens a 2x short position (runShort2x).

  4. User deposits another 10 USDC (deposit).

  5. Simulate full liquidation returning 0.005 ETH (deal to vault2x, sizeInTokens = 0).

  6. Keeper calls runNextAction with GMX swap data, which fails, locking funds.

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_Scenario2_IndexTokenLockup() external {
address keeper = PerpetualVault(vault2x).keeper();
address alice = makeAddr("alice");
address gmxProxy = address(PerpetualVault(vault2x).gmxProxy());
IERC20 collateralToken = PerpetualVault(vault2x).collateralToken();
IERC20 indexToken = IERC20(PerpetualVault(vault2x).indexToken());
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(vaultReader),
abi.encodeWithSelector(VaultReader.getPositionSizeInTokens.selector, PerpetualVault(vault2x).curPositionKey()),
abi.encode(0)
);
deal(address(indexToken), vault2x, 5e15); // 0.005 ETH
vm.prank(gmxProxy);
PerpetualVault(vault2x).afterLiquidationExecution();
MarketPrices memory prices = mockData.getMarketPrices();
bytes[] memory swapData = new bytes[](2);
swapData[0] = abi.encode(3380000000000000);
swapData[1] = abi.encode(PROTOCOL.GMX, abi.encode(address(alice), 5e15, 0));
vm.prank(keeper);
vm.expectRevert(); // Memory allocation error
PerpetualVault(vault2x).runNextAction(prices, swapData);
assertEq(collateralToken.balanceOf(vault2x), 1e10); // Funds stuck
assertEq(indexToken.balanceOf(vault2x), 5e15); // IndexToken stuck
assertEq(PerpetualVault(vault2x).flow(), PerpetualVault.FLOW.DEPOSIT);
}

Tools Used

  • Manual review.

  • Foundry (Forge) for testing and simulation.

  • Solidity compiler for contract analysis.

Recommendations

  • Protocol Check: Modify runNextAction to check PROTOCOL and call _doGmxSwap for PROTOCOL.GMX, preventing the decoding error.

  • Auto-Swap: In afterLiquidationExecution, detect indexToken balance and initiate a GMX swap to collateralToken, avoiding flow stalls.

  • Owner Recovery: Document the setVaultState workaround as a temporary mitigation, but prioritize automated handling.

Updates

Lead Judging Commences

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

Admin is trusted / Malicious keepers

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. Keepers are added by the admin, there is no "malicious keeper" and if there is a problem in those keepers, that's out of scope. ReadMe and known issues states: " * System relies heavily on keeper for executing trades * Single keeper point of failure if not properly distributed * Malicious keeper could potentially front-run or delay transactions * Assume that Keeper will always have enough gas to execute transactions. There is a pay execution fee function, but the assumption should be that there's more than enough gas to cover transaction failures, retries, etc * There are two spot swap functionalies: (1) using GMX swap and (2) using Paraswap. We can assume that any swap failure will be retried until success. " " * Heavy dependency on GMX protocol functioning correctly * Owner can update GMX-related addresses * Changes in GMX protocol could impact system operations * We can assume that the GMX keeper won't misbehave, delay, or go offline. " "Issues related to GMX Keepers being DOS'd or losing functionality would be considered invalid."

Support

FAQs

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