Free tools Windows power users keep installed
One-click scans. No signup required.
Reentrancy is an unexpected control-flow bug: a contract calls untrusted code before completing its own state update, and that code calls back into the contract while its accounting is still stale. The classic safe order is checks → effects → interactions, backed by focused tests and, where appropriate, a reentrancy guard.
Everything below is for a local sandbox. Do not deploy the demonstration attacker against a live protocol or third-party contract.
Reentrancy in one minute
The Ethereum Virtual Machine executes instructions sequentially; reentrancy is not simultaneous multithreading. The danger is that an external call transfers control to another contract before the first contract has finished its state transition. The callee can then enter the original contract again.
That differs from ordinary recursion. Recursion is deliberately structured code calling itself or another function. Reentrancy is an external contract regaining control during an incomplete operation. Ethereum’s security guidance describes the classic pattern and the checks-effects-interactions remedy at ethereum.org’s smart-contract security documentation.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstall#1 Best Overall
| Unsafe order | Safe order |
|---|---|
| Check balance → external call → update balance | Check balance → update balance → external call |
Why external calls are a security boundary
Any interaction with an untrusted contract can transfer control. That includes sending Ether with call, calling an arbitrary contract function, and interacting with token contracts:
(bool ok, ) = payable(msg.sender).call{value: amount}("");
otherContract.externalFunction();
token.transfer(to, amount);
An ERC-20-shaped interface does not guarantee code-free behavior. A malicious or non-standard token can execute code during a transfer or callback, and NFT and token standards may invoke receiver or sender hooks. Solidity’s security considerations explain why external calls must be treated as potential control-flow transfers: docs.soliditylang.org/en/latest/security-considerations.html.
A deliberately vulnerable bank
This Solidity 0.8.20 example is educational only and is not production code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableBank {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing to withdraw");
// Vulnerable: control leaves the contract first.
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "ETH transfer failed");
// Too late: a callback may already have re-entered.
balances[msg.sender] = 0;
}
}
The problem is not that call is forbidden. The problem is that the balance remains nonzero while the recipient’s code is running.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Local-only attacker and call trace
For a local test, the recipient can deliberately call withdraw again from its receive function:
Rank #2
contract ReentrancyAttacker {
VulnerableBank public immutable bank;
uint256 public attackCount;
constructor(address payable bankAddress) {
bank = VulnerableBank(bankAddress);
}
function beginAttack() external payable {
require(msg.value > 0, "Seed required");
bank.deposit{value: msg.value}();
bank.withdraw();
}
receive() external payable {
attackCount++;
if (address(bank).balance >= 1 ether) {
bank.withdraw();
}
}
function collect() external {
payable(msg.sender).transfer(address(this).balance);
}
}
The important sequence is:
- An externally owned account calls
beginAttack. - The attacker deposits its seed Ether and calls the bank’s withdrawal function.
- The bank sends Ether to the attacker contract.
- The attacker’s
receivefunction runs before the first withdrawal sets the balance to zero. receivecallswithdrawagain, so the bank reads the same stale balance.- The cycle ends when the callback condition is false, available funds are exhausted, or the transaction runs out of gas.
At the first reentrant check, the victim still records 1 ETH for the attacker even though Ether has already left:
| Execution point | Recorded attacker balance | Victim Ether balance |
|---|---|---|
| Before attack | 1 ETH | Seed plus other funds |
| First withdrawal check | 1 ETH | Unchanged |
| Inside first callback | 1 ETH | Reduced by first send |
| Reentrant withdrawal check | Still 1 ETH | Reduced again |
| After final unwind | 0 ETH | Potentially drained |
The exact amount depends on the bank’s available funds, gas, and callback condition. The point is the stale accounting, not a target balance.
Fix one: checks-effects-interactions
Move the accounting update before the external call:
Recommended Free Tools
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing to withdraw");
// Effect: commit the state transition first.
balances[msg.sender] = 0;
// Interaction: transfer control last.
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "ETH transfer failed");
}
The three stages are:
- Checks: validate permissions, balances, limits and other preconditions.
- Effects: update storage to represent the completed operation.
- Interactions: call external contracts or send Ether.
If the attacker re-enters after the balance is set to zero, the nested call fails its balance check. CEI is foundational, but it is not a proof that every invariant, callback path or cross-contract interaction is safe.
Fix two: OpenZeppelin’s reentrancy guard
With OpenZeppelin Contracts 5.x, import the storage-based guard explicitly:
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SafeBank is ReentrancyGuard {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "Nothing to withdraw");
balances[msg.sender] = 0;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "ETH transfer failed");
}
}
The nonReentrant modifier blocks nested entry into functions using that guard. It does not secure unguarded sibling functions, other contracts, proxy storage assumptions or the protocol’s wider invariants. OpenZeppelin’s API is documented at docs.openzeppelin.com/contracts/5.x/api/utils.
The shared-lock limitation
Two nonReentrant functions cannot directly call one another because they share one lock. Expose a guarded external entry point and put shared logic in a private function:
function withdraw() external nonReentrant {
_withdraw(msg.sender);
}
function _withdraw(address account) private {
// Core state transition and interaction
}
Transient-storage variant
OpenZeppelin 5.x also documents ReentrancyGuardTransient, imported from @openzeppelin/contracts/utils/ReentrancyGuardTransient.sol. It requires a network supporting EIP-1153. Treat its availability as version- and network-dependent; do not assume a universal gas saving or migration requirement. See the implementation notes at github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/ReentrancyGuardTransient.sol.
Fix three: pull payments
Instead of delivering Ether during another business operation, record what a recipient is owed and let the recipient claim it separately:
mapping(address => uint256) public pendingPayments;
function recordPayment(address recipient, uint256 amount) internal {
pendingPayments[recipient] += amount;
}
function withdrawPayment() external nonReentrant {
uint256 amount = pendingPayments[msg.sender];
require(amount > 0, "No payment due");
pendingPayments[msg.sender] = 0;
(bool ok, ) = payable(msg.sender).call{value: amount}("");
require(ok, "Payment failed");
}
Pull payments separate business accounting from delivery, reduce accidental callbacks to arbitrary recipients and can limit denial-of-service caused by one recipient reverting. Users must claim separately, accounting must remain correct, and the withdrawal path still needs CEI or a guard.
Rank #4
Why transfer and send are not complete defenses
Older tutorials recommend transfer or send because they forward a 2,300-gas stipend. Gas-cost changes can invalidate assumptions about what fits in that stipend, and current static-analysis guidance warns against treating either operation as a reentrancy guarantee. Slither documents this limitation at github.com/crytic/slither/wiki/Detector-Documentation. Make state updates safe first, then choose an Ether-transfer mechanism appropriate for the application. OpenZeppelin’s call utility guidance is at github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol.
Variants auditors must consider
Cross-function reentrancy
A guard on withdraw may not protect transfer, claim or borrow if those functions read or write the same state. Review invariants across every externally callable function.
Cross-contract reentrancy
Contract A can call B, and B can call back into A. The vulnerable state may be distributed across several contracts, so reviewing one source file is insufficient.
Read-only reentrancy
A view function can expose intermediate state while another function is executing. A separate protocol may use that value for pricing, collateral or accounting. OpenZeppelin 5.x provides nonReentrantView() for this specific coordination problem; it does not make a view function state-changing or prove the underlying design safe.
Token and NFT callbacks
ERC-721 transfers can invoke onERC721Received; ERC-777-style transfers can invoke sender or recipient hooks; arbitrary token contracts can behave maliciously around transfers or balance queries. Treat these paths as external interactions.
Proxies, delegatecall and failed calls
delegatecall runs implementation code in proxy storage, so guard slots and upgrade assumptions must be reviewed together. Separately, every failed external call needs an intentional policy: revert the whole operation or preserve a withdrawal credit. A pause switch may contain an incident, but it does not repair the bug.
OWASP classifies reentrancy as a current smart-contract risk, including several variants: OWASP SC05: Reentrancy Attacks.
Testing the vulnerable and fixed versions
Minimum local test plan
- Use a local EVM or development framework; never test the exploit against a live third-party protocol.
- Give the victim the attacker’s 1 ETH deposit plus another account’s funds.
- Assert that the vulnerable transaction succeeds and the attacker receives more than its recorded deposit.
- Run the same scenario against the CEI-fixed contract and assert that the attacker cannot withdraw more than its balance.
- Test a recipient whose
receivefunction reverts. Decide whether the operation intentionally reverts or creates a claimable credit. - Test repeated callbacks, token hooks and NFT receiver hooks where the production system supports them.
Guard-specific tests
- A direct protected call succeeds.
- A nested call to another guarded function fails.
- An external guarded entry point calling a private core succeeds.
- The guard resets after a reverted transaction.
Static analysis and fuzzing
Run Slither from the project root:
slither .
slither . --triage-mode
Its documented detectors include reentrancy-eth, reentrancy-no-eth, reentrancy-benign, reentrancy-events, reentrancy-unlimited-gas and reentrancy-balance. Slither finds recognizable patterns, but it cannot prove economic safety or every cross-contract invariant. Add fuzz and invariant tests that assert total liabilities, balances and authorization rules remain consistent across arbitrary callback sequences.
Production review checklist
- Locate every external call, Ether transfer, token operation and receiver hook.
- Confirm storage effects occur before interactions where the invariant requires it.
- Review all state-sharing sibling functions, not just the function that sends Ether.
- Identify whether any recipient can be a contract, even if an EOA was expected.
- Use a guard where appropriate, while checking composability and unguarded paths.
- Review proxy, delegatecall and upgrade storage assumptions.
- Test reverting recipients and define recovery behavior.
- Do not treat
transfer,send, pausing, static analysis or an audit as a universal fix. - Pin compiler and library versions; avoid legacy syntax such as
.call.value(amount)(). - Keep exploit demonstrations in isolated local environments.
Safe ways to keep learning
For guided practice, use OpenZeppelin Ethernaut and Damn Vulnerable DeFi. For defensive building blocks, consult OpenZeppelin Contracts. A simulator such as Tenderly can help trace local or forked transactions without broadcasting them. None of these tools replaces protocol-specific testing and review.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




