What is a Re-Entrancy Attack?

Learn what a re-entrancy attack is, how callback-based control flow breaks smart contract assumptions, and how CEI, guards, and pull payments help.

Sara ToshiMar 21, 2026
Summarize this blog post with:
What is a Re-Entrancy Attack? hero image

Introduction

Re-entrancy attack is the name for a smart contract bug where code hands control to an external contract before it has finished updating its own state, and that external contract uses the opening to call back in and exploit the half-finished state. The reason this matters is simple: blockchains are often described as deterministic and atomic, but inside a single transaction, control can still bounce between contracts in surprising ways. If a developer reasons about a function as though it runs straight from top to bottom without interruption, they can build a contract that looks correct in isolation and is exploitable in practice.

That tension is the heart of the topic. A smart contract function may appear sequential, yet the moment it calls another contract or transfers value, it may no longer be in sole control of execution. The callee can run arbitrary logic and sometimes call back before the caller has committed its intended effects. Re-entrancy is what happens when that callback reaches state the caller assumed was still private to the current execution path.

This is one of the most important smart contract vulnerabilities because it is not a niche edge case. It grows directly out of composability: contracts calling contracts, payment flows invoking recipients, token hooks triggering downstream logic, middleware firing callbacks, and protocols depending on external state during execution. The same mechanism that makes smart contract systems useful also makes re-entrancy a recurring security problem.

How can one transaction cause the same function to run multiple times?

At first glance, the attack sounds contradictory. If a user calls withdraw(), how can withdraw() run again before the first call finishes? In ordinary application code, that feels like a threading or interrupt problem. In smart contracts, the mechanism is different. The issue is not hidden parallelism. It is explicit control transfer.

When contract A calls contract B, or sends value in a way that executes B's code, A pauses and B starts running. If B then calls back into A, the blockchain virtual machine does not say, “wait until the first call is done.” It treats the callback as another valid call frame in the same transaction. So the execution stack can look like A -> B -> A.

That is the core fact to hold onto: re-entrancy is not magic recursion from nowhere; it is callback-based re-entry caused by an external call before local invariants are restored. The vulnerable contract is entered, gives up control, and gets entered again while still partway through its own logic.

The dangerous moment is the gap between deciding something and recording it. If a contract checks that a user has a balance, then makes an external call, and only later reduces that balance, the external callee may return through the still-open door and observe the old balance again. The contract is then acting on stale assumptions it created for itself.

How does re-entrancy occur in smart contracts?

A useful way to think about re-entrancy is as an invariant violation caused by bad ordering. An invariant is something your contract intends to keep true. For a withdrawal function, the intended invariant might be: “after a user withdraws amount, their stored balance is lower by amount, so they cannot withdraw the same funds again.”

If the contract updates storage first and only then makes the external call, that invariant is preserved even if the recipient calls back. The callback sees the new reduced balance. But if the contract sends funds first and updates storage second, there is a period where the old balance is still visible even though value is already leaving the contract. That period is the vulnerability.

The Solidity documentation states the underlying rule directly: any interaction from one contract with another, and any Ether transfer that executes the recipient’s code, hands control to the recipient. The SWC-107 entry describes reentrancy in the same terms: a malicious contract calls back into the caller before the first invocation has finished. Those two statements are enough to derive most of the practical advice.

So the deepest intuition is not “external calls are bad.” External calls are normal. The real lesson is narrower and more precise: never expose an inconsistent intermediate state to code you do not control.

Why does sending funds before updating the balance enable re-entrancy?

Consider a contract that stores user balances in a mapping and offers a withdrawal function. The function checks whether credit[msg.sender] is at least amount. If so, it sends amount to msg.sender and then subtracts amount from credit[msg.sender].

That ordering feels natural to many developers because it mirrors a story they tell themselves: “first pay the user, then write down that they were paid.” But the machine does not experience that as a single indivisible step. The send is an external interaction. During it, the recipient can run code.

Now imagine the recipient is not a simple wallet but an attacker contract. It calls withdraw(10). The vulnerable contract checks the stored balance and sees 10, so it proceeds to send 10. As soon as that send gives the attacker contract control, the attacker’s fallback or receive logic calls withdraw(10) again. The original function has not yet reached the line that subtracts from the stored balance, so the second call sees the same 10 balance and sends another 10. The attacker repeats the pattern until the vulnerable contract no longer has enough funds to keep paying.

This is the classic shape reflected in the SWC SimpleDAO example, where the contract performs msg.sender.call.value(amount)() before decrementing credit[msg.sender]. The bug is not that the contract lacks a balance check. It has one. The bug is that the check is performed against state that remains unchanged during the external interaction, so the same check can succeed repeatedly.

Notice the deeper point: each individual call to withdraw is locally plausible. The exploit emerges from the interleaving of multiple invocations through callbacks. That is why re-entrancy is best understood as a workflow-ordering bug, not merely a missing if statement.

Why are external contract calls especially risky for re-entrancy?

Smart contracts often call out for legitimate reasons: sending assets, invoking token logic, querying another contract, triggering a callback, composing with a protocol, or forwarding execution to a module. Every such interaction creates a boundary where assumptions can fail.

On EVM chains, the Solidity docs make two details especially relevant. First, reentrancy is not limited to Ether transfers. Any external function call can trigger it. If your contract calls a token contract, a vault, an oracle adapter, or another app-specific module, that callee can in turn call somewhere else or back into you. Second, low-level call can forward large amounts of gas, which gives the callee enough budget to execute complex logic, including expensive callback chains.

Older guidance sometimes emphasized transfer or send because they passed only a limited gas stipend, historically making some re-entrant payloads harder to execute. But that was never a complete defense, and modern best practice does not treat limited-gas sends as a security model. The real question is not “did I send little gas?” but “what state was visible when I handed control away?”

That is why the problem appears in many forms beyond a simple Ether withdrawal. A token transfer can invoke hooks. A vault share redemption can call pricing logic that depends on another contract’s temporary state. A protocol may read data from a contract that is itself mid-execution. Even so-called read-only reentrancy can matter if a callback observes a transient state and uses it to make some other decision elsewhere.

What is the Checks-Effects-Interactions (CEI) pattern and how does it prevent re-entrancy?

StagePurposeWhen to runQuick example
ChecksValidate inputs and permissionsBefore any state changerequire balance sufficient
EffectsUpdate internal storageImmediately after checkscredit[msg.sender]-=amount
InteractionsExternal calls and transfersOnly after effects are writtenexternal call or transfer
Figure 178.1: Checks‑Effects‑Interactions (CEI) summary

The most widely taught mitigation is the Checks-Effects-Interactions pattern, usually shortened to CEI. Its logic is simple because it follows directly from the mechanism.

First, do your checks: validate permissions, balances, arguments, and preconditions. Then apply your effects: update your own storage so your invariants hold under the new reality. Only after that should you perform interactions: external calls, value transfers, hook invocations, or calls into other contracts.

Why does this work? Because a re-entrant callback can only exploit what it can still observe. If the callback re-enters after your effects are already written, it sees the updated state rather than the stale pre-call state. In the withdrawal example, deducting credit[msg.sender] before sending means a re-entering call no longer passes the balance check for the already-withdrawn amount.

The fixed SimpleDAO example in SWC-107 does exactly this: it subtracts the amount from the user’s credit first, then performs the call. The Solidity docs recommend the same ordering. This is not just a style preference. It is a way of making the contract’s own storage reflect the new truth before outside code gets a chance to react.

There is an important subtlety here. CEI is fundamentally about preserving invariants at every externally observable boundary. If updating one variable is not enough to restore consistency, then updating just that variable is not enough. In more complex contracts, the “effects” stage may need to update several pieces of state before any interaction is safe.

How do reentrancy guards (nonReentrant) stop nested calls?

Ordering is the first defense because it addresses the root cause. But many systems also add an explicit lock: a reentrancy guard. OpenZeppelin’s ReentrancyGuard exposes a nonReentrant modifier that prevents nested calls into protected functions while one such function is already executing.

The idea is mechanical. When the protected function starts, the guard marks the contract as entered. If execution tries to enter again before the function exits, the second call reverts. When the first call completes, the entered flag is cleared.

This control is powerful because it does not depend on the attacker failing to find some stale state. It refuses the callback path outright. That makes it especially useful in codebases with many contributors, upgradeable systems, or complex interactions where a future refactor might accidentally reintroduce a dangerous ordering.

But guards are not a substitute for careful design. OpenZeppelin notes that its guard is a single guard, which means functions marked nonReentrant cannot call one another directly. The usual workaround is to keep internal logic in private functions and expose external guarded entry points. More importantly, a guard only protects the call paths it actually covers. If the dangerous state can be manipulated through a different public function, cross-function reentrancy may still be possible unless the design accounts for it.

So the right way to see a guard is as a runtime gate, not a proof that the whole contract is conceptually safe.

How do pull-payment (withdrawal) patterns reduce re-entrancy risk?

PatternTiming of transferReentrancy surfaceTrade-off
PushImmediate transfer during workflowHigh if mid-updateSimpler UX but risky
PullRecord owed, later withdrawLower during state updatesMore steps, safer accounting
Figure 178.2: Pull vs Push Payments

Sometimes the safest fix is not to harden a push-based interaction but to redesign it. Solidity’s documentation recommends a withdrawal pattern, and OpenZeppelin provides a PullPayment module built around that same idea.

The principle is that the paying contract should not immediately push funds to an arbitrary recipient in the middle of some larger state transition. Instead, it records that a recipient is owed funds. The recipient later withdraws those funds in a separate action. This separates the accounting step from the external payment step.

That separation matters because it narrows the vulnerable moment. When the contract first records the debt, no arbitrary recipient code needs to run. Its internal bookkeeping can complete cleanly. The later withdrawal still involves an external interaction, so it must still be written carefully, but the system as a whole is often easier to reason about because obligations and payouts are decoupled.

Even here, though, the details matter. OpenZeppelin notes that withdrawPayments forwards all gas to the recipient, which means reentrancy remains possible unless the surrounding logic follows CEI or uses a guard. So pull payments reduce attack surface, but they do not repeal the fundamental rule that external calls can hand control away.

What common misunderstandings cause developers to miss re-entrancy risks?

A common misunderstanding is to equate re-entrancy with “sending ETH.” That is too narrow. The Solidity docs explicitly warn that any function call on another contract can create the same issue. Ether transfer is just the most famous case because the exploit story is easy to visualize.

Another misunderstanding is to think of re-entrancy as only “calling the same function again.” In practice, the callback may reach a different function that touches the same state. If function deposit() updates some accounting variable and function claimReward() relies on that variable, a callback into claimReward() while deposit() is half-finished may be enough to break invariants even if deposit() itself is protected.

A third misunderstanding is to treat CEI as a ceremonial checklist rather than a reasoning tool. Developers sometimes reorder one obvious line and declare victory, but the real question is broader: after your effects stage, is the contract truly in a coherent state relative to every external observer that might call back? If the answer is no, the pattern has not been fully applied.

There is also a tendency to over-trust low-level constraints such as gas stipends, stack depth, or assumptions about “benign” counterparties. These are secondary frictions, not the core defense. Security should not depend on the callee being too resource-constrained or too polite to exploit your mistake.

What are read-only and cross-contract re-entrancy attacks and why do they matter?

The simplest reentrancy attack drains funds because a balance update happens too late. But the broader category includes cases where the callback does not directly mutate the vulnerable contract’s storage and still causes harm.

Recent research on read-only reentrancy describes situations where complex interactions across multiple DApps let an attacker observe inconsistent intermediate state and use that observation elsewhere. The cited SmartReco paper argues that many existing tools miss these cases because they do not analyze cross-DApp interactions deeply enough. That matters because modern smart contract systems are rarely isolated. They compose with vaults, pricing functions, wrappers, staking systems, AMMs, and cross-protocol adapters.

The underlying logic is the same as in the classic attack. A system exposes an externally visible state that it intends as temporary, but another execution path treats it as meaningful input. The exploit surface is wider because what counts as “harm” is wider: not only repeated withdrawals, but distorted pricing, incorrect minting, broken collateral calculations, or reward manipulation.

This is where the phrase read-only can mislead. The read itself may be read-only, but the consequences usually are not. A stale or transient value read during re-entry can inform a different action that moves real assets elsewhere.

How do re-entrancy risks differ across blockchains (Ethereum, Solana, Cosmos, Cardano)?

PlatformCallback policyPractical riskTypical mitigation
EVMAllows A→B→A callbacksClassic reentrancy possibleUpdate ordering and reentrancy locks
SolanaIndirect reentry blockedSome callback shapes preventedAccount validation and budget checks
Cosmos (IBC)Hook/timeouts can callbackTimeout-hook reentry possiblePatch ibc-go; restrict uploads
EUTXO / CardanoUTXO-style semanticsDifferent concurrency modelStatic/state-machine checks
Figure 178.3: Re-Entrancy across blockchains

Re-entrancy is most famously discussed in the EVM, but the exact risk depends on the execution model of the platform.

On Solana, for example, the platform documents explicit Cross-Program Invocation semantics. Programs can invoke other programs, but the runtime allows direct self-recursion while rejecting indirect re-entry of the form A -> B -> A with ReentrancyNotAllowed. That means the classic callback path is constrained by the runtime itself. The risk profile changes: some reentrant patterns are blocked by policy, while other issues such as shared compute budget and privilege propagation still matter for secure composition.

That is a good reminder that re-entrancy is not just a name for “any complicated call graph.” It depends on what callback structures a runtime permits. A platform that forbids a particular call pattern can eliminate one attack shape while leaving adjacent logic bugs or denial-of-service patterns intact.

In Cosmos-related systems, the picture is different again. A recent ibc-go advisory described a potential reentrancy using timeout callbacks in ibc-hooks, where a malicious CosmWasm contract could trigger the same timeout handling before packet commitment deletion, allowing recursive execution and potentially causing escrow fund loss or unexpected minting. The important lesson is not that every CosmWasm contract behaves like an EVM contract. It is that callback-style vulnerabilities can reappear wherever systems expose multi-step workflows before prior steps are fully finalized.

Cardano’s extended UTXO model highlights the opposite design tendency. Research on EUTXO emphasizes the semantic simplicity of the UTXO-style model in concurrent distributed environments. That does not mean “all bugs disappear,” but it does mean the state-transition model is different from the account-based, callback-heavy style that made classical EVM reentrancy such a central concern.

So the cross-chain takeaway is careful and specific: the general pattern is broader than Ethereum, but the exact mechanics are execution-model dependent.

Why have re-entrancy incidents historically caused large losses?

The DAO exploit made re-entrancy impossible to dismiss as a theoretical programming mistake. Reporting at the time described a “recursive call” attack that drained 3.6 million ether into a child DAO, triggering one of the most consequential governance crises in blockchain history. The precise historical responses are less important here than the underlying lesson: a tiny ordering error in a contract entrusted with large balances can scale into systemic damage.

What made such incidents severe was not only the presence of a bug, but the environment around it. Smart contracts are public, composable, and often hold pooled funds. Once deployed, vulnerable logic can be observed, tested against, and exploited quickly. If the contract governs treasury assets, lending positions, or widely used infrastructure, a single re-entrancy path can affect many users at once.

This is also why testing is hard. Re-entrancy bugs often do not reveal themselves in straight-line unit tests unless the tests model a hostile callee that deliberately calls back at the worst possible moment. As the broader Ethereum engineering experience around complex bugs has shown, conventional review and testing can miss issues that only appear under specific interleavings and compositions.

What layered defenses do real systems use against re-entrancy?

In practice, robust defenses usually combine several layers because each addresses a different failure mode.

The first layer is architectural: minimize unnecessary external calls, separate accounting from settlement when possible, and prefer designs where obligations are recorded before counterparties are invoked. The second layer is local correctness: use CEI so storage reflects the intended new state before any external interaction. The third layer is runtime hardening: apply a reentrancy guard to sensitive entry points. The fourth layer is operational containment: emergency stop mechanisms such as OpenZeppelin’s Pausable can help limit damage if suspicious behavior is detected in production.

No single layer should be treated as universal. A pause switch does not prevent the initial bug. A guard can be misapplied or bypassed via unguarded paths. CEI can fail if the developer misunderstands which variables jointly encode the invariant. Pull payments help, but the withdrawal leg still touches external code.

The unifying discipline is to ask, at every external boundary: if the callee immediately calls back, what state will it see, and what authority will it still have? If that question is answered rigorously during design, many re-entrancy bugs become obvious before code is deployed.

Conclusion

A re-entrancy attack is fundamentally an ordering bug in a composable execution environment. A contract checks some condition, gives control away too early, and gets called again before it has made its own new state true. That is why the most important defense is not a magic library but a habit of reasoning: restore your invariants before any external interaction.

If you remember one sentence tomorrow, let it be this: the moment your contract calls out, assume the outside world can call back immediately; and write your state as though that callback will happen.

How do you secure your crypto setup before trading?

Secure your account and verify transfer flows before you trade. On Cube Exchange you keep control using a non-custodial MPC-backed account; use the checklist below to reduce risks like mistaken destinations, unsafe approvals, and transfer-related attacks.

  1. Verify custody and recovery: confirm your Cube MPC recovery method or securely back up your private key/seed in at least two offline locations (hardware wallet, safe deposit box).
  2. Run a small on-chain test: send a low-value transfer on the same chain (for example 0.01 ETH), wait for the chain-specific finality threshold (e.g., ~12 confirmations on Ethereum) and confirm the funds arrive before moving larger amounts.
  3. Limit token approvals: when granting allowances for trading, set the minimum necessary amount or use per-order approvals; immediately revoke or reduce unlimited approvals after use.
  4. Verify destinations and contracts: check the destination address checksum and inspect the receiving contract’s source or audit status on a block explorer before sending funds.
  5. Review execution details: confirm the network, amount, estimated fees, and use limit orders for large trades to control execution and avoid accidental over-spend.

Frequently Asked Questions

Can Checks-Effects-Interactions (CEI) and a reentrancy guard together guarantee my contract is safe from all re-entrancy attacks?
+
Checks-Effects-Interactions plus a reentrancy guard mitigate many common cases but do not provide a mathematical guarantee: CEI only works if every piece of state that must be consistent is updated before any external call, and guards are runtime gates that only cover the entry points they protect and have practical limits (for example OpenZeppelin's single nonReentrant guard prevents guarded functions from calling one another). Complex cross-function or cross-contract interactions can still produce exploitable interleavings unless the design and coverage are comprehensive.
Is using .transfer() or .send() instead of .call() a reliable defense against re-entrancy?
+
No — relying on transfer/send gas limits is unsafe: historically limited gas stipends made some exploits harder but never eliminated the underlying callback problem, and .call can forward much more gas which makes re-entrancy easier; security should assume the callee can execute arbitrary logic when you hand it control.
What is read-only re-entrancy and how can a callback that only reads state still be dangerous?
+
Read-only re-entrancy is when a callback only reads a transient or inconsistent state but that observation is then used elsewhere to cause real harm (for example by affecting pricing, minting, or collateral logic), and it is harder to detect because it spans multiple DApps and read-only traces that many tools do not track.
How does a reentrancy guard like nonReentrant work, and what are its limitations?
+
A reentrancy guard works by marking a function as entered and reverting any nested entry while that mark is set, which blocks re-entrant callbacks into guarded functions, but it is a mechanical runtime defense with trade-offs (e.g., the common single-flag nonReentrant pattern prevents guarded functions from calling each other directly and only covers the functions where the guard is applied).
If I switch to a pull-payment (withdrawal) pattern, can I stop worrying about re-entrancy?
+
Pull payments reduce the attack surface by separating the accounting of who is owed from the act of sending funds, making the critical bookkeeping complete before an external transfer, but they do not remove re-entrancy risk entirely because the later withdrawal still performs an external call (and some implementations forward all gas to the recipient unless combined with CEI or guards).
How should I test or fuzz my contracts to find re-entrancy bugs before they go live?
+
Detecting re-entrancy before deployment is difficult with straight-line unit tests or casual review because attacks depend on hostile callbacks and specific interleavings; you need tests or fuzzers that model malicious callees, cross-contract analysis, and complementary tooling or network-level simulation to increase confidence.
Are re-entrancy risks identical across blockchains like Ethereum, Solana, Cosmos, and Cardano?
+
No — the mechanics and permitted call patterns differ by platform: for example, Solana's runtime rejects certain indirect re-entry A->B->A while allowing direct recursion and uses shared compute budgets, Cosmos/IBC had hook-related recursive timeout concerns, and Cardano's EUTXO model presents a different state-transition semantics that changes which callback-style bugs are possible.
Can re-entrancy happen without sending Ether or tokens?
+
Yes — any external call (not only sending ETH or tokens) hands control to code you do not control, and a callee can call back into a different function that touches the same state, so re-entrancy can occur without an explicit value transfer.

Related reading

Keep exploring

Your Trades, Your Crypto