
Reentrancy attacks remain one of the most devastating vulnerabilities in smart contract security. The 2016 DAO hack drained $60 million through a reentrancy exploit, and similar attacks continue to cost protocols millions today. Understanding how to prevent reentrancy attacks is essential for any developer building on Ethereum or other EVM-compatible chains.
A reentrancy attack occurs when a malicious contract calls back into your contract before the first function call completes. This lets attackers manipulate your contract's state and drain funds by repeatedly withdrawing before balances are updated.
Think of it like this: imagine a bank teller processing your withdrawal. If you could freeze time and ask for another withdrawal before the teller updates your account balance, you could drain the entire vault. That's essentially what reentrancy attacks do to smart contracts.
Here's a vulnerable contract that's susceptible to reentrancy:
contract VulnerableBank {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
// Vulnerable: external call before state update
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}
}
The problem? The contract sends funds before updating the balance. An attacker can exploit this by creating a malicious contract with a fallback function that calls withdraw() again:
contract Attacker {
VulnerableBank public bank;
function attack() public payable {
bank.deposit{value: msg.value}();
bank.withdraw(msg.value);
}
receive() external payable {
if (address(bank).balance >= msg.value) {
bank.withdraw(msg.value);
}
}
}
Each time the attacker receives funds, their contract immediately calls withdraw() again. Since the balance hasn't been updated yet, the check passes repeatedly until the contract is drained.
The most fundamental defense is organizing your code properly. Always follow this order:
Here's the secure version:
function withdraw(uint256 amount) public {
// 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");
}
By updating the balance before sending funds, even if the attacker tries to reenter, their balance is already zero.
OpenZeppelin provides a battle-tested ReentrancyGuard modifier that prevents reentrancy with a simple lock mechanism:
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureBank is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
The nonReentrant modifier sets a lock before execution and releases it afterward. Any attempt to reenter during execution will fail.
Instead of sending funds directly, let users withdraw them:
contract SecurePayments {
mapping(address => uint256) public pendingWithdrawals;
function allowWithdrawal(address user, uint256 amount) internal {
pendingWithdrawals[user] += amount;
}
function withdraw() public nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No funds available");
pendingWithdrawals[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
This pattern gives you more control over when funds are transferred and naturally implements checks-effects-interactions.
The transfer() and send() functions limit gas forwarded to 2300, preventing most reentrancy attacks:
payable(msg.sender).transfer(amount);
However, this approach has limitations. With EIP-1884 and changing gas costs, relying on gas limits isn't recommended for new contracts. The checks-effects-interactions pattern or ReentrancyGuard are more robust.
Manual code review isn't enough. Modern security requires automated tools that catch vulnerabilities before deployment.
Olympix's static analysis detects reentrancy vulnerabilities with 75% accuracy—significantly higher than traditional tools like Slither (15%). Our platform analyzes your code for:
Combined with automated unit testing and mutation testing, Olympix helps you identify and fix reentrancy issues during development, not after deployment.
The classic attack pattern where a function calls itself recursively through an external contract.
More subtle attacks where one function's external call allows reentering through a different function:
contract CrossFunction {
mapping(address => uint256) public balances;
function withdraw() public {
uint256 balance = balances[msg.sender];
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
balances[msg.sender] = 0;
}
function transfer(address to, uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
An attacker could call withdraw(), then reenter through transfer() before withdraw() completes.
Attacks spanning multiple contracts where state inconsistencies exist between related contracts during execution.
Recent high-profile attacks demonstrate why reentrancy prevention matters:
What's particularly concerning? Many of these contracts were audited. Traditional audits caught obvious single-function reentrancy but missed more complex attack vectors.
Research shows 90% of exploited smart contracts were previously audited. Why?
This is why Olympix works with protocols like Circle, Uniswap Foundation, and Cork Protocol to integrate continuous security throughout the development lifecycle—not just at the end.
Olympix provides proactive security that prevents reentrancy vulnerabilities before deployment:
Static Analysis: Our engine analyzes your code for reentrancy patterns with industry-leading accuracy, flagging violations of checks-effects-interactions and missing guards.
Automated Unit Testing: Generate comprehensive test cases that specifically target reentrancy vulnerabilities across single-function, cross-function, and cross-contract scenarios.
Mutation Testing: Verify your reentrancy defenses actually work by introducing mutations and ensuring tests catch them.
Continuous Integration: Integrate security checks directly into your development workflow, catching new vulnerabilities as code changes.
Unlike traditional audits that provide one-time snapshots, Olympix works alongside your development team to maintain continuous security. Our partnerships with industry leaders like Circle and integration with the Uniswap Foundation Security Fund demonstrate our commitment to protecting the Web3 ecosystem.
Reentrancy attacks are preventable. With proper coding patterns, automated testing, and continuous security analysis, you can eliminate this vulnerability class from your smart contracts.
Don't wait until after deployment to discover reentrancy vulnerabilities. Learn how Olympix can integrate proactive security into your development workflow.
Ready to prevent the next major exploit? Contact Olympix to see how our security tools can protect your protocol from reentrancy attacks and other critical vulnerabilities.
Olympix provides proactive smart contract security for Web3 protocols. Our static analysis, automated testing, and continuous security tools help development teams prevent exploits before deployment. Trusted by Circle, Uniswap Foundation, and leading DeFi protocols.