Reentrancy attacks have drained millions from decentralized applications built on blockchain platforms like Ethereum. Learn what exactly reentrancy attacks are, how to guard smart contracts against them, and best practices for blockchain software development.
What is a Reentrancy Attack?
A reentrancy attack refers to a malicious smart contract exploiting a vulnerability in another contract to recursively call functions and drain funds. By repeatedly calling functions before the victim contract can update state, the attacker extracts more funds each round – like multiple withdrawals from a bank account before balances and limits can update.
These attacks target the core mechanisms that enable reentrancy in software – features like callbacks and recursive calls. The attacker contract leaves execution of the first call unfinished, starts a second call, and leverages the unfinished state to their advantage before state gets locked.
The Three Reentrancy Variants You Need to Know
Not all reentrancy attacks look the same. The original DAO exploit used a single function calling itself recursively. Modern attacks are more complex, and single-function guards do not stop them.
Single-Function Reentrancy
The attacker re-enters the exact same function before it finishes. This is the classic pattern. It is also the easiest to catch and the least common variant in current exploits.
Cross-Function Reentrancy
The attacker calls a different function in the same contract during the reentrant call. Because the second function shares state with the first but has no guard, it executes against stale data. This variant bypasses per-function nonReentrant modifiers if they are not applied consistently across all state-sharing functions.
Read-Only Reentrancy
The attacker re-enters a view function that reads state mid-update. No funds move in the view call itself, but the stale state read is then used by a third contract to make a bad decision, such as mispricing an asset or approving an oversized loan. This pattern has appeared in recent DeFi exploit chains on Arbitrum and other L2s.
๐ Quick takeaway: Single-function reentrancy is the most contained variant โ a per-function guard is sufficient if applied correctly. Cross-function reentrancy requires guards across every function that touches shared state. Read-only reentrancy modifies no state directly but can still be exploited to manipulate pricing logic.
| Variant | Entry Point | State Modified? | Per-Function Guard Sufficient? |
|---|---|---|---|
| Single-Function | Same function | โ ๏ธ Yes | ๐ข Yes โ if applied |
| Cross-Function | Different function, same contract | ๐ด Yes | ๐ด No โ must cover all shared-state functions |
| Read-Only | View function | ๐ข No | ๐ด No โ requires contract-wide mutex or CEI discipline |
Choosing Your Defense: CEI, ReentrancyGuard, or Contract-Wide Mutex?
Three main defenses exist. Each has a different scope and a different failure mode. Pick the wrong one and you may stop single-function attacks while leaving cross-function vectors open.
Checks-Effects-Interactions (CEI)
The CEI pattern requires you to complete all state changes before making any external call. Check conditions first. Update state second. Call external contracts last. This order alone prevents most single-function and cross-function reentrancy because there is no stale state left to exploit when the external call fires.
Example of vulnerable ordering:
- Check balance (Checks)
- Send ETH to caller (Interactions) — external call fires here
- Deduct balance (Effects) — too late; attacker already re-entered
Correct CEI ordering:
- Check balance (Checks)
- Deduct balance (Effects) — state updated before any external call
- Send ETH to caller (Interactions)
OpenZeppelin ReentrancyGuard (nonReentrant)
OpenZeppelin’s ReentrancyGuard provides a battle-tested mutex modifier. Apply the nonReentrant modifier to any function that makes external calls or modifies sensitive state. This is the production standard for per-function protection and is simpler than writing your own mutex.
Contract-Wide Mutex
For contracts where multiple functions share state and any of them can be called during an external call, a single per-function guard is not enough. A contract-wide mutex locks the entire contract for the duration of any sensitive operation. The Sentinel framework (2026) demonstrated 100% coverage across all four reentrancy categories using this approach on production Solidity.
๐ Quick takeaway: CEI pattern and OpenZeppelin’s nonReentrant modifier are the lowest-effort starting points. CEI covers more ground if applied consistently, but neither fully addresses read-only reentrancy. A contract-wide mutex is the practical choice for full coverage without custom tooling. Sentinel guards provide the strongest coverage but require the highest implementation investment.
| Defense | Covers Single-Function | Covers Cross-Function | Covers Read-Only | Implementation Effort |
|---|---|---|---|---|
| CEI Pattern | ๐ข Yes | โ ๏ธ Yes โ if applied everywhere | โ ๏ธ Partial |
๐ข Low Code discipline only |
OpenZeppelin nonReentrant |
๐ข Yes | โ ๏ธ Only on guarded functions | ๐ด No |
๐ข Low Import + modifier |
| Contract-Wide Mutex | ๐ข Yes | ๐ข Yes |
๐ข Yes โ with care ๐ Best full-coverage option without custom tooling |
โ ๏ธ Medium |
| Sentinel / Decoupled Guard | ๐ข Yes | ๐ข Yes |
๐ข Yes ๐ Strongest coverage of all four defenses |
๐ด High Tooling required |
For most production contracts, layer CEI discipline with OpenZeppelin ReentrancyGuard. Add a contract-wide mutex if your contract has multiple state-sharing functions that call external contracts.
Real Incidents: What Recent Exploits Actually Looked Like
The Clober DEX Exploit (2024-2025)
CertiK’s post-mortem on the Clober DEX incident attributes the loss to a reentrancy pattern embedded in a cross-contract interaction chain. The attacker used a callback during a token swap to re-enter a settlement function before the swap state was finalized. A per-function guard on the swap function alone would not have stopped it because the reentrant call targeted a separate settlement path.
Arbitrum DeFi Protocols (2026)
DeFi Coverage’s 2026 analysis of Arbitrum exploit activity notes that classic reentrancy patterns remain active vectors in high-throughput L2 environments. The speed of L2 block production does not eliminate reentrancy risk. It can compress the window for detection.
Wallet-Contract Interaction Surfaces
An MDPI Applied Sciences case study (BlockScribe, 2025) identified reentrancy amplification through wallet-contract interaction surfaces in production DApps. When a wallet’s callback logic can trigger contract functions, the attack surface extends beyond the contract itself. Permission management and transaction ordering become part of the reentrancy defense perimeter.
What these incidents share: all involved cross-contract or cross-function call paths that bypassed per-function guards. CEI discipline applied at every external call boundary, combined with contract-wide mutex coverage, would have reduced the attack surface in each case.
How to Detect Reentrancy in Existing Contracts
Writing new contracts with CEI and ReentrancyGuard is straightforward. Auditing existing codebases for reentrancy is harder, especially as cross-function and read-only variants do not always trigger simple pattern-matching tools.
Static Analysis Tools
Slither and Mythril flag known reentrancy patterns automatically. They work well for single-function variants. Cross-function and read-only reentrancy require manual review or more advanced tooling because the call graph spans multiple functions.
LLM-Assisted Detection
A 2026 ArXiv paper on reentrancy detection in the age of LLMs identifies limitations in current detection datasets and argues that existing tools miss modern Solidity variants. LLM-based classifiers trained on updated datasets catch patterns that static analyzers skip, particularly in complex callback chains.
Formal Verification
Formal verification tools analyze the mathematical properties of a contract rather than its surface patterns. A 2026 ArXiv paper (Sol2Vy) introduced zero-shot vulnerability detection for Solidity contracts, showing improvements for low-resource contexts where training data is limited. The Sentinel framework claims 100% coverage across four reentrancy categories on production Solidity by separating guard logic from business logic entirely.
Audit Checklist for Reentrancy
- Identify every external call in the contract (call, delegatecall, transfer, send, ERC-20 token transfers).
- For each external call, verify that all state changes affecting the caller’s balance or permissions occur before the call.
- Check that nonReentrant modifiers are applied to every function that shares state with a guarded function.
- Review view functions for read-only reentrancy exposure, especially if they are called by other contracts during state transitions.
- Run Slither or Mythril as a first pass, then manually review any cross-contract call graph.
- For high-value contracts, commission a formal verification review.
Reentrancy Attack Triggers Vulnerability in Smart Contracts
Smart contracts are especially vulnerable due to their inherent trust of other contracts and the irreversible nature of transactions. An attacker contract can call functions to siphon crypto funds little by little with the right exploit.
Once a reentrancy vulnerability exists, attackers can drain millions before detection. The DAO hack in 2016 was the first major example, with $60 million in Ethereum stolen. The problem has not gone away. Reentrancy attacks contributed to roughly $80 million in losses across the DApp ecosystem in 2025 alone, and documented losses across all chains since 2016 exceed $500 million. Real-world datasets now compile 70 or more distinct reentrancy incidents through 2024, used to benchmark modern defenses.
Such attacks expose logical flaws in code. Rather than a direct breach of infrastructure, the contract logic incorrectly assumes state will get updated instantly before another call can start. By exploiting this window, the attack contract recursively drains funds.
Guarding Against Reentrancy Attack Solidity Smart Contracts
Three defenses dominate current Solidity security practice: the Checks-Effects-Interactions pattern, OpenZeppelin’s ReentrancyGuard, and contract-wide mutex locks. Each covers a different portion of the attack surface. Using only one leaves gaps. The sections below explain how each works and when to combine them.
Defense 1: Checks-Effects-Interactions (CEI) Pattern
The current production standard is OpenZeppelin’s ReentrancyGuard. Import it, inherit from it, and apply the nonReentrant modifier to any function that makes external calls or touches sensitive state.
import “@openzeppelin/contracts/security/ReentrancyGuard.sol”;
contract SafeBank is ReentrancyGuard {
mapping(address => uint256) private balances;
function withdraw(uint256 amount) external nonReentrant {
// Checks
require(balances[msg.sender] >= amount, “Insufficient balance”);
// Effects
balances[msg.sender] -= amount;
// Interactions
(bool success, ) = msg.sender.call{value: amount}(“”);
require(success, “Transfer failed”);
}
}
This example also follows CEI order: the balance is deducted before the external call fires. Both defenses work together. If you write a hand-rolled mutex instead of using ReentrancyGuard, ensure the lock resets even if the function reverts.
Defense 2: OpenZeppelin ReentrancyGuard (nonReentrant Modifier)
When you cannot avoid external calls, guard the function. OpenZeppelin’s ReentrancyGuard is the standard implementation. Import it, inherit from it, and mark every sensitive function with nonReentrant. The modifier sets a lock before the function body runs and clears it after. Any attempt to call the function again before it finishes reverts with ‘ReentrancyGuard: reentrant call’.
Apply nonReentrant to every function that makes an external call or modifies state that another function also reads. Missing one function in a shared-state group is the most common implementation error.
Guarding Against Reentrancy Attacks – An Example
The Vulnerable Flow (without CEI)
- Attacker contract calls withdraw() requesting 1 ETH.
- withdraw() sends 1 ETH to the attacker contract via call().
- The attacker contract’s receive() function fires automatically on receiving ETH.
- Inside receive(), the attacker immediately calls withdraw() again before the first call returns.
- Because balances[attacker] has not been deducted yet (state change happens after the send), the balance check passes again.
- Another 1 ETH is sent. Steps 3-5 repeat until the contract is empty or gas runs out.
The net result: the attacker deposited 1 ETH and withdrew the entire contract balance.
The Fixed Flow (with CEI + nonReentrant)
- Attacker contract calls withdraw().
- nonReentrant modifier sets the lock. Any reentrant call reverts immediately.
- withdraw() checks balance. It passes.
- withdraw() deducts balance first (Effects before Interactions).
- withdraw() sends ETH.
- Even if receive() fires and calls withdraw() again, the reentrant call hits the lock and reverts.
- The original call completes. Lock releases.
Audit Early, Then Audit Again
Reeentrancy vulnerabilities are cheapest to fix during design. A missed guard in a deployed contract requires a migration or an upgrade proxy. Neither is cheap.
Start during code review. Flag every function that makes an external call. Apply CEI ordering and nonReentrant modifiers before the function reaches a test environment. Run Slither on every pull request. Static analysis catches the obvious patterns in seconds.
Before mainnet deployment, commission an independent audit from a firm that uses both automated tooling and manual cross-contract call graph review. OWASP’s Smart Contract Top 10 2026 includes reentrancy among the top risk categories and provides a secure development lifecycle framework for integrating these checks into your release process.
After deployment, monitor for unexpected external call patterns. On-chain monitoring tools can alert on recursive call depth anomalies. If your contract is upgradeable, re-audit every upgrade that touches state-changing or external-call logic. The Radiant Capital 2024 exploit demonstrated that even audited contracts can carry reentrancy risk when upgrade paths are not reviewed with the same rigor as the original deployment.
Reentrancy and the OWASP Smart Contract Top 10 2026
OWASP released its Smart Contract Top 10 for 2026, built on observed exploit data across production deployments. Reentrancy remains among the top risk categories. The list provides a secure development lifecycle framework that maps specific attack patterns to recommended controls, making it a useful reference for teams building audit checklists or security policies.
For teams that need to justify security investment internally, the OWASP Top 10 framing helps communicate reentrancy risk in terms that non-technical stakeholders recognize.
Frequently Asked Questions
Is the CEI pattern alone sufficient to prevent reentrancy?
For most straightforward contracts, yes. CEI prevents reentrancy by ensuring no stale state exists when an external call fires. For contracts with multiple state-sharing functions or complex callback paths, add OpenZeppelin’s nonReentrant modifier as a second layer. Formal research suggests that neither defense alone covers all four reentrancy categories in complex production contracts.
Does OpenZeppelin ReentrancyGuard protect against cross-function reentrancy?
Only if you apply the nonReentrant modifier to every function that shares state with a guarded function. A nonReentrant modifier on withdraw() does not protect a separate transfer() function that reads the same balance mapping unless transfer() is also guarded.
Are reentrancy attacks still happening in 2025 and 2026?
Yes. Reentrancy contributed to approximately $80 million in losses in 2025. Recent incidents include Clober DEX and multiple Arbitrum DeFi protocols. The attack pattern has evolved from single-function to cross-function and read-only variants, which are harder to detect with legacy tools.
What is read-only reentrancy and why is it dangerous?
Read-only reentrancy occurs when an attacker re-enters a view function during a state transition. The view function itself does not change state, but a third contract that calls the view function during the attack reads stale data and makes a bad decision based on it. It is particularly dangerous in DeFi because pricing oracles and collateral checks often rely on view functions.
What tools can detect reentrancy in existing Solidity contracts?
Slither and Mythril handle single-function variants well. For cross-function and read-only variants, manual call graph review is necessary. LLM-assisted detection tools trained on updated Solidity datasets are an emerging option, with 2026 research showing improved coverage over legacy static analyzers.
