Reentrancy

How the attack works and how to prevent it

JohnnyTime
JohnnyTime · Updated August 05, 2026
21 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.

Every external call is a control-flow handoff. 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?

A reentrancy attack happens when a smart contract makes an external call to an untrusted address before updating relevant internal state, such as a user's balance.

If the recipient is a malicious contract, it can call back into the vulnerable contract while the old state is still visible. Repeated calls may pass the same checks and withdraw more than the attacker's recorded balance.

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

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 hands you $1,000 in cash before updating the account ledger. If you could request another withdrawal before the ledger changed, the old balance would pass the same check again.

Repeated withdrawals could empty the bank before the first ledger update. A reentrancy attack exploits the equivalent ordering error in contract 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 Still Matters

Reentrancy vulnerabilities have caused significant losses and protocol failures:

$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

Nearly a decade after The DAO, reentrancy still ranks in the top 5 smart contract vulnerability classes reported in 2025.

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 types, read-only reentrancy matters in DeFi protocols because a view function can return stale data during an ongoing state transition. Token standards with callback hooks, including ERC-777 (tokensReceived) and ERC-1155 (onERC1155Received), also introduce reentrancy paths during 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: 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 DAO hack exposed a reentrancy flaw that led to the diversion of roughly $60 million in ETH and contributed to Ethereum's 2016 hard fork. It remains an important case because the technical exploit and the response both shaped smart contract security practice.

The DAO: what it was

Launched in April 2016, The DAO was a blockchain-based investment fund governed by token holders.

At the time:

  • Over $150 million raised in ETH

  • Held 14% of all ether in circulation

  • Backed by 11,000+ investors

  • One of the largest crowdfunding campaigns at the time

Investors received DAO tokens, voted on funding proposals, and shared returns. The system relied on contract code and token-holder governance rather than centralized management.

The Attack: June 17, 2016

On June 17, 2016, an attacker exploited the withdrawal logic.

The attacker used a reentrancy vulnerability in The DAO's withdrawal function to divert 3.6 million ETH (roughly $50-60 million at the time) into a child DAO under their control.

Several security researchers warned about recursive-call vulnerabilities before the exploit, but the required fixes were not deployed in time.

The attacker did not bypass the EVM's rules. The vulnerable contract executed its withdrawal logic as written, which intensified the debate about how the community should respond.

In an ecosystem where "code is law" was a common principle, the incident raised a difficult question: should the network preserve the original execution or intervene to return funds?

The Great Ethereum Split

The Ethereum community considered two broad options:

Option A: preserve the original chain history and accept the loss as a consequence of immutability.

Option B: change the protocol state through a hard fork so affected users could recover funds.

After extensive debate, the community adopted a hard fork that moved the diverted funds into a recovery contract. The network officially forked on July 20, 2016, at block 1,920,000.

Participants who rejected the fork continued operating the original chain, now known as Ethereum Classic (ETC).

The incident divided the community and made protocol governance, immutability, and emergency response central security concerns.


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

The sequence below shows where the vulnerable contract exposes stale state.

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.


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 a real attack the loop keeps running past the attacker's own deposit and takes every ETH the contract holds.


How to Prevent Reentrancy Attacks: Best Practices

Preventing reentrancy requires correct state-update order, explicit guards, and review of the full call graph.

1. The Checks-Effects-Interactions Pattern

This pattern places state changes before external interactions.

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}("")
Update relevant state before external calls.
The Checks-Effects-Interactions pattern blocks the common case where a callback observes an unchanged balance. It should be combined with call-graph review for variants that cross functions or contracts.

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: Apply this pattern to contracts that send ETH or hand control to external contracts.

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

The following example combines Checks-Effects-Interactions with OpenZeppelin's ReentrancyGuard.

// SECURE CONTRACT - Example with CEI and ReentrancyGuard
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

Zeroing the balance before the external call is exactly the line The DAO's withdrawal function was missing in 2016.


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 and ReentrancyGuard address common callback paths. Full protocol review is still required for cross-function, cross-contract, and read-only variants.


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 can be combined with other exploit techniques. Flash loan attacks can provide temporary capital for an exploit path that depends on protocol accounting or collateral state. The bZx incidents combined borrowed liquidity with callback and accounting weaknesses.

Reentrancy is also related to call attacks and delegatecall vulnerabilities. Reentrancy uses callbacks to observe stale state, while delegatecall vulnerabilities arise from executing external code in the caller's context. Both require careful review of where control leaves the contract.


Test your reentrancy knowledge

5 questions on call ordering, guards, and read-only variants

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.

No single mechanism covers every variant. Use Checks-Effects-Interactions, apply a reentrancy guard where appropriate, and review cross-function, cross-contract, callback, and read-only paths. These controls provide defense-in-depth but still require protocol-specific testing.

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