Reentrancy is the most-cited smart-contract vulnerability, taught in every security curriculum, and yet somehow still shipping in production contracts a decade after The DAO. Understanding it — via real exploits that lost real money — is required literacy for anyone auditing, developing, or seriously evaluating smart-contract risk.
The pattern is simple. The consequences are enormous. And the fixes are well-known, but keeping them applied requires discipline.
The pattern
A reentrancy vulnerability exists when a contract makes an external call (like sending ETH to another address) before it updates its own state. If the recipient is a malicious contract, it can call back into the calling contract before the state is updated, and abuse the fact that the state still reflects a pre-call condition.
The canonical example: a withdraw function.
``` function withdraw() public { uint256 amount = balances[msg.sender]; (bool success, ) = msg.sender.call{value: amount}(""); // send ETH require(success); balances[msg.sender] = 0; // update state } ```
The vulnerability: when the contract sends ETH via `.call()`, control passes to the recipient's contract. If the recipient is malicious, it can call `withdraw()` again — and because `balances[msg.sender] = 0` hasn't executed yet, the contract sees the same balance and sends the same amount again. Repeat until the contract is drained.
The fix is the checks-effects-interactions pattern:
``` function withdraw() public { uint256 amount = balances[msg.sender]; balances[msg.sender] = 0; // update state FIRST (bool success, ) = msg.sender.call{value: amount}(""); // then interact require(success); } ```
Update state before making external calls. The re-entrant call now sees zero balance and does nothing.
The DAO (2016)
The DAO was a decentralized venture-capital fund launched on Ethereum in April 2016. It raised $150 million in ETH. In June 2016, an attacker exploited a reentrancy vulnerability in The DAO's splitDAO function to drain about $60 million worth of ETH.
The attacker used the same pattern: a recursive call into splitDAO that repeatedly transferred out ETH before the state update executed. Because the contract sent ETH before decrementing the attacker's balance, the attacker could split repeatedly until the contract emptied.
The response fractured Ethereum. A hard fork rolled back the exploit, creating what we now call Ethereum (ETH). A chain following the original state where the exploit was permanent became Ethereum Classic (ETC). The decision remains philosophically controversial.
The DAO exploit is the reason reentrancy is chapter 1 of every smart-contract security course. It is the archetypal vulnerability, and it happened to the largest and most-scrutinized smart contract of its era.
Fei Protocol (2022)
Fei was a stablecoin project. In April 2022, an attacker exploited a reentrancy vulnerability in Fei's Rari Fuse lending pools, draining ~$80 million.
The mechanism was slightly different from The DAO. Rari Fuse pools allowed users to deposit collateral and borrow against it. The vulnerability was in the borrowing flow: the pool sent the borrowed tokens to the user via a call that transferred control, and only then updated the user's borrow balance.
By constructing a callback that re-entered the borrow function during the transfer, the attacker could borrow multiple times against the same collateral. They drained multiple pools this way.
Fei's team responded with post-mortem, security fixes, and eventually a decision to shut down the protocol. The exploit was cited as a symptom of Rari's original design decisions rather than solely a mistake by the Fei team, but it happened on their combined product.
Cream Finance (2021)
Cream Finance suffered multiple exploits. The most-cited reentrancy-related one was the October 2021 hack where an attacker drained $130 million using a combination of flash loans and reentrancy exploits in Cream's price oracle and lending markets.
The specifics involved manipulating oracle prices via flash loan while simultaneously borrowing against inflated collateral values. The reentrancy came from an unusual interaction between two Cream markets that hadn't been considered in isolation.
Cream's story is a reminder that even after The DAO, even after years of published guidance, reentrancy in complex protocols with many moving parts is still shipping in production. It's not a bug you fix once and forget; it's a class of bug that requires continuous discipline.
Siren Protocol (2021)
Siren was an options protocol. In September 2021, a reentrancy vulnerability in their AMM logic was exploited for a smaller loss (~$3.5M), but the incident illustrated a subtle variant.
The vulnerability wasn't in a direct token-transfer flow. It was in how the AMM calculated payouts to option holders, which involved a nested call to a callback function. The callback could re-enter the AMM and take advantage of a stale calculation.
The fix required not just checks-effects-interactions but also careful analysis of the entire callback flow to make sure no assumption about state persisted across an external call.
Read-only reentrancy
The classic reentrancy exploit involves calling back into a function that modifies state. Read-only reentrancy is a subtler variant that exploits functions that only read state.
The setup: Contract A calls Contract B, and B calls back into A's view function. The view function returns some derived value based on state that's inconsistent because A's transaction is mid-execution.
Example: Balancer V2 pools use a view function to report price. If that price is queried during a swap that's mid-transaction, the returned value can be manipulated. A protocol relying on this "safe" view function for its own logic can be attacked.
Read-only reentrancy has caused several exploits since 2022. It's harder to spot because the "vulnerability" isn't in the calling contract — it's in a contract that trusts a view function that turns out to not be view-safe.
The nonReentrant modifier
OpenZeppelin's ReentrancyGuard library provides a `nonReentrant` modifier that prevents reentrancy at the language level. It uses a lock variable that's set at the start of a function and cleared at the end. If the function is entered while the lock is set, it reverts.
Every DeFi contract that handles external calls should use this modifier on user-facing state-changing functions. It's a defense-in-depth measure — the checks-effects-interactions pattern is the primary fix, but the modifier catches cases where CEI was forgotten.
However, `nonReentrant` protects only against reentrancy into functions on the same contract. Cross-contract reentrancy (calling into another contract during a callback) is not caught by a modifier on a single contract. This is what makes protocols with many contracts (like Cream) still vulnerable.
Why reentrancy keeps happening
A decade of published warnings hasn't eliminated reentrancy exploits. Why?
- **New patterns introduce new callback flows**: NFT royalties, cross-chain bridges, flash loans, hooks — each new primitive creates new potential callback paths that developers may not fully analyze.
- **Composability creates surface area**: DeFi contracts are called by other contracts. Any assumption about caller behavior can be violated.
- **Audits are point-in-time**: a contract can be audited in isolation but then integrated with other contracts in ways the audit didn't consider.
- **Read-only reentrancy is subtle**: it requires understanding not just the direct call chain but also secondary effects on functions that seem like they should be safe.
The 2023 Curve Vyper compiler bug was essentially a reentrancy failure at the compiler level — the compiler's automatic reentrancy guard didn't behave as expected in a specific configuration.
For anyone reviewing a contract
The reentrancy audit checklist:
1. Find every external call in the contract (calls to other contracts, ETH transfers, token transfers). 2. For each external call, check the state before and after the call. Is the state updated before the call? 3. Check if the called contract could be malicious or under attacker control. If yes, the CEI pattern is critical. 4. Look for `nonReentrant` modifiers on user-facing state-changing functions. 5. For cross-contract flows, trace the entire call chain and check for cross-contract reentrancy. 6. Check any callback flows (ERC-777 hooks, NFT transfers with acceptance callbacks, custom callback patterns).
This isn't foolproof but it catches the common patterns. Serious audits go deeper with fuzzing, formal verification, and manual review of every state transition.
The one-sentence version
Reentrancy is when an external call gives control back to the caller before the caller has finished updating its own state — and the caller was assuming the state was already updated. The fix is checks-effects-interactions: always update state before making external calls. The habit of checking this in every function is what separates safe smart-contract code from the code that ends up as a case study.




