Web3Exploit logoWeb3Exploit
← Back to Attack Patterns
Attack pattern

Reentrancy

An external call hands control to an attacker before state is finalized, letting them re-enter and act on stale storage.

Definition

Reentrancy occurs when a contract makes an external call before it has finished updating its own state. The callee (attacker-controlled) can call back into the original function while the first invocation is still in progress, observing and exploiting state that has not yet been written.

How it works

  1. A function performs an external call (ETH transfer, token hook, or callback) before persisting its state changes.
  2. The external target is attacker-controlled and re-enters the same or a related function.
  3. On re-entry the contract still reads pre-update state (e.g. an un-decremented balance), so a guard or accounting invariant is evaluated against stale values.
  4. The attacker repeats the loop to drain funds or desynchronize accounting across contracts.

Vulnerable vs. fixed

Vulnerable: state written after the external callsolidity
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount, "insufficient");
    // External call BEFORE state update — hands control to the caller.
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok, "transfer failed");
    balances[msg.sender] -= amount; // too late: re-entry saw the old balance
}
Fixed: checks-effects-interactions + reentrancy guardsolidity
function withdraw(uint256 amount) external nonReentrant {
    require(balances[msg.sender] >= amount, "insufficient");
    // Effects first: update state before any external interaction.
    balances[msg.sender] -= amount;
    // Interaction last.
    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok, "transfer failed");
}

Detection steps

  1. Identify the sensitive value or permission the pattern can influence.
  2. Trace every path that can update or consume that value, including callbacks, routers, and privileged helpers.
  3. Reproduce the worst-case attacker flow with adversarial ordering, manipulated inputs, and maximum feasible capital.

Common signals

  • Security depends on an assumption that is not enforced by code.
  • A critical value is consumed immediately after an attacker-controlled interaction.
  • A privileged call path crosses multiple contracts without one clear authorization boundary.

False positives

  • The risky-looking operation is read-only and cannot affect settlement or authorization.
  • The value is bounded by independent checks before it moves assets.
  • All privileged entry points share the same tested guard and monitoring path.

Review questions

  • Which invariant would fail if this input or caller were attacker-controlled?
  • Can the attacker compose setup, trigger, settlement, and cleanup atomically?
  • What independent check stops the exploit if the first guard is wrong?

Defensive checklist

  • Apply checks-effects-interactions: finalize all state before external calls.
  • Add a reentrancy guard (e.g. OpenZeppelin ReentrancyGuard) to functions with external calls.
  • Treat cross-contract accounting as one atomic unit — never read a value from contract A that contract B updates on a separate path.
  • Audit keeper/callback hooks: any address you hand control to can re-enter.