November 19, 2025
|
Developer-First Security

How to Prevent Reentrancy Attacks: A Complete Guide for Smart Contract Developers

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.

What Is a Reentrancy Attack?

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.

How Reentrancy Attacks Work

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.

5 Proven Methods to Prevent Reentrancy Attacks

1. Follow the Checks-Effects-Interactions Pattern

The most fundamental defense is organizing your code properly. Always follow this order:

  1. Checks: Validate conditions and requirements
  2. Effects: Update your contract's state
  3. Interactions: Call external contracts

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.

2. Use ReentrancyGuard from OpenZeppelin

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.

3. Implement Pull Over Push Payments

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.

4. Use Transfer or Send Instead of Call (With Caveats)

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.

5. Leverage Static Analysis and Automated Testing

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:

  • Violation of checks-effects-interactions patterns
  • Missing reentrancy guards on critical functions
  • Cross-function reentrancy vulnerabilities
  • Cross-contract reentrancy risks

Combined with automated unit testing and mutation testing, Olympix helps you identify and fix reentrancy issues during development, not after deployment.

Types of Reentrancy Attacks to Watch For

Single-Function Reentrancy

The classic attack pattern where a function calls itself recursively through an external contract.

Cross-Function Reentrancy

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.

Cross-Contract Reentrancy

Attacks spanning multiple contracts where state inconsistencies exist between related contracts during execution.

Real-World Reentrancy Exploits

Recent high-profile attacks demonstrate why reentrancy prevention matters:

  • The DAO (2016): $60 million stolen through recursive withdrawals
  • Cream Finance (2021): $130 million lost to reentrancy combined with flash loans
  • Various DeFi protocols: Hundreds of millions in losses from cross-function and cross-contract reentrancy

What's particularly concerning? Many of these contracts were audited. Traditional audits caught obvious single-function reentrancy but missed more complex attack vectors.

Why Traditional Audits Miss Reentrancy Vulnerabilities

Research shows 90% of exploited smart contracts were previously audited. Why?

  • Audits provide point-in-time snapshots, not continuous security
  • Complex cross-function and cross-contract reentrancy requires deep analysis
  • Manual review can't catch every edge case in large codebases
  • Code changes after audits often reintroduce vulnerabilities

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.

Best Practices for Reentrancy Prevention

During Development

  1. Always use checks-effects-interactions pattern
  2. Apply ReentrancyGuard to all functions with external calls
  3. Minimize external calls and bundle them at the end of functions
  4. Document all external calls and their reentrancy implications
  5. Use static analysis tools during coding

During Testing

  1. Write specific reentrancy attack tests
  2. Test cross-function reentrancy scenarios
  3. Use mutation testing to verify defenses work
  4. Fuzz test with various attack patterns
  5. Test with realistic gas limits

Before Deployment

  1. Run comprehensive static analysis
  2. Verify all external calls follow best practices
  3. Get security audits from reputable firms
  4. Consider bug bounties for additional review
  5. Monitor post-deployment for unusual patterns

How Olympix Prevents Reentrancy Attacks

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.

Take Action: Secure Your Smart Contracts Today

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.

What’s a Rich Text element?

The rich text element allows you to create and format headings, paragraphs, blockquotes, images, and video all in one place instead of having to add and format them individually. Just double-click and easily create content.

A rich text element can be used with static or dynamic content. For static content, just drop it into any page and begin editing. For dynamic content, add a rich text field to any collection and then connect a rich text element to that field in the settings panel. Voila!

Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.

  1. Follow-up: Conduct a follow-up review to ensure that the remediation steps were effective and that the smart contract is now secure.
  2. Follow-up: Conduct a follow-up review to ensure that the remediation steps were effective and that the smart contract is now secure.

In Brief

  • Remitano suffered a $2.7M loss due to a private key compromise.
  • GAMBL’s recommendation system was exploited.
  • DAppSocial lost $530K due to a logic vulnerability.
  • Rocketswap’s private keys were inadvertently deployed on the server.

Hacks

Hacks Analysis

Huobi  |  Amount Lost: $8M

On September 24th, the Huobi Global exploit on the Ethereum Mainnet resulted in a $8 million loss due to the compromise of private keys. The attacker executed the attack in a single transaction by sending 4,999 ETH to a malicious contract. The attacker then created a second malicious contract and transferred 1,001 ETH to this new contract. Huobi has since confirmed that they have identified the attacker and has extended an offer of a 5% white hat bounty reward if the funds are returned to the exchange.

Exploit Contract: 0x2abc22eb9a09ebbe7b41737ccde147f586efeb6a

More from Olympix:

No items found.

Ready to Shift Security Assurance In-House? Talk to Our Security Experts Today.