What Is a Reentrancy Attack? Smart Contract Bug Explained
The reentrancy vulnerability pattern explained with a simplified code example, and how modern contracts prevent it.
A reentrancy attack is a smart contract exploit in which a malicious contract repeatedly calls back into a vulnerable function before that function has finished updating its own internal records — allowing the attacker to withdraw funds multiple times against a balance that should have already been reduced after the first withdrawal, effectively draining far more than they were entitled to.
Reentrancy is one of the earliest and most well-known categories of smart contract vulnerability, made famous by the DAO hack in 2016, and it remains a foundational concept in smart contract security education specifically because the underlying pattern is simple enough to understand without a deep technical background, yet was directly responsible for one of the largest exploits in crypto history.
The vulnerable pattern
The vulnerability comes down to the order of operations inside a function that both sends funds and updates a stored balance. Consider a simplified withdrawal function structured like this (simplified for explanation, not real code):
function withdraw(amount):
require(balance[caller] >= amount)
send(caller, amount) // sends funds first
balance[caller] -= amount // updates balance after
The problem is the ordering: funds are sent to the caller before the caller's recorded balance is reduced. If the caller is itself a smart contract (rather than a simple wallet address), receiving the funds can automatically trigger that contract's own code to execute — and a malicious contract can use that automatic execution to immediately call withdraw again, before the first call has reached the line that reduces the balance.
How the recursive exploit plays out
Because the balance hasn't been reduced yet when the second call happens, the require check at the top still passes — the recorded balance still shows the original, unreduced amount. This lets the malicious contract call withdraw again, and again, recursively, each time receiving another payout while the balance-reducing step keeps getting deferred until the recursive calls finally unwind. In the case of The DAO, this recursive pattern allowed a single initial withdrawal request to effectively multiply into a repeated, near-total drain of the pooled funds available to that call path.
The fix: checks-effects-interactions
The standard defense, now considered a fundamental best practice in smart contract development, is the checks-effects-interactions pattern — structuring functions so that all internal state changes happen before any external calls or fund transfers:
function withdraw(amount):
require(balance[caller] >= amount) // checks
balance[caller] -= amount // effects (state updated first)
send(caller, amount) // interactions (external call last)
With this ordering, by the time a malicious contract's recursive call re-enters withdraw, the balance has already been reduced, so the require check correctly fails on the second attempt, preventing the recursive drain entirely.
Additional modern defenses
Beyond reordering operations, developers commonly use additional safeguards: reentrancy guards (a lock mechanism that explicitly prevents a function from being re-entered while it's already executing, regardless of operation order) and drawing on audited, standard libraries for common patterns like token transfers, rather than writing custom withdrawal logic from scratch, since these libraries have been extensively reviewed and hardened against exactly this class of bug over years of industry use.
Reentrancy defenses compared
| Defense | How it works | Strength |
|---|---|---|
| Checks-effects-interactions ordering | State updated before external calls | Strong — addresses root cause |
| Reentrancy guard (lock) | Explicitly blocks re-entry during execution | Strong — works even if ordering is imperfect |
| Standard, audited libraries | Reuses hardened, reviewed code | Reduces risk of introducing new bugs |
Why this still matters today
Despite being a well-understood vulnerability for years, variations of reentrancy bugs continue to appear in newer, less rigorously audited DeFi protocols, sometimes in more subtle forms involving multiple contracts calling each other (cross-contract reentrancy) rather than a single function calling itself directly. This is part of why smart contract audits remain a standard, expected step before any serious DeFi protocol launches, and why protocols with a strong audit history and a longer track record without incidents are generally considered lower-risk than newer, unaudited alternatives — a distinction worth weighing when evaluating where to use DeFi lending or other protocols.
Single-function vs. cross-contract reentrancy
The classic version of this bug, sometimes called single-function reentrancy, involves a malicious contract calling back into the exact same function it originally invoked, as in the withdrawal example above. A more subtle variant, cross-function reentrancy, occurs when the recursive call targets a different function within the same contract that happens to share the same unprotected state — meaning a reentrancy guard applied narrowly to only one function can still leave the contract vulnerable through a second, related function that touches the same balance. Cross-contract reentrancy goes a step further, spanning multiple separate contracts that interact with shared state indirectly, making the vulnerability considerably harder to spot through a simple code read of any single contract in isolation. Auditors specifically look for all three variants, since a contract can appear safe against the textbook case while remaining exposed through one of the less obvious paths.
What this means if you use DeFi protocols
As an individual user, you generally can't inspect a protocol's contract for reentrancy vulnerabilities yourself unless you have smart contract development experience, but you can factor audit history into your own risk assessment. A protocol that has published multiple independent audits, has a long track record of handling significant value without incident, and discloses its audit reports publicly is a meaningfully different risk proposition than a brand-new, unaudited fork of existing code. Checking a protocol's TVL alongside its audit and incident history — rather than TVL alone — gives a fuller picture, since scale of deposits says nothing on its own about whether the underlying contracts have been properly reviewed for exactly this class of bug.
Bottom line
A reentrancy attack exploits a simple ordering mistake — sending funds before updating records — that recursive calls can turn into a repeated, compounding drain. The fix is equally simple in principle (update state before making external calls) but requires disciplined application across every function that touches both funds and balances, which is exactly why reentrancy checks remain a standard, non-negotiable part of any credible smart contract audit today.
Related articles
This article is for educational purposes only and is not financial advice. DeFi involves significant risk, including total loss of funds. Always do your own research.