Skip to content

Reentrancy attack

A reentrancy attack uses an external call or callback to re-enter contract logic while its state is inconsistent. Learn the attack paths, defenses and review limits.

Updated

For educational purposes only; not investment advice. A reentrancy flaw can cause rapid and irreversible loss of contract assets, and no single defense or audit proves a contract safe.

Direct answer

A reentrancy attack occurs when contract logic makes an external call and the callee calls back before the original execution has finished and restored its invariants. If stale balances, shares, permissions or prices remain visible, the callback may repeat an action or influence another action using inconsistent state.

The callback need not enter the same function or transfer native currency. Token hooks, NFT receiver callbacks, vault or strategy calls and flash-loan callbacks can transfer control to untrusted code. Reentrancy can cross functions, contracts and modules; read-only reentrancy can also make another protocol consume a transient value.

How it works

A typical exploit follows this sequence:

  1. The attacker enters a state-changing function and passes its initial checks.
  2. The vulnerable contract calls an address, token or protocol before completing its accounting.
  3. Attacker-controlled code enters the original or a connected contract while old state remains visible.
  4. The nested call repeats an effect or changes shared state, and execution unwinds with an invariant broken.

External control transfer can be explicit, such as a low-level call, or hidden behind a token transfer, receiver hook, safe-mint operation, strategy adapter or arbitrary-call interface. A revert normally rolls back the reverting call tree, but an attacker may arrange a successful nested sequence that returns normally after extracting value or corrupting accounting.

Defenses should be layered:

  • Apply checks-effects-interactions: validate first, commit all relevant internal effects, and interact externally last.
  • Use a reentrancy guard across every entry point sharing the protected invariant, not only the function with the obvious call.
  • Prefer pull-based claims where appropriate, and minimize calls to untrusted tokens, receivers, hooks, proxies and integrations.
  • Define invariants across contracts and test malicious callbacks, cross-function paths, nested multicalls and read-only consumers.
  • Combine implementation review with monitoring, pause and incident-response controls where the design supports them.

Example

Consider a vault that sends value and clears the user’s balance only after the call:

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

    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok, "transfer failed");

    balances[msg.sender] = 0;
}

The recipient gains control while balances[msg.sender] still contains the old amount. Its receive function can call withdraw() again, pass the same check and request another transfer. Moving the balance update before the call closes this particular window; a guard can reject nested entry. Neither change proves the whole system safe because another entry point or connected contract may expose the same incomplete invariant.

Risks

  • A guard protects one function while another function exposes the same state.
  • Local call ordering is correct, but a cross-contract invariant remains inconsistent during a callback.
  • A token, receiver, flash-loan callback or strategy adapter unexpectedly executes external code.
  • A view function publishes a transient price or exchange rate that another protocol uses in the same transaction.
  • An upgrade, module or storage-layout change bypasses or corrupts the original lock.
  • Tests cover recursive withdrawal but omit cross-function, cross-contract and read-only paths.

For users, an audit badge or reentrancy guard is evidence of a control, not a guarantee. Upgrades, integrations and privileged emergency actions can change the attack surface. Limit approvals and exposure, verify the deployed implementation, and do not assume losses can be reversed.

Common misconceptions

  • Reentrancy only means repeating one withdrawal function. It can enter another function or contract, or expose inconsistent data to a reader.
  • Only native-currency transfers trigger callbacks. Token standards, receiver hooks and protocol integrations can execute external code.
  • A guard or checks-effects-interactions makes the contract safe. Its scope, every shared entry point and cross-contract invariant still require review.
  • A successful audit rules out reentrancy. Audits are bounded reviews; later changes and unreviewed integrations can introduce new paths.

Sources

Navigation

Search the wiki...