A reentrant call occurs when a smart contract function makes an external call to another contract, which then calls back into the original function before its first invocation has finished. This creates a recursive loop where the contract's state—such as an account balance—is not updated before the second execution begins. The most famous exploit of this pattern was the 2016 DAO hack on Ethereum, where an attacker's malicious contract repeatedly withdrew funds because the balance was only subtracted after the external transfer.
Reentrant Call
What is a Reentrant Call?
A reentrant call is a programming vulnerability where an external contract's callback into the calling contract interrupts and re-executes its logic before the initial execution is complete, potentially draining funds.
The vulnerability stems from violating the checks-effects-interactions pattern. Secure code should first perform all checks (e.g., validating sufficient balance), then apply all effects (updating the internal state), and only finally make external interactions. A reentrant function that interacts before updating its state allows the attacker's fallback function to re-enter and pass the same checks again, as the original balance remains unchanged. This is a specific type of race condition within a single transaction's execution.
Preventing reentrancy involves using reentrancy guards, such as OpenZeppelin's ReentrancyGuard modifier, which employs a boolean lock (nonReentrant) to block recursive calls. Alternatively, developers can adhere strictly to the checks-effects-interactions pattern. Some languages, like Vyper, have built-in protections against state reentrancy. It's crucial to understand that reentrancy can also be cross-function, where a callback enters a different function that shares sensitive state, requiring careful design of state variable access.
How a Reentrant Call Works
A reentrant call is a critical smart contract vulnerability where an external contract maliciously calls back into the original function before its initial execution has completed, often to drain funds.
A reentrant call occurs when a smart contract function makes an external call to another untrusted contract, which then recursively calls back into the original function before its first invocation has finished. This is possible because the Ethereum Virtual Machine (EVM) does not automatically lock a contract's state during an external call. The most famous exploitation of this was the DAO attack in 2016, where a malicious contract repeatedly called the vulnerable withdraw function, tricking the balance accounting logic and siphoning millions in Ether.
The vulnerability stems from a violation of the Checks-Effects-Interactions (CEI) pattern. A secure pattern dictates that a function should: 1) perform all checks (e.g., validating balances), 2) apply all effects (e.g., updating internal state like deducting a balance), and 3) only then make external interactions (e.g., sending funds). A reentrancy bug inverts this order, typically sending funds (interaction) before updating the internal state (effect), allowing the attacker's fallback function to re-enter and pass the unchanged checks repeatedly.
Developers prevent reentrancy using two primary methods. The first is strict adherence to the CEI pattern. The second, and more robust for complex logic, is using a reentrancy guard. This is a function modifier, like OpenZeppelin's ReentrancyGuard, that sets a boolean lock (nonReentrant) for the function's duration, throwing an error if a reentrant call is attempted. For ultimate safety, the pull-over-push pattern is recommended, where the contract designates owed funds to users who must call a separate function to withdraw them, eliminating untrusted external calls from critical state-changing functions altogether.
Key Features & Characteristics
A reentrant call is a programming vulnerability where an external contract's callback function maliciously re-enters the calling contract before its initial execution is complete, often to drain funds. These are the core mechanisms and properties that define this security flaw.
The Core Vulnerability
A reentrant call exploits the sequential nature of state updates in smart contracts. The vulnerability occurs when a contract:
- Sends funds (e.g., via
call.value()) to an untrusted address. - Updates its internal state (e.g., reducing the user's balance) after the external call. An attacker's contract can execute a fallback or receive function that calls back into the vulnerable function before the balance is deducted, allowing repeated withdrawals from the same initial balance.
The Checks-Effects-Interactions Pattern
The primary defense against reentrancy is the Checks-Effects-Interactions (CEI) pattern. This coding standard mandates a strict order of operations:
- Checks: Validate all conditions and inputs (e.g.,
require(balance >= amount)). - Effects: Update all internal state variables before any external calls (e.g.,
balances[msg.sender] -= amount). - Interactions: Perform external calls to other contracts or addresses last. By updating state first, any reentrant call will see the already-modified balance, preventing double-spending.
Reentrancy Guard Modifiers
A reentrancy guard is a code-level lock that prevents a function from being called recursively. It uses a boolean state variable (e.g., locked) that is set to true on entry and false on exit. The OpenZeppelin ReentrancyGuard contract provides a standard nonReentrant modifier. When applied to a function, any reentrant call will fail the require(!locked) check, providing a robust, single-line defense. This is considered a best practice for any function performing external calls.
Historical Impact: The DAO Hack
The most famous reentrancy attack was the 2016 DAO hack, which resulted in the theft of 3.6 million ETH (worth ~$50M at the time). The vulnerable splitDAO function in The DAO's contract sent ETH to an attacker before updating the attacker's internal token balance. The attacker's fallback function repeatedly re-entered splitDAO, draining funds in a loop. This event directly led to the Ethereum hard fork that created Ethereum (ETH) and Ethereum Classic (ETC).
Cross-Function & Cross-Contract Reentrancy
Reentrancy is not limited to a single function. Cross-function reentrancy occurs when an attacker's callback re-enters a different function in the same contract that shares state. Cross-contract reentrancy involves re-entering a separate contract that shares a common state variable (e.g., a pair of pools using the same token contract). These are subtler and harder to detect than single-function reentrancy, as the malicious path doesn't directly loop within one function.
Interaction with ERC-777 and ERC-1155
Modern token standards like ERC-777 and ERC-1155 introduce built-in hooks (tokensToSend, tokensReceived, onERC1155Received). These are callback functions that execute during a token transfer. If a contract's logic is sensitive to token balances and performs actions after a transfer, these hooks can be used to launch a reentrant attack before the contract's state is finalized. Developers must apply the CEI pattern and reentrancy guards even when using these standards.
Code Example: Vulnerable Pattern
A practical demonstration of a smart contract vulnerability where an external call allows an attacker to re-enter the function before its state is finalized.
A reentrant call vulnerability occurs when a smart contract makes an external call to an untrusted contract before it has updated its own internal state. The canonical example is a simple bank contract with a withdraw function. If the contract sends Ether via call.value() to the user before updating their balance to zero, a malicious contract can implement a receive or fallback function that calls withdraw again. This creates a recursive loop, draining the contract's funds in a single transaction.
The core issue is a violation of the Checks-Effects-Interactions (CEI) pattern. Secure code should first check all conditions (e.g., sufficient balance), then effect all state changes (e.g., set balance to zero), and only finally perform interactions with external addresses. The vulnerable pattern inverts this order, performing the interaction (the Ether transfer) before the crucial state effect. This leaves the contract in an inconsistent state where the attacker's balance is still non-zero during the recursive call.
To mitigate this, developers must adhere to the CEI pattern rigorously. For Ether transfers, using transfer or send (which forward a limited 2300 gas) can prevent reentrancy, though this is not a complete solution. The recommended modern practice is to use a reentrancy guard, a boolean modifier that locks a function during execution. Furthermore, for complex logic, consider using pull payment patterns, where users withdraw funds themselves, separating the transaction of value from the core business logic and eliminating the dangerous external call context.
Security Considerations & Attack Vectors
A reentrant call is a type of smart contract vulnerability where an external contract maliciously calls back into the original function before its initial execution is complete, often to drain funds.
Core Mechanism
The attack exploits the order of operations in a function. A typical pattern is:
- 1. Contract A sends funds to Contract B.
- 2. Contract B's fallback/receive function is triggered.
- 3. Before Contract A's state (like balance) is updated, Contract B calls back into Contract A's original function.
- 4. Contract A sees its old, unchanged balance, allowing the same withdrawal to repeat. This creates a recursive loop that drains assets.
The DAO Hack (2016)
The most famous reentrancy attack resulted in the theft of 3.6 million ETH (worth ~$50M at the time) from The DAO, leading to the Ethereum hard fork. The vulnerable splitDAO function sent ETH before updating the user's internal token balance. The attacker's contract recursively called splitDAO, draining funds in a loop before the balance was set to zero.
Checks-Effects-Interactions Pattern
The primary defense is a strict coding pattern:
- Checks: Validate all conditions and inputs (e.g.,
require(balance > 0)). - Effects: Update all internal state variables (e.g.,
balances[msg.sender] = 0). - Interactions: Perform external calls to other contracts or addresses last (e.g.,
msg.sender.call{value: amount}("")). By updating state before interacting externally, you eliminate the reentrancy condition.
ReentrancyGuard Modifier
A common implementation uses a mutex lock. The nonReentrant modifier sets a boolean flag (_locked) when a function executes and reverts if re-entry is attempted.
soliditymodifier nonReentrant() { require(!_locked, "Reentrant call"); _locked = true; _; _locked = false; }
Libraries like OpenZeppelin's ReentrancyGuard provide a standardized, audited version of this protection.
Cross-Function & Cross-Contract Reentrancy
Reentrancy isn't limited to a single function.
- Cross-Function: An attacker re-enters a different function in the same contract that shares state (e.g., a
withdrawfunction and atransferfunction both use the same balance map). - Cross-Contract: An attack involves multiple contracts that share state. Defending requires careful state isolation and applying the checks-effects-interactions pattern across the entire system.
Related Vulnerabilities
Reentrancy often interacts with other flaws:
- Race Conditions: Similar to reentrancy, where outcome depends on sequence of events.
- Unchecked Call Return Values: Using low-level
callwithout handling failure can compound reentrancy damage. - ERC-777 Hooks: The
tokensToSend/tokensReceivedhooks can be used for reentrancy if state isn't secured. Audits must consider these composite risks.
Historical Examples & Real-World Impact
Reentrant calls are most famously associated with critical security vulnerabilities that have led to some of the most significant financial losses in blockchain history. These real-world incidents serve as definitive case studies for developers.
The Checks-Effects-Interactions Pattern
The primary defensive programming pattern developed in direct response to reentrancy attacks like The DAO. It mandates a strict function structure:
- Checks: Validate all conditions (e.g., balances, permissions).
- Effects: Update all internal state variables (e.g., deduct balance).
- Interactions: Perform external calls to other contracts or users. This pattern prevents state corruption by ensuring all state changes are finalized before any external interaction occurs.
ReentrancyGuard & Modern Mitigations
A standard modifier-based lock (nonReentrant) that uses a boolean state variable to prevent recursive function entry. Widely adopted in libraries like OpenZeppelin Contracts. Modern Solidity compilers also emit warnings for patterns that pose reentrancy risks. This represents the evolution from ad-hoc fixes to standardized, audited security primitives in smart contract development.
Impact on Auditing & Formal Verification
Reentrancy attacks fundamentally changed the smart contract security landscape, making static analysis for reentrancy a cornerstone of security audits. Tools like Slither and MythX include dedicated detectors. The severity of historical losses also accelerated research into formal verification methods to mathematically prove the absence of such vulnerabilities in critical contract code.
Prevention and Mitigation Strategies
A reentrant call is a critical vulnerability in smart contracts where an external contract maliciously calls back into the original function before its initial execution is complete, potentially draining funds or corrupting state.
The canonical example is the 2016 DAO hack, where a malicious contract exploited a withdraw function. The attacker's fallback function was designed to be called upon receiving Ether, creating a recursive loop that repeatedly withdrew funds before the contract's internal balance was updated. This attack led to a historic hard fork of the Ethereum network. The core issue is a violation of the checks-effects-interactions pattern, where state changes (effects) should occur before any external calls (interactions) to prevent the contract's state from being inconsistent during a callback.
The primary defense is the reentrancy guard, a simple mutex lock implemented via a boolean state variable (e.g., nonReentrant). Functions protected with this modifier set the lock upon entry and release it only after all logic completes, blocking any nested calls to the same function. The OpenZeppelin ReentrancyGuard library provides a standardized, audited implementation. For broader protection, the pull-over-push pattern is recommended: instead of contracts actively sending funds (pushing), they allow users to withdraw funds themselves (pulling), which moves the reentrancy risk to the user's end.
Beyond guards, adhering to the checks-effects-interactions pattern is fundamental. First, validate all conditions (checks). Second, update all internal state variables (effects). Finally, perform external calls or transfers (interactions). This ensures the contract's state is finalized and immutable before any potentially dangerous external interaction occurs. For Ether transfers, using transfer or send (which forward a limited 2300 gas) can mitigate simple reentrancy, but this is not a complete solution as gas costs can change and it does not protect against calls to other functions.
Advanced mitigation involves formal verification and static analysis tools like Slither or MythX, which can automatically detect potential reentrancy paths in contract code. Furthermore, developers should be wary of cross-function reentrancy, where a call back into a different function of the same contract exploits shared state. Comprehensive testing, including fuzzing and invariant testing with tools like Foundry, is essential to simulate complex attack vectors and ensure all state transitions are secure before and after any external call.
Common Misconceptions
Reentrancy is a critical vulnerability pattern in smart contracts, often misunderstood as a simple 'function calling itself.' This section clarifies its precise mechanism, common pitfalls, and the difference between single-function and cross-function attacks.
A reentrant call is an exploit where a malicious contract's fallback or receive function recursively calls back into the vulnerable function of the original contract before its initial execution and state changes are finalized. The attack works by exploiting the order of operations in a vulnerable function, typically one that performs an external call (e.g., sending Ether via .transfer(), .send(), or .call()) before updating its internal state (like reducing a balance). The malicious contract intercepts the funds transfer, and its fallback function contains code to call the vulnerable function again, which may see the old, unchanged state and allow funds to be withdrawn multiple times.
solidity// VULNERABLE PATTERN function withdraw() public { uint amount = balances[msg.sender]; // External call BEFORE state update (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); // State update happens too late balances[msg.sender] = 0; }
Comparison: Reentrancy Guard vs. CEI Pattern
A comparison of two primary smart contract security patterns used to prevent reentrancy attacks.
| Feature / Characteristic | Reentrancy Guard | Checks-Effects-Interactions (CEI) Pattern |
|---|---|---|
Core Mechanism | State variable mutex lock | Strict function execution order |
Implementation Complexity | Low (library/ modifier) | Medium (requires manual discipline) |
Gas Overhead | ~5k-10k gas per protected call | Minimal (structural only) |
Protection Scope | Entire function | Specific external calls |
Common Use Case | Functions with multiple external calls | All state-changing functions |
Audit Friendliness | Explicit and easily verified | Requires careful line-by-line review |
Risk if Misapplied | Deadlock (if misconfigured) | Remaining vulnerability |
Frequently Asked Questions (FAQ)
Reentrancy is a critical security vulnerability in smart contracts. These questions address its mechanics, famous exploits, and best practices for prevention.
A reentrant call occurs when an external contract is called during the execution of a function, and that external call is able to recursively call back into the original function before its initial execution is complete. This can lead to state inconsistencies and is the root cause of the reentrancy attack, where an attacker's contract repeatedly withdraws funds because the victim contract's balance is not updated until after the external call. The classic pattern involves a function that performs a call (e.g., sending Ether via .transfer(), .send(), or .call()) before updating its internal state variables to reflect that the funds have been disbursed.
Get In Touch
today.
Our experts will offer a free quote and a 30min call to discuss your project.