Smart-Contract Vulnerabilities: Reentrancy
Code is law — including its bugs. The most famous crypto exploit works by calling back into a contract before it finishes updating its own books.
Lesson 17 covered how users lose funds (phishing, bad approvals). But there’s a whole other attack surface: the smart contract’s own code. Recall from lesson 8 that a smart contract runs exactly as written and its logic is final (“code is law”) — which means a bug in that code is also law, and there’s no support line to reverse a drained protocol. The single most famous class of these bugs has emptied hundreds of millions of dollars, and its mechanism is devilishly simple once you see it. Hold the question: if a contract does its steps in the “obvious” order, how could an attacker trick it into paying out the same balance over and over?
The setup: contracts can call other contracts
Recall that smart contracts can call each other — one contract can send funds or a message to another, and that contract’s code then runs. This is normal and powerful (it’s how DeFi composes). But it hides a trap. When a contract sends money to an address, if that address is itself a contract, sending the money can trigger the receiving contract’s code to run — and control doesn’t return to the sender until the receiver is done. So an external call (a call out to another, possibly attacker-controlled, contract) briefly hands the attacker the steering wheel in the middle of your function. That handoff is the crack the whole exploit pries open.
Reentrancy: drain before the books update
Reentrancy is the classic bug. Picture a vulnerable withdraw function that does its steps in this “obvious” order: (1) check your balance is enough, (2) send you the funds, (3) set your balance to zero. The flaw is that step 2 is an external call — and if you’re a malicious contract, receiving the funds runs your code, which simply calls withdraw again before step 3 ever runs. Your balance hasn’t been zeroed yet, so the check in step 1 passes again, and it pays you a second time… and a third… looping until the whole contract is drained. The attacker “re-enters” the function repeatedly through the open door of the external call, each time exploiting that the books were updated too late. This is the exact shape of crypto’s most infamous early hack.
A reentrancy drain, step by step: • Attacker deposits 1 coin, then calls withdraw. • Contract checks balance (1 ✓), sends 1 coin to the attacker’s contract — which triggers the attacker’s code. • Before the contract can set the attacker’s balance to 0, the attacker’s code calls withdraw again: balance still shows 1 ✓, another coin sent… repeat. • The loop drains far more than the attacker ever deposited, because the “set balance to 0” step kept getting postponed.
The fix: checks, effects, then interactions
The defense is a discipline called checks-effects-interactions: do all your checks first (is the balance enough?), then apply all your effects — update your own internal state (set the balance to zero) — and only then do the interactions (the external call that sends the funds). If the books are zeroed before the money goes out, a re-entrant call finds a zero balance and the check fails — the door is shut. (There are also “lock” guards that block re-entry outright.) The deep lesson generalizes beyond reentrancy: update your own state before trusting an outside party with control, because any external call can hand control to code you don’t govern. This is why smart-contract security is its own discipline — the code is public, unstoppable, and holds real money, so a single ordering mistake is catastrophic, which is exactly why audits (a later lesson) exist.
Imagine an ATM that hands you your cash first and only then updates your account balance. Normally fine — but suppose you could hit “withdraw again” in the split second after the cash comes out but before the balance updates. The machine still thinks you have your full balance, so it pays again… and if you could repeat that faster than it updates, you’d empty the ATM on a single deposit. Reentrancy is exactly that trick: the attacker’s contract “presses withdraw again” during the payout, and the vulnerable code hadn’t yet written down that you’d already been paid. The fix is just as intuitive — update the balance before handing over the cash.
Vulnerable vs safe ordering: 1. Vulnerable: check balance → send funds (external call!) → zero the balance. The gap between “send” and “zero” is the exploit window. 2. Attacker re-enters during the send, before the balance is zeroed, and loops to drain the contract. 3. Safe (checks-effects-interactions): check balance → zero the balance → send funds. Now a re-entrant call sees a zero balance and is rejected. 4. Same three actions — only the order changed — and that ordering is the whole difference between secure and drained.
This is the reading. The interactive version — active-recall quiz, a hands-on experiment you run in your own AI, and an earned mastery check — is free in the app.
Start this lesson free →