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.
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 — 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 — 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);
}
ReentrancyGuard modifier (nonReentrant) on functions that send Ether or call untrusted contracts.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).