Reentrancy Is Not Just a withdraw() Problem

Reentrancy is more than the classic DAO example. This guide covers callbacks, external calls, token hooks, read-only reentrancy, CEI, and Solidity mitigations.

· · 9 min read

Reentrancy is a pattern, not one function name

The familiar example is withdraw(): a contract sends ETH, the recipient fallback runs, and the recipient calls withdraw() again before the internal balance is updated.

That example matters. But if you memorize “reentrancy = withdraw,” you can miss other forms. The core idea is this: an external call gives other code a chance to enter again before the contract invariant is safe.

---

The simplest vulnerable shape

function withdraw() external {
uint256 amount = balances[msg.sender];
(bool ok,) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] = 0;
}

The balance changes after the external call. The recipient can call back while the old balance is still visible.

Checks-Effects-Interactions reverses that order: check conditions, update state, then interact with the external contract.

---

Reentrancy appears in other places

Token callbacks. ERC-777, ERC-721, ERC-1155, and tokens with hooks can call recipient code. A transfer that looks complete may still be handing control to another contract.

External protocol calls. DEXes, lending markets, oracles, and vaults call each other. An invariant can break when internal state is not finalized before the call leaves the contract.

Cross-function reentrancy. The attacker does not need to call the same function. It can enter another function that reads partially updated state.

Read-only reentrancy. A view function may not mutate state, but another protocol can read temporary values during a callback. If that value drives pricing or collateral decisions, the effect can still be dangerous.

---

Review checklist

1. Mark every call, delegatecall, token transfer, and callback.
2. Record which state must remain consistent before and after the call.
3. Check whether state changes happen before external interaction.
4. Find other functions reachable from a callback.
5. Inspect guards such as ReentrancyGuard, but do not treat one modifier as a complete design.
6. Test with an attacker contract, not only a happy-path unit test.
7. Check whether oracle and vault integrations can read temporary state.

ReentrancyGuard helps, but it does not replace invariant design. It may not cover another function, a cross-contract path, or a wrong assumption about callbacks.

---

Closing

Reentrancy is not fixed by adding one modifier. The question is when a contract trusts external code and when its internal state is final.

Whenever there is an external call, I ask: what can be called again, what state will it see, and is that state still consistent?

---

Sources

• Solidity: Security Considerations
• OpenZeppelin: ReentrancyGuard
• OWASP Smart Contract Security

*Written because many checklists stop at “add ReentrancyGuard,” while the bug often lives in cross-contract assumptions.*