Securing Solidity Contracts with the Checks-Effects-Interactions Pattern
Prevent reentrancy attacks in Solidity by implementing the Checks-Effects-Interactions pattern. Learn how to order state changes to secure funds and optimize gas.
02 Jun 2026, 02:16 UTC

The Vulnerability: State Inconsistency
In Solidity, a reentrancy attack occurs when a contract makes an external call to an untrusted address before updating its own internal state. Because the external call hands over control to the recipient, a malicious contract can call back into the original function recursively. If the contract still believes the user has a balance because the state wasn't updated yet, it may send funds multiple times, potentially draining the entire contract.
The CEI Engineering Standard
The Checks-Effects-Interactions (CEI) pattern is a structural design choice that ensures a contract's state is always consistent before it relinquishes control. By strictly ordering operations, you eliminate the window of opportunity for a recursive call to exploit an outdated state.
1. Checks
The first phase involves validating all preconditions. This includes using require statements to check for sufficient balances, verifying that the caller has the necessary permissions (access control), and ensuring input parameters are within valid ranges.
2. Effects
In this phase, you update the contract's internal state. This means deducting balances, updating mappings, or changing status flags. The critical rule is that no external calls should happen before this step is complete.
3. Interactions
The final phase is the interaction with external addresses. This includes transferring Ether via call or invoking functions on other smart contracts. Since the state was already updated in the Effects phase, any attempt to re-enter the function will hit the Checks phase and fail because the balance has already been reduced.
Worked Example: Secure Withdrawal
The following example demonstrates a vault contract implementing CEI. This assumes Solidity version 0.8.0 or higher, where integer overflow/underflow checks are built-in.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SecureVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
// 1. Checks
require(amount > 0, "Amount must be greater than 0");
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. Effects
// Update state BEFORE the external call
balances[msg.sender] -= amount;
// 3. Interactions
// Transfer Ether to the caller
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Verification and Testing:
To verify this implementation, deploy the contract to a local environment (like Hardhat or Foundry). Create a second "Attacker" contract with a fallback() or receive() function that calls withdraw() again. In a CEI-compliant contract, the second call will fail at the require(balances[msg.sender] >= amount) check because the balance was already deducted in the first call's Effects phase.
Trade-offs and Limitations
CEI is generally more gas-efficient than using a ReentrancyGuard (a mutex that locks the function), as it avoids the overhead of writing a lock status to storage. However, it has a significant limitation: cross-function reentrancy.
If your contract has two different functions that rely on the same state variable, and one function follows CEI while the other does not, an attacker could call function A, re-enter via function B, and manipulate the shared state. To prevent this, CEI must be applied consistently across every function that modifies shared state.
Implementation Checklist
- Audit External Calls: Search for
.call,.transfer, and.send, as well as calls to other contract interfaces. - Shift State Updates: Move all balance deductions or status changes to occur before these calls.
- Validate Pre-conditions: Ensure
requirestatements are the very first operations in the function. - Consistency Check: Verify that all functions sharing the same storage variables follow the same pattern to prevent cross-function exploits.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.