In the Ethereum Virtual Machine (EVM), call reentrancy occurs when a smart contract makes an external call to another contract, and that contract's fallback or receive function maliciously calls back into the original function. Because the original contract's state (like user balances) hasn't been been updated before the external call, the reentering function can pass the same checks again, executing its logic multiple times. This is the core mechanism behind infamous exploits like the 2016 DAO hack, which resulted in the loss of 3.6 million ETH.
How to Plan for EVM Call Reentrancy
Introduction to EVM Call Reentrancy
Call reentrancy is a critical vulnerability where an external contract call allows an attacker to re-enter the calling function before its state is finalized, leading to drained funds and corrupted logic.
The vulnerability stems from violating the Checks-Effects-Interactions (CEI) pattern. The safe pattern dictates: first perform all Checks (e.g., balance sufficient), then apply all Effects (update internal state), and finally make external Interactions. Reentrancy happens when this order is reversed, placing the external interaction before the state update. A simple example is a withdrawal function that sends Ether via address.call{value: amount}("") before reducing the user's internal balance.
Consider this vulnerable withdraw function:
solidityfunction withdraw() public { uint amount = balances[msg.sender]; // Check (bool success, ) = msg.sender.call{value: amount}(""); // Interaction FIRST require(success, "Transfer failed"); balances[msg.sender] = 0; // Effect LAST - Too late! }
An attacker's contract with a malicious receive() function would receive the Ether, then call withdraw() again. Since the balance wasn't set to zero yet, the check passes, and the contract sends Ether again, looping until gas runs out or funds are exhausted.
To prevent reentrancy, strictly follow the CEI pattern. The corrected function updates state before the interaction:
solidityfunction withdraw() public { uint amount = balances[msg.sender]; balances[msg.sender] = 0; // Effect FIRST (bool success, ) = msg.sender.call{value: amount}(""); // Interaction LAST require(success, "Transfer failed"); }
Additionally, use Solidity's built-in reentrancy guard via the nonReentrant modifier from OpenZeppelin's ReentrancyGuard contract. This uses a mutex lock (_status) to prevent recursive calls into the marked function, providing a robust secondary defense layer.
Beyond simple Ether transfers, reentrancy can affect any state variable updated after a call, including ERC-20 token balances, NFT ownership, or voting tallies. Cross-function reentrancy is a subtler variant where an attacker re-enters a different function that shares state with the first. Auditing tools like Slither and MythX can detect common patterns, but manual review for CEI violations remains essential. Always assume any external call can be malicious and plan your state transitions accordingly.
How to Plan for EVM Call Reentrancy
Understanding the fundamental concepts and tools required to analyze and mitigate reentrancy vulnerabilities in Ethereum smart contracts.
Reentrancy is a critical vulnerability where an external contract call allows an attacker to re-enter the calling function before its initial execution completes. This can lead to drained funds, as famously demonstrated by the 2016 DAO hack. To plan for it, you must first understand the Ethereum Virtual Machine's (EVM) single-threaded execution model and how the CALL opcode transfers control flow. A solid grasp of Solidity's function visibility (public, external), state variable access, and the gas stipend for external calls is essential. Familiarity with common patterns like Checks-Effects-Interactions is a foundational prerequisite.
You will need practical experience with development and testing tools. Setting up a local environment with Hardhat or Foundry is crucial for writing, deploying, and testing contracts. Foundry is particularly powerful for reentrancy analysis due to its built-in fuzzing and invariant testing capabilities via forge. You should be comfortable writing and running tests that simulate malicious contracts. Understanding how to use a mainnet fork to test interactions with real-world protocols is also a valuable skill for advanced planning.
A deep understanding of different reentrancy types is required. The classic single-function reentrancy involves calling back into the same function. Cross-function reentrancy exploits a different function that shares state. The most subtle is cross-contract reentrancy, where the state shared is via a separate contract, like an ERC-20 token. Planning must account for all vectors. Reviewing historical exploits from platforms like Rekt.news or Immunefi provides concrete examples of how these vulnerabilities manifest in production code.
Effective planning involves proactive defense strategy. You must know the standard mitigation techniques: using reentrancy guards (like OpenZeppelin's ReentrancyGuard), adhering strictly to the Checks-Effects-Interactions pattern, and employing pull-over-push payment designs. However, planning also means anticipating guard bypasses, such as those possible with delegatecall or through logic in token transfer hooks (ERC-777, ERC-721). Your plan should include steps for manual code review, specifying invariants for formal verification, and scheduling regular audits.
How to Plan for EVM Call Reentrancy
Reentrancy attacks are among the most critical vulnerabilities in smart contract development. This guide explains the core concepts and provides a structured approach to planning secure contracts.
EVM call reentrancy occurs when an external contract call allows the called contract to re-enter the calling function before the initial execution is complete. This is possible because the EVM does not automatically lock a contract's state during external calls. The infamous DAO hack in 2016, which resulted in a loss of 3.6 million ETH, exploited this vulnerability. The attack pattern typically follows: Contract A calls Contract B; Contract B maliciously calls back into Contract A while A's state (like a user balance) is still inconsistent, allowing B to drain funds.
To plan for reentrancy, you must first identify all external calls in your functions. These include .call(), .send(), .transfer() (pre-EIP-1884), and calls to other contract functions via interfaces. Any state change that happens after such a call is at risk. The primary defense is the Checks-Effects-Interactions (CEI) pattern. This pattern mandates: 1) Perform all condition checks first, 2) Update all internal state variables, and 3) Only then, interact with external contracts or send Ether. This ensures state is finalized before any re-entrant call can occur.
Beyond CEI, use specific technical guards. The ReentrancyGuard modifier, like OpenZeppelin's implementation, uses a boolean lock (nonReentrant) that prevents a function from being called recursively. For more complex interactions, consider pull payment over push payment architectures. Instead of sending funds to users (push), allow users to withdraw funds themselves (pull), which moves the external call to a function where state is already settled. Always assume that the address you are calling could be a malicious contract.
Testing your reentrancy defenses is crucial. Use tools like Slither for static analysis to detect vulnerable patterns and Foundry or Hardhat for dynamic testing. Write invariant tests that simulate a malicious contract repeatedly calling back into your function. For critical protocols, formal verification with tools like Certora can mathematically prove the absence of reentrancy under specified conditions. Remember, planning for reentrancy is not a one-time check but a fundamental part of the smart contract development lifecycle.
Types of Reentrancy Attacks
A comparison of the primary reentrancy attack patterns, their mechanisms, and their relative complexity.
| Attack Type | Mechanism | Complexity | Common Target |
|---|---|---|---|
Single-Function Reentrancy | Reenters the same vulnerable function before its state updates complete. | Low | Simple withdraw/transfer functions |
Cross-Function Reentrancy | Reenters a different function that shares state with the vulnerable function. | Medium | Multi-function contracts (e.g., lending pools) |
Cross-Contract Reentrancy | Exploits state dependencies between two or more separate contracts. | High | Protocols with external dependencies |
Read-Only Reentrancy | Uses a view/pure function call to read inconsistent state during another operation. | Medium-High | Oracles, price feeds, on-chain calculations |
Deferred Reentrancy | Triggers a malicious callback after the initial call completes, often via events or hooks. | High | Contracts with complex post-execution logic |
Step 1: Identify Vulnerable Patterns
The first step in securing a smart contract is recognizing the specific code patterns that enable reentrancy attacks.
Reentrancy occurs when an external contract call allows the caller to re-enter the calling function before its initial execution and state changes are complete. The classic vulnerability pattern follows a checks-effects-interactions violation. A function performs an external call (interaction) to an untrusted address before it updates its own internal state (effects). This gives the called contract a window to call back into the original function, which may see outdated state, leading to logic errors like double-spending of funds.
Look for functions that manage valuable assets (ETH or ERC-20 tokens) and contain a .call(), .send(), .transfer(), or an external call to another contract's function. The most dangerous pattern is a single-function reentrancy, where the same function is called recursively. For example, a simple withdrawal function is vulnerable if it sends Ether before zeroing the user's balance.
solidity// VULNERABLE PATTERN function withdraw() public { uint amount = balances[msg.sender]; (bool success, ) = msg.sender.call{value: amount}(""); // INTERACTION FIRST require(success, "Transfer failed"); balances[msg.sender] = 0; // EFFECTS LATER }
More complex variants include cross-function reentrancy, where the reentrant call targets a different function that shares state with the vulnerable one, and delegatecall reentrancy in proxy patterns. Tools like Slither or Mythril can automatically flag these patterns, but manual review is essential. Key indicators are any state variable read after an external call that could be manipulated by a reentrant callback. Planning your defense starts by meticulously mapping all possible reentrancy paths in your contract's control flow.
Step 2: Implement Prevention Strategies
After identifying reentrancy risks, developers must apply established security patterns to prevent exploits. This guide covers the most effective strategies for protecting your smart contracts.
The Checks-Effects-Interactions (CEI) pattern is the fundamental defense against reentrancy. It mandates a strict order of operations: first perform all checks (e.g., validating balances), then update all internal effects (modifying contract state), and only then make external interactions (calls to other contracts). By updating state before making a call, you ensure the attacker's reentrant call fails the initial checks, as the state already reflects the first withdrawal. For example, deducting a user's balance from storage before sending them ETH prevents them from draining the entire contract in a single transaction.
For more robust protection, use a reentrancy guard. This is a boolean lock that prevents any function from being called recursively. OpenZeppelin provides a widely-audited ReentrancyGuard contract. Import it and apply the nonReentrant modifier to vulnerable functions. The modifier sets a lock when the function is entered and only releases it upon completion, blocking any nested calls to the same function. This is especially crucial for functions that make multiple external calls or interact with untrusted contracts, as it provides a blanket protection that is easier to audit than manually verifying CEI compliance in complex logic.
When using transfer methods, prioritize address.send() or address.transfer() for simple ETH transfers, as they forward a limited 2300 gas stipend. This is often insufficient for the recipient to make another external call, effectively acting as a gas-based reentrancy guard. However, for interactions with complex contracts (like making a call to an ERC-20 transfer function), you must use call{value: amount}(""). In these cases, the CEI pattern or a reentrancy guard is non-negotiable, as the called contract controls its execution and receives ample gas.
Finally, pull-over-push architecture is a strategic design choice for payments. Instead of a contract actively "pushing" funds to users (which creates a reentrancy point), have users "pull" their owed funds from the contract via a separate function. Store each user's withdrawable balance in a mapping (e.g., mapping(address => uint256) public withdrawals;). A withdraw() function then transfers the mapped amount and resets it to zero before the transfer, adhering to CEI. This confines the reentrancy risk to a single, well-guarded function and limits the damage of any potential exploit to that user's allocated share.
Step 3: Test for Reentrancy
This step covers the practical methods for testing your smart contracts for reentrancy vulnerabilities, moving from theory to verification.
The most effective way to test for reentrancy is to write dedicated unit and integration tests that simulate an attack. Using a framework like Foundry or Hardhat, you can create a malicious contract that attempts to re-enter your function. A basic test involves deploying your vulnerable contract, funding it, then having the attacker contract call the vulnerable function, which triggers a callback to re-enter before the state update completes. Your test should assert that the attacker cannot drain funds beyond their entitlement. This approach verifies both the presence of a bug and the effectiveness of your fix.
For more complex analysis, use static analysis tools and formal verification. Tools like Slither, MythX, or Securify can automatically scan Solidity code for common patterns, including missing checks-effects-interactions and use of call.value(). For critical protocols, consider formal verification with tools like Certora Prover or KEVM, which mathematically prove that certain invariant properties (e.g., "total contract balance equals sum of user balances") hold even under reentrant calls. These tools are essential for high-value DeFi contracts where manual review is insufficient.
Finally, conduct manual code review with a focus on external calls. Scrutinize every use of address.call(), address.send(), address.transfer(), address.functionCall(), and interactions with known external contracts (e.g., ERC-777 tokens or other protocol integrations). Ask: does any state change happen after this call? Is the function protected by a reentrancy guard? Could the called address be a contract you don't control? Documenting these external interactions in a security checklist ensures no call site is overlooked during audits.
Reentrancy Mitigation Techniques
A comparison of common patterns for preventing reentrancy attacks in Solidity smart contracts.
| Technique | Checks-Effects-Interactions (CEI) | ReentrancyGuard | Pull Payment Pattern |
|---|---|---|---|
Core Mechanism | Code ordering principle | State variable mutex lock | Separate withdrawal step |
Gas Overhead | None | ~5k gas per function | Variable (user pays withdrawal) |
Complexity | Low (requires discipline) | Low (library import) | Medium (requires two functions) |
Protection Scope | Single function | Entire contract or function | Specific payment flows |
Prevents Cross-Function Reentrancy | |||
Audit Friendliness | High (logic is explicit) | High (standardized pattern) | Medium (requires flow analysis) |
Common Use Case | Simple state updates | Multi-function contracts (e.g., ERC721) | Escrow, trusted payouts |
Vulnerability if Misapplied | High (ordering errors) | Low (if lock is bypassed) | Medium (if withdrawal logic is flawed) |
Essential Tools and Libraries
A curated selection of tools, libraries, and best practices to help developers identify, prevent, and test for reentrancy vulnerabilities in EVM smart contracts.
CEI Pattern & Pull Payments
The fundamental code pattern to prevent reentrancy: Checks-Effects-Interactions. For payments, use the Pull-over-Push pattern.
- CEI Order: 1. Check conditions, 2. Update state, 3. Interact externally.
- Pull Payments: Instead of pushing ETH to users (
address.send()), let them withdraw it, separating the state change from the transfer. - Vulnerability: Violating CEI (doing the external call before state changes) is the root cause of most reentrancy attacks.
Frequently Asked Questions
Common questions and solutions for developers dealing with the complexities of reentrancy attacks in EVM smart contracts.
A reentrancy attack is a critical vulnerability where an external contract maliciously calls back into a vulnerable function before its initial execution is complete. This exploits the EVM's single-threaded nature and the attacker's control flow.
The classic attack pattern involves:
- A victim contract with a
withdraw()function that sends Ether before updating the user's internal balance. - An attacker contract with a
fallback()orreceive()function that callswithdraw()again when it receives Ether. - The victim's state (the attacker's balance) remains unchanged during the recursive calls, allowing multiple withdrawals of the same funds.
This flaw famously led to the 2016 DAO hack, resulting in the loss of 3.6 million ETH.
Further Resources
These resources focus on concrete techniques, patterns, and tools you can use to prevent EVM call reentrancy in production smart contracts.
Conclusion and Next Steps
This guide has covered the mechanics and mitigation of reentrancy attacks. Here's how to solidify your understanding and protect your applications.
Reentrancy is a foundational security concept for EVM developers. The core vulnerability stems from the order of operations: performing external calls before updating internal state. While the Checks-Effects-Interactions (CEI) pattern is the primary defense, modern Solidity provides built-in tools like ReentrancyGuard from OpenZeppelin. For critical functions, consider using the nonReentrant modifier, which uses a mutex lock to prevent recursive calls. Remember, reentrancy isn't limited to call(); it can also occur via transfer() or send() if the recipient is a malicious contract with a fallback function.
To move from theory to practice, analyze historical exploits. Study the 2016 DAO hack, the 2022 Fei Protocol incident, or the 2023 Euler Finance attack. Decompile the attacker contracts to see how they recursively called back into the vulnerable function. Tools like Etherscan's "Contract Source Code" tab and Tenderly's debugger are invaluable for this. Writing your own simple vulnerable contract and a proof-of-concept attack script in a test environment (like Foundry or Hardhat) is the best way to internalize the attack vector and the effectiveness of mitigations.
Your security checklist should extend beyond basic CEI. For complex protocols, consider cross-function reentrancy, where a call from Function A re-enters through Function B. Use ReentrancyGuard on all state-changing external functions, not just the obvious ones. Integrate static analysis tools like Slither or MythX into your CI/CD pipeline to automatically detect patterns. For maximum assurance, engage in formal verification for core contract logic or commission audits from reputable firms. The Consensys Diligence and OpenZeppelin blogs are excellent resources for ongoing education.
The next step is to explore related vulnerability classes. Reentrancy often interacts with other flaws. Study race conditions in ERC-777/ERC-677 token callbacks, read-only reentrancy affecting oracle price feeds, and the implications of delegatecall in proxy patterns. Understanding these advanced topics is crucial for developers working on lending protocols, decentralized exchanges, or any system with complex financial logic. Security is iterative; treat every audit finding and post-mortem as a lesson to harden your development process.