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

Inconsistent Transfer Safety Pattern in PerpetualVault._transferToken()

Summary

The _transferToken() function in the PerpetualVault contract uses inconsistent token transfer safety patterns, using safeTransfer() for fee transfers but standard transfer() for user withdrawals. This can lead to issues when handling the specific token implementations of WETH, WBTC, LINK, and USDC.

Vulnerability Details

The current implementation looks like this:

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) {
// SafeTransfer used here
collateralToken.safeTransfer(treasury, fee);
}
}
// Standard transfer with try-catch here
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);
}

The issue is that different ERC20 tokens behave differently on failure:

  1. USDC and WBTC: These tokens typically revert on transfer failure

  2. WETH: Returns false on failure without reverting

  3. LINK: Behavior can depend on the implementation version

Using transfer() directly is problematic because:

  1. For tokens that return false on failure (like WETH), the failure is silently ignored

  2. Even in the catch block, another transfer() is used instead of safeTransfer()

Impact

The inconsistent pattern could result in:

  1. Silent failures: With tokens like WETH that return false instead of reverting, a failed transfer might not trigger the catch block, resulting in lost tokens

  2. Treasury transfer failures: If the fallback transfer to treasury also fails, there's no further handling

    Given the specific tokens used (WETH, WBTC, LINK, and USDC), this inconsistency creates unpredictable behavior depending on which token is being used as collateral.

Tools Used

Manual code review

Recommendations

The simplest and most reliable solution is to use safeTransfer() consistently throughout the function:

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);
}
}
// Use safeTransfer with try-catch
try {
collateralToken.safeTransfer(depositInfo[depositId].recipient, amount - fee);
} catch {
// Also use safeTransfer here for consistency
collateralToken.safeTransfer(treasury, amount - fee);
emit TokenTranferFailed(depositInfo[depositId].recipient, amount - fee);
}
totalDepositAmount -= depositInfo[depositId].amount;
emit GovernanceFeeCollected(address(collateralToken), fee);
}

Using SafeERC20's safeTransfer() is particularly important for WETH, which doesn't revert on failure and would otherwise silently fail with the current implementation.

Updates

Lead Judging Commences

n0kto Lead Judge 9 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 9 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.

Give us feedback!