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()
.
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:
This check calls _isFundIdle()
, which is defined as:
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:
Off-chain keeper calls run()
to open a new position
If positionIsClosed == true
, _isFundIdle()
is called to check if there is enough collateral.
Incorrect idle fund calculation
_isFundIdle()
only looks at collateralToken.balanceOf(address(this))
, not taking into account locked or pending collateral.
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.
Initial State:
PerpetualVault
has 100 USDC in collateral.
minDepositAmount
is 50 USDC.
80 USDC is locked in an ongoing trade.
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.
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.
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.
Manual code review
Modify _isFundIdle()
to check actual available collateral, factoring in locked funds, pending transactions, and open positions. A better approach would be:
Alternatively, integrate _isFundIdle()
with GMX position tracking to ensure only truly available collateral is considered before allowing position openings.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.