Reentrancy Attacks Explained: External Calls, Checks-Effects-Interactions, ReentrancyGuard, and DeFi Risk
A reentrancy attack occurs when a smart contract makes an external call before completing the state changes that protect the current operation, allowing external code to call back into the contract while its internal state is temporarily inconsistent. The resulting control-flow loop can repeat withdrawals, bypass accounting assumptions, distort prices, duplicate claims, alter collateral calculations, or exploit interactions across several functions and contracts. This guide explains how reentrancy works, why external calls create execution boundaries, how checks-effects-interactions and ReentrancyGuard reduce risk, and what users should verify before trusting a DeFi protocol.
TL;DR
- Reentrancy is a control-flow vulnerability. External code regains execution before the original function has completed its protected state transition.
- The external call is the interruption point. Ether transfers, token hooks, callbacks, safe NFT transfers, flash loans, oracle calls, and protocol integrations can all execute code outside the calling contract.
- State order matters. If a contract sends value before reducing a balance, marking a claim used, burning shares, or updating debt, a callback may observe the old state and repeat the action.
- Checks-effects-interactions reduces the vulnerable window. Validate conditions first, update internal state second, and interact with external contracts last.
- ReentrancyGuard blocks nested entry into protected functions. It is a useful defense, but it protects only the functions and execution paths covered by the guard.
- Reentrancy is not limited to one function. A callback can enter another public function that reads or modifies shared state.
- Read-only reentrancy can corrupt decisions without directly changing the vulnerable contract. Another protocol may read a temporary price, exchange rate, balance, or reserve during an unfinished state transition.
- Using transfer or send is not a complete security strategy. Safe design should not depend on a fixed gas stipend remaining an effective barrier to external behavior.
- Audits reduce risk but do not guarantee safety. Users should confirm the audited commit, deployed implementation, upgrade history, unresolved findings, bug bounty, and post-audit changes.
- Reentrancy can combine with flash loans, oracle manipulation, accounting bugs, and weak access control. The callback is often one part of a larger economic exploit.
The vulnerable value may be a token balance, vault share supply, lending position, claim flag, reward index, pool reserve, oracle-dependent price, governance weight, liquidation status, or any other state used to authorize an action. Funds are often the final target, but inconsistent control flow is the underlying defect.
Verify the contract users are actually trusting
Start with the TokenToolHub smart contract verification guide when reviewing source code, proxy implementations, constructor data, and deployed bytecode. The OpenZeppelin Contracts guide explains reviewed components such as ReentrancyGuard, access controls, pausing, and token utilities that can support a safer implementation when used correctly.
What a reentrancy attack is
A smart contract function normally appears to execute from top to bottom. It checks the caller, reads state, performs calculations, writes new state, and may send assets or call another contract.
The execution becomes more complex when the function calls an external address. That address may contain contract code. The external contract can run arbitrary logic within the gas and permissions available to it. It may call the original contract again before the first invocation has returned.
That callback is reentrancy. It is not automatically malicious. Many protocols intentionally use callbacks for swaps, flash loans, token receipt hooks, liquidation flows, account abstraction, and composable integrations. The vulnerability appears when the calling contract is not prepared to be re-entered at that point.
A simple withdrawal model
Assume a vault stores each user's deposited balance. A withdrawal function checks that the caller has 10 ETH, sends 10 ETH to the caller, and only afterward sets the recorded balance to zero.
If the recipient is a contract, receiving ETH can execute its receive function. Before the vault reaches the line that clears the balance, the recipient calls the vault's withdrawal function again.
The second call reads the old balance of 10 ETH because the first call has not updated it yet. The vault sends another 10 ETH. The callback can repeat while the vault has funds and execution resources.
The callback does not rewind execution
Reentrancy does not restart the blockchain or reverse completed instructions. The original call remains paused on the execution stack while the external contract executes. The callback creates another invocation above it.
When the nested call finishes, execution returns to the earlier call. If several nested calls occurred, each one eventually resumes and continues from the point after its external interaction.
The contract sees valid callers and valid code paths
A reentrancy exploit often uses ordinary public functions. The callback may satisfy every access-control check. The problem is that the checks are evaluated against state that should already have changed but has not.
The attacker contract is not always the direct beneficiary
The callback can manipulate accounting so that assets are later withdrawn by another address, mispriced by another protocol, or credited through a different function. The economic beneficiary may be several calls removed from the vulnerable interaction.
Reentrancy Loop: how unfinished state becomes reusable
The classic loop begins with a legitimate withdrawal request. The vault confirms a balance, performs an external value transfer, and gives control to the recipient. The recipient calls back before the vault clears the balance.
A withdrawal starts
The vault confirms that the caller has a recorded balance, but the balance has not yet been reduced or reserved.
The vault calls external code
Sending ETH, tokens, or callback-enabled assets transfers execution control to another contract.
The recipient calls back
The callback enters the same function or another function that relies on the still-unchanged balance.
The old state is reused
The nested call repeats an operation before the original call reaches its protective state update.
Why external calls are security boundaries
An external call is not merely a transfer of data or value. It gives another execution context an opportunity to run code before the caller resumes.
Low-level call
Solidity's low-level call can invoke another address and optionally send ETH. When the target is a contract, its receive function, fallback function, or selected function may execute.
The caller receives a success value and return data after the external execution completes. During that interval, the target can call other contracts, including the original caller.
Calling an interface function
A normal interface call such as vault.deposit(), oracle.getPrice(), or token.transfer() is still an external interaction when the target is another contract. The higher-level syntax does not remove callback risk.
Token transfers
Standard ERC-20 transfers are often treated as simple balance changes, but integrations should not assume every token is behaviorally simple. Tokens can contain fees, custom external calls, upgradeable logic, callbacks, blocklists, or unusual transfer behavior.
Callback-oriented token standards can deliberately notify sender or recipient contracts. ERC-777, for example, defines token hooks that can execute during a token movement.
Safe NFT transfers
Safe NFT transfers call a receiver hook when the destination is a contract. That hook can call back into the sending protocol before the original transfer workflow finishes.
Flash loan callbacks
A flash lender sends assets and calls the borrower's callback so the borrower can use and repay them within one transaction. The callback is intentional. The lender and every integrated protocol must remain correct while that temporary execution path is active.
Swap callbacks
Some automated market makers send output assets before collecting the required input, then invoke a callback that must settle the trade. The pool's invariant and lock design must account for reentrant control flow.
Oracle and pricing calls
A protocol may call an external price source during a state transition. The oracle or another integrated contract could call back, or a separate protocol could read temporarily inconsistent state exposed by the first protocol.
Upgradeable integrations
A contract that is harmless today can become callback-capable after an upgrade. Trusting a target based only on current behavior can fail when its implementation or administrator changes.
Even a widely used token, router, vault, or oracle can change through a proxy, governance action, dependency upgrade, or administrator compromise. Security should come from the caller's own state discipline rather than assumptions about external politeness.
Why the order of state updates matters
Smart contracts enforce rules by reading and writing state. A function may allow a withdrawal only when the caller has enough credit, allow a reward claim only when a flag is false, or allow redemption only when the user owns enough shares.
If external code runs after the check but before the protective state update, the callback can observe the same pre-action state.
Balances
A vault checks a user's balance, sends funds, and reduces the balance afterward. A callback sees the old balance and withdraws again.
Claim flags
A rewards contract verifies that an address has not claimed, transfers a callback-enabled asset, and marks the claim afterward. The recipient hook can claim again before the flag changes.
Share supply
A vault sends assets before burning or reducing the user's shares. A callback can redeem the same shares again or influence an exchange-rate calculation that still includes them.
Debt accounting
A lending protocol transfers collateral or borrowed assets before updating debt. Reentrant logic may borrow again, withdraw collateral, or avoid a solvency check based on the unfinished position.
Reserve accounting
A pool transfers tokens before updating reserves. Another function or another protocol may read the temporary reserve state and make a price-sensitive decision.
Governance and voting state
A system could transfer voting assets, delegate power, or count participation before completing the state changes that prevent duplicate use. The result may affect voting weight rather than directly drain a balance.
Global limits
A protocol may enforce a withdrawal cap, daily limit, mint ceiling, or debt ceiling. If the used amount is updated after an external interaction, several nested calls may each pass the same limit check.
The main types of reentrancy
The classic same-function withdrawal loop is only one form. Modern DeFi systems expose many functions and integrate several contracts, creating broader reentrant paths.
| Reentrancy type | Callback path | State at risk | Main defense |
|---|---|---|---|
| Single-function reentrancy | The callback enters the same function before its first execution completes. | Balance, claim, redemption, mint, withdrawal, or payment state. | Checks-effects-interactions and a reentrancy guard. |
| Cross-function reentrancy | The callback enters a different public function sharing the same state. | Balances, shares, debt, rewards, limits, reserves, or permissions. | Guard all connected entry points and reason about shared invariants. |
| Cross-contract reentrancy | The callback moves through another contract or module before returning to vulnerable shared state. | Protocol-wide accounting spread across vaults, routers, markets, and managers. | System-level locks, synchronized accounting, and invariant-based design. |
| Read-only reentrancy | A callback or external observer reads temporary state during an unfinished transition. | Prices, exchange rates, reserves, collateral values, reward indices, or share values. | Prevent inconsistent views, lock sensitive reads, and avoid unsafe spot-state dependencies. |
| Callback-token reentrancy | A token transfer invokes sender or recipient hooks. | Claims, deposits, withdrawals, rewards, minting, and redemption accounting. | Update state first and treat token movement as an external interaction. |
| Cross-chain or asynchronous reentrancy-like state conflict | Messages and callbacks arrive while related state remains incomplete or duplicated. | Bridge accounting, message status, escrow, and settlement state. | Unique message consumption, finality rules, and explicit state machines. |
Single-function reentrancy
A withdrawal function calls the recipient and the recipient invokes the same withdrawal function again. This pattern is easy to visualize because the recursive path is direct.
Cross-function reentrancy
A callback does not need to enter the same function. Suppose withdraw() temporarily leaves a user's balance unchanged. The callback could enter transferBalance(), borrow(), claimReward(), or redeemShares() if those functions rely on the same balance.
Adding a guard to only the withdrawal function may therefore be insufficient. The security boundary must cover every external entry point that can observe or modify the vulnerable invariant.
Cross-contract reentrancy
Protocols often separate custody, accounting, pricing, rewards, routing, and governance into different contracts. A callback can enter another module that modifies shared economic state or calls back into the first module.
A local function-level guard cannot automatically protect independent contracts. The protocol may need a shared lock, state-machine status, centralized accounting boundary, or design that remains safe under any call order.
Read-only reentrancy
Read-only reentrancy occurs when a contract exposes a view of state that is temporarily inconsistent during an external interaction. The vulnerable read may not modify the first contract, but another protocol trusts it and makes an economic decision.
For example, a vault's exchange rate may be calculated from assets and share supply. During a withdrawal, one side of that ratio may change before the other. A callback causes a lending market to read the temporary rate and overvalue collateral.
A standard nonReentrant modifier on state-changing functions does not automatically prevent another contract from calling a public view function. Developers must identify whether sensitive views can expose intermediate state.
A simplified vulnerable vault example
The following example demonstrates the unsafe ordering without providing a complete attacker implementation. It is deliberately incomplete and must not be deployed.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract UnsafeVault {
mapping(address => uint256)
public balances;
function deposit() external payable {
require(msg.value > 0, "Zero deposit");
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(
amount > 0,
"Nothing to withdraw"
);
(bool success, ) = msg.sender.call{
value: amount
}("");
require(
success,
"Transfer failed"
);
// Unsafe ordering:
// external code already executed.
balances[msg.sender] = 0;
}
}
The vulnerability is the ordering. The recipient receives execution control while its recorded balance is still positive. A callback can re-enter a function that relies on the unchanged balance.
The balance check is correct but no longer sufficient
The contract correctly checks that the caller has funds. The check fails as a security control because the state it relies on remains unchanged during the external call.
The recipient controls the callback behavior
An externally owned account does not execute contract code when it receives ETH. A contract recipient can. The vault should not depend on the recipient being an ordinary wallet.
The final zero assignment cannot account for repeated transfers
Several nested calls can each send the same amount. When they unwind, each call writes zero. The accounting ends at zero even though the vault transferred the amount several times.
Solidity overflow protection does not solve this bug
Modern Solidity versions revert on ordinary arithmetic overflow and underflow, but reentrancy is not primarily an arithmetic error. The wrong number of valid operations occurred before the state transition completed.
Arithmetic safety remains important in related calculations. The integer overflow and underflow guide explains how arithmetic protections differ from control-flow protections.
Checks-Effects-Interactions explained
Checks-effects-interactions is a state-ordering pattern designed to reduce reentrancy risk.
Validate the operation
Confirm authorization, balance, deadline, limits, invariant conditions, supported assets, and any other requirements.
Commit internal state
Reduce balances, burn shares, mark claims, reserve liquidity, update debt, and consume limits before yielding control.
Call external contracts last
Transfer assets, invoke hooks, call routers, notify recipients, or interact with other protocols after state is safe.
Checks
The function first validates all preconditions. For a withdrawal, this includes confirming that the amount is positive, the user owns enough credit, the vault is active, limits are not exceeded, and the caller has the required authority.
Effects
The function then commits the internal accounting changes. It may set the balance to zero, reduce the balance by the withdrawal amount, burn shares, increase withdrawn totals, and update reserve or debt records.
If a callback occurs after this point, the nested call sees the post-withdrawal state and should fail the original eligibility check.
Interactions
Only after state reflects the intended result does the contract send value or call another contract.
A checks-effects-interactions vault
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract CEIVault {
mapping(address => uint256)
public balances;
function deposit() external payable {
require(msg.value > 0, "Zero deposit");
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(
amount > 0,
"Nothing to withdraw"
);
// Effects before interaction.
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{
value: amount
}("");
require(
success,
"Transfer failed"
);
}
}
If the transfer fails, the transaction reverts, including the earlier balance update. If the recipient calls withdraw again during the transfer, the nested call sees a zero balance.
Reverting restores earlier state
Developers sometimes worry that updating state before a transfer could permanently erase a user's balance when the transfer fails. In the example, the failed transfer causes the function to revert. The transaction's state changes are rolled back.
Checks-effects-interactions is a design discipline, not a magic modifier
The pattern must cover the complete invariant. Updating one balance may be insufficient if total supply, debt, reserves, reward indices, withdrawal limits, or another contract's state remains inconsistent.
Some protocols cannot place every interaction last
Flash loans, swaps, hooks, and callback settlement intentionally require an external action before final validation. These protocols need explicit state machines, locks, repayment checks, invariant enforcement, and carefully designed callback interfaces.
How ReentrancyGuard works
ReentrancyGuard provides a lock that prevents nested entry into functions marked with the nonReentrant modifier.
When a protected function begins, the guard changes an internal status to entered. If a callback tries to enter another protected function while that status remains active, the guard reverts. The status is restored after the original function completes.
A guarded vault
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {
ReentrancyGuard
} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract GuardedVault is ReentrancyGuard {
mapping(address => uint256)
public balances;
event Deposited(
address indexed account,
uint256 amount
);
event Withdrawn(
address indexed account,
uint256 amount
);
function deposit() external payable {
require(msg.value > 0, "Zero deposit");
balances[msg.sender] += msg.value;
emit Deposited(
msg.sender,
msg.value
);
}
function withdraw()
external
nonReentrant
{
uint256 amount = balances[msg.sender];
require(
amount > 0,
"Nothing to withdraw"
);
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{
value: amount
}("");
require(
success,
"Transfer failed"
);
emit Withdrawn(
msg.sender,
amount
);
}
}
The example uses both checks-effects-interactions and a reentrancy guard. Defense in depth is preferable to relying on one control while leaving unsafe state ordering in place.
ReentrancyGuard does not identify malicious callers
The guard blocks nested entry regardless of whether the callback is intentional, accidental, or malicious. It is an execution-state lock, not a reputation or address filter.
Only marked functions are protected
A callback can still enter an unguarded function. Developers must identify every external entry point that accesses the same invariant.
Protected functions may need an internal worker pattern
Implementations commonly avoid having one nonReentrant external function call another nonReentrant function directly. Shared logic can be moved into an internal function, while separate external entry points apply the guard.
One contract lock does not automatically lock another contract
Cross-contract reentrancy can move through modules with independent storage. A guard in the vault does not necessarily protect a rewards manager, pricing module, lending market, or router.
A guard can block legitimate composition
Overly broad locking may prevent valid callback workflows or nested operations expected by integrators. Protocols should define which reentrant paths are prohibited and which callbacks are part of the intended architecture.
ReentrancyGuard does not repair incorrect accounting
A function can remain economically unsafe without any nested call. The guard should support correct state transitions, not replace them.
Reentrancy defense layers
Strong protection combines state ordering, execution locks, minimized external trust, invariant checks, and emergency controls.
Commit internal effects first
Reduce balances, consume claims, burn shares, update debt, and mark operation status before external execution.
Guard connected entry points
Apply the execution lock to every function that can observe or modify the protected invariant during callbacks.
Enforce final invariants
Confirm repayment, reserve conservation, collateral safety, share accounting, and protocol-wide state before completion.
Prepare emergency controls
Use monitoring, bounded limits, carefully governed pausing, tested upgrades, and incident procedures.
Pull payments and separating accounting from delivery
A pull-payment design records what a user is owed and lets the user withdraw through a dedicated function. This can separate complex protocol operations from external value delivery.
Push payment model
A function completes an auction, sale, settlement, or reward calculation and immediately sends funds to several recipients. One failing or malicious recipient can interrupt the entire operation and create callback exposure.
Pull payment model
The protocol records each recipient's credit. Recipients claim separately. Each withdrawal can apply checks-effects-interactions and a guard.
Pull payments reduce coupling
The main operation does not need to trust every recipient's callback behavior. Failed withdrawals affect the requesting recipient rather than blocking unrelated accounting.
Credits must still be protected
A pull-payment function can itself be reentrant if it sends funds before reducing the stored credit. The pattern improves architecture but does not eliminate the need for safe ordering.
Unclaimed balances create operational duties
Protocols must decide how long credits remain available, whether administrators can move them, how upgrades preserve them, and how users prove ownership after account changes.
Why a fixed gas stipend is not a complete defense
Older Solidity guidance sometimes treated transfer or send as reentrancy protection because they forwarded a limited gas stipend. That approach is not a robust security boundary.
Gas costs can change
Protocol changes can reprice operations. A callback that was previously too expensive may become possible, while a legitimate recipient contract that once worked may later fail.
Limited gas can break composability
Contract wallets, payment recipients, and application modules may need more execution than the stipend permits. Depending on the stipend can create denial-of-service problems.
Reentrancy can originate through other calls
Even if one ETH transfer path forwards limited gas, token hooks, interface calls, routers, callbacks, and other integrations may forward enough gas for reentry.
Correct state ordering remains necessary
Checks-effects-interactions and explicit guards remain understandable even when gas schedules or external contract behavior changes.
Call return values must be checked
When using low-level calls, the contract must verify success and handle failure consistently. Ignoring a failed call can separate internal accounting from actual asset delivery.
Read-only reentrancy and temporary state
Read-only reentrancy is difficult to detect because the callback may not call a visibly dangerous withdrawal function. Instead, another protocol reads a view function while the first protocol is between state updates.
Temporary exchange rates
A vault may calculate share value from total assets divided by share supply. During a withdrawal, assets and shares may not update at the same moment. A callback asks a lending market to price the vault token using the temporary rate.
Temporary pool reserves
A pool transfers one asset before synchronizing reserves. Another protocol reads the pool's state as a spot-price oracle while the transition is incomplete.
Temporary reward indices
A reward distributor changes balances before updating accumulated reward values, or vice versa. A callback reads or claims based on a transient index.
A view function can be economically dangerous
Solidity's view keyword means the function does not modify state through ordinary execution. It does not mean the returned value is safe to use as an oracle during every possible call context.
Another protocol may be the direct victim
The contract exposing temporary state may end the transaction with correct accounting. The loss occurs in the external protocol that trusted a transient observation.
Mitigating read-only reentrancy
- Update all variables used by sensitive views before yielding control.
- Expose state-machine status so unsafe reads can revert during active transitions.
- Avoid using manipulable spot values as external collateral or pricing sources.
- Use time-weighted, delayed, or independently validated pricing where appropriate.
- Test view functions from callback contexts, not only after transactions complete.
- Document when integrations must not read exchange rates or reserves.
Reentrancy and pricing failures can overlap. The oracle manipulation guide explains how temporary or attacker-controlled prices can influence borrowing, liquidations, minting, redemptions, and protocol solvency.
Token hooks and unexpected callbacks
Developers may reason that an ERC-20 transfer is a simple state update, but integrations frequently support assets and wrappers with more complex behavior.
ERC-777 recipient hooks
ERC-777 can notify a recipient contract through a token-received hook. A protocol transferring such tokens may trigger recipient code before finishing its own accounting.
Sender hooks
Callback standards may also notify the sender before or during token movement. The relevant reentrant path can begin from either side of the transfer.
ERC-721 and ERC-1155 receiver callbacks
Safe NFT transfers call receiver functions on contract recipients. Minting, claiming, staking, and withdrawing NFTs can therefore yield execution control.
Wrapped assets and adapters
A protocol may call an adapter that unwraps, stakes, bridges, or deposits an asset. The adapter can invoke several external contracts, expanding the callback surface.
Upgradeable tokens
A token that behaves as a standard ERC-20 today may gain external-call behavior after an implementation upgrade. Protocols should not rely solely on a past code review of the token.
Malicious tokens
Permissionless protocols may accept arbitrary token addresses. A malicious token can call back from transfer, transferFrom, balanceOf, approval logic, or another custom function if the protocol trusts behavior beyond the standard interface.
Balance checks are external calls too
Calling balanceOf on an untrusted token invokes external code, even though the function is expected to be read-only. Developers should treat unknown token contracts as adversarial.
Users evaluating an unfamiliar token can use the TokenToolHub Token Safety Checker to review token-level ownership, fees, restrictions, and contract indicators. A token scan does not replace a protocol audit or prove that a vault, router, or lending market is reentrancy-safe.
Reentrancy, flash loans, and DeFi composability
A flash loan is not a reentrancy vulnerability. It is a transaction-level liquidity mechanism. An attacker can borrow substantial assets, execute several operations, and repay before the transaction ends.
Flash liquidity can amplify the economic impact of a reentrancy bug by providing temporary capital, manipulating reserves, satisfying deposit requirements, or combining several protocol states in one transaction.
Flash loan callback is intentional
The lender sends assets and calls the borrower's callback. The lender must verify repayment and fees after the callback returns.
Reentrancy can occur inside the callback
The borrower can interact with vulnerable protocols, trigger token hooks, call routers, manipulate temporary state, and invoke nested callbacks before repaying.
Capital is not the root vulnerability
Removing flash loans would not repair unsafe state ordering. An attacker could use owned or externally borrowed capital when economically practical.
Atomicity increases composition
Several protocols can be called in one transaction. A state inconsistency exposed for only a few instructions may still be monetized before the transaction ends.
The flash loan attack guide explains how temporary liquidity interacts with oracle manipulation, governance, liquidations, pool accounting, and other DeFi vulnerabilities.
Cross-function and protocol-wide accounting risk
Reentrancy review should begin with invariants rather than individual functions. An invariant is a condition that must remain true across the protocol.
Vault solvency invariant
Total withdrawable claims should not exceed controlled assets. A callback through any function must not create claims without corresponding assets or withdraw assets without consuming claims.
Lending solvency invariant
A borrower should not withdraw collateral or increase debt beyond what the protocol's validated collateral value supports.
Pool reserve invariant
Trades, liquidity changes, fees, and callbacks must leave reserves consistent with the pool's pricing and accounting rules.
Reward conservation invariant
Users should not claim more rewards than accrued, even when balances, staking shares, and reward indices change during external interactions.
Share conservation invariant
Deposits and withdrawals must keep asset value, minted shares, burned shares, and total supply consistent under every callback sequence.
Function-level locks can hide system-level gaps
A protocol may mark withdraw() as nonReentrant while leaving transferShares(), claim(), borrow(), or updatePosition() unguarded. If those functions share state, a callback can bypass the intended lock boundary.
Modules can disagree about operation status
A vault may know that a withdrawal is active while its pricing module, rewards contract, or lending adapter does not. Cross-contract locks or explicit in-progress state may be necessary when modules share assumptions.
Upgradeable contracts and reentrancy controls
Upgradeable systems add another layer because protection can change while the proxy address and user balances remain the same.
Initialization must establish guard state
Upgradeable components use initialization patterns rather than ordinary constructors. Missing or incorrect initialization can leave security modules or administrative roles in an unexpected state.
Storage layout must remain compatible
An upgrade that changes storage layout incorrectly can corrupt balances, guard status, or accounting variables. The result may create vulnerabilities that resemble or enable reentrancy.
New functions can open unguarded entry paths
An audited implementation may protect all original functions. A later upgrade can introduce a new public function that accesses the same state without applying the lock.
External dependencies can change
An upgrade may replace a token adapter, oracle, router, or settlement contract with one that performs callbacks.
Audits become stale after material upgrades
Users should compare the audit commit and implementation address with the currently deployed code. An audit of version one does not automatically cover version three.
Emergency upgrades need governance controls
Fast response can reduce losses during an active exploit, but unrestricted upgrade authority creates its own risk. Review administrator identity, multisig thresholds, timelocks, pause powers, and public upgrade history.
How users should evaluate audits and protections
Most users will not manually prove a protocol's reentrancy safety. They can still evaluate whether the project provides credible evidence and operational controls.
Audit scope and identity
- Confirm the auditor: Use the auditor's official publication or repository rather than a screenshot supplied by the project.
- Confirm the audit date: Determine whether the review predates important upgrades, migrations, integrations, or token additions.
- Confirm the commit: The report should identify the source commit, tag, or exact code scope reviewed.
- Confirm the contracts: Check whether the vault, proxy, implementation, router, oracle, rewards module, and adapters were included.
- Read unresolved findings: A report can exist while medium, high, or informational risks remain accepted or partially fixed.
- Check retesting: Remediation claims should identify whether fixes were reviewed.
Reentrancy-specific evidence
- Look for external-call mapping: The review should consider ETH transfers, token interactions, receiver hooks, callbacks, routers, oracles, and arbitrary-call paths.
- Look for cross-function review: Guarding one withdrawal function is not enough when several entry points share balances or limits.
- Look for read-only reentrancy analysis: Pricing, share-value, reserve, and collateral views should be tested during unfinished state transitions.
- Look for invariant tests: Strong testing checks solvency, conservation, claim limits, and accounting under unusual call sequences.
- Look for malicious-token tests: Permissionless protocols should test callback-enabled and nonstandard assets.
- Look for upgrade analysis: Proxy initialization, storage compatibility, administrator powers, and implementation changes matter.
Operational security
- Bug bounty: An active program gives researchers a responsible disclosure path.
- Monitoring: The protocol should watch unusual withdrawals, reserve changes, repeated callbacks, share-price deviations, and large atomic transactions.
- Pause controls: Emergency pausing can limit damage, but users should understand who controls it and which functions it covers.
- Withdrawal limits: Rate limits and caps can reduce maximum loss while creating availability and governance tradeoffs.
- Incident plan: Mature projects document communication, investigation, pausing, upgrades, and user guidance.
- Post-deployment review: New integrations and upgrades should receive additional testing and assessment.
An audit is evidence, not a guarantee
Audits are scoped reviews under time and information constraints. They can miss issues, and correct findings can become irrelevant after code changes.
Multiple audits do not prove deployed-code identity
The protocol must connect each report to the deployed proxy and implementation. The smart contract verification guide explains why source verification, proxy resolution, implementation tracking, and constructor or initializer state matter.
How users can reduce exposure to a reentrancy exploit
Users cannot patch protocol code, but they can reduce concentration, verify security evidence, and respond more effectively when warning signs appear.
Limit protocol concentration
Avoid placing a disproportionate share of holdings into one unaudited or highly upgradeable contract. Diversification does not remove technical risk, but it can reduce the impact of one failure.
Verify the exact contract
Confirm that the interface points to the documented vault, router, proxy, and implementation. A copied frontend can direct deposits to unrelated code.
Review upgrade controls
Determine whether one administrator can replace the implementation immediately. Timelocks, multisigs, transparent governance, and upgrade monitoring can provide warning and response time.
Check audit freshness
A report from two years earlier may not cover the current implementation, callback integrations, or newly supported assets.
Watch for emergency communications
During an incident, use official project channels and verify messages independently. Attackers often create fake compensation, migration, or recovery pages.
Do not sign unexplained rescue approvals
A protocol exploit can be followed by phishing campaigns. Wallet connections, approvals, and typed signatures should be reviewed separately from the original vulnerability.
Separate active DeFi funds from long-term custody
Dedicated activity wallets can limit the assets exposed to protocol interactions, malicious approvals, and compromised frontends.
Hardware-wallet options such as Ledger, OneKey, or SafePal can support separated signing and custody workflows. They protect keys, but they cannot make a vulnerable protocol safe or reverse a contract exploit.
Use on-chain evidence during investigation
Analysts can inspect callback sequences, token flows, contract labels, transaction traces, and related addresses. Nansen can support wallet and transaction research on covered networks. Labels should be verified against code, events, traces, and official incident reports.
What protocols should do during a suspected reentrancy incident
Reentrancy exploits can move quickly because the entire loop may execute inside one transaction. Response controls are most useful for stopping later transactions, protecting unaffected markets, and preventing repeated exploitation through related paths.
Confirm the execution path
Trace external calls, callbacks, nested functions, token movements, balance changes, and the first inconsistent state transition.
Contain affected functions
Pause vulnerable entry points, disable integrations, restrict new deposits, or apply bounded controls through authorized governance.
Protect dependent systems
Notify integrators that may rely on affected prices, shares, collateral, pools, bridges, or token allowances.
Verify and disclose remediation
Publish affected contracts, transaction evidence, fix scope, independent review, upgrade details, and user guidance.
Trace the complete call stack
Investigators should identify the original entry point, every external call, each callback, state values at each level, and the function where the invariant first became reusable.
Do not assume the visible drain function is the root cause
The final transfer may occur through a normal redemption or liquidation function. The original defect may be an earlier callback that inflated shares, avoided debt, manipulated a price, or duplicated credit.
Review related contracts
Shared libraries, cloned markets, vault versions, adapters, and networks may contain the same pattern. Fixing one deployment while leaving equivalent code active can allow repeated incidents.
Protect downstream integrators
Lending markets, aggregators, bridges, and derivative protocols may rely on affected exchange rates or balances. They may need to pause collateral, freeze pricing, or adjust risk parameters.
Preserve evidence
Save transaction traces, source commits, implementation addresses, storage values, logs, governance actions, administrator activity, and monitoring alerts before upgrades alter the environment.
Communicate through verified channels
Publish the affected addresses and specific user actions. Vague warnings create opportunities for fake support and recovery scams.
TokenToolHub Research Note: reentrancy is a control-flow bug, not just a balance bug
Reentrancy is commonly taught through a vault that sends ETH before clearing a balance. That example is useful, but it can narrow the reviewer's attention to direct withdrawals.
The deeper issue is that external execution interrupts a state transition. Any rule that depends on the transition being complete may fail during the interruption.
What is temporarily inconsistent?
Identify balances, shares, debt, reserves, prices, claims, limits, rewards, roles, or operation status that have not reached final state.
Where does execution leave?
Map ETH transfers, token calls, hooks, routers, oracles, bridges, callbacks, adapters, and receiver interfaces.
Where can execution return?
Review every public or external function, proxy route, module, fallback, callback, and cross-contract entry point.
What must remain true?
Define solvency, conservation, withdrawal limits, share value, debt safety, claim uniqueness, and protocol-wide accounting rules.
This framework explains why a balance-zeroing fix may be incomplete. The same callback may exploit a different function, read a transient exchange rate, trigger another module, or use unfinished accounting in an integrated protocol.
Reentrancy review should therefore model control flow across the whole protocol. The question is not only whether a function sends funds before clearing a balance. The question is whether any external execution can observe or change state before the protocol's invariant is fully established.
Reentrancy risk matrix
| Contract pattern | Risk level | Main concern | Review priority |
|---|---|---|---|
| External transfer before clearing user balance | Critical | Same-function withdrawal can reuse the old balance. | Move effects before interaction and apply a guard. |
| One guarded function with other unguarded functions sharing state | High | Callback can bypass the guarded entry point through another function. | Map shared invariants and guard every connected path. |
| Vault price exposed during withdrawal callback | High integration risk | Another protocol may read a temporary exchange rate. | Review read-only reentrancy and downstream oracle use. |
| Permissionless support for arbitrary tokens | High | Malicious or callback-enabled tokens can execute unexpected code. | Test adversarial tokens and minimize assumptions about interfaces. |
| CEI plus ReentrancyGuard on all shared entry points | Lower, not zero | Economic logic, cross-contract state, or unsafe views may remain vulnerable. | Test protocol invariants and integrated callbacks. |
| Flash loan with explicit lock and repayment invariant | Designed callback risk | Borrower controls complex execution before repayment. | Verify repayment, fees, accounting, callback identity, and nested integrations. |
| Upgradeable protocol with old audit | Uncertain | Current implementation may differ from audited code. | Resolve proxy, implementation, upgrade history, and current source. |
| Push payments to many arbitrary recipients | Moderate to high | Callbacks and failures can interrupt settlement. | Consider pull payments and isolated withdrawals. |
| Use of transfer as the only claimed defense | Weak security assumption | Gas behavior can change and other callback paths remain. | Use state discipline and explicit guards instead. |
Reentrancy checklist for smart contract builders
Map external control flow
- List every external call: Include ETH sends, token functions, receiver hooks, routers, adapters, oracles, bridges, callbacks, delegate calls, and arbitrary-call facilities.
- Assume callbacks are possible: Do not rely on current target behavior when the target is upgradeable or user-supplied.
- Map fallback and receive functions: Incoming value and unknown calls can create additional execution paths.
- Review view calls to untrusted contracts: A read-looking interface call still executes external code.
- Review callback standards: Include NFT receivers, token hooks, flash-loan callbacks, swap callbacks, and account hooks.
Protect state transitions
- Use checks-effects-interactions: Commit internal accounting before yielding control when architecture permits.
- Define invariants: State the solvency, conservation, debt, share, reserve, claim, and limit conditions that must always hold.
- Update complete accounting: Do not fix one balance while leaving total supply, reserves, debt, or limits temporarily inconsistent.
- Consume one-time rights early: Mark claims, withdrawals, orders, and messages used before external execution.
- Use explicit operation states: Complex callback protocols may need active, settled, cancelled, or locked states.
Apply guards deliberately
- Guard all shared entry points: Include cross-function paths that read or modify protected state.
- Review cross-contract locks: Independent modules may require shared operation status or protocol-level coordination.
- Do not rely only on a modifier: Preserve correct effects ordering and final invariant checks.
- Avoid accidental lock conflicts: Structure external guarded functions around internal workers where appropriate.
- Document intentional callbacks: Explain which reentrant paths are expected and which are prohibited.
Test adversarial execution
- Use callback test contracts: Attempt same-function, cross-function, cross-contract, and read-only reentry.
- Use malicious tokens: Test tokens that call back, fail, return unusual values, charge fees, or change balances unexpectedly.
- Fuzz call sequences: Vary deposits, withdrawals, callbacks, prices, shares, debt, and limits.
- Test invariants: Assert protocol solvency and accounting conservation across arbitrary transaction sequences.
- Test upgrades: Repeat reentrancy and invariant tests against every new implementation.
- Test downstream reads: Call price and share-value functions during active callbacks.
Practical reentrancy scenarios
Scenario one: classic ETH withdrawal
A vault sends ETH before reducing the caller's balance. The recipient contract calls withdraw again from its receive function.
The nested call sees the original balance and sends another payment. Checks-effects-interactions and a guard prevent the balance from being reused.
Scenario two: reward token callback
A rewards contract transfers a callback-enabled token before marking the user as claimed. The recipient hook calls claim again.
The fix is to consume the claim state before transferring the token and protect related claim functions.
Scenario three: cross-function vault exploit
A guarded withdrawal function sends assets after reducing one balance, but an unguarded share-transfer function reads another still-inconsistent accounting value.
The callback enters the share-transfer function and moves rights that should have been locked during withdrawal. Function-level review misses the shared invariant.
Scenario four: read-only exchange-rate manipulation
A vault begins removing assets before burning shares. During a callback, a lending protocol reads the temporarily inflated exchange rate and allows excessive borrowing.
The vault may finish with internally correct state, but the lending protocol has already made an unsafe decision.
Scenario five: NFT claim callback
A contract safely mints an NFT to a contract recipient before incrementing the recipient's claimed count. The receiver callback calls the mint function again.
Claim state should be updated before the safe mint triggers the receiver hook.
Scenario six: flash loan settlement
A lender sends assets and invokes the borrower callback. The lender has a lock and later verifies repayment, but another unguarded fee-withdrawal function shares accounting with the loan.
The borrower enters the unguarded function during the callback. The protocol must protect the complete loan invariant, not only the flash-loan entry point.
Scenario seven: arbitrary token deposit
A vault supports any token address. A malicious token calls back during transferFrom before the vault finishes crediting or validating the deposit.
The vault must treat the token as untrusted external code and order accounting so callbacks cannot create duplicate credit.
Scenario eight: proxy upgrade introduces callback behavior
A protocol was audited when its token adapter made simple transfers. A later adapter upgrade calls an external staking contract during withdrawal.
The new callback surface invalidates assumptions from the earlier review. Post-upgrade testing and audit are necessary.
Scenario nine: pull payment implemented incorrectly
A settlement contract correctly credits recipients instead of paying them immediately. Its separate withdrawal function sends funds before reducing the credit.
The architecture reduced coupling, but the final withdrawal remains reentrant. Pull payments still require safe state ordering.
Scenario ten: oracle read during liquidation
A liquidation changes collateral and debt across several calls. During an external token transfer, a callback causes another market to read a temporary health factor.
Protocol-wide state sequencing and price isolation are needed because the loss may occur outside the contract performing the liquidation.
Related TokenToolHub research
Reentrancy frequently overlaps with contract verification, reusable security components, temporary liquidity, oracle design, token behavior, and arithmetic safety.
Smart contract verification
Use the smart contract verification guide to resolve source code, proxies, implementations, constructor data, metadata, and deployed-bytecode identity.
OpenZeppelin Contracts
Read the OpenZeppelin Contracts guide for ReentrancyGuard, access controls, pausing, token utilities, inheritance, and safe integration practices.
Flash loan attacks
Use the flash loan attack guide to understand temporary liquidity, callback execution, atomic composition, and exploit amplification.
Oracle manipulation
Read the oracle manipulation guide for spot-price risk, temporary state, collateral valuation, liquidations, and price-source design.
Token Safety Checker
Use the Token Safety Checker to review token-level ownership, restrictions, fees, and contract indicators before interacting with unfamiliar assets.
Integer overflow and underflow
Read the integer safety guide to separate arithmetic failures from control-flow and accounting failures.
Common misconceptions about reentrancy
Reentrancy only affects ETH withdrawals
False. Token hooks, NFT receivers, flash loans, swaps, oracles, adapters, reward systems, and cross-contract modules can all create callbacks.
Only the same function needs protection
False. A callback can enter another function that shares the same balances, shares, debt, rewards, reserves, limits, or permissions.
ReentrancyGuard makes state ordering irrelevant
False. Correct accounting and checks-effects-interactions remain important. Unguarded functions, other contracts, unsafe views, and economic logic can remain vulnerable.
Checks-effects-interactions prevents every form of reentrancy
False. The pattern substantially reduces risk, but complex callbacks, protocol-wide state, read-only reentrancy, and cross-contract accounting may require additional controls.
A view function cannot cause a loss
False. Another protocol can make an economic decision using a temporary value returned during an unfinished state transition.
Using transfer instead of call solves reentrancy
False. Security should not depend on a fixed gas stipend, and reentrancy can occur through many other external-call paths.
Modern Solidity automatically prevents reentrancy
False. Modern compiler checks help with arithmetic and language safety, but developers must still design external interactions and state transitions correctly.
A verified contract is reentrancy-safe
False. Verification makes code review possible. It does not prove that the code is correct or that the verified implementation is the current proxy target.
An audited contract cannot have a reentrancy bug
False. Audits are scoped reviews and may miss issues. Later upgrades and integrations can also introduce new callback paths.
Flash loans cause reentrancy
False. Flash loans provide temporary liquidity and a callback structure. Unsafe state handling creates the vulnerability.
Only malicious contracts call back
False. Legitimate token standards, NFT receivers, swap systems, flash lenders, smart wallets, and protocol adapters use callbacks intentionally.
A hardware wallet protects funds inside a vulnerable protocol
False. A hardware wallet protects the signing key. Once assets are deposited into a vulnerable contract, protocol code governs their behavior.
Conclusion: secure the whole state transition before yielding control
Reentrancy begins when a contract gives external code an opportunity to execute before the protocol has completed the state changes that define the current operation.
The classic withdrawal loop demonstrates the principle: a balance is checked, value is sent, a callback repeats the withdrawal, and the balance is cleared too late. Modern DeFi systems create more complex versions involving shares, debt, rewards, reserves, token hooks, NFT receivers, swap callbacks, flash loans, oracles, routers, and upgradeable modules.
Checks-effects-interactions reduces the vulnerable window by committing internal accounting before external calls. ReentrancyGuard blocks nested entry into protected functions. Pull payments separate complex settlement from value delivery. Explicit state machines and final invariant checks support protocols that intentionally require callbacks.
These controls must be applied across the complete protocol. Guarding one function does not protect another function that shares state. Locking one contract does not automatically protect another module. A read-only function can expose a temporary price or exchange rate even when no direct balance drain occurs.
Users should evaluate more than the presence of an audit badge. Confirm the audited commit, current proxy implementation, contract verification, upgrade history, unresolved findings, callback coverage, bug bounty, monitoring, and emergency controls.
Builders should map every external call, assume callbacks are possible, define protocol invariants, update state before interaction when possible, guard connected entry points, test malicious tokens, simulate cross-function reentry, and examine temporary state from the perspective of downstream protocols.
Your next action is to open the TokenToolHub smart contract verification guide and verify whether the protocol's current implementation matches the code and audit you are relying on. Then inspect where the contract makes external calls, which functions share state, and whether the documented protections cover the full callback surface.
Verify the protection, not just the audit badge
Resolve the deployed implementation, inspect external calls, confirm checks-effects-interactions, identify guarded functions, review upgrade controls, and check whether audits cover the current protocol version.
FAQs
What is a reentrancy attack?
A reentrancy attack occurs when external code calls back into a smart contract before the original execution has completed the state changes needed to protect the operation.
Why are external calls dangerous in Solidity?
An external call transfers execution control to another address. If that address is a contract, it can run code and call the original contract again before the first call returns.
Does reentrancy always involve withdrawing ETH?
No. Reentrancy can affect tokens, NFTs, rewards, shares, debt, reserves, prices, claims, governance, lending positions, flash loans, and protocol integrations.
What is the classic reentrancy sequence?
The contract checks a balance, makes an external transfer, receives a callback, repeats the action using unchanged state, and updates the balance only after the callback ends.
What is checks-effects-interactions?
It is a Solidity design pattern that validates conditions first, commits internal state changes second, and makes external calls last.
Why does checks-effects-interactions reduce reentrancy risk?
A callback observes the completed internal state, so a repeated withdrawal, claim, or redemption should fail the same eligibility check.
What is ReentrancyGuard?
ReentrancyGuard is a contract component that provides a nonReentrant modifier to block nested entry into protected functions while one protected execution is active.
Does ReentrancyGuard protect every function automatically?
No. Only functions covered by the guard are protected, and independent contracts may use separate lock state.
Should developers use both CEI and ReentrancyGuard?
Using both can provide defense in depth. Correct state ordering remains important even when a guard is present.
What is cross-function reentrancy?
It occurs when a callback enters a different function that reads or modifies state shared with the interrupted function.
What is cross-contract reentrancy?
It occurs when the callback path moves through several contracts or modules and exploits protocol-wide state that is not protected by one local lock.
What is read-only reentrancy?
Read-only reentrancy occurs when another protocol reads temporarily inconsistent state, such as a price or exchange rate, during an unfinished external interaction.
Can a view function create reentrancy risk?
Yes. A view function can expose a temporary value that another protocol uses for borrowing, pricing, liquidation, minting, or collateral decisions.
Can ERC-20 token calls cause reentrancy?
Potentially. Tokens can contain custom logic, hooks, upgrades, fees, or malicious behavior. Protocols should treat unknown token contracts as external code.
Can NFT transfers cause callbacks?
Yes. Safe ERC-721 and ERC-1155 transfers call receiver functions when the recipient is a contract.
Are flash loans a form of reentrancy?
No. Flash loans intentionally use callbacks and temporary liquidity. Reentrancy appears when a protocol mishandles state during nested execution.
Can flash loans amplify a reentrancy exploit?
Yes. Temporary liquidity can increase the economic scale of a larger exploit involving reentrancy, pricing, collateral, reserves, or accounting.
Does Solidity 0.8 prevent reentrancy?
No. Solidity 0.8 provides arithmetic safety checks for ordinary overflow and underflow, but smart contract developers must still secure external control flow.
Is transfer safer than call for preventing reentrancy?
A fixed gas stipend should not be treated as a complete defense. Correct state ordering, guards, and invariant checks are more reliable security controls.
What is a pull-payment pattern?
A pull-payment pattern records funds owed to users and lets them withdraw separately instead of pushing payments during a larger operation.
Can a pull-payment function still be reentrant?
Yes. The withdrawal function must reduce the user's credit before making the external transfer and should use appropriate protection.
Does an audit guarantee that a protocol is reentrancy-safe?
No. Audits are scoped reviews, may miss issues, and can become outdated after upgrades, new integrations, or contract migrations.
What should users verify in a smart contract audit?
Check the auditor, date, source commit, contract scope, proxy implementation, unresolved findings, remediation review, and whether the deployed code matches the audited version.
Can an upgrade introduce reentrancy?
Yes. A new implementation can add external calls, callback-enabled integrations, unguarded functions, or incompatible storage changes.
Can a hardware wallet prevent a protocol reentrancy exploit?
No. A hardware wallet protects signing keys. It cannot repair vulnerable smart contract code after assets are deposited.
What should a protocol do during a reentrancy incident?
Trace the full call path, contain affected functions, protect dependent protocols, preserve evidence, verify remediation, and communicate through official channels.
What is the most important reentrancy review question?
Ask whether any external execution can observe or modify protocol state before the complete invariant for the current operation has been established.
References and further learning
Use maintained technical documentation and primary standards when reviewing Solidity control flow, callback behavior, reentrancy protection, and token hooks.
- Solidity Documentation: Security Considerations
- Solidity Documentation: External Calls and Control Structures
- OpenZeppelin Contracts: ReentrancyGuard
- OpenZeppelin Contracts Documentation
- Ethereum.org Smart Contract Security
- ERC-777 Token Standard and Recipient Hooks
- EIP-1153 Transient Storage Opcodes
This TokenToolHub guide is educational research only. It is not investment advice, trading advice, legal advice, tax advice, cybersecurity advice, accounting advice, or a smart contract audit. Always verify the deployed contract, proxy implementation, source code, external calls, state-update order, guarded functions, callback interfaces, audit scope, upgrade controls, and protocol-wide invariants before depositing assets.