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
// 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.
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.
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.