DEV Community

Abdelrahman ELsaheir
Abdelrahman ELsaheir

Posted on

GMX V1 Exploit Analysis: How a $42M Classic Reentrancy Attack Unfolded

A Technical Post-Mortem on a Preventable Vulnerability

In the fast-paced world of Decentralized Finance (DeFi), protocols are built on a foundation of open-source code and immutable trust. But this code, no matter how innovative, is still written by humans. In July 2025, the Web3 community received a powerful reminder of this fact when GMX V1, one of the most popular perpetuals exchanges, was exploited in a sophisticated attack that drained $42 million from its vault.

The surprise? The vulnerability wasn't a novel zero-day attack. It was an old, well-documented adversary: Reentrancy.

In this technical deep dive, we will dissect the attack, identify the vulnerable code, explain how the attacker (later identified as a white-hat) exploited this flaw, and most importantly, discuss how it could have been prevented.

Quick Background: What is GMX V1?

To understand the hack, you must first understand the GMX model. It's a decentralized exchange that allows users to open leveraged long and short positions. Instead of a traditional order book, GMX uses a single liquidity pool model called GLP.

Users deposit assets (like ETH, WBTC) into the Vault contract and receive GLP tokens in return. This Vault acts as the counterparty to all traders; when traders win, the Vault (and GLP holders) lose, and vice-versa. Orders are executed by off-chain "Keepers" that call specific functions on the smart contract.

The Vulnerability: A Deep Dive

The flaw resided within the PositionManager contract, specifically in the executeDecreaseOrder function. This function is responsible for executing an order to close or reduce a user's position.

The flawed logic was as follows:

  1. The contract calculates the collateral to be returned to the user.
  2. The contract sends the collateral (in this case, ETH) back to the user.
  3. After the transfer, the contract updates the protocol's internal state (like global position sizes, average entry prices, etc.).

This sequence is a critical violation of a core smart contract security principle.

The Vulnerable Code
Let's look at a simplified pseudo-code representation of the vulnerability within the PositionManager and its _transferOutETH helper function:

// Inside the PositionManager Contract
function executeDecreaseOrder(address _account, uint _sizeDelta, ...) {
    // ... (Various checks are performed)

    // Calculate collateral to be returned
    uint collateralAmount = ...; 

    // ... 

    // [!] THE VULNERABILITY IS HERE [!]
    // The contract sends ETH *before* updating its internal state.
    // This is an "Interaction" before an "Effect".
    _transferOutETH(_account, collateralAmount);

    // [!] STATE UPDATES HAPPEN *AFTER* THE EXTERNAL CALL [!]
    // These lines are supposed to run *after* the transfer
    _updateGlobalShorts(token, _sizeDelta); // Updates global position size
    _isLeverageEnabled[_account] = false;  // Resets leverage flag
    // ... (other state changes)
}

function _transferOutETH(address _receiver, uint _amount) internal {
    // ...
    // The .call{value: _amount}("") function transfers ETH.
    // If _receiver is a smart contract, this triggers its receive() or fallback() function.
    // This is the re-entrancy entry point.
    (bool success, ) = _receiver.call{value: _amount}("");
    require(success, "ETH transfer failed");
    // ...
}
Enter fullscreen mode Exit fullscreen mode

As shown, _transferOutETH uses _receiver.call{value: _amount}.

When sending ETH to another smart contract, this function gives the receiving contract (the attacker's contract) the opportunity to execute its own code (via a receive() function) before the original executeDecreaseOrder function completes its execution.

The Attack: Step-by-Step

The attack was brilliant in its execution. Here is how it unfolded:

  • Step 1: The Setup
    • The white-hat attacker deployed a malicious AttackContract.
    • The attacker opened a small, legitimate short position on GMX.
  • Step 2: Triggering the Attack
    • The attacker called executeDecreaseOrder from their AttackContract to close this small, legitimate position.
  • Step 3: The Entry Point
    • The PositionManager contract began executing. It calculated the small collateral refund (an amount of ETH) and called _transferOutETH to send it to the attacker's address (the AttackContract).
  • Step 4: The Re-Entrancy
    • As the AttackContract received the ETH, its receive() function was automatically triggered.
    • This is the core of the attack: Inside this receive() function, the attacker's code called the Vault.increasePosition() function again, effectively "re-entering" the GMX protocol.
  • Step 5: The Exploit
    • Because this new call happened before the original executeDecreaseOrder call could update the state (_updateGlobalShorts or _isLeverageEnabled), the protocol was in an "inconsistent state."
    • The attacker was able to open a new, massive short position (with 30x leverage) while the protocol was still in a state that allowed it, and before the global price trackers were updated.
    • This re-entrant loop was repeated, allowing the attacker to massively manipulate key state variables, particularly the globalShortAveragePrices.
  • Step 6: The Profit
    • This manipulation artificially made the new, massive short positions appear deeply "unprofitable."
    • According to GMX's logic, when short positions are losing, the Vault (and thus GLP holders) are "winning." This artifical, massive "profit" for the vault dramatically inflated the calculated Assets Under Management (AUM).
    • An inflated AUM, in turn, artificially inflated the price of the GLP token.
    • The attacker then simply redeemed their previously acquired GLP at this massively inflated price, walking away with a $42 million profit.

The Mitigation: How to Prevent This

This attack was 100% preventable by adhering to fundamental Solidity security patterns.

  1. The Checks-Effects-Interactions (CEI) Pattern This is the golden rule of Solidity development:
    • Checks: Perform all checks (e.g., require, if) first.
    • Effects: Update all internal state variables (e.g., balances, flags) second.
    • Interactions: Perform all external calls to other contracts (e.g., transferring funds) last. GMX should have updated _updateGlobalShorts and _isLeverageEnabled (the Effects) before calling _transferOutETH (the Interaction).

2. Use a Reentrancy Guard
The most direct and common fix is to use a simple modifier that "locks" a function, preventing it from being called again while it's already executing. The OpenZeppelin library provides a battle-tested solution.

The Fix: Corrected Code

Here is how the contract should have been written, applying both the ReentrancyGuard and the CEI pattern:

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

// Contract inherits from ReentrancyGuard
contract PositionManager is ReentrancyGuard {

    // Add the "nonReentrant" modifier to the function
    function executeDecreaseOrder(...) external nonReentrant {

        // [FIXED] 1. Checks
        // ... (All require() statements)

        // [FIXED] 2. Effects
        // State updates now happen *before* the external call
        _updateGlobalShorts(token, _sizeDelta);
        _isLeverageEnabled[_account] = false;

        // ... (other calculations)
        uint collateralAmount = ...;

        // [FIXED] 3. Interaction
        // External call is the *last* step in the function
        _transferOutETH(_account, collateralAmount); 
    }

    // ...
}

Enter fullscreen mode Exit fullscreen mode

The nonReentrant modifier alone would have stopped this specific attack vector, but adhering to the CEI pattern is the more robust and correct architectural solution.

Conclusion & Key Takeaways for Developers

The GMX V1 incident is a powerful lesson that trust in Web3 is not just about the blockchain; it's about the quality of the code running on it.

  1. Never Trust External Calls: Always assume that any contract you call (receiver.call) is malicious and will try to re-enter your functions.
  2. Master the CEI Pattern: The "Checks-Effects-Interactions" pattern must be the first law of your Solidity development process.
  3. Use Secure Libraries: Don't reinvent the wheel. Use battle-tested libraries like OpenZeppelin for ReentrancyGuard and SafeMath.
  4. Audit Every Change: This vulnerability was reportedly introduced in a fix for a separate, earlier bug, and that fix was not sufficiently audited. Every single line of code pushed to production, no matter how small, must undergo a rigorous security review.

For us as Web3 security professionals, these incidents are invaluable case studies. Understanding them at a code level is what separates an expert from an amateur.

References
For further technical reading and verification of this incident:

Top comments (0)