The Check-Effects-Interactions (CEI) pattern is a best practice in Solidity for writing secure smart contracts, particularly to prevent reentrancy attacks. It involves structuring the contract's logic in a specific order: checking conditions, making state changes (effects), and then interacting with external contracts. The idea is to minimize the risk of external contracts making unexpected changes during interactions.
CEI Pattern Steps: Checks: First, check all the necessary preconditions (e.g., if the sender has enough balance, if the contract is in a valid state). Effects: Second, update the contract's internal state (e.g., deduct balance, change ownership). Interactions: Lastly, interact with external contracts or send ETH/tokens (this is the point where a reentrancy attack could happen if done earlier).
pragma solidity ^0.8.0;
contract SafeWithdrawal { mapping(address => uint256) public balances;
// Function to withdraw funds securely
function withdraw() external {
uint256 amount = balances[msg.sender];
// 1. CHECK: Ensure the user has a balance to withdraw
require(amount > 0, "Insufficient balance");
// 2. EFFECTS: Update the state before interacting with external contracts
balances[msg.sender] = 0;
// 3. INTERACTIONS: Send the amount to the user (this could call an external contract)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// Function to deposit funds into the contract
function deposit() external payable {
balances[msg.sender] += msg.value;
}
}
Breakdown of the CEI Pattern:
- Check:
The contract checks that the msg.sender has a balance greater than 0.
- Effects:
The contract sets the balances[msg.sender] to 0 before interacting with the external contract (or sending funds).
- Interaction:
The contract transfers ETH to the caller using call. If this interaction triggered a fallback or other external function, the state has already been updated, so a reentrant call would not be able to exploit the contract. Why CEI Pattern Prevents Reentrancy: A reentrancy attack happens when an external contract (or malicious user) calls back into the vulnerable contract before the state has been updated. In this example, if the external contract (via call) calls the withdraw function again before the balances[msg.sender] has been set to 0, it could withdraw multiple times. By ensuring that the state is updated before the external call, the reentrancy attack is prevented.
Key Points:
State change before external calls: The most critical aspect is to change the state before making any external calls. Use .call over .send or .transfer: With gas limits becoming less predictable, .call is preferred for sending ETH since it does not impose a hard gas limit.
This pattern, along with other techniques like reentrancy guards (e.g., ReentrancyGuard from OpenZeppelin), ensures that Solidity contracts are more secure from potential attacks.
Top comments (0)