Reentrancy Attack

A reentrancy attack is one of the most infamous vulnerabilities in Solidity — it was exploited in the 2016 DAO hack, which drained 3.6 million ETH. The attack works by tricking a contract into calling back into itself (or into the attacker's contract) before it has finished updating its internal state.

How it works

Imagine a contract that sends Ether to a user and then records the withdrawal. If the contract sends Ether first, an attacker's fallback function can call withdraw() again before the balance is zeroed out — and keep doing so until the contract is empty.

Vulnerable pattern

// VULNERABLE — state update happens after the external call
function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);
    (bool ok,) = msg.sender.call{value: amount}("");  // attacker re-enters here
    require(ok);
    balances[msg.sender] -= amount;  // too late!
}

Safe pattern (Checks-Effects-Interactions)

// SAFE — state is updated before the external call
function withdraw(uint amount) public {
    require(balances[msg.sender] >= amount);
    balances[msg.sender] -= amount;  // effect first
    (bool ok,) = msg.sender.call{value: amount}("");
    require(ok);
}

Real-world exploits

  • The DAO (2016) — 3.6 million ETH drained; led to the Ethereum hard fork.
  • Cream Finance (2021) — $18.8 million lost via a flash-loan reentrancy combination.
  • Lendf.me (2020) — $25 million drained using ERC-777 token reentrancy.

How to prevent it

  • Follow the Checks-Effects-Interactions pattern — always update state before making external calls.
  • Use OpenZeppelin's ReentrancyGuard modifier (nonReentrant) on functions that send Ether or call untrusted contracts.
  • Prefer transfer() or send() over .call{value: ...}() where gas limits provide a natural guard (though .call is generally preferred for other reasons — just pair it with CEI or a mutex).
← Back to Glossary