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

Zero Collateral Withdrawal Issue Leading to Loss of Depositor Funds

Summary

Under extreme market conditions or when trading fees exhaust the vault's collateral, a withdrawal request will result in no collateral being transferred to the depositor. Instead of reverting, the system burns the depositor's shares, effectively depositors loose their funds permanenty. This vulnerability is classified as medium severity given the low-probability but high-impact nature of the issue.

Vulnerability Details

During withdraw and positionIsClosed=true, if the vault’s collateral balance is zero, the code branch responsible for token transfers is bypassed because the calculated withdrawal amount equals zero. Instead of reverting or providing a more graceful handling, the user's shares are effectively burned.

In PerpetualVault.sol, user starts from withdraw(), code flow goes in _handleReturn():

1134 @> amount = collateralToken.balanceOf(address(this)) * shares / totalShares; //collateral 0 makes this amount 0.
...
1139 if (amount > 0) {
1140 _transferToken(depositId, amount);
1141 }
1142 emit Burned(depositId, depositInfo[depositId].recipient, depositInfo[depositId].shares, amount);
1143 @> _burn(depositId); //instead of revert system burn user depositId.

Impact

  • Depositors who withdraw while the vault collateral is zero permanently lose their deposit assets.

POC

  1. Add test into test/PerpetualVault.t.sol:

function test_POC_ZeroCollateralExploit() external {
// Setup user
address user = makeAddr("user");
IERC20 collateralToken = PerpetualVault(vault).collateralToken();
uint256 depositAmountUser = 1000 * 1e6; // 1000 USDC
// user deposits 1000 USDC
deal(address(collateralToken), user, depositAmountUser);
vm.startPrank(user);
collateralToken.approve(vault, depositAmountUser);
uint256 execFeeuser = PerpetualVault(vault).getExecutionGasLimit(true);
PerpetualVault(vault).deposit{value: execFeeuser * tx.gasprice}(depositAmountUser);
uint256[] memory userDeposits = PerpetualVault(vault).getUserDeposits(user);
(, uint256 userShares, , , , ) = PerpetualVault(vault).depositInfo(userDeposits[0]);
console.log("User minted shares:", userShares/1e14); // Divide by 1e14- as for every 100usdc deposits-> shares will be 100e14(100e6*1e8)
vm.stopPrank();
// Simulate a scenario where the vault's collateral balance becomes zero.
// For testing purposes, we forcefully set the vault's collateral balance to 0.
deal(address(collateralToken), vault, 0);
uint256 newVaultCollateral = collateralToken.balanceOf(vault);
console.log("Vault collateral after force zeroing:", newVaultCollateral);
// Warp forward to bypass lockTime
uint256 lockTime = PerpetualVault(vault).lockTime();
vm.warp(block.timestamp + lockTime + 1);
// user withdraws his deposit
vm.startPrank(user);
uint256 execFeeWithdraw = PerpetualVault(vault).getExecutionGasLimit(false);
PerpetualVault(vault).withdraw{value: execFeeWithdraw * tx.gasprice}(user, userDeposits[0]);
vm.stopPrank();
// Log user's final balance
uint256 userFinalBalance = collateralToken.balanceOf(user);
console.log("User balance after withdrawal:", userFinalBalance/1e6);
(, uint256 userShares2, , , , ) = PerpetualVault(vault).depositInfo(userDeposits[0]);
console.log("User shares after withdraw:", userShares2/1e14);
}
  1. Run forge test --match-test test_POC_ZeroCollateralExploit --rpc-url https://arb1.arbitrum.io/rpc -vv --via-ir

  2. Output:

[PASS] test_POC_ZeroCollateralExploit() (gas: 701454)
Logs:
User minted shares: 1000
Vault collateral after force zeroing: 0
User balance after withdrawal: 0
User shares after withdraw: 0
  • its clear that even users shares burned, but no assets transferred.

Tools Used

Manual Review

Recommendations

  • Revert on Zero Collateral

if (amount == 0) { revert() }
Updates

Lead Judging Commences

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

Informational or Gas

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.

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

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

Informational or Gas

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.

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

Appeal created

mohitisimmortal Submitter
5 months ago
n0kto Lead Judge
5 months ago
n0kto Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas

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.

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

Support

FAQs

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