How it happens
- 01External callControl leaves the contract.
- 02CallbackAttacker re-enters before completion.
- 03Stale stateOld accounting is still trusted.
- 04Double-useThe same value is reused.
Vulnerable pattern
Value leaves before accounting is finalized.
(bool ok,) = msg.sender.call{value: amount}("");
balances[msg.sender] = 0;
Reentrancy Explained in Detail
Reentrancy appears when a contract gives control to external code before its own state is safely finalized. In Solidity, that usually happens around external calls, token hooks, callbacks, or integrations that invoke untrusted contracts.
The bug is not merely that a function can be called twice. The risk is that the second call observes stale state from the first call and uses that stale state to withdraw funds, bypass accounting, mint too much value, or break an invariant.
Smart contract example
The withdrawal pattern below is dangerous because it sends ETH before reducing the user's recorded balance:
contract UnsafeVault {
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "Transfer failed");
balances[msg.sender] = 0;
}
}
If msg.sender is a contract, its receive function can call withdraw() again before balances[msg.sender] = 0 runs.
The safer pattern updates state before the external interaction:
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0;
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "Transfer failed");
}
Reentrancy in Auditing
Reentrancy is audit-critical because external integrations can return control before the contract finishes updating state. A function that looks safe in isolation can become unsafe once another contract, token hook, bridge adapter, vault callback, or marketplace receiver is involved.
Auditors should ask: what state can a callback observe or change before this function finishes?
Red flags in code
-
External calls before state updates.
-
call,delegatecall, token transfers, or callback registration inside sensitive functions. -
ERC777, ERC1155, ERC721, flash loan, bridge, or marketplace receiver hooks.
-
Multiple functions sharing the same balance, debt, share, or collateral state.
-
View functions that can be called during an external interaction and return temporarily inconsistent values.
-
Reentrancy guards applied to one function while another unguarded function touches the same state.
How to test or review it
-
Trace every external call and mark what state is updated before and after it.
-
Build a malicious receiver contract that calls back into the target during the external interaction.
-
Test cross-function paths, not only repeated calls to the same function.
-
Check whether invariants such as total shares, total debt, total collateral, or user balances can be violated during the callback.
-
Use the checks-effects-interactions pattern where possible, but do not rely on it blindly for complex protocol state machines.
-
Consider a reentrancy guard for high-risk functions, while still reviewing shared state and callback surfaces manually.