Stopping Reentrancy: Implementing the Checks-Effects-Interactions Pattern
Learn how to prevent reentrancy attacks in Solidity using the Checks-Effects-Interactions (CEI) pattern to ensure contract state is updated before external calls.
03 Jul 2026, 05:38 UTC

The Danger of the 'External Call'
In Solidity, calling an external contract is not a simple request for data; it is a handover of control. When your contract sends Ether or calls a function on another address, the receiving contract can execute its own code. If that receiving contract calls back into your function before the first execution finishes, it can trigger a reentrancy attack.
The most common result is a "double-spend" where a user withdraws their balance multiple times because the contract hasn't yet recorded that the funds were removed. The solution is the Checks-Effects-Interactions (CEI) pattern, a structural requirement for any function that handles value transfers.
Breaking Down the CEI Sequence
CEI is a mental checklist for ordering your code. By following this specific sequence, you ensure that by the time an external actor gains control, your contract's internal state is already finalized.
1. Checks
Validate all preconditions first. This includes checking if the caller has the required permissions, if the contract has enough liquidity, or if the user's balance is sufficient. Use require() or revert() statements here. If a check fails, the transaction reverts immediately, and no state is changed.
2. Effects
Update the contract's internal state. This means decrementing balances, updating mapping values, or changing status flags. The critical rule is: never perform an external call before these updates are complete. If the function is re-entered, the second call will hit the updated state (e.g., a zero balance) and fail at the 'Checks' phase.
3. Interactions
Perform the external interaction. This includes call(), transfer(), or calling a function on another contract. Because this is the final step, any attempt to call back into the function will find the state already updated, neutralizing the attack.
Worked Example: Secure Vault Implementation
Consider a vault where users deposit and withdraw funds. Below is a comparison of a vulnerable implementation versus one using the CEI pattern (assuming Solidity 0.8.x).
Vulnerable Implementation (Anti-Pattern)
// RISK: Reentrancy vulnerability
function withdraw() public {
uint amount = balances[msg.sender];
require(amount > 0, "Insufficient balance");
// INTERACTION happens before EFFECT
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0; // This line is never reached if the caller re-enters
}
Secure Implementation (CEI Pattern)
// SECURE: Following CEI
function withdraw() public {
// 1. CHECKS
uint amount = balances[msg.sender];
require(amount > 0, "Insufficient balance");
// 2. EFFECTS
balances[msg.sender] = 0;
// 3. INTERACTIONS
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
Verification and Testing
To verify this logic, you can deploy a "Attacker" contract in a local environment (like Hardhat or Foundry). The Attacker contract should have a receive() or fallback() function that calls the vault's withdraw() function again.
- Vulnerable Result: The Attacker can drain the entire vault because
balances[msg.sender]remains positive during every recursive call. - CEI Result: The first call sets the balance to 0. The second recursive call fails at the
require(amount > 0)check, stopping the attack.
Trade-offs and Limitations
While CEI is a powerful architectural defense, it has limitations. It protects the specific function it is implemented in, but it may not protect the entire contract if multiple functions share the same state (cross-function reentrancy).
For complex contracts with interdependent state changes across different functions, CEI should be paired with a ReentrancyGuard (a mutex). A mutex uses a state variable (e.g., bool locked) to prevent any function from being entered while another is still executing. While this adds a small amount of gas overhead for storage writes, it provides a secondary layer of defense.
Actionable Summary
When writing Solidity functions that interact with external addresses:
- Place all
requirestatements at the top. - Update all internal mappings and variables next.
- Execute
callortransferas the very last operation. - If the logic spans multiple functions, implement a
nonReentrantmodifier.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.