Checks-Effects-Interactions: Pattern, When to Use It, and Common Implementation Bugs

Checks-Effects-Interactions: Pattern, When to Use It, and Common Implementation Bugs (Complete Guide)

Checks-Effects-Interactions (CEI) is not a slogan. It is a control-flow boundary. You validate what must be true, you write down the new truth inside your storage, and only then you hand control to anything outside your contract. The pattern sounds simple, yet many real losses still come from two failures: teams forget that “interaction” can be hidden, and teams update only part of the state that actually matters. This guide goes deep on CEI as a practical engineering habit: what counts as a check, what counts as an effect, what counts as an interaction, and how to audit, test, and operationalize the pattern in real protocols.

TL;DR

  • CEI means: Checks (validate) then Effects (update all internal state that must be true) then Interactions (external calls, transfers, hooks, or anything that can execute external code).
  • The point is not aesthetics. The point is to ensure reentrancy, callbacks, and external weirdness observe the updated state, not a vulnerable “in-between” state.
  • Most CEI bugs are not “sent ETH before updating balance.” They are partial effects, hidden interactions, and cross-function reentrancy where another function reads shared storage during the interaction window.
  • Token transfers are interactions. Proxies are interactions. Hooks are interactions. External “view” reads can also be dangerous if you use them to compute effects without bounding assumptions.
  • CEI is a layer, not the full security system. You often pair it with reentrancy guards, pull payments, safe token wrappers, pause modes, and strict access control.
  • Use a repeatable workflow: map interactions, list invariants, make effects complete, test adversarial flows, then lock the pattern with review checklists and fuzzing.
  • Practical help: triage token behavior before integrating unknown assets using Token Safety Checker, and build solid foundations with Blockchain Technology Guides and Blockchain Advance Guides.
Start here Foundations, then the advanced lens

If you want CEI to feel obvious instead of ceremonial, you need a mental model of EVM control flow, msg.sender, call stacks, and how tokens and proxies behave in the wild. Start with Blockchain Technology Guides, then level up with Blockchain Advance Guides. For updates and new security playbooks, you can Subscribe.

1) What CEI really is (and what it is not)

Checks-Effects-Interactions is a sequencing rule for functions that do anything meaningful with value, permissions, or state that other logic depends on. It is the habit of completing your contract’s internal truth before you step into the outside world. That outside world can be honest, buggy, upgraded, malicious, or simply unexpected.

CEI is not a reentrancy guard by itself. It can prevent many reentrancy exploits by making reentrant reads see the new state. But if your system has multiple functions that share storage and those functions assume certain invariants, a reentrant call to a different function can still break things. CEI also does not prevent economic exploits like oracle manipulation, MEV sandwiching, or incentive abuse. It helps you write code that is harder to trick at the control-flow layer.

The simplest mental model is: the moment you interact externally, assume the callee can call back immediately, with full creativity, using as much gas as it wants (within the transaction), and it can do so at the worst possible time for you. If your state is not complete before that moment, you are playing roulette.

Why it matters even when you “do not call anything”

Many devs believe their function has no external calls because they do not see a direct someContract.someMethod(). But if you transfer tokens, that is a call to the token contract. If you send ETH to a contract address, that triggers receive or fallback. If you call an address that might be a proxy, you might be calling logic that changes later. And if you rely on a library that wraps transfers, you can still be calling out.

CEI is a defense against surprise, not just against obviously malicious contracts. Surprise is common. The ecosystem is not standardized enough to assume “safe defaults.” If you integrate unknown tokens or unknown protocols, treat every interaction as hostile until proven otherwise. This is exactly why a fast token behavior sanity check can save time: Token Safety Checker.

CEI creates a safe boundary before external control. Checks Auth, inputs, invariants Effects Write complete new state Interactions Calls, transfers, hooks What can go wrong at the interaction boundary? 1) External code can reenter your contract (same or different function). 2) External call can revert, lie, consume gas, or trigger token hooks. 3) If your effects are incomplete, reentrancy observes old truth and exploits it. 4) If your effects are complete, reentrancy is forced to operate on the new truth.

2) The three stages, correctly defined

Most CEI explanations are too shallow. They say “checks are require statements,” “effects are state updates,” and “interactions are external calls.” That description is not wrong, but it misses the tricky part: classification and completeness. In real contracts, there are checks that look like effects, effects that look like interactions, and interactions hiding inside helpers.

A) Checks

Checks are all preconditions that must be true for the function to proceed safely. Checks include authorization and business logic, not just “amount > 0.” Good checks prevent you from doing a partial state update that later becomes irreversible.

  • Authorization: who can call this function (owner, role, allowlist, signature, timelock).
  • Input validation: amount ranges, non-zero addresses, token allowlists, slippage bounds.
  • State preconditions: balance exists, cooldown passed, position health ok, system not paused.
  • Invariant checks: global caps, per-user limits, expected mode or phase, nonce correctness.

If your protocol has emergency states, “not paused” is a check and belongs early. If your protocol has upgrade or governance modes, “only while in mode X” is a check. If your protocol uses signatures, “nonce unused and signature valid” is a check.

B) Effects

Effects are every internal state update that must reflect the function outcome. Effects must be complete, not just “reduce balance.” Complete means: all linked state variables that logically change for this action are updated in the same call, before any interaction.

  • Accounting: balances, shares, totals, debt principal, indices, fee accumulators.
  • Permissions: internal approvals, operator flags, nonce increments, claim markers.
  • State machines: mark phase changes, mark orders cancelled, mark escrow closed.
  • Bookkeeping for later: store owed amounts, store pending withdrawals, store queued actions.

A subtle rule: if you only update some of the state and rely on the external call “succeeding” to update the rest, you are taking on risk. You might revert on failure and be safe, but reentrancy can happen before failure. In other words, you must design effects so your invariants are already safe at the interaction boundary.

C) Interactions

Interactions are anything that can execute code you do not control, or anything whose behavior you cannot treat as a pure function. The obvious ones are external calls. The non-obvious ones are more important, because they are where most teams get surprised.

  • ETH send: sending ETH to a contract triggers code execution via receive or fallback.
  • Token transfer: calling a token contract can reenter or behave non-standard.
  • Token hooks: ERC777 hooks, ERC1363 callbacks, ERC1155 receiver hooks, custom token logic.
  • Proxy calls: the logic behind the address can change after you deploy.
  • Oracle reads: if used to compute effects, they are dependency interactions, even if view.
  • Low-level generic calls: arbitrary call, delegatecall, plugin systems, callbacks.
Thing you do Stage Why Common mistake
Check paused flag Checks It is a precondition that changes allowed behavior Checking pause after partial state updates or after external reads
Increment nonce, mark claim used Effects Prevents replay and double-claim during reentrancy Marking claim after transfer, enabling double-claim
ERC20 transfer Interactions Calls external token code that can lie or hook Treating transfer as “safe” and placing it before effects
Send ETH via call Interactions Receiver can execute fallback and reenter Updating balances after sending ETH
Read oracle price to compute debt Checks + dependency External dependency can be stale or manipulated Using unbounded external reads to compute effects
Emit events Usually after effects Logs should represent committed state Emitting “withdrawn” before state or before interaction success

3) When CEI is mandatory, and when it is just helpful

CEI should be your default for any function that touches value or could be called in adversarial contexts. The more composable your protocol is, the more likely it is that an attacker can become “a normal user” and then behave adversarially. In DeFi, attackers do not need special access. They only need a path.

CEI is mandatory for these categories

  • Withdrawals and refunds: anything that sends assets out.
  • Claims and distributions: rewards, airdrops, vesting claims, fee claims.
  • Escrow settlement: release of funds based on conditions.
  • Callback-based flows: flash loan receivers, swap callbacks, hooks.
  • Bridges and wrappers: mint or burn paired with external message verification.
  • Vault rebalancing: strategy moves that call external protocols.

CEI is still helpful for these categories

  • Admin actions: even if only callable by a role, compromised keys exist.
  • Parameter updates: if they trigger external calls or depend on external reads.
  • Batch operations: loops that do transfers or calls can be griefed or reentered.
  • Router contracts: aggregator style routing has many hidden interactions.

Where CEI is not enough

CEI reduces risk, but it cannot guarantee safety in these situations:

  • Cross-function reentrancy: function A does CEI, but reentrancy enters function B which reads shared storage and breaks invariants.
  • Externalized accounting: you compute your internal state from external balances or external share prices that can be manipulated.
  • Upgradeable systems: future code changes can break assumptions even if today’s CEI is perfect.
  • MEV and sandwiching: attackers exploit transaction ordering, not reentrancy.
  • Oracle and pricing exploits: CEI can be correct and you still lose money.

In those cases, you often combine CEI with additional controls: a reentrancy guard, a strict state machine, tighter access control, rate limits, pause modes, and robust oracle design. CEI is one layer, but it is a layer you can enforce mechanically and review consistently.

4) Hidden interactions: the real reason CEI fails

A large share of CEI failures happen because teams misidentify interactions. They think “the external call is at the bottom,” but the contract is already interacting with external code earlier. The fix is not just “reorder lines.” The fix is to map the call graph and treat certain operations as interactions even when they look harmless.

A) ETH transfers to contract addresses

ETH transfers are not just value movement. They can be control transfer. If the recipient is a contract, its fallback or receive function can execute code and call you back. Many teams learned this lesson through classic reentrancy incidents in the early days, but it still bites protocols when they build “refund” or “payout” helpers.

If you send ETH before you update state, you have created a reentrancy window. If you update state first and then send, you have reduced that window, but you still need to consider what happens if the send fails. Does your state remain correct? Do you revert and roll back? Do you store pending withdrawals? Those are design choices, not just code choices.

B) Tokens are programs, not coins

ERC20 is “a standard,” but the ecosystem includes many tokens that do not behave like the textbook ERC20. Some return no boolean, some return false instead of reverting, some burn or tax on transfer, some blacklist addresses, some rebase balances, and some implement hooks. If your protocol accepts arbitrary tokens, you accept arbitrary token behavior.

This is why token integration should be treated as part of your threat model. A fast first-pass scan helps you spot obvious hazards early: Token Safety Checker. It is not a replacement for audit work, but it reduces the chance that you integrate something dangerous without noticing.

C) Hooks and callbacks (ERC777, ERC1155, ERC1363, custom)

Some token standards deliberately call receiver hooks. Some implementations do it even when they should not. Receiver hooks mean that a transfer can cause external code execution, which means a transfer is an interaction that can trigger reentrancy.

Even if you never use ERC777, you can still be affected if a token claims to be ERC20 but includes extra logic. If you are building a general-purpose vault or router, you must assume tokens can be hostile. The “safe” approach is either to restrict tokens (allowlist) or to design accounting that is robust to hostile behavior, plus adversarial testing.

D) Proxies and upgradeable dependencies

If you call a proxy, you call an address whose logic can change. That means your integration can become unsafe even if it was safe when you deployed. CEI cannot prevent future upgrades from being malicious or buggy, but it can ensure your internal state updates are atomic before calling out. For upgrades, you need governance controls: timelocks, audits, and clearly defined emergency procedures.

E) External “view” calls that still matter

A view call cannot modify state, but it can revert, return garbage, or return manipulated values. If you use external view calls to compute your effects, you have a dependency risk. This is adjacent to CEI because it is still about boundaries between your truth and outside truth.

Good practice is to bound external inputs, use trusted oracles, validate expected ranges, and design for stale values. In many protocols, oracle safety is a bigger risk than reentrancy. CEI is still valuable, but it is not the whole story.

5) Common CEI implementation bugs that cause real incidents

The phrase “common bug” can sound like beginner mistakes. In reality, many CEI bugs come from sophisticated systems: multi-asset vaults, lending markets, reward distributors, routers, and upgradeable modules. The root cause is often the same: an incomplete mental model of what state matters at the interaction boundary.

Bug 1: Effects are partial, not complete

This is the top CEI failure mode. A developer updates the obvious variable, but forgets a second variable that is logically coupled. The contract looks safe, because “balance is reduced,” yet another path still uses the old value.

Examples of coupled state:

  • User balance and total assets.
  • Shares and price-per-share, or shares and total shares.
  • Debt principal and debt index.
  • Claimed flag and claimable amount.
  • Nonce and signature usage marker.
  • Order status and reserved inventory.

A safe review habit is: for each external interaction, list which invariants could be exploited if observed in the old state. Then ensure every variable that enforces those invariants is updated before the interaction.

Bug 2: Hidden interaction in a helper function

Teams refactor code into helpers and libraries. Later, a helper is changed to do a transfer or call a token hook. The top-level function still looks like CEI, but the helper introduces an interaction early. This is why “CEI audit” must look at the call graph, not just the top-level function.

Defensive habit: annotate helpers that call external contracts, and treat them like “interaction boundaries.” If a helper can interact, call it after all effects, or redesign so it returns data without calling out.

Bug 3: Cross-function reentrancy and shared storage

You do CEI inside one function, then the external call reenters into a different function that shares state. That function might read a variable that is partially updated or assume a global invariant that is temporarily false. The result is an exploit that “looks like CEI was used” yet still breaks.

This is common in:

  • Vaults with deposit, withdraw, and harvest sharing state.
  • Lending systems where borrow and repay share indexes.
  • Routers where callbacks can enter administrative or settlement functions.
  • Diamond patterns where multiple facets share storage.

Solution patterns: reentrancy guard (global or per-module), explicit state machines, and carefully partitioned storage and permissions.

Bug 4: External calls used to compute internal effects

A dangerous pattern is: call an external contract to get data, then compute internal accounting, then write state. If the external contract is manipulated or reenters, the computed effect can be wrong. Even if you do “checks then effects,” the checks depend on external truth that is not stable.

Safer choices include:

  • Use robust oracles that are hard to manipulate.
  • Use time-weighted data or multi-source aggregations.
  • Clamp values to expected ranges.
  • Separate “observation” from “commitment” with explicit delays.

Bug 5: try/catch paths that commit partial state

try/catch can be helpful, but it can also create “partial success” outcomes. If you commit state, attempt an interaction, then catch failure and keep going, you may have written effects without the corresponding external result. That can cause stuck funds, broken accounting, or griefing vectors where an attacker deliberately makes a call fail at the right time.

A simple guideline: if the interaction is essential to correctness, revert on failure so state rolls back. If the interaction is optional, design state so optional failure does not create imbalance. For example, store a pending withdrawal and let the user pull later, instead of “trying and continuing.”

Bug 6: Event ordering that lies to the world

Events do not change state, but they change how off-chain systems behave. If you emit “Withdrawn” before effects are committed, monitoring systems can make wrong decisions. If you emit “Withdrawn” before the transfer succeeds, analytics can show withdrawals that never happened.

Keep your events aligned to reality: emit after effects, and only emit “success” events after interactions succeed. If you need to record intent, emit an “Initiated” event before the interaction and a “Completed” event after success.

6) Practical examples: right order, wrong order, and the subtle traps

These examples are intentionally small and focus on control flow. They are not production vaults. The goal is to help you build an instinct for “where the boundary is” and what must be finalized before you cross it.

Example 1: The classic wrong withdraw

// WRONG: interaction before effects
function withdraw(uint256 amount) external {
  require(balances[msg.sender] >= amount, "insufficient");

  // Interaction first: receiver may run code and reenter
  (bool ok, ) = msg.sender.call{value: amount}("");
  require(ok, "send failed");

  // Effects later: too late
  balances[msg.sender] -= amount;
}

If msg.sender is a contract, it can reenter during the ETH send and call withdraw again. The second call reads the old balance and drains more. Attackers will not use an EOA. They will use a contract that is built for reentrancy.

Example 2: CEI ordering for ETH withdraw

// BETTER: checks, effects, then interaction
function withdraw(uint256 amount) external {
  uint256 bal = balances[msg.sender];
  require(bal >= amount, "insufficient");

  // Effects first
  unchecked { balances[msg.sender] = bal - amount; }

  // Interaction last
  (bool ok, ) = msg.sender.call{value: amount}("");
  require(ok, "send failed");
}

Now reentrancy sees the reduced balance. This blocks the classic “double withdraw” path. But you still need to think about cross-function reentrancy if other functions depend on balances in unexpected ways.

Example 3: The partial effects trap

Suppose you track balances, but you also track a global totalAssets used to compute share prices. If you reduce the user balance but forget to reduce totalAssets before the interaction, a reentrant call can exploit the share pricing. This is common in vaults and staking contracts.

The lesson is: effects must include all coupled state. CEI is not “update one mapping.” CEI is “finalize the new reality.”

Example 4: Token transfers are interactions too

// Sketch: treat token transfer as interaction
function withdrawToken(IERC20 token, uint256 amount) external {
  require(userBal[msg.sender][token] >= amount, "insufficient");

  // Effects first
  userBal[msg.sender][token] -= amount;
  totalBal[token] -= amount;

  // Interaction last (external token code runs)
  require(token.transfer(msg.sender, amount), "transfer failed");
}

Even if this looks fine, the token can still be weird: it might return false, revert under certain conditions, tax transfers, or trigger hooks. Most teams use safe wrappers that handle non-standard returns and revert behavior. If you accept arbitrary tokens, test with adversarial tokens. If you only accept specific tokens, consider allowlists and explicit behavior assumptions.

Example 5: Pull payments as a CEI-friendly architecture

Sometimes the cleanest way to avoid risky interactions is to avoid them in the same function. “Pull payments” means you record an owed amount and let the user withdraw later. That reduces the complexity of functions that do complex state updates and also do transfers.

// Sketch: record entitlement (effects), user pulls later
mapping(address => uint256) public owed;

function settle(address user, uint256 amount) internal {
  // Effects: record obligation
  owed[user] += amount;
  emit Owed(user, amount);
}

function pull() external {
  uint256 a = owed[msg.sender];
  require(a > 0, "none");

  // Effects first
  owed[msg.sender] = 0;

  // Interaction last
  (bool ok,) = msg.sender.call{value: a}("");
  require(ok, "send failed");
}

Pull payments can reduce complexity, but they can also create UX friction. The best design depends on your protocol. For high-value flows where correctness matters more than one-transaction UX, pull payments are often worth it.

7) How to audit CEI like an attacker

A good CEI review is not “does the function look like check then update then call.” It is “is there any way to reenter while the system is in a vulnerable state.” That requires thinking about call graphs, shared state, and the true set of interactions.

Step 1: Map interactions and control handoff points

In each critical function, mark every line where control can leave your contract: external calls, token transfers, ETH sends, hooks, proxies, and library calls that might call out. If you are unsure, treat it as a potential interaction until you confirm otherwise.

Step 2: Write invariants for the function outcome

Invariants are statements that must be true after the function completes. Example invariants:

  • User cannot withdraw more than their balance.
  • Total assets equal sum of user balances (or you account for the difference explicitly).
  • Claim can only be used once.
  • Debt cannot underflow and interest index progression is consistent.
  • Order cannot be filled after cancellation.

The critical question: are these invariants already true before the first interaction occurs? If not, reentrancy can observe a broken invariant and exploit it.

Step 3: Identify coupled state and ensure complete effects

For each action, list the “state bundle” that must move together. Many exploits come from bundles that were not updated atomically. If your contract has a price-per-share, an index, or a fee accumulator, treat it as coupled to balances.

Step 4: Assume reentrancy enters the worst function

Do not assume reentrancy will call the same function. Assume it calls the most damaging function that is callable. That might be a deposit, an admin function, a settlement function, or a helper that was not expected to be reachable. Ask: what can be called during the interaction boundary? If your system does not have a reentrancy guard, the answer might be “many things.”

Step 5: Consider revert and gas griefing

External calls can revert, and external calls can consume gas. If you do effects then interactions and the interaction fails, you usually revert so effects roll back. But if you store state and do not revert, you might create stuck funds or inconsistent accounting. Decide intentionally: do you want atomic success or do you want partial progress with a safe recovery path?

Copyable CEI audit checklist

  • All interactions mapped: external calls, token transfers, ETH sends, hooks, proxies, library calls that call out.
  • Effects complete before first interaction: every coupled variable updated, not just the obvious one.
  • Cross-function reentrancy considered: identify which other functions can run during the interaction window.
  • Revert behavior intentional: if interaction fails, either revert atomically or store safe pending state with clear recovery.
  • Events truthful: success events emitted only when state and transfer are both complete.
  • Token behavior assumptions explicit: handle non-standard returns and consider allowlists for critical paths.
  • Upgradeable dependencies reviewed: proxy integrations treated as mutable risk surfaces.

8) CEI patterns for real protocol types

CEI looks different across architectures because “effects” are different. A simple escrow has small effects. A lending market has coupled indices, health checks, liquidation logic, and price dependencies. Below are practical guidance patterns for common protocol types.

A) Vaults and staking contracts

Vaults usually maintain shares and total assets. Staking contracts often maintain balances and reward indices. These systems are full of coupled state.

  • Update user shares and total shares before any external transfer.
  • Update reward indices and user reward debt before transferring rewards.
  • Be careful with “harvest” functions that call external strategies. Reentrancy might reenter deposit or withdraw if not guarded.
  • If you support arbitrary tokens, test fee-on-transfer and rebasing behavior explicitly, or reject such tokens.

B) Lending markets and perps

Lending markets depend on oracles and interest indexes. “Effects” often include updating indices, updating principal, and updating collateral and health. A CEI mistake here is often an index update that happens after a transfer or after an external call.

  • Settle interest and update indices before allowing any borrow or repay transfers.
  • When calling a token, assume it can be adversarial. Use safe wrappers and do not trust return values blindly.
  • Use reentrancy guards more often in lending systems due to cross-function invariants.
  • If an oracle is used, treat it as a dependency risk. Validate staleness, bounds, and fail-closed behavior where appropriate.

C) Routers and aggregators

Routers are interaction factories. They often call multiple tokens, multiple pools, and pass control to callback logic. The biggest CEI risk is “we did CEI in this function,” while the router is actually a system of nested interactions.

  • Minimize internal state. If you do not need storage, do not use storage.
  • If you must store state for callbacks, store it fully before the external call.
  • Assume tokens and pools can call back through hooks or callback interfaces.
  • Use strict permissions for callback entry points and verify msg.sender and expected pool addresses.

D) Bridges and wrappers

Bridges combine external messages with asset minting and releasing. CEI matters because you must not release or mint before you finalize message consumption state.

  • Mark messages as consumed (effect) before releasing assets (interaction).
  • Use nonces and replay protection as effects that happen early.
  • Ensure reentrancy cannot cause double-consumption or double-release.
  • Be explicit about what happens if release fails. Revert or store pending release with safe retry.

9) Testing CEI the right way (adversarial, not demo)

If your tests only cover the happy path, you do not know if CEI is correct. CEI is a defense against adversarial behavior. Your tests should include adversarial behavior.

Minimum tests you should have

  • Reentrancy attempt: receiver contract reenters during ETH send or token transfer.
  • Cross-function reentrancy attempt: receiver reenters a different function that touches shared state.
  • External revert: token transfer reverts, receiver reverts, external protocol call reverts.
  • Non-standard token: token returns false or returns no value, fee-on-transfer behavior, rebasing behavior (if relevant).
  • Gas griefing: receiver consumes gas or causes failure in a loop scenario.

Fuzzing and invariant tests

For complex protocols, add invariant testing: define truths that must always hold and let a fuzzer call functions in random sequences. CEI bugs often appear in “weird sequences,” not in straight-line flows.

Invariants to consider:

  • Totals match sums or match tracked deltas.
  • No user can withdraw more than they deposited, adjusted for fees and rewards.
  • Nonces never decrease, claims never un-claim.
  • Debt and collateral relationships are consistent after any sequence of calls.

Formal thinking without full formal verification

You do not need full formal verification to benefit from formal thinking. Write the invariant statements in plain English, then translate the core ones into tests. That alone catches a surprising number of “partial effects” bugs.

Adversarial test checklist

  • Reenter on ETH receive: fallback calls back into target function.
  • Reenter on token transfer: token or receiver triggers callback and reenters.
  • Reenter into a different function: attempt cross-function invariant break.
  • Token returns false: ensure safe wrapper or proper handling.
  • Fee-on-transfer token: ensure accounting matches actual received amounts, or revert if unsupported.
  • Revert paths: confirm atomic rollback or safe pending state design.
  • Sequence fuzz: random call sequences with assertions on invariants.

10) A repeatable CEI workflow you can run before shipping

The easiest way to “actually do CEI” is to turn it into a workflow. The workflow below is designed to be used in PR reviews and pre-deploy checklists. It is boring on purpose. Boring is good.

Step 1: List all interactions in the function

Write them down. Include transfers, hooks, calls to proxies, and library calls that call out. If you cannot list them, you do not understand the interaction surface yet.

Step 2: List invariants that must hold at the interaction boundary

Not only “after the function returns,” but “before external code can observe us.” This is the key difference between CEI thinking and “normal correctness thinking.”

Step 3: Identify coupled state bundles for the action

If the action is withdraw, what state variables define withdrawability? If the action is claim, what variables define claim uniqueness? If the action is borrow, what variables define health and debt? Those variables must be updated as a bundle.

Step 4: Commit complete effects, then interact

If you cannot commit complete effects without interacting, consider redesign: pull payments, staged settlement, explicit state machines, or smaller functions. Many protocols get safer by splitting “settle state” from “send funds.”

Step 5: Add guardrails for cross-function reentrancy

If your contract has more than one sensitive function, use a reentrancy guard or a structured state machine. CEI reduces risk, but guardrails prevent future refactors from reintroducing the same bug in a different way.

Step 6: Test adversarial flows and reverts

Make a small attacker contract and try to break your invariants. Treat tests as your last line of defense against subtle partial-effects issues.

Turn CEI into a system: learn, triage, ship, and review

CEI works when it is repeatable. Map interactions. Write invariants. Make effects complete. Assume reentrancy into the worst function. Test adversarial flows. If you integrate tokens, treat token behavior as part of your threat model and sanity check unknown assets before you treat them as building blocks.

Foundations: Blockchain Technology Guides. Updates: Subscribe.

11) Advanced CEI notes that trip up experienced teams

If you are still reading, this is where CEI becomes genuinely powerful. In advanced systems, the “effects” are not a single mapping update. They are a web of coupled accounting, indices, cached values, and phase transitions. CEI is still the right idea, but you have to apply it at the system level.

Atomicity over ordering

Ordering is the visible part of CEI. Atomicity is the real part. Atomicity means the system has no externally observable intermediate state that violates invariants. If you update three variables and forget the fourth, the order is still “checks then effects,” but the effects are incomplete. That is not CEI in spirit, and attackers will find the gap.

Shared storage across modules

In diamond patterns, modular proxies, and multi-contract systems, reentrancy can cross module boundaries. Function A in module X can call out and then reentrancy enters module Y that shares storage. CEI must be evaluated across that shared storage boundary. If module Y reads partially updated state, you can get unexpected outcomes even if each module “looks correct.”

Callbacks and expected sender validation

Some systems rely on callbacks intentionally: flash loans, swap callbacks, cross-chain message receivers. In these systems, you must validate who is calling the callback and what phase you are in. This becomes a state machine problem. Your “checks” include validating the callback sender, validating the expected pool, and validating the expected data. Your “effects” include marking the callback as in-progress or consumed. Your “interactions” include the external transfers and downstream calls.

Non-standard ERC20 returns and safe transfer wrappers

Many production systems use safe transfer libraries to handle tokens that do not return booleans correctly. This is not just convenience. It is risk reduction. If your transfer returns false and you do not check it, you might treat a failed transfer as success. That can lead to broken accounting. If your transfer returns no value, a naive interface call might revert or mis-handle it.

Even with CEI ordering, a failed or weird transfer can break your invariants if you do not revert or you do not design a safe pending state model. So CEI needs the token handling layer to be correct too.

Deferred settlement and queued actions

Some protocols intentionally defer settlement. They store pending withdrawals, pending mints, or queued rebalances. This can be safer if done correctly, because it reduces complex actions in a single call. But it can also create new risk: a queue can be manipulated, and settlement functions can become the new “critical function.” Apply CEI and reentrancy thinking to the settlement phase as well.

Operational reality: key compromise is part of your threat model

CEI prevents a class of contract bugs, but it does not protect against compromised admin keys. If you have upgrade keys, pause guardians, or treasury operations, secure those keys. Multisig signers should avoid signing on compromised machines. A hardware wallet is materially relevant for operational safety: Ledger link.

12) Red flags you can spot in minutes

If you only have five minutes to review a contract, look for these. They are not perfect, but they catch a lot of CEI-related risk fast.

Red flag Why it is risky What to ask
External call before all state updates Reentrancy sees old truth Which invariants are broken at that moment?
Only one variable updated in a coupled bundle Partial effects enable exploitation What other state should move with this?
Generic execute or callback function Attacker controls call graph Who can call it and what does it assume?
Token transfer assumed safe Tokens are non-standard and can hook Do you use safe wrappers and adversarial token tests?
External views used for accounting Dependency manipulation, stale data How do you bound, validate, and fail safely?
try/catch continues after failure Partial commit risks and griefing What guarantees correctness if call fails?

13) Incident playbook: what CEI gives you during the worst day

CEI is mostly discussed as a coding pattern, but it has an operational side. When an incident occurs, you want to understand which functions might be exploitable via reentrancy or callbacks. CEI makes that analysis easier because it forces you to define interaction boundaries and invariant commitments.

During an incident, teams often do three things: restrict actions, pause or partially pause, and patch. If your contract uses CEI consistently, you can more confidently isolate which actions might be safe to keep and which must be blocked. This becomes especially important if your system should keep safety exits available, like repay or close positions.

If you are designing emergency controls, combine CEI with selective pausing patterns and role-safe governance. CEI helps prevent control-flow bugs. Pausing helps reduce exploit bandwidth while humans respond.

FAQs

What is the Checks-Effects-Interactions pattern?

It is a sequencing discipline: validate preconditions (checks), then finalize internal state changes (effects), and only then perform external calls, transfers, or any operation that can execute external code (interactions). The goal is to ensure reentrancy and callbacks observe the updated state, not an exploitable intermediate state.

Is CEI only about reentrancy?

Reentrancy defense is the main reason CEI exists, but it also improves correctness by reducing partial updates and inconsistent state during external interactions. It does not prevent oracle manipulation, MEV, or economic exploits by itself.

Do I still need a reentrancy guard if I use CEI?

Often yes. CEI reduces risk, but cross-function reentrancy and shared-storage complexity can still create attack paths. A reentrancy guard is a simple safety belt that helps prevent future refactors from reintroducing vulnerabilities.

Are token transfers considered interactions?

Yes. A token transfer is an external call to the token contract. Tokens can be non-standard, can revert, can return false, can tax transfers, and in some cases can trigger hooks and callbacks that reenter. Treat transfers as interactions and finalize state before them.

What is the most common CEI implementation mistake?

Partial effects. Teams update the obvious variable (like a balance) but forget a coupled variable (like totals, indices, or claim markers). Reentrancy or later calls exploit the stale coupled state even when the main balance is reduced.

How do I find hidden interactions in my contract?

Map the call graph. Treat token transfers, ETH sends to contracts, proxy calls, hooks, and library helpers that call out as interactions. If you are unsure whether an operation can execute external code, assume it can until you verify it cannot.

How can TokenToolHub help with CEI-related safety?

Use structured learning paths for foundations and advanced security frameworks, and use Token Safety Checker to quickly triage token behaviors before integrating unknown assets: Token Safety Checker.

What should I test to verify CEI is correct?

Test adversarial flows: reentrancy attempts (including cross-function), external call reverts, non-standard ERC20 return behavior, fee-on-transfer tokens, and receiver contracts that execute code during ETH or token receipt. Add invariants and fuzzing for complex protocols.

References

Reputable starting points for deeper study:

Note: examples in this guide are sketches for learning and review checklists. Production systems need full threat modeling, token behavior handling, and rigorous testing.
About the author: Wisdom Uche Ijika Verified icon 1
Founder @TokenToolHub | Web3 Research, Token Security & On-Chain Intelligence | Building Tools for Safer Crypto | Solidity & Smart Contract Enthusiast