Back

SC05: Reentrancy Attacks - Overview

What is Reentrancy?

Reentrancy is a class of smart-contract vulnerability in which a contract makes an external call—sending ETH or invoking another contract—before it finishes updating its own state. The called party can then call back (“re-enter”) into the original function while it is still mid-execution, observing and acting on stale state that has not yet been written back. Repeated re-entry against that stale state lets an attacker perform an action—most famously a withdrawal—many times when the contract intended to allow it only once.

The root cause is an ordering mistake. On most blockchains an external call transfers control synchronously to the callee, and the callee is free to run arbitrary code, including a call straight back into the caller. If the caller has not yet recorded the effects of what it is doing (for example, zeroing the user's balance), the world the callee sees is a lie: the balance still looks available, so the caller happily pays out again.

Core Concept

Vulnerable ordering (Interactions BEFORE Effects):
  1. Check    -> require(balance[user] >= amount)
  2. Interact  -> (bool ok, ) = user.call{value: amount}("")   <-- control leaves here
                    |                                              re-enters step 1
                    |  attacker's receive() calls withdraw() again, balance UNCHANGED
  3. Effect   -> balance[user] -= amount     <-- runs too late, many times over

Safe ordering (Checks-Effects-Interactions):
  1. Check    -> require(balance[user] >= amount)
  2. Effect   -> balance[user] -= amount     <-- state written FIRST
  3. Interact  -> (bool ok, ) = user.call{value: amount}("")   <-- re-entry now sees 0

The difference between the two orderings is the whole vulnerability. In the first, the external call sits in the middle of an unfinished transaction; in the second, every state change the function promises has already happened before control ever leaves the contract, so a re-entrant call finds nothing left to steal.

Why It's Critical for Smart Contracts

Several properties of blockchain execution make reentrancy uniquely dangerous:

Why Does This Matter?

Business Impact

Technical Impact

Technical Context

The Classic Vulnerable Pattern

The canonical bug is a withdrawal function that sends ETH before zeroing the caller's recorded balance:

// VULNERABLE: external call happens before state update
mapping(address => uint256) public balance;

function withdraw() external {
    uint256 amount = balance[msg.sender];
    require(amount > 0, "nothing to withdraw");

    // Interaction BEFORE effect: hands control to msg.sender
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok, "transfer failed");

    balance[msg.sender] = 0;   // too late: attacker already re-entered
}

When msg.sender is an attacker contract, the call invokes that contract's receive(), which calls withdraw() again. Because balance[msg.sender] is still the original amount, the check passes and another payment is sent—looping until the contract is empty or gas runs out.

Categories of Reentrancy

1. Single-function (same-function) reentrancy

The attacker re-enters the same function that made the external call—the classic withdraw loop above. This is the original DAO-class bug.

2. Cross-function reentrancy

The attacker re-enters a different function that shares state with the one making the call. For example, withdraw() sends ETH before updating balance, and during that call the attacker invokes transfer(friend, balance)—which reads the not-yet-zeroed balance and moves it elsewhere before withdraw resets it.

3. Cross-contract reentrancy

Two or more contracts share state (or one caches another's state). An external call lets the attacker re-enter Contract A through Contract B while a shared value is mid-update, so B acts on state A has not yet finalised.

4. Read-only reentrancy

Even a view function can be dangerous. During a re-entrant callback, a getter (for example, getPrice() or getVirtualPrice()) returns a value computed from state that is temporarily inconsistent. The attacking transaction calls an integrating protocol that reads that getter and makes a decision—pricing collateral, minting shares—on the stale number. Nothing is written in the victim getter, which is exactly why it is easy to miss.

5. Token-hook (ERC777 / ERC721) reentrancy

Some token standards call the recipient during a transfer. ERC777 invokes tokensReceived on the recipient; ERC721 safeTransferFrom invokes onERC721Received. These hooks are legitimate features, but they hand control to the recipient in the middle of a transfer, enabling reentrancy even when no raw ETH is sent. A contract that treats an ERC20-style transfer as a “safe, non-reentrant” step can be blindsided when the token is actually ERC777.

6. Delegatecall reentrancy

Proxy and library patterns use delegatecall, which runs external code in the caller's own storage context. If the delegated code makes an external call before the proxy's state is settled—or if an upgradeable contract's storage layout can be re-entered mid-operation—an attacker can manipulate the shared storage through re-entry.

Where the External Call Hides

MechanismHow control leaves the contractRe-entry hook
ETH via call{value:}Low-level call with gas forwardedRecipient receive() / fallback()
ETH via transfer/send2300 gas stipend (historically limiting)Recipient fallback (gas-starved, but not safe to rely on)
ERC777 token transferStandard-mandated recipient callbacktokensReceived hook
ERC721 safeTransferFromSafe-transfer acceptance checkonERC721Received hook
Arbitrary contract callCalling any external/untrusted addressCallee runs any code, including re-entry
delegatecall to library/logicExternal code runs in caller's storageDelegated code re-enters shared storage

Real-World Impact

Incident Class 1: The DAO-Class Reentrancy (2016)

Pattern:

Impact:

Root Cause: Interactions-before-Effects ordering in the withdrawal path, with no reentrancy guard. It is the archetype from which the whole category takes its name.

Incident Class 2: Token-Callback Reentrancy (ERC777-style)

Pattern:

Impact: Protocols that assumed “a token transfer cannot call back” were drained or left with corrupted accounting when the token turned out to hand control to the recipient mid-transfer.

Root Cause: An unrecognised external call inside a token transfer, combined with state updated after that transfer. The lesson generalised into: treat every token transfer as a potential external call.

Incident Class 3: Read-Only Reentrancy in DeFi Integrations

Pattern:

Impact: Lending markets and other integrators that trusted the getter mid-transaction could be induced to over-value or under-value assets, enabling under-collateralised borrows or unfair mints—without the vulnerable getter itself ever writing state.

Root Cause: A getter that returns intermediate state during a reentrant window, trusted by an external consumer. It shows that reentrancy defences must cover view functions and cross-protocol reads, not just state-changing withdrawals.

Prevalence and Significance

Reentrancy is one of the most recognised and studied vulnerability classes in smart-contract security, and it is included as SC05 in the OWASP Smart Contract Top 10 (2025). Despite being well understood since 2016, it continues to appear—usually in a newer form (cross-function, cross-contract, read-only, or token-hook) rather than the textbook single-function case.

Rather than cite precise loss totals (which vary by source and year), the defensible picture is:

Note: exact loss figures differ between reports. Treat any single number as illustrative; the durable takeaway is that reentrancy remains high-impact and keeps resurfacing in new forms as composability grows.

Common Misunderstandings

Myth 1: “Using transfer/send instead of call makes me safe”

Reality: The 2300-gas stipend of transfer/send historically made re-entry hard, but it is not a security guarantee—gas costs change, and cross-function or token-hook paths do not depend on that stipend. Modern guidance is to use call with Checks-Effects-Interactions and a guard, not to rely on gas limits.

Myth 2: “Only functions that send ETH can be re-entered”

Reality: Any external call is a re-entry point. ERC777 tokensReceived, ERC721 onERC721Received, and any call to an untrusted contract all hand over control—no raw ETH required.

Myth 3: “view functions are harmless”

Reality: Read-only reentrancy exploits view getters that return inconsistent state mid-transaction. The getter writes nothing, but an integrating protocol acting on its value can be drained.

Myth 4: “A reentrancy guard on one function is enough”

Reality: Cross-function reentrancy re-enters a different function that shares state. A guard must cover every function that touches the same state, and getters consumed by others may need protection too.

Myth 5: “My external call is to a trusted contract, so re-entry can't happen”

Reality: “Trusted” contracts can themselves be upgradeable, call untrusted code, or hold tokens with hooks. Treat every external call as potentially reentrant and order your state changes accordingly.

Myth 6: “Checks-Effects-Interactions and a mutex are redundant—pick one”

Reality: They are complementary. CEI removes the incentive to re-enter by finalising state first; a nonReentrant mutex is defence-in-depth that also covers cross-function paths and cases where an external call is genuinely unavoidable mid-logic.

How Reentrancy Differs from Related Issues

AspectReentrancy (SC05)Access Control (SC01)Logic Errors (SC03)
Root causeExternal call before state updateMissing/incorrect authorizationFlawed business rules/math
TriggerRe-entry via callee codeUnauthorized callerValid caller, wrong outcome
Typical fixCEI ordering + reentrancy guardProper role/permission checksCorrect and verify invariants
DetectionCall-then-write pattern analysisAuth-path reviewSpec & invariant testing

Key Takeaways

  1. Reentrancy is an ordering bug—an external call made before state is finalised lets the callee act on stale state.
  2. Any external call is a door—ETH sends, ERC777/ERC721 hooks, arbitrary calls, and delegatecall all pass control to code that may re-enter.
  3. The variants are what bite today—cross-function, cross-contract, and read-only reentrancy survive audits that only look for the classic loop.
  4. Fix by ordering, then guard—apply Checks-Effects-Interactions first, add a nonReentrant mutex as defence-in-depth.
  5. Losses are final—a single reentrant transaction can drain the whole balance, and on-chain there is no undo.

How to Identify if You're Vulnerable

Ask these questions about your contract:

If you answered “yes” to the call-before-write questions or “not sure” to the guard questions, you likely have exploitable reentrancy today.

Next Steps