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

Improper ERC20 Transfer Error Handling in `PerpetualVault._transferToken()` Leads to Permanent Fund Locking

Summary

This vulnerability within PerpetualVault contract's withdrawal mechanism exposes user funds to permanent loss due to improper ERC20 transfer error handling. The PerpetualVault._transferToken() function fails to properly detect and handle USDC transfer failures, allowing funds to become irreversibly locked in the contract. The root cause stems from using try/catch blocks that only capture reverting transactions, while USDC (the designated collateral token) implements ERC20's boolean return pattern for error reporting. This critical mismatch between error handling implementation and token behavior violates core protocol functionality guarantees.

Vulnerability Details

The vulnerability exists in the token transfer error handling within the PerpetualVault._transferToken function (PerpetualVault.sol#L1171-L1175). The implementation incorrectly uses a try/catch block to handle ERC20 transfer failures, which is ineffective for tokens like USDC that return boolean status codes rather than reverting on failed transfers.

contract PerpetualVault is IPerpetualVault, Initializable, Ownable2StepUpgradeable, ReentrancyGuardUpgradeable {
function _transferToken(uint256 depositId, uint256 amount) internal {
uint256 fee;
if (amount > depositInfo[depositId].amount) {
fee = (amount - depositInfo[depositId].amount) * governanceFee / BASIS_POINTS_DIVISOR;
if (fee > 0) {
collateralToken.safeTransfer(treasury, fee);
}
}
@> try collateralToken.transfer(depositInfo[depositId].recipient, amount - fee) {}
@> catch {
@> collateralToken.transfer(treasury, amount - fee);
emit TokenTranferFailed(depositInfo[depositId].recipient, amount - fee);
}
totalDepositAmount -= depositInfo[depositId].amount;
emit GovernanceFeeCollected(address(collateralToken), fee);
}
}

This approach creates a silent failure scenario where:

  1. Failed transfers to recipients don't trigger the fallback to treasury

  2. Withdrawal transactions complete without properly moving funds

  3. User balances get stuck in the contract indefinitely

The protocol's documentation explicitly states USDC is used as collateralToken, making this a protocol-specific vulnerability rather than a general ERC20 handling issue. The current implementation directly contradicts the stated design intent to redirect failed transfers to the treasury.

Impact

This vulnerability directly breaks core protocol functionality in a way that leads to irreversible financial losses for users:

Exploit Scenario:

  1. User deposits 10,000 USDC

  2. Later initiates withdrawal for 10,000 USDC

  3. Recipient address is USDC-blacklisted (common regulatory compliance feature) or USDC contract paused

  4. Transfer returns false but doesn't revert

  5. Contract state updates complete (totalDepositAmount reduced)

  6. 10,000 USDC remains locked in contract permanently

  7. User cannot retry withdrawal as deposit record is deleted in _handleReturn() that calls _transferToken()

Direct Impacts:

  • Permanent locking of user funds in contract when transfers fail

  • Inability to recover stuck funds due to state cleanup after failed transfers

  • Protocol accounting mismatch between internal records and actual token balances

Systemic Risks:

  • Complete loss of withdraw functionality for affected users

  • Protocol insolvency risk as locked funds accumulate

  • Erosion of user trust in withdrawal guarantees

Tools Used

Manual Review

Recommendations

Replace try/catch with return value check and use safeTransfer for treasury transfer:

bool success = collateralToken.transfer(depositInfo[depositId].recipient, amount - fee);
if (!success) {
collateralToken.safeTransfer(treasury, amount - fee);
emit TokenTranferFailed(depositInfo[depositId].recipient, amount - fee);
}
Updates

Lead Judging Commences

n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

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 8 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

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.