Smart-contract security & auditing

reentrancy attack

A reentrancy attack happens when a contract calls out to another contract before it has finished updating its own bookkeeping, and that second contract calls straight back in to exploit the half-finished state. Picture a bank teller who hands you the cash first and only marks your balance down afterward: if you can somehow ask for cash again in that gap, you walk away with the money twice. On the EVM, sending Ether or making an external call hands control to the recipient, who can re-enter your function while your records still say it owes them.

Mechanically, the danger is an external call placed before a state change. A naive withdraw() checks the caller's balance, sends the Ether with a low-level call (which runs the recipient's receive or fallback function), and only then sets the balance to zero. The attacker's fallback simply calls withdraw() again; the balance is still non-zero, the check passes, and the loop repeats until the contract is drained or runs out of gas. This is exactly how The DAO was emptied of about 3.6 million ETH in June 2016, the event that led to the Ethereum/Ethereum Classic fork.

The standard defense is the checks-effects-interactions ordering: validate everything, then update state (zero the balance), and only then make the external call. A reentrancy guard, a mutex modifier such as OpenZeppelin's nonReentrant, adds a lock that reverts any nested re-entry. Be aware of variants a single-function mutex misses: cross-function reentrancy (re-entering a different function that shares the same state) and read-only reentrancy (a view function returning stale values mid-call, which a third contract then trusts).

// VULNERABLE
function withdraw() external {
    uint256 bal = balances[msg.sender];
    (bool ok, ) = msg.sender.call{value: bal}(""); // hands control to attacker
    require(ok);
    balances[msg.sender] = 0;                       // too late
}

// SAFE: checks-effects-interactions
function withdraw() external {
    uint256 bal = balances[msg.sender];
    balances[msg.sender] = 0;                       // effect first
    (bool ok, ) = msg.sender.call{value: bal}("");  // interaction last
    require(ok);
}

Move the state update before the external call.

A reentrancy guard alone is not a cure-all. Read-only reentrancy bypasses it entirely: an attacker re-enters during your external call and queries a view function that still reports pre-update state, then feeds that wrong value to a third protocol that has no idea a call is in flight.

Also called
re-entrancy重入漏洞