Back to Blog
Developer GuidesApril 22, 2026by Theo Nova

Understanding Reentrancy Attacks and How to Never Fall Victim

Understanding Reentrancy Attacks and How to Never Fall Victim

Understanding Reentrancy Attacks and How to Never Fall Victim

Reentrancy is one of the oldest and most destructive vulnerability classes in smart contract development. From the 2016 DAO hack to exploits happening today, the pattern keeps recurring , because developers keep underestimating it. Here's everything you need to know to make reentrancy impossible in your contracts.

The DAO Hack and Why Reentrancy Still Matters

In June 2016, an attacker drained 3.6 million ETH from The DAO , roughly $60 million at the time , using a reentrancy exploit. The attack was so significant it split Ethereum into two chains. It was also, in retrospect, entirely preventable.

Nearly a decade later, reentrancy vulnerabilities are still being exploited. Not because the attack is new or sophisticated, but because the conditions that enable it , external calls made before state updates , keep appearing in production code written by developers who either don't recognize the pattern or assume their specific case is safe.

It isn't. This guide breaks down every variant of reentrancy in practical terms, shows you how each one works at the code level, and gives you the defensive patterns to eliminate the risk entirely.

How Reentrancy Works: The Call Stack Explained

Reentrancy occurs when a contract makes an external call to another contract , and that external contract calls back into the original contract before the first execution has finished.

The key insight is that Solidity's execution model is synchronous and re-entrant by default. When your contract calls an external address, you hand control to that address. If that address is a malicious contract, it can immediately call back into your contract. At that point, your contract's state hasn't been updated yet , so it still thinks the attacker is entitled to whatever they're trying to take.

The call stack looks like this:

Attacker calls victim.withdraw()

Victim checks balance , valid

Victim sends ETH to attacker via .call{value: ...}()

Attacker's receive() function triggers , calls victim.withdraw() again

Victim checks balance again , still valid (not yet updated)

Victim sends ETH again

Steps 4–6 repeat until the victim is drained

Balances finally updated , but it's too late

Classic Single-Function Reentrancy

The simplest and most well-known form. A withdrawal function sends ETH before updating the caller's balance:

// VULNERABLE

contract VulnerableVault {

mapping(address => uint256) public balances;

function deposit() external payable {

balances[msg.sender] += msg.value;

}

function withdraw() external {

uint256 amount = balances[msg.sender];

require(amount > 0, "Nothing to withdraw");

// External call BEFORE state update , reentrancy window is open

(bool success, ) = msg.sender.call{value: amount}("");

require(success, "Transfer failed");

balances[msg.sender] = 0; // Too late

}

}

// ATTACKER

contract Attacker {

VulnerableVault public vault;

uint256 public count;

constructor(address _vault) { vault = VulnerableVault(_vault); }

function attack() external payable {

vault.deposit{value: msg.value}();

vault.withdraw();

}

receive() external payable {

if (count < 5) {

count++;

vault.withdraw(); // Re-enter before balance is zeroed

}

}

}

Cross-Function Reentrancy: Harder to Spot, Just as Dangerous

Cross-function reentrancy is subtler. The attacker re-enters a different function than the one that made the external call , one that reads shared state that hasn't been updated yet.

// VULNERABLE , cross-function reentrancy

contract LendingPool {

mapping(address => uint256) public deposits;

mapping(address => uint256) public debt;

function withdraw(uint256 amount) external {

require(deposits[msg.sender] >= amount);

// External call , attacker re-enters transfer() before deposits updated

(bool success, ) = msg.sender.call{value: amount}("");

require(success);

deposits[msg.sender] -= amount; // Still not updated during reentrance

}

function transfer(address to, uint256 amount) external {

// Reads deposits[msg.sender] , still inflated during reentrancy

require(deposits[msg.sender] >= amount);

deposits[msg.sender] -= amount;

deposits[to] += amount;

}

}

The attacker calls withdraw(), and during the ETH transfer, re-enters transfer() , moving their full (not yet decremented) balance to another address before the withdrawal deducts it.

Read-Only Reentrancy: The Newer, Sneakier Variant

Read-only reentrancy doesn't steal funds directly from the vulnerable contract , it exploits the fact that other protocols reading your contract's state during a mid-execution callback may get stale, manipulated data.

This has been the root cause of several DeFi exploits in 2024–2025, particularly in protocols that use another protocol's token price or balance as an oracle input.

The pattern: attacker triggers a callback mid-execution, and during that callback, calls a view function on the original contract that returns temporarily incorrect state. A lending protocol consuming that view function as a price oracle then makes decisions based on bad data.

Mitigation: ensure your view functions cannot be called during an execution where state is mid-update, or add reentrancy guards to functions that update state used by sensitive view functions.

Defensive Pattern 1: Checks-Effects-Interactions

The simplest and most effective defense. Always structure your functions in this order:

Checks , validate all conditions (require, revert)

Effects , update all state variables

Interactions , make external calls last

// SAFE , Checks-Effects-Interactions

function withdraw() external {

// 1. Checks

uint256 amount = balances[msg.sender];

require(amount > 0, "Nothing to withdraw");

// 2. Effects , state updated BEFORE external call

balances[msg.sender] = 0;

// 3. Interactions , external call last

(bool success, ) = msg.sender.call{value: amount}("");

require(success, "Transfer failed");

}

By zeroing the balance before the external call, any re-entry attempt finds a balance of zero and reverts immediately.

Defensive Pattern 2: ReentrancyGuard

For complex functions where CEI is difficult to enforce , or where cross-function reentrancy is a concern , use OpenZeppelin's ReentrancyGuard:

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract SafeVault is ReentrancyGuard {

mapping(address => uint256) public balances;

function withdraw() external nonReentrant {

uint256 amount = balances[msg.sender];

require(amount > 0, "Nothing to withdraw");

balances[msg.sender] = 0;

(bool success, ) = msg.sender.call{value: amount}("");

require(success, "Transfer failed");

}

}

nonReentrant sets a lock at the start of the function and releases it at the end. Any attempt to re-enter any nonReentrant function while the lock is held will revert.

Apply nonReentrant to all functions that make external calls or transfer ETH/tokens. For cross-function reentrancy protection, apply it to all functions that share sensitive state.

Real-World Case Studies From 2024–2025

Curve Finance (2023, $62M): A reentrancy vulnerability in Vyper's compiler , not Solidity , affected several Curve pools. The root cause was a broken reentrancy lock in older Vyper versions, demonstrating that reentrancy risk exists at the compiler level, not just the code level.

Read-only reentrancy exploits (ongoing): Multiple lending protocols in 2024 were exploited via read-only reentrancy in Curve and Balancer integrations. Protocols reading LP token prices mid-callback received manipulated values and issued undercollateralized loans.

The lesson from both: reentrancy isn't just your code's problem. It's any external dependency your code trusts.

Automated Detection Tools

Don't rely solely on manual review. Run these tools on every contract:

# Slither , static analysis, flags reentrancy automatically

slither . -detect reentrancy-eth,reentrancy-no-eth,reentrancy-benign

# Aderyn , Cyfrin's Rust-based analyzer

aderyn .

# Mythril , symbolic execution

myth analyze src/MyContract.sol

These tools catch classic reentrancy reliably. Cross-function and read-only variants require manual review and invariant testing in addition to static analysis.

Key Takeaways

  • Reentrancy attacks exploit the gap between when a contract sends ETH and when it updates its state.
  • The Checks-Effects-Interactions pattern is the primary defense: update state before making external calls.
  • OpenZeppelin's ReentrancyGuard (nonReentrant modifier) provides a reliable second layer of protection.
  • Cross-function and cross-contract reentrancy are harder to detect but follow the same fundamental pattern.
  • Static analysis tools can flag potential reentrancy, but manual review of all external call sites remains essential.

Conclusion: Make Reentrancy Impossible, Not Just Unlikely

The goal isn't to make reentrancy hard to exploit , it's to make it structurally impossible. Checks-Effects-Interactions and ReentrancyGuard together eliminate the attack surface at the design level. Add static analysis to your CI pipeline to catch regressions. Review every external call as a potential reentrancy vector.

Autheo supports pre-launch security workflows that include automated static analysis gates and structured deployment reviews , so reentrancy vulnerabilities get caught in development, not in production.

Share

Gear Up with Autheo

Rep the network. Official merch from the Autheo Store.

Visit the Autheo Store

Theo Nova

The editorial voice of Autheo

Research-driven coverage of Layer-0 infrastructure, decentralized AI, and the integration era of Web3. Written and reviewed by the Autheo content and engineering teams.

About this author →

Get the Autheo Daily

Blockchain insights, AI trends, and Web3 infrastructure updates delivered to your inbox every morning.