IntermediateCritical Severity

Understanding Reentrancy Attacks

10 min readUpdated Dec 15, 2024By Hexific Team

What is Reentrancy?

A reentrancy attack occurs when a malicious contract exploits the fact that external calls can transfer control to an untrusted contract, which can then call back into the vulnerable contract before the first execution is complete.

This happens because Solidity allows external calls during function execution, and if state changes happen after the external call, an attacker can manipulate the contract state.

🚨

Historical Impact

The most famous reentrancy attack was the DAO hack in 2016, which resulted in the loss of 3.6 million ETH (worth ~$60M at the time) and led to the Ethereum hard fork.

How the Attack Works

The attack follows this general pattern:

  1. Attacker calls a vulnerable withdraw() function
  2. The contract sends ETH to the attacker before updating state
  3. Attacker's receive() or fallback() function is triggered
  4. Attacker re-enters the withdraw() function
  5. Since state wasn't updated, the check passes again
  6. Process repeats until funds are drained

🔄 Attack Flow

1. Attacker → withdraw() → sends ETH

2. ETH transfer → triggers receive()

3. receive() → calls withdraw() again

4. Balance not updated → withdraw succeeds

5. Repeat until contract is drained

Real-World Examples

The DAO Hack (2016)

$60M lost due to reentrancy in the splitDAO function. Led to the ETH/ETC fork.

🔴

Curve Finance (2023)

$70M lost due to a Vyper compiler bug that broke reentrancy guards.

🦊

Grim Finance (2021)

$30M lost through reentrancy in the depositFor function.

Prevention Techniques

1. Checks-Effects-Interactions Pattern (CEI)

The most fundamental defense. Always follow this order:

  • Checks: Validate all conditions and inputs first
  • Effects: Update all state variables
  • Interactions: Make external calls last

2. Reentrancy Guards

Use OpenZeppelin's ReentrancyGuard modifier or implement your own mutex lock:

SecureVault.sol
1import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
2
3contract SecureVault is ReentrancyGuard {
4    mapping(address => uint256) public balances;
5    
6    function withdraw(uint256 amount) external nonReentrant {
7        require(balances[msg.sender] >= amount, "Insufficient");
8        
9        // Effects before interactions
10        balances[msg.sender] -= amount;
11        
12        // Interaction last
13        (bool success, ) = msg.sender.call{value: amount}("");
14        require(success, "Transfer failed");
15    }
16}

3. Pull Over Push Pattern

Instead of sending ETH to users, let them withdraw. This limits the attack surface.

Best Practice

Combine multiple techniques: CEI pattern + ReentrancyGuard + Pull payments for maximum security.

Code Examples

Let's compare vulnerable code with its secure counterpart:

Vulnerable Code
function withdraw() external {
    uint256 balance = balances[msg.sender];
    require(balance > 0, "No balance");
    
    // ❌ External call BEFORE state update
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success, "Transfer failed");
    
    // ❌ State updated AFTER external call
    balances[msg.sender] = 0;
}
Secure Code
function withdraw() external nonReentrant {
    uint256 balance = balances[msg.sender];
    require(balance > 0, "No balance");
    
    // ✅ State updated BEFORE external call
    balances[msg.sender] = 0;
    
    // ✅ External call AFTER state update
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success, "Transfer failed");
}

Detection Tools

Use these tools to detect reentrancy vulnerabilities in your code:

🔧 Slither

Static analysis framework that detects reentrancy patterns.

slither . --detect reentrancy-eth

🔧 Mythril

Symbolic execution tool for deep vulnerability analysis.

myth analyze contract.sol

🔧 Foundry

Write reentrancy tests with invariant testing.

forge test --match-test testReentrancy

🔧 Hexific Audit

AI-powered automated audit with manual review.

Get a free audit →