freecoding.school100% FREE · NO SIGNUP
The EVM CoreISSUE #37 of 48

checks-effects-interactions · the safe order

SoliaVSThe Reentrancy Reaper
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

The Reentrancy Reaper attacks — common mistakes

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>
▶ Open the interactive comic issue
‹ Minimal Proxies · Eip-1167 ClonesReentrancy Guards · The Mutex Pattern ›