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

Front-Runnable Execution Fees

Summary

The PerpetualVault contract calculates execution fees using tx.gasprice, which is vulnerable to volatility and front-running. Users underpay fees if gas prices spike after submission, leading to transaction failures.

https://github.com/CodeHawks-Contests/2025-02-gamma/blob/e5b98627a4c965e203dbb616a5f43ec194e7631a/contracts/PerpetualVault.sol#L804

Vulnerability Details

Faulty Code Snippet:

// PerpetualVault.sol
function _payExecutionFee(uint256 depositId, bool isDeposit) internal {
uint256 minExecutionFee = getExecutionGasLimit(isDeposit) * tx.gasprice; // ⚠️ Volatile gas price
require(msg.value >= minExecutionFee, "Insufficient fee");
payable(address(gmxProxy)).transfer(msg.value);
}

Volatile tx.gasprice: The gas price at transaction execution time (tx.gasprice) can spike due to network congestion or front-running bots.

No Buffer: Fees are calculated exactly, leaving no margin for gas price increases, risking failed transactions.

Example Attack Scenario

  • User Submits Deposit: User sends a deposit with minExecutionFee = 0.01 ETH (based on current tx.gasprice).

  • Front-Runner Inflates Gas Price: Bots flood the network, increasing tx.gasprice by 50%.

  • Transaction Fails: User’s msg.value is now insufficient, reverting the transaction and wasting gas.

Impact

  1. Failed Transactions: Users lose gas fees on reverted transactions.

  2. Fund Locking: Deposits/withdrawals may be delayed or stuck.

  3. MEV Exploitation: Bots profit by front-running and manipulating gas prices.

Tools Used

Manual review, Static Analysis

Recommendations

Add a Fee Buffer: Multiply tx.gasprice by a safety margin (e.g., 1.2x).

Use Gas Oracle: Fetch gas prices from a reliable oracle (e.g., Chainlink) instead of tx.gasprice.

// PerpetualVault.sol
function _payExecutionFee(uint256 depositId, bool isDeposit) internal {
uint256 gasPriceWithBuffer = (tx.gasprice * 120) / 100; // 20% buffer
uint256 minExecutionFee = getExecutionGasLimit(isDeposit) * gasPriceWithBuffer;
require(msg.value >= minExecutionFee, "Insufficient fee");
payable(address(gmxProxy)).transfer(msg.value);
// Refund excess ETH (optional)
if (msg.value > minExecutionFee) {
payable(msg.sender).transfer(msg.value - minExecutionFee);
}
}

Fixes:

  • 20% Buffer: Ensures fees cover gas spikes (e.g., tx.gasprice * 1.2).

  • Gas Oracle Integration (Optional):

// Fetch gas price from Chainlink oracle
uint256 gasPrice = ChainlinkGasOracle.latestGasPrice();
uint256 minExecutionFee = getExecutionGasLimit(...) * gasPrice;

Verification

Test Case 1 (Gas Price Spike):

Original tx.gasprice: 100 Gwei.

Buffer: 120 Gwei.

Actual gas price at execution: 110 Gwei.

Result: Fee sufficient ✅.

Test Case 2 (Oracle Integration):

Chainlink reports average gas price: 90 Gwei.

Buffer: 108 Gwei (90 * 1.2).

Result: Stable fees even during spikes ✅.

Updates

Lead Judging Commences

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

invalid_tx-gasprice_instable

The frontrunner won’t trigger "congestion" without a huge amount of transactions, and it will cost a lot. Moreover, the execution gas limit is overestimated to prevent such cases: ``` executionGasLimit = baseGasLimit + ((estimatedGasLimit + _callbackGasLimit) * multiplierFactor) / PRECISION; ``` The keeper won’t wait long to execute the order; otherwise, GMX would not be competitive.

Support

FAQs

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