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)

Need Professional Security Audit?

Our experts can help secure your smart contracts

Get Audit Quote