Skip to main content
Create your own

Reentrancy: Checks-Effects-Interactions Pattern

Welcome back. In our last lesson, we explored function modifiers and saw how they can "sandwich" a function's logic between pre- and post-execution code. We focused on access control but I noted this capability is also critical for advanced security patterns. Today, we address one of the most famous and dangerous vulnerabilities in smart contract history: reentrancy.

This lesson is crucial for your goal of building applications for tokenized money. Contracts that handle financial value are prime targets for attacks, and reentrancy has been responsible for millions of dollars in losses, most notably in the infamous 2016 DAO hack. We will dissect this vulnerability to understand how it works and then learn the fundamental design pattern to prevent it: Checks-Effects-Interactions. For someone with your background in ETL and database systems, you can think of this as ensuring all internal state changes are committed before triggering any external processes, preventing race conditions and ensuring data integrity.

By the end of this lesson, you will be able to describe how a reentrancy attack is executed and apply the Checks-Effects-Interactions pattern to write secure functions that are immune to this threat.

1. Understanding the Reentrancy Vulnerability

At its core, a reentrancy attack occurs when a function in a smart contract makes an external call to another, untrusted contract before it has finished updating its own state. The untrusted contract can then use this opportunity to call back into the original function—re-entering it—before the first invocation is complete.

Because the original function hasn't updated its state yet (e.g., reducing a user's balance), its internal checks can be passed multiple times, allowing the attacker to repeatedly execute a privileged action like a withdrawal.

Solidity Tutorial on Reentrancy Attacks in Smart Contracts

The Cyfrin blog provides an excellent definition and breakdown of the problem.

Please read the introduction, starting from the top down to just before the "Types of smart contract Reentrancy Attack" section. Focus on the definition and the explanation that reentrancy is fundamentally a state synchronization problem. Pay close attention to the vulnerable order of operations described: Checks -> Interactions -> Effects.

The vulnerable sequence—checking a balance, sending funds, and then updating the balance—is the root of the issue. When the funds are sent to an attacker's contract, it can trigger a receive or fallback function. This function can then recursively call the withdraw function again. Since the balance in the victim contract hasn't been updated, the initial check passes, and more funds are sent. This loop continues until the contract is drained.

The diagram below illustrates this malicious cycle.

This diagram shows the flow of a reentrancy attack. Contract X sends funds to Contract Y (1), but before updating its own balance, Contract Y's fallback function calls back into Contract X (2), exploiting the stale state to withdraw funds again.

To see this in action, let's watch a clear explanation that uses a simple bank contract as an example.

Smart Contracts Hacking: ReEntrancy Attack in Solidity Explained with EASY Examples

This video from JohnnyTime provides a great visual and code-based explanation of the attack.

First, watch the conceptual overview from the start. This section explains how a malicious contract uses a fallback function to create an endless loop. Then, watch the walkthrough of the vulnerable code, which pinpoints the exact issue in the withdraw function.

The key takeaway is that sending Ether or calling an external function hands over the flow of execution. If you do this before your contract has settled its own internal accounting, you create a window for an attack.

2. The Solution: Checks-Effects-Interactions (CEI)

To prevent reentrancy, we must follow a strict ordering of operations within our functions. This is known as the Checks-Effects-Interactions (CEI) pattern. It is the industry standard for writing secure state-modifying functions.

The pattern dictates the following sequence:

  1. Checks: Perform all validations first. This includes checking user permissions, input values, and required balances (e.g., require(balances[msg.sender] >= _amount)). If any check fails, the transaction should revert.
  2. Effects: After all checks pass, apply all effects to the contract's internal state. This means updating all relevant state variables (e.g., balances[msg.sender] -= _amount). This is the most critical step. By updating the state before any external interaction, you close the window for reentrancy. Even if the attacker's contract calls back, the state has already been changed, and the "Check" will now fail.
  3. Interactions: Only after all internal state changes are complete should you interact with other contracts. This includes sending Ether (.call{value: ...}) or calling functions on external contracts.

The flowchart below clearly visualizes this secure pattern.

This flowchart and code example illustrate the secure CEI pattern. It shows the logical flow: first perform checks, then apply effects to the state, and finally, interact with external contracts.

Let's read the official Solidity documentation's recommendation on this pattern.

Security Considerations

The official Solidity documentation provides a concise and authoritative explanation of the CEI pattern.

Find the heading "Reentrancy" and read the two sections just below the vulnerable code examples. Start with the text beginning "To avoid reentrancy", which explains the pattern and shows the corrected, secure code. Then, continue to the section under the blue "info" box titled "Use the Checks-Effects-Interactions Pattern" for a more detailed breakdown of the three steps.

Let's apply this to the vulnerable withdraw function we saw earlier.

Vulnerable Code (Interactions before Effects):

function withdraw() public {
    uint amount = balances[msg.sender];
    require(amount > 0);

    // Interaction: Sends Ether BEFORE updating the balance
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed.");

    // Effect: Updates balance AFTER the external call
    balances[msg.sender] = 0; 
}

Secure Code (Following CEI):

function withdraw() public {
    uint amount = balances[msg.sender];

    // Check
    require(amount > 0, "Insufficient balance.");

    // Effect: Update the balance BEFORE the external call
    balances[msg.sender] = 0;

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

By simply moving the line balances[msg.sender] = 0; to before the .call, we have completely neutralized the reentrancy vulnerability. It's a small change with enormous security implications.

This practical demonstration from Alchemy drives the point home by showing how swapping two lines of code foils the attack script.

What is a Re-entrancy Attack in Solidity? How to protect your code!

This clip from Alchemy shows the fix in a live-fire scenario.

Watch the segment from "how do we solve this". It clearly demonstrates how reordering the code according to the CEI pattern prevents the attack from draining the contract.

Conclusion

In this lesson, we have tackled one of Solidity's most critical security pitfalls. You now understand the mechanics of a reentrancy attack and, more importantly, the fundamental design pattern used to prevent it. This knowledge is non-negotiable for anyone building financial applications on the blockchain.

Key Takeaways:

  • A reentrancy attack occurs when an external call from a function allows an attacker to recursively call back into that function before its state has been fully updated.
  • The vulnerable code structure is Checks -> Interactions -> Effects.
  • The secure pattern is Checks-Effects-Interactions (CEI). You must always update the contract's internal state variables (Effects) before sending funds or calling other contracts (Interactions).

While the CEI pattern is the core principle for preventing reentrancy, developers often use additional safeguards. In our next lesson, we will explore a practical tool for this: OpenZeppelin's ReentrancyGuard. We will see how it uses the function modifiers we studied previously to provide a simple but powerful nonReentrant lock, adding another layer of defense to your contracts.

Can't find a good explanation? Sign up and we'll make it for you

Sign up