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

PerpetualVault’s GMX Settlement Process Allows Withdrawal Inflation via External Token Injection

Brief

The PerpetualVault contract’s multi-step withdrawal process allows attackers to inflate the withdrawn amount by injecting extra collateral tokens into the contract during GMX’s asynchronous settlement. By relying solely on balance differentials (collateralToken.balanceOf(this) - prevCollateralBalance) to measure what was “returned” from GMX, the contract cannot distinguish legitimate GMX settlements from unauthorized external transfers. This enables malicious actors to artificially boost final withdrawal amounts.

Details

The issue comes from when the vault initiates a withdrawal and awaits a GMX MarketDecrease settlement. During this time window, the contract simply stores its balance as prevCollateralBalance, then later calculates how much was withdrawn from GMX by doing:

function _finalize(bytes memory data) internal {
if (flow == FLOW.WITHDRAW) {
(uint256 prevCollateralBalance, bool positionClosed, bool refundFee) = abi.decode(data, (uint256, bool, bool));
uint256 withdrawn = collateralToken.balanceOf(address(this)) - prevCollateralBalance;
_handleReturn(withdrawn, positionClosed, refundFee);
}
}

Because there is no validation on where new tokens come from, an attacker can directly transfer additional collateral tokens into the vault’s address after prevCollateralBalance is recorded but before _finalize() runs. The contract then naively interprets any net balance increase as legitimate settlement proceeds:

  1. Attacker requests a legitimate withdrawal, which triggers a GMX settlement and logs prevCollateralBalance.

  2. Before GMX’s order callback calls _finalize(), attacker sends extra tokens to the vault address.

  3. The vault computes withdrawn as the entire difference of collateral balance, erroneously boosting the final withdrawal.

The problem grows more severe in partial withdrawals, where _handleReturn calculates share-based amounts from the vault’s "old” and newly credited balances:

function _handleReturn(uint256 withdrawn, bool positionClosed, bool /*refundFee*/) internal {
uint256 depositId = flowData;
uint256 shares = depositInfo[depositId].shares;
// If not fully closed, incorporate `withdrawn` plus a proportional share:
uint256 balanceBeforeWithdrawal = collateralToken.balanceOf(address(this)) - withdrawn;
uint256 amount = withdrawn + (balanceBeforeWithdrawal * shares / totalShares);
// ...
}

Because withdrawn effectively includes the attacker’s unauthorized transfer, the user’s share calculation is also inflated. Repeated exploits can systematically pull more collateral from the vault than legitimately owed.

Specific Impact

This vulnerability enables theft of vault collateral by artificially inflating a withdrawal’s final token count. Attackers can repeatedly exploit each GMX settlement window to manipulate the vault’s balance calculation and extract more value than entitled, potentially draining a significant portion of stored collateral.

Updates

Lead Judging Commences

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

Users mistake, only impacting themselves.

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.

Support

FAQs

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