Reentrancy Attacks Explained: How They Work and How to Prevent Them
Reentrancy is one of the most well-known and dangerous vulnerabilities in smart contract development. Despite being widely documented, it continues to appear in production contracts and remains a common finding during security audits.
In this guide, you'll learn what a reentrancy attack is, why it happens, how attackers exploit it, and the best practices for preventing it in Solidity.
What Is a Reentrancy Attack?
A reentrancy attack occurs when a smart contract sends Ether or calls another contract before updating its own internal state.
If the receiving contract is malicious, it can call back into the vulnerable function before the first execution finishes.
Because the contract's state hasn't been updated yet, the attacker can repeat the same action multiple times and withdraw more funds than intended.
In simple terms:
- The victim contract sends funds.
- The attacker receives the funds.
- Before the victim updates its balance, the attacker calls the withdrawal function again.
- The cycle repeats until the contract is drained.
Why Does It Happen?
The Ethereum Virtual Machine (EVM) allows contracts to call other contracts.
Whenever you make an external call, you temporarily give control to another contract.
If your own contract hasn't updated its internal state before that call, the external contract can exploit the inconsistent state.
This is why developers should always assume that external contracts are untrusted.
A Vulnerable Example
The following contract allows users to deposit and withdraw Ether.
mapping(address => uint256) public balances;
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0;
}
At first glance, this function seems correct.
However, notice that the contract transfers Ether before setting the user's balance to zero.
A malicious contract can repeatedly call withdraw() before the balance is updated.
The Secure Version
The safest approach is to update the contract's state before making any external calls.
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
Now, even if the receiving contract attempts to call withdraw() again, the stored balance is already zero.
The attack immediately fails.
The Checks-Effects-Interactions Pattern
One of the oldest and most effective Solidity security principles is the Checks-Effects-Interactions pattern.
Every function should execute operations in this order:
1. Checks
Validate all conditions.
Examples:
- Verify permissions
- Validate inputs
- Ensure balances are sufficient
2. Effects
Update your own contract's state.
Examples:
- Reduce balances
- Update storage
- Record events
3. Interactions
Only after the state is safe should the contract interact with external contracts.
Examples:
- Transfer Ether
- Call ERC20 contracts
- Invoke another protocol
Following this pattern prevents many reentrancy vulnerabilities.
Using OpenZeppelin's ReentrancyGuard
Another common defense is OpenZeppelin's ReentrancyGuard.
It prevents a protected function from being entered more than once during the same transaction.
Example:
contract Vault is ReentrancyGuard {
function withdraw() external nonReentrant {
// secure logic
}
}
Although ReentrancyGuard is extremely useful, it should not replace good contract design.
Developers should still follow secure coding practices.
Cross-Function Reentrancy
Not every reentrancy attack targets the same function.
Sometimes an attacker enters one function and re-enters through another function that modifies the same storage variables.
For this reason, auditors review how state variables are shared across the entire contract—not just individual functions.
Cross-Contract Reentrancy
Modern DeFi protocols frequently interact with multiple contracts.
Examples include:
- Lending protocols
- DEX routers
- Staking contracts
- Vaults
A reentrancy attack may involve several contracts instead of a single vulnerable function.
Security reviews should consider the complete protocol architecture.
How Auditors Detect Reentrancy
Professional auditors combine several techniques:
- Manual code review
- Static analysis tools
- Fuzz testing
- Invariant testing
- Attack simulations
Rather than looking only for obvious mistakes, auditors evaluate every external interaction and determine whether state changes occur in the correct order.
Best Practices
When writing Solidity contracts, follow these recommendations:
- Update storage before external calls.
- Follow the Checks-Effects-Interactions pattern.
- Use
ReentrancyGuardwhen appropriate. - Minimize unnecessary external interactions.
- Avoid complex callback-based designs.
- Write comprehensive unit tests.
- Include fuzz and invariant tests.
- Have contracts independently audited before deployment.
Final Thoughts
Reentrancy attacks have shaped the history of Ethereum security and continue to influence modern smart contract development.
Fortunately, they are also among the easiest vulnerabilities to prevent when secure development practices are followed.
By understanding how reentrancy works and consistently applying proven design patterns, developers can significantly reduce the attack surface of their smart contracts.
Security begins with thoughtful architecture—not just defensive libraries.