Smart Contract Design Patterns
Chapters 1 and 2 gave you Solidity's syntax and the EVM's real mechanics. This chapter covers something different: the real, established patterns experienced Solidity developers actually reach for, several of which exist specifically because of one of the most expensive lessons in blockchain history — the same DAO hack Course 1 Chapter 6 already introduced, revisited here with the specific technical detail that chapter deliberately left out.
Checks-Effects-Interactions: The Pattern the DAO Hack Made Famous
The Checks-Effects-Interactions (CEI) pattern is a real, foundational rule for structuring any function that both updates a contract's own state and calls out to another address: always perform these three kinds of operations in this exact order.
- Checks — validate every condition first (does the caller have permission, is the requested amount valid, and so on).
- Effects — update the contract's own internal state (Chapter 1's state variables) to reflect the outcome, before talking to anyone else.
- Interactions — only now, last, call out to an external address or contract (for example, actually sending ETH).
Why the Order Matters: The Real DAO Hack Mechanism
Course 1 Chapter 6 covered the real, documented 2016 DAO hack — roughly $50 million drained from a $150 million smart contract, resolved by a contentious hard fork. What it deliberately didn't cover is precisely how the attacker actually did it — and the real mechanism is a textbook violation of this exact ordering.
Applying Checks-Effects-Interactions correctly would have prevented this outright: if the DAO's own contract had updated the caller's balance before sending any ETH, the second and every subsequent recursive call would have seen a balance already reduced to zero, and simply failed.
In the fixed version, even if the recipient's own contract tries the exact same recursive
re-entry trick, balances[msg.sender] has already been reduced by the time the
external call happens — so a second, recursive call to withdraw() now correctly
fails the very first require check. Chapter 7's own dedicated security chapter
covers reentrancy and several other real vulnerability classes in full depth — this
chapter's job is establishing the pattern that prevents this one by default.
Pull Over Push Payments
A closely related real pattern: when a contract needs to pay out to multiple recipients, don't
loop through them and actively push ETH to each one directly. Instead, record
how much each address is owed, and let each recipient pull their own payment
by calling a separate withdraw() function themselves, whenever they choose.
This has two real, concrete benefits: a single recipient with a broken or malicious contract can't block payment to everyone else in the same loop (since each pull is now a fully independent transaction), and it naturally reduces how many places in your own code perform a real external interaction — directly shrinking the surface area where a Checks-Effects-Interactions mistake could occur in the first place.
Access Control: Modifiers and the Ownable Pattern
Solidity's real modifier feature lets you define a reusable condition, checked before a function's own body runs. The most common real example restricts a function to one specific, privileged address — typically called the Ownable pattern:
| Pattern | Real Problem It Solves |
|---|---|
| Checks-Effects-Interactions | Prevents reentrancy by updating state before making external calls |
| Pull over push | Prevents one broken recipient from blocking payment to everyone else |
| Ownable / access control | Restricts sensitive functions to one privileged address |
A Real, Honest Complication: Contracts Are Immutable by Default
Course 1 Chapter 3 established that a blockchain's history is tamper-evident by design — and the exact same immutability applies to a deployed contract's own code. Once deployed, a contract's bytecode cannot simply be edited later, even by its own original developer, even to fix a genuine bug. This is a deliberate, structural consequence of everything Course 1 already established, not a separate limitation.
Real projects that genuinely need to upgrade their own logic over time use a real, more advanced approach called a proxy pattern: users interact with one fixed, permanent "proxy" contract address, which internally delegates its actual logic to a separate implementation contract — and upgrading later means pointing the proxy at a new implementation contract, rather than editing any contract's own code after the fact. This is a genuinely more complex pattern with its own real trade-offs and risks, deliberately left out of full depth here — the honest, important point for this chapter is simply that "just fix the bug and redeploy in place" is never actually an option once a contract is live.
OpenZeppelin: The Real, Standard Library
Ownable contract, a ReentrancyGuard modifier providing an
extra layer of reentrancy protection on top of CEI, and standard, audited ERC-20/ERC-721
implementations (Course 1, Chapter 8). Real, major DeFi protocols and financial platforms rely on
it directly rather than hand-writing these same patterns themselves each time. In real-world
development, using an established, reviewed library like this for foundational patterns is
almost always safer than writing your own from scratch.
Hands-On Exercises
Three exercises applying Checks-Effects-Interactions and this chapter's other real patterns, before Chapter 4 turns to DeFi mechanics built directly on top of them.
withdraw() function in this chapter allows a malicious contract to drain far more
than its own actual balance, while the fixed version doesn't.
Quick Reference
- Checks-Effects-Interactions — validate, then update your own state, then call external addresses last — in that exact order.
- The DAO hack, precisely — a real reentrancy exploit (17 June 2016) that sent ETH before updating balances, letting a recursive callback drain ~3.6M ETH (~$50M) before the balance was ever reduced.
- Pull over push — let recipients withdraw their own payments individually, rather than pushing to everyone in one loop.
- Modifiers / Ownable — reusable pre-conditions (like
onlyOwner) restricting sensitive functions to a privileged address. - Immutability & proxies — deployed contract code can't be edited in place; real upgradability needs a dedicated proxy pattern instead.
- OpenZeppelin — the real, standard, audited library implementing these patterns rather than hand-rolling them.