
Understanding and Preventing Reentrancy Attacks
4 minutes ago
Jul 31, 2026

A reentrancy attack happens when a malicious contract calls back into a vulnerable contract before that contract's first execution has finished updating its own state. In plain terms, a contract sends funds or triggers an external call, and before it gets around to actually recording that the funds were sent, the receiving contract calls right back in and repeats the same withdrawal again. And again. Sometimes dozens of times in a single transaction, all before the original function ever reaches the line of code that was supposed to update the balance.
It sounds almost too simple to work, and that is exactly what makes it dangerous. Reentrancy does not require some exotic cryptographic flaw or an obscure edge case. It exploits a basic assumption that a lot of developers make without thinking twice: that a function will run from top to bottom without anything else interrupting it in the middle. In a language built around external calls to other contracts, that assumption is often wrong, and reentrancy is what happens when nobody accounts for that.
Reentrancy earned its reputation the hard way. In 2016, an attacker exploited exactly this pattern in The DAO, one of the largest and most prominent projects on Ethereum at the time, draining roughly sixty million dollars worth of ETH before the community intervened. The incident was significant enough that it led to a contentious hard fork, ultimately splitting Ethereum and Ethereum Classic into two separate chains, a split that still exists today. Our roundup of the biggest cryptocurrency hacks of all time covers this incident alongside several others that reshaped how the industry thinks about security.
Almost a decade later, reentrancy is still showing up in audit findings and, worse, in real exploits. Solidity's tooling has improved, best practices are far better documented, and yet the underlying mistake keeps recurring in new codebases, usually because a developer under time pressure reaches for a pattern that looks correct at a glance and simply is not.
The root cause almost always comes down to the same structural mistake: a contract makes an external call, to send ETH, to transfer tokens, to call another contract, before it updates its own internal state to reflect that action. This violates what security researchers call the checks effects interactions pattern, a simple ordering principle that says a function should validate conditions first, update its own state second, and only then interact with anything external.
When that order gets reversed, even briefly, an attacker has an opening. If the external call goes to a contract the attacker controls, that contract can immediately call back into the original function before the state update ever happens, and from the vulnerable contract's perspective, nothing looks wrong yet, because as far as its internal bookkeeping is concerned, the withdrawal has not been recorded.
Picture a simple withdrawal function. A user calls withdraw, the contract checks their balance, sends them the requested ETH, and then reduces their recorded balance afterward. An attacker deploys their own contract, deposits funds normally, then calls withdraw. The vulnerable contract sends ETH to the attacker's contract, which triggers that contract's fallback function automatically. Instead of just receiving the funds quietly, the fallback function calls withdraw again, immediately, before the original call has finished executing and before the balance has been reduced. The vulnerable contract checks the balance again, sees the same original amount because it was never updated, and sends the funds a second time. This repeats until the contract runs out of ETH, gas runs out, or the attacker simply decides to stop. Our explainer on fallback function vulnerabilities goes deeper into this specific mechanism, since fallback functions are frequently the exact entry point reentrancy exploits rely on.
Reentrancy is not a single pattern. It shows up in a few distinct forms, and auditors need to check for all of them.
This is the classic version described above, where a function calls back into itself repeatedly before its own state update executes. It is the easiest variant to spot and, thankfully, the one most developers are aware of by now.
A more subtle variant involves two different functions that happen to share the same state variable. An attacker calls back into a different function than the one that triggered the external call, one that manipulates the same underlying balance or flag, and because the two functions were never designed with each other's interaction in mind, the shared state ends up corrupted in ways that are much harder to spot during a quick review.
Here, the reentrant call jumps into an entirely separate contract that happens to share state or trust assumptions with the original one, often through shared storage patterns or proxy architectures. This variant is genuinely difficult to catch through manual review alone, since it requires understanding the interaction between multiple contracts rather than reviewing one in isolation.
A newer and less intuitive variant exploits view functions, ones that are not supposed to modify state at all. An attacker reenters during a state transition and reads a value mid-update, when it is temporarily inconsistent, then uses that stale or manipulated read elsewhere, often in a different protocol entirely that trusted the first contract's reported state. This has become a genuine concern in DeFi, where protocols frequently read price or balance data directly from other contracts.
Here is a stripped down illustration of the vulnerable pattern, using pseudocode rather than a fully compilable contract:
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] -= amount;
}
Notice the ordering. The external call happens before the balance is reduced. An attacker's fallback function, triggered automatically by that external call, can call withdraw again while the original balance value is still sitting unchanged. The fix is straightforward once you see it:
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
Simply reordering these two lines, updating state before making the external call, closes off the entire attack, at least for this specific function.
This is the single most important habit to build. Validate your conditions first, update all relevant state second, and only then make any external call. If you internalize this ordering as a default rather than something you have to consciously remember, a large share of reentrancy risk disappears before it ever gets written.
A reentrancy guard is a simple modifier that locks a function while it is executing, rejecting any attempt to call back into it before the first call completes. OpenZeppelin's widely used ReentrancyGuard is the standard implementation most Solidity developers reach for, and it is a genuinely cheap, effective safeguard for any function handling value transfers.
Rather than a contract actively pushing funds out to users, a pull based pattern lets users withdraw their own funds on their own initiative, in a separate, carefully guarded transaction. This removes the automatic external call that reentrancy depends on in the first place, shifting the timing of risk entirely onto a transaction the recipient controls.
Treat every external call as a potential handoff of control to code you do not own and cannot fully trust, even to a contract that looks benign. This mindset applies well beyond ETH transfers, to token transfers, to calls into other protocols, and to anything that triggers code execution outside your own contract's boundaries.
Given how well documented reentrancy is at this point, it is fair to ask why it keeps showing up in new exploits. The honest answer is a mix of complacency and complexity. Developers copy patterns from older, sometimes flawed code without fully understanding why a particular ordering matters. Cross function and cross contract variants are genuinely harder to spot than the classic single function version, and they slip past teams that only checked for the well known pattern. And increasingly complex DeFi composability, protocols calling into other protocols calling into still more protocols, creates exactly the kind of tangled interaction that read only reentrancy and cross contract reentrancy thrive in. Reentrancy in Rust based ecosystems is genuinely less common, thanks to stricter handling of state and execution flow, though it is not impossible, as covered in our broader guide to Rust security audits.
A proper smart contract audit checks explicitly for every variant described above, not just the textbook single function pattern most automated scanners catch reliably. This means manual review tracing the actual order of state changes and external calls function by function, combined with the kind of black box and white box testing that surfaces cross contract interactions a purely static read of the code might miss. For particularly high value contracts, formal verification can mathematically prove that specific state invariants hold even under adversarial reentry attempts, offering a stronger guarantee than manual review alone. Once a contract is live, real time monitoring adds a further layer, flagging the kind of rapid, repeated withdrawal pattern that characterizes an active reentrancy exploit in progress, ideally before it drains very far. If you want a broader sense of what auditors specifically check for, our overview of common smart contract vulnerabilities covers reentrancy alongside the other patterns that show up most often in real audit findings.
Reentrancy is one of the oldest known smart contract vulnerabilities, and nearly a decade after The DAO, it remains one of the most consistently dangerous. The underlying fix, updating state before making external calls, is simple to state and not particularly hard to implement once you understand why it matters. What makes reentrancy genuinely tricky in practice is the variants that go beyond the textbook case, cross function, cross contract, and read only reentrancy all require a deeper, more deliberate review than a quick scan for the obvious pattern. Getting this right, through disciplined coding practices and a genuinely thorough audit before launch, remains one of the highest leverage things any team building on-chain can do to protect the funds their contract is responsible for.