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

[M-04] Incorrect `_isFundIdle()` Check May Result in Incorrect Position Opening Logic

Summary

The run() function in the PerpetualVault contract is responsible for opening and closing positions based on off-chain signals. It includes a check using _isFundIdle() to determine whether the contract has enough idle collateral to proceed with opening a position. However, _isFundIdle() only checks if the balance of collateralToken in the contract is greater than or equal to minDepositAmount. This check is flawed as it does not account for collateral already allocated to an existing position or other locked funds, leading to potential incorrect behavior in the execution of run().

Vulnerability Details

The run() function first determines whether the position is open or closed and, if closed, verifies whether the vault has enough idle funds to open a new position:

if (_isFundIdle() == false) {
revert Error.InsufficientAmount();
}

This check calls _isFundIdle(), which is defined as:

function _isFundIdle() internal view returns (bool) {
if (collateralToken.balanceOf(address(this)) >= minDepositAmount) {
return true;
} else {
return false;
}
}

However, _isFundIdle() only checks the contract's collateralToken balance without considering:

  • Collateral already allocated to an open position

  • Pending swaps, withdrawals, or executions in progress

  • Locked collateral due to ongoing transactions

This leads to the following problematic scenario:

  1. Off-chain keeper calls run() to open a new position

    • If positionIsClosed == true, _isFundIdle() is called to check if there is enough collateral.

  2. Incorrect idle fund calculation

    • _isFundIdle() only looks at collateralToken.balanceOf(address(this)), not taking into account locked or pending collateral.

  3. Potential incorrect execution

    • If there is collateral in use but not reflected in the balance, run() may incorrectly proceed with _runSwap() or _createIncreasePosition(), even though there aren’t sufficient funds to execute the new position properly.

    • If _isFundIdle() mistakenly returns false even when there are available funds (but temporarily in another state), run() will revert incorrectly.

Example Flow Leading to Incorrect Behavior

  1. Initial State:

    • PerpetualVault has 100 USDC in collateral.

    • minDepositAmount is 50 USDC.

    • 80 USDC is locked in an ongoing trade.

  2. Off-chain keeper sends a signal to open a position

    • run(true, true, prices, metadata) is called.

    • _isFundIdle() checks collateralToken.balanceOf(address(this)) and sees 20 USDC.

    • Since 20 < 50, _isFundIdle() returns false, and the function reverts.

  3. Incorrect Reversion

    • Even though 80 USDC is available after the trade completes, the function unnecessarily reverts, preventing valid trade execution.

Alternatively, if _isFundIdle() returns true in cases where some of the collateral is locked but not truly available, run() may attempt an invalid position increase, leading to failure in execution.

Impact

  • False reverts: run() could fail even when funds are available but locked in an open position.

  • Invalid position execution: The function may try to open a position even though sufficient collateral isn't truly available, potentially causing the transaction to revert later.

  • Wasted gas fees: Users or keepers attempting to execute trades may face failed transactions due to incorrect fund availability calculations.

Tools Used

  • Manual code review

Recommendations

Modify _isFundIdle() to check actual available collateral, factoring in locked funds, pending transactions, and open positions. A better approach would be:

function _isFundIdle() internal view returns (bool) {
uint256 availableCollateral = collateralToken.balanceOf(address(this));
uint256 lockedCollateral = vaultReader.getLockedCollateral(); // Hypothetical function
if (availableCollateral - lockedCollateral >= minDepositAmount) {
return true;
} else {
return false;
}
}

Alternatively, integrate _isFundIdle() with GMX position tracking to ensure only truly available collateral is considered before allowing position openings.

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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