Solia saysChecks-Effects-Interactions reorders a function to update state BEFORE making any external call — turning the canonical reentrancy attack from a drained vault into a no-op.
The single most famous bug in smart contracts is reentrancy: a contract sends ETH to an external address, that address re-enters back into the contract, and finds the same state it had before — drained twice. The fix is the Checks-Effects-Interactions ordering. First check preconditions (require), then apply effects (write state), and only then perform interactions (external calls). When the attacker re-enters, the state already reflects the previous call, so the recursion sees nothing left to steal.
This is the safest single discipline in Solidity, often combined with a reentrancy guard for belt-and-braces. The demo runs a vulnerable withdraw (drains 4× the deposit) and the same function with CEI ordering (drains 1×, correctly).
Power-ups you unlock
Order: Checks → Effects → Interactions
External calls can re-enter your function before it returns
Update state BEFORE the external call to make reentry harmless
Pairs well with a reentrancy guard for defense in depth
Famous in The DAO hack — the entire pattern was named afterward
The Reentrancy Reaper attacks — common mistakes
Sending funds before zeroing the recipient balance
Assuming external calls return synchronously without callbacks
Updating state in the middle (between two external calls)
Believing CEI alone fixes cross-function reentrancy (use a guard too)
Boss battleShow a vulnerable withdraw draining 4× the deposit and the CEI-ordered version draining only once.
Example code
<!doctype html><html><head><meta charset="utf-8"></head>
<body style="background:#06040d;color:#e6e0ff;font-family:monospace;padding:20px"><pre id="o"></pre>
<script>
// reentrancy attack: external call BEFORE state update → drained on every re-entry.
function vault(safe){
let bal = 100; const stolen = [];
function withdraw(depth){
if(depth > 3 || bal <= 0) return;
const amount = bal;
if(safe){ // CEI: effects first
bal = 0;
stolen.push(amount);
withdraw(depth + 1); // re-entry now sees bal=0
} else { // BUG: interaction first
stolen.push(amount); // "send" to attacker
withdraw(depth + 1); // re-entry sees bal still 100
bal = 0; // too late
}
}
withdraw(0); return stolen;
}
const v = vault(false), c = vault(true);
document.getElementById('o').textContent = [
'vulnerable order (send → zero balance):',
' stolen: [' + v.join(', ') + '] total = ' + v.reduce((a,b)=>a+b,0) + ' (vault had 100)',
'',
'CEI order (zero balance → send):',
' stolen: [' + c.join(', ') + '] total = ' + c.reduce((a,b)=>a+b,0) + ' (correct)',
'',
'reordering one line turns a catastrophe into a no-op'
].join('\n');
</script></body></html>