Reentrancy attacks

The vulnerability that drained $60M from The DAO in 2016 — and how to prevent it.

What is reentrancy?

Reentrancy happens when a contract makes an external call to another contract before updating its own state. The called contract can call back into the original function, re-entering it while the state is still in an intermediate (pre-update) state.

The classic vulnerable pattern

solidity
// VULNERABLE CONTRACT
contract VulnerableBank {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, 'No balance');

        // DANGER: sends ETH before updating balance
        (bool success, ) = msg.sender.call{value: amount}('');
        require(success);

        // TOO LATE: attacker's receive() has already called withdraw() again
        balances[msg.sender] = 0;
    }
}

// ATTACKER CONTRACT
contract Attacker {
    VulnerableBank public bank;
    uint256 public attackCount;

    constructor(address bankAddress) {
        bank = VulnerableBank(bankAddress);
    }

    function attack() external payable {
        bank.deposit{value: msg.value}();
        bank.withdraw();
    }

    receive() external payable {
        if (address(bank).balance >= 1 ether && attackCount < 10) {
            attackCount++;
            bank.withdraw();  // re-enter before balance is zeroed!
        }
    }
}

How to prevent reentrancy

1. Checks-Effects-Interactions pattern

Always update state before making external calls.

solidity
function withdraw() external {
    uint256 amount = balances[msg.sender];
    require(amount > 0, 'No balance');  // CHECK

    balances[msg.sender] = 0;           // EFFECT — state updated first

    (bool success, ) = msg.sender.call{value: amount}('');  // INTERACTION — last
    require(success, 'Transfer failed');
}

2. ReentrancyGuard

OpenZeppelin's nonReentrant modifier uses a mutex to prevent any function marked with it from being called while it's already executing.

solidity
import '@openzeppelin/contracts/utils/ReentrancyGuard.sol';

contract SafeBank is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, 'No balance');
        balances[msg.sender] = 0;
        (bool success, ) = msg.sender.call{value: amount}('');
        require(success);
    }
}

Cross-function reentrancy

Reentrancy isn't limited to a single function. An attacker might enter via function A and call function B while A's state update is still pending. The fix is the same: update state before any external call, and consider using nonReentrant on all functions that interact with balances.

The DAO hack in brief

In 2016, an attacker exploited reentrancy in "The DAO," a decentralized venture fund, to drain approximately 3.6 million ETH (~$60M at the time). The Ethereum community controversially forked the chain to reverse the theft, creating Ethereum (ETH) and Ethereum Classic (ETC). The incident permanently changed how the ecosystem thinks about smart contract security.

←   Common vulnerabilitiesAccess control patterns   →