100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Reentrancy and Common Vulnerabilities

How reentrancy attacks drain contracts by re-entering functions before state updates, plus the defensive patterns and other common Solidity vulnerability classes.

Security & StandardsIntermediate11 min readJul 10, 2026
Analogies

Understanding Reentrancy Attacks

Reentrancy is the most infamous Solidity vulnerability: it occurs when a contract makes an external call to another (potentially malicious) contract before it has finished updating its own internal state. Because the external contract can call back into the original function while the first invocation is still executing, it sees a stale balance that has not yet been reduced. The 2016 DAO hack, which drained roughly 3.6 million ETH, was a reentrancy exploit. The root cause is always an ordering mistake: interacting with the outside world before persisting the effects of that interaction.

🏏

Cricket analogy: It is like a no-ball free hit where the batsman smashes runs before the fielders reset their positions; the contract's balance is not 'reset' before the external call, so the attacker keeps scoring off the same delivery.

How the Attack Works

In a classic reentrancy exploit the attacker deploys a malicious contract whose fallback or receive function calls back into the victim's withdraw. When the victim uses a low-level call to send ETH, control transfers to the attacker's code before the victim zeroes the balance. The attacker's receive function immediately calls withdraw again, and because balances[msg.sender] is still non-zero, the check passes and more ETH is forwarded. This recursion repeats until the victim's ETH is exhausted or the gas runs out, all inside a single transaction.

🏏

Cricket analogy: It behaves like an overthrow where the ball keeps rolling to the boundary because no fielder backs up; the attacker's receive function keeps sending the ball back for more runs on every call.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// VULNERABLE: external call happens before the state update
contract VulnerableVault {
    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");

        // Control transfers to msg.sender here...
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "Transfer failed");

        balances[msg.sender] = 0; // ...but the balance is zeroed too late
    }
}

The Checks-Effects-Interactions Pattern

The primary defense is the Checks-Effects-Interactions ordering: first validate preconditions (checks), then update all internal state (effects), and only then interact with external addresses (interactions). By zeroing balances[msg.sender] before the call, any reentrant call finds a zero balance and reverts on the require. A complementary defense is a reentrancy guard, such as OpenZeppelin's ReentrancyGuard and its nonReentrant modifier, which sets a lock flag on entry and reverts if the function is entered again before it returns. Using both a correct ordering and a guard is standard defense-in-depth.

🏏

Cricket analogy: It is like a wicketkeeper who whips off the bails to record the dismissal before appealing to the umpire, so no batsman can sneak a run because the state is committed first.

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

// SAFE: Checks-Effects-Interactions plus a reentrancy guard
contract SafeVault is ReentrancyGuard {
    mapping(address => uint256) public balances;

    function withdraw() external nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Nothing to withdraw"); // Check

        balances[msg.sender] = 0;                    // Effect first

        (bool ok, ) = msg.sender.call{value: amount}(""); // Interaction last
        require(ok, "Transfer failed");
    }
}

Reentrancy is not limited to ETH transfers. Any external call can hand control to attacker code: ERC-777 token hooks, ERC-721 safeTransferFrom's onERC721Received callback, and arbitrary contract calls can all trigger cross-function and read-only reentrancy. Guard every function that changes state around an external call, not just those that send ETH.

Other Common Vulnerabilities

Reentrancy is one of several recurring bug classes. Integer overflow and underflow were a major hazard before Solidity 0.8, which now reverts on overflow by default (bypassable only inside unchecked blocks). Authorization using tx.origin instead of msg.sender enables phishing, because an intermediary contract can relay a victim's transaction. Unchecked return values from low-level calls can silently swallow failures. Denial-of-service arises from unbounded loops that exceed the block gas limit, or from pushing funds to an address that always reverts. Recognizing each class as a distinct pattern is the core of secure Solidity review.

🏏

Cricket analogy: Just as bowled, LBW, and run-out are distinct ways to lose your wicket, overflow, tx.origin phishing, and unchecked calls are distinct ways a contract loses.

Since Solidity 0.8.0 arithmetic is checked by default and reverts on overflow or underflow, so the old SafeMath library is no longer needed for basic math. Use an unchecked { } block only when you have proven the operation cannot overflow, typically to save gas on loop counters.

  • Reentrancy happens when a contract makes an external call before updating its own state, letting the callee re-enter with stale data.
  • The Checks-Effects-Interactions pattern (validate, then update state, then call out) is the primary structural defense.
  • OpenZeppelin's ReentrancyGuard nonReentrant modifier adds a lock as defense-in-depth on top of correct ordering.
  • Reentrancy can be triggered by any external call, including ERC-721 and ERC-777 receive hooks, not just raw ETH transfers.
  • Solidity 0.8+ makes arithmetic overflow revert by default, removing the need for SafeMath outside unchecked blocks.
  • Use msg.sender, never tx.origin, for authorization to avoid phishing through intermediary contracts.
  • Always check the boolean return value of low-level calls and avoid unbounded loops that can hit the block gas limit.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#SolidityStudyNotes#ReentrancyAndCommonVulnerabilities#Reentrancy#Common#Vulnerabilities#Attacks#Security#StudyNotes#SkillVeris