Checks-Effects-Interactions Pattern
Checks-Effects-Interactions Pattern
The Checks-Effects-Interactions (CEI) pattern is a fundamental security practice in Solidity development that helps prevent reentrancy and other vulnerabilities.
The Pattern
Follow this order in your functions:
1. **Checks**: Validate all conditions and requirements
2. **Effects**: Update all state variables
3. **Interactions**: Make external calls
Why It Matters
This pattern prevents reentrancy attacks by ensuring state is updated before any external calls that could call back into your contract.
Example
Bad (Vulnerable to Reentrancy)
function withdraw(uint amount) public {
// Checks
require(balances[msg.sender] >= amount);
// Interactions (BEFORE effects)
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
// Effects (TOO LATE!)
balances[msg.sender] -= amount;
}Good (Following CEI Pattern)
function withdraw(uint amount) public {
// 1. Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
require(amount > 0, "Amount must be positive");
// 2. Effects (update state FIRST)
balances[msg.sender] -= amount;
// 3. Interactions (external calls LAST)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}Detailed Breakdown
Checks Phase
Validate all conditions:
// Input validation
require(amount > 0, "Invalid amount");
require(amount <= maxAmount, "Exceeds maximum");
// Authorization
require(msg.sender == owner, "Not authorized");
// State validation
require(balances[msg.sender] >= amount, "Insufficient balance");
require(!paused, "Contract is paused");
// Business logic checks
require(block.timestamp >= startTime, "Too early");Effects Phase
Update all state variables:
// Update balances
balances[msg.sender] -= amount;
balances[recipient] += amount;
// Update counters
totalSupply -= amount;
userTransactionCount[msg.sender]++;
// Emit events (considered effects)
emit Withdrawal(msg.sender, amount);Interactions Phase
Make external calls:
// Transfer tokens
token.transfer(recipient, amount);
// Call external contract
externalContract.notify(amount);
// Send ETH
(bool success, ) = recipient.call{value: amount}("");
require(success);Complex Example
function executeComplexOperation(
address recipient,
uint amount,
bytes calldata data
) external nonReentrant {
// ===== CHECKS =====
require(recipient != address(0), "Invalid recipient");
require(amount > 0 && amount <= MAX_AMOUNT, "Invalid amount");
require(balances[msg.sender] >= amount, "Insufficient balance");
require(isAuthorized[msg.sender], "Not authorized");
require(!paused, "Paused");
// ===== EFFECTS =====
// Update all state
balances[msg.sender] -= amount;
balances[recipient] += amount;
totalTransferred += amount;
lastTransferTime[msg.sender] = block.timestamp;
// Emit events
emit Transfer(msg.sender, recipient, amount);
// ===== INTERACTIONS =====
// External calls last
if (data.length > 0) {
(bool success, ) = recipient.call(data);
require(success, "Callback failed");
}
// Notify external contract
if (shouldNotify) {
externalRegistry.recordTransfer(msg.sender, recipient, amount);
}
}Common Pitfalls
1. Hidden Interactions
Be aware that these are also interactions:
- Token transfers (ERC20/ERC721)
- ETH transfers
- Any call to external contracts
- Even view functions on untrusted contracts!
2. Multiple Interactions
When multiple external calls are needed, ensure all state updates happen first:
function multipleOperations() public {
// All checks first
require(condition1);
require(condition2);
// All effects
updateState1();
updateState2();
emit Event1();
emit Event2();
// All interactions last
externalCall1();
externalCall2();
}When to Deviate
Sometimes you need to deviate from strict CEI (e.g., checking return values from external calls). In these cases:
1. Use ReentrancyGuard
2. Carefully document why you're deviating
3. Ensure you're not vulnerable to reentrancy
4. Have it reviewed in audit
Benefits
1. **Prevents reentrancy**: State is updated before external calls
2. **Clear code structure**: Easy to review and maintain
3. **Predictable behavior**: State changes are finalized before interactions
4. **Gas efficient**: Often more efficient than complex guards
Verification
During code review, for each function:
- [ ] All require statements at the beginning
- [ ] All state updates before external calls
- [ ] All external calls at the end
- [ ] Events emitted after state changes but before interactions
- [ ] No state changes after external calls
References
- [Consensys Best Practices](https://consensys.github.io/smart-contract-best-practices/)
- [Solidity Docs: Security Considerations](https://docs.soliditylang.org/en/latest/security-considerations.html)