Reentrancy
An external call hands control to an attacker before state is finalized, letting them re-enter and act on stale storage.
Definition
Reentrancy occurs when a contract makes an external call before it has finished updating its own state. The callee (attacker-controlled) can call back into the original function while the first invocation is still in progress, observing and exploiting state that has not yet been written.
How it works
- A function performs an external call (ETH transfer, token hook, or callback) before persisting its state changes.
- The external target is attacker-controlled and re-enters the same or a related function.
- On re-entry the contract still reads pre-update state (e.g. an un-decremented balance), so a guard or accounting invariant is evaluated against stale values.
- The attacker repeats the loop to drain funds or desynchronize accounting across contracts.
Vulnerable vs. fixed
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "insufficient");
// External call BEFORE state update — hands control to the caller.
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
balances[msg.sender] -= amount; // too late: re-entry saw the old balance
}function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "insufficient");
// Effects first: update state before any external interaction.
balances[msg.sender] -= amount;
// Interaction last.
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}Detection steps
- Identify the sensitive value or permission the pattern can influence.
- Trace every path that can update or consume that value, including callbacks, routers, and privileged helpers.
- Reproduce the worst-case attacker flow with adversarial ordering, manipulated inputs, and maximum feasible capital.
Common signals
- Security depends on an assumption that is not enforced by code.
- A critical value is consumed immediately after an attacker-controlled interaction.
- A privileged call path crosses multiple contracts without one clear authorization boundary.
False positives
- The risky-looking operation is read-only and cannot affect settlement or authorization.
- The value is bounded by independent checks before it moves assets.
- All privileged entry points share the same tested guard and monitoring path.
Review questions
- Which invariant would fail if this input or caller were attacker-controlled?
- Can the attacker compose setup, trigger, settlement, and cleanup atomically?
- What independent check stops the exploit if the first guard is wrong?
Defensive checklist
- Apply checks-effects-interactions: finalize all state before external calls.
- Add a reentrancy guard (e.g. OpenZeppelin ReentrancyGuard) to functions with external calls.
- Treat cross-contract accounting as one atomic unit — never read a value from contract A that contract B updates on a separate path.
- Audit keeper/callback hooks: any address you hand control to can re-enter.
Related cases
Related tools
Related glossary
Further reading
