Appendix F: Smart Contract Design Patterns Catalog
This appendix catalogs the design patterns introduced in Chapter 14 and applied throughout the smart contract development chapters (Chapters 12-26). Each pattern is presented with: the problem it solves, the solution approach, a working code example, guidance on when to use it, and warnings about when it is the wrong choice.
Smart contracts are uniquely constrained: they are immutable after deployment, handle real financial value, execute in an adversarial environment, and charge gas for every operation. These constraints make design patterns not merely best practices but essential survival tools.
F.1 Checks-Effects-Interactions (CEI)
Problem
External calls to untrusted contracts can re-enter the calling function before state updates are complete, enabling attackers to drain funds by repeatedly invoking the function before the balance is decremented. This is the reentrancy vulnerability that caused the 2016 DAO hack ($60M loss).
Solution
Structure every function in three phases, strictly in this order:
- Checks — Validate all conditions and revert if any fail.
- Effects — Update all state variables.
- Interactions — Make external calls (transfers, contract calls) last.
Because state is updated before the external call, a reentrant call sees the already-updated state and cannot exploit stale values.
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Vault {
mapping(address => uint256) public balances;
function deposit() external payable {
// Effects
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
// 1. Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. Effects (state updated BEFORE the external call)
balances[msg.sender] -= amount;
// 3. Interactions (external call is LAST)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
For additional protection, combine CEI with a reentrancy guard:
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SecureVault is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
When to Use
- Every function that makes an external call should follow CEI ordering.
- Any function that transfers ETH or calls an external contract.
- Functions that interact with untrusted or user-supplied contract addresses.
When NOT to Use
CEI is not optional; it is always applicable. However, the nonReentrant modifier adds ~2,600 gas per call. For internal-only functions with no external calls, the modifier is unnecessary overhead.
F.2 Pull Over Push
Problem
Pushing payments to multiple recipients in a loop is dangerous. If any recipient is a contract that reverts on receive (deliberately or due to a bug), the entire transaction fails, blocking all other payments. An attacker can exploit this to permanently freeze a contract.
Solution
Instead of pushing payments to recipients, record the amounts owed and let recipients pull (withdraw) their funds individually. Each withdrawal is an independent transaction that cannot block others.
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title PullPayment - Recipients withdraw their own funds
contract Auction {
address public highestBidder;
uint256 public highestBid;
mapping(address => uint256) public pendingReturns;
bool public ended;
function bid() external payable {
require(!ended, "Auction ended");
require(msg.value > highestBid, "Bid too low");
if (highestBidder != address(0)) {
// Record the amount owed (do NOT send it now)
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
}
/// @notice Previous bidders call this to retrieve their outbid funds
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
require(amount > 0, "Nothing to withdraw");
// CEI: update state before transfer
pendingReturns[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function endAuction() external {
ended = true;
// Send winning bid to seller via pull pattern too, or directly
}
}
When to Use
- Distributing funds to multiple recipients (auctions, prize pools, revenue sharing).
- Any scenario where a failing recipient should not block the entire contract.
- Refund mechanisms.
When NOT to Use
- Single-recipient payments where the recipient is a known, trusted address (e.g., the contract owner). A direct transfer is simpler.
- When immediate payment is a business requirement and the recipient address is controlled (e.g., paying an oracle fee).
F.3 Access Control
Problem
Smart contract functions need authorization mechanisms to restrict who can call sensitive operations (minting tokens, pausing the contract, upgrading logic, withdrawing treasury funds). Without access control, any address can call any public function.
Solution
Implement role-based access control. Two standard approaches:
- Ownable — A single privileged address (the owner). Simple but centralized.
- AccessControl — Multiple named roles with granular permissions. More complex but decentralized.
Code: Ownable Pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/Ownable.sol";
contract Treasury is Ownable {
constructor() Ownable(msg.sender) {}
/// @notice Only the owner can withdraw funds
function withdraw(uint256 amount) external onlyOwner {
(bool success, ) = owner().call{value: amount}("");
require(success, "Transfer failed");
}
/// @notice Transfer ownership to a multisig for production
function transferToMultisig(address multisig) external onlyOwner {
transferOwnership(multisig);
}
receive() external payable {}
}
Code: Role-Based Access Control
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/access/AccessControl.sol";
contract TokenWithRoles is AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
mapping(address => uint256) public balances;
bool public paused;
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(MINTER_ROLE, admin);
_grantRole(PAUSER_ROLE, admin);
}
function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
require(!paused, "Contract is paused");
balances[to] += amount;
}
function pause() external onlyRole(PAUSER_ROLE) {
paused = true;
}
function unpause() external onlyRole(PAUSER_ROLE) {
paused = false;
}
/// @notice Admin can grant minter role to other addresses
// Inherited: grantRole(MINTER_ROLE, newMinter) — callable by DEFAULT_ADMIN_ROLE
}
When to Use
- Ownable: Prototyping, simple contracts with a single admin, contracts intended for multisig ownership.
- AccessControl: Production contracts with multiple admin functions, DAO-governed protocols, systems where different operations require different authorization levels.
When NOT to Use
- Fully permissionless protocols where no function requires privileged access. If no function needs restriction, access control adds unnecessary complexity.
- Be cautious with Ownable in production: a single owner key is a critical point of failure. Transfer ownership to a multisig before mainnet deployment.
F.4 Proxy Patterns (Upgradability)
Problem
Smart contracts are immutable after deployment. If a bug is discovered or new features are needed, the contract cannot be modified. Migrating to a new contract requires all users to update addresses and re-approve token allowances.
Solution
Separate the contract into a proxy (which holds storage and receives calls) and an implementation (which holds the logic). The proxy delegates all calls to the implementation using delegatecall. To upgrade, deploy a new implementation and point the proxy to it.
Code: UUPS Proxy (Recommended)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
/// @title Implementation V1
contract VaultV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
mapping(address => uint256) public balances;
/// @notice Replaces constructor for upgradeable contracts
function initialize(address initialOwner) public initializer {
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Failed");
}
/// @notice Only owner can authorize upgrades
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
/// @title Implementation V2 (adds interest calculation)
contract VaultV2 is VaultV1 {
// New storage variables MUST be appended, never reordered
mapping(address => uint256) public depositTimestamps;
function depositWithTimestamp() external payable {
balances[msg.sender] += msg.value;
depositTimestamps[msg.sender] = block.timestamp;
}
}
Deployment with Hardhat:
const { ethers, upgrades } = require("hardhat");
// Deploy V1
const VaultV1 = await ethers.getContractFactory("VaultV1");
const proxy = await upgrades.deployProxy(VaultV1, [owner.address], { kind: "uups" });
await proxy.waitForDeployment();
// Upgrade to V2
const VaultV2 = await ethers.getContractFactory("VaultV2");
await upgrades.upgradeProxy(proxy.target, VaultV2);
Proxy Pattern Comparison
| Pattern | Upgrade Logic Location | Gas Overhead | Admin Slot Collision Risk |
|---|---|---|---|
| Transparent Proxy | In the proxy contract | Higher (admin checks on every call) | None (admin slot is explicit) |
| UUPS | In the implementation | Lower (no admin check overhead) | None |
| Beacon Proxy | In a beacon contract | Low per proxy | None |
| Diamond (EIP-2535) | In facet contracts | Variable | Managed by storage slots |
When to Use
- Long-lived contracts that will need bug fixes or feature additions.
- Protocol contracts governed by a DAO where upgrades go through governance votes.
- Early-stage projects where requirements may change.
When NOT to Use
- Contracts where immutability is a feature and a selling point (e.g., Uniswap V2 core contracts are intentionally non-upgradeable).
- Simple, well-audited contracts with no foreseeable need for changes.
- When trust minimization is paramount: upgradeable contracts require trusting the upgrade authority.
Critical Rule: Never reorder, rename, or remove existing storage variables in an upgraded implementation. Only append new variables at the end.
F.5 Factory Pattern
Problem
Many protocols need to deploy multiple instances of the same contract (one pool per token pair, one vault per strategy, one escrow per transaction). Deploying each manually is error-prone and makes it impossible to maintain a registry.
Solution
A factory contract deploys new instances of a child contract and maintains a registry of all deployed instances. The factory can enforce configuration, set initial parameters, and provide a single entry point for discovery.
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Escrow {
address public buyer;
address public seller;
uint256 public amount;
bool public released;
constructor(address _buyer, address _seller) payable {
buyer = _buyer;
seller = _seller;
amount = msg.value;
}
function release() external {
require(msg.sender == buyer, "Only buyer");
require(!released, "Already released");
released = true;
(bool success, ) = seller.call{value: amount}("");
require(success, "Transfer failed");
}
}
contract EscrowFactory {
// Registry of all deployed escrows
address[] public escrows;
mapping(address => address[]) public userEscrows;
event EscrowCreated(address indexed escrow, address indexed buyer, address indexed seller);
function createEscrow(address seller) external payable returns (address) {
require(msg.value > 0, "Must send ETH");
Escrow escrow = new Escrow{value: msg.value}(msg.sender, seller);
address escrowAddr = address(escrow);
escrows.push(escrowAddr);
userEscrows[msg.sender].push(escrowAddr);
userEscrows[seller].push(escrowAddr);
emit EscrowCreated(escrowAddr, msg.sender, seller);
return escrowAddr;
}
function getEscrowCount() external view returns (uint256) {
return escrows.length;
}
function getUserEscrows(address user) external view returns (address[] memory) {
return userEscrows[user];
}
}
For gas-efficient deployment of many identical contracts, use CREATE2 (deterministic addresses) or Clones (EIP-1167):
import "@openzeppelin/contracts/proxy/Clones.sol";
contract CheapEscrowFactory {
address public implementation;
constructor() {
implementation = address(new Escrow(address(1), address(1)));
}
function createEscrow(address seller) external payable returns (address) {
address clone = Clones.clone(implementation);
// Initialize the clone (since constructor is not called for clones)
Escrow(clone).initialize(msg.sender, seller);
return clone;
}
}
When to Use
- Protocols that deploy many instances of the same contract (DEX pools, lending markets, escrows).
- When you need an on-chain registry of deployed contracts.
- When deployment parameters must be validated or standardized.
When NOT to Use
- One-off contract deployments that do not need a registry.
- When the child contracts are significantly different from each other (use inheritance instead).
F.6 State Machine
Problem
Many contracts have a lifecycle with distinct phases (crowdfunding: Active -> Successful -> Finalized, or Active -> Failed -> Refunding). Functions should only be callable during the appropriate phase, and transitions between phases must be controlled.
Solution
Model the contract's lifecycle as an explicit state machine using an enum. Use modifiers to restrict function access based on the current state.
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract Crowdfund {
enum State { Active, Successful, Failed, Finalized }
State public state;
address public creator;
uint256 public goal;
uint256 public deadline;
uint256 public totalRaised;
mapping(address => uint256) public contributions;
modifier inState(State expected) {
require(state == expected, "Invalid state for this action");
_;
}
constructor(uint256 _goal, uint256 _durationDays) {
creator = msg.sender;
goal = _goal;
deadline = block.timestamp + (_durationDays * 1 days);
state = State.Active;
}
function contribute() external payable inState(State.Active) {
require(block.timestamp < deadline, "Deadline passed");
contributions[msg.sender] += msg.value;
totalRaised += msg.value;
}
/// @notice Anyone can call to transition state after deadline
function checkDeadline() external inState(State.Active) {
require(block.timestamp >= deadline, "Deadline not reached");
if (totalRaised >= goal) {
state = State.Successful;
} else {
state = State.Failed;
}
}
function claimFunds() external inState(State.Successful) {
require(msg.sender == creator, "Only creator");
state = State.Finalized;
(bool success, ) = creator.call{value: totalRaised}("");
require(success, "Transfer failed");
}
function refund() external inState(State.Failed) {
uint256 amount = contributions[msg.sender];
require(amount > 0, "No contribution");
contributions[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Refund failed");
}
}
When to Use
- Contracts with a clear lifecycle (crowdfunding, auctions, escrows, governance proposals).
- When certain functions must be disabled after a phase transition.
- Multi-party workflows with sequential steps.
When NOT to Use
- Simple contracts without distinct phases.
- When the state transitions are trivial (a single boolean flag suffices).
F.7 Guard Check
Problem
Functions need to validate inputs, enforce preconditions, and ensure invariants hold. Invalid inputs can lead to unexpected behavior, wasted gas, or security vulnerabilities.
Solution
Use require, revert, and custom errors at the start of functions to validate all preconditions. Custom errors (Solidity 0.8.4+) are gas-efficient alternatives to error strings.
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @notice Custom errors are cheaper than require strings (~50 gas per revert)
error InvalidAmount(uint256 provided, uint256 minimum);
error ZeroAddress();
error DeadlineExpired(uint256 deadline, uint256 current);
error Unauthorized(address caller, address expected);
contract GuardedToken {
mapping(address => uint256) public balances;
address public admin;
uint256 public constant MIN_TRANSFER = 1e15; // 0.001 ETH equivalent
constructor() {
admin = msg.sender;
}
function transfer(address to, uint256 amount) external {
// Guard: zero address
if (to == address(0)) revert ZeroAddress();
// Guard: minimum amount
if (amount < MIN_TRANSFER) revert InvalidAmount(amount, MIN_TRANSFER);
// Guard: sufficient balance
if (balances[msg.sender] < amount) {
revert InvalidAmount(balances[msg.sender], amount);
}
balances[msg.sender] -= amount;
balances[to] += amount;
}
function adminAction() external {
if (msg.sender != admin) revert Unauthorized(msg.sender, admin);
// ... admin logic
}
}
Guard Patterns Summary
| Guard Type | Purpose | Example |
|---|---|---|
| Input validation | Prevent invalid arguments | if (amount == 0) revert |
| Authorization | Restrict access | if (msg.sender != owner) revert |
| State precondition | Enforce lifecycle phase | if (state != State.Active) revert |
| Invariant check | Ensure internal consistency | assert(totalSupply == sum(balances)) |
| External result | Verify external call success | if (!success) revert |
When to Use
- Every public and external function should have guard checks. This is not optional.
- Use custom errors for production contracts (gas savings and better error decoding).
- Use
requirewith string messages for learning and prototyping (clearer error output).
When NOT to Use
Guard checks are always appropriate. The only consideration is choosing between require (with string), custom error, and assert:
require/ customerror: For input validation and precondition checks. Reverts refund remaining gas.assert: For invariant checks that should never fail. Consumes all remaining gas (signals a bug, not user error).
F.8 Emergency Stop (Pausable)
Problem
If a critical vulnerability is discovered or an exploit is underway, there must be a mechanism to immediately halt all sensitive operations while the issue is investigated and resolved.
Solution
Implement a pause mechanism that allows an authorized address (or multisig / DAO) to freeze critical functions. OpenZeppelin's Pausable contract provides a battle-tested implementation.
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract PausableVault is Pausable, Ownable {
mapping(address => uint256) public balances;
constructor() Ownable(msg.sender) {}
/// @notice Deposits are allowed even when paused (users can still add funds)
function deposit() external payable {
balances[msg.sender] += msg.value;
}
/// @notice Withdrawals are paused during emergencies
function withdraw(uint256 amount) external whenNotPaused {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Failed");
}
/// @notice Transfers are paused during emergencies
function transfer(address to, uint256 amount) external whenNotPaused {
require(balances[msg.sender] >= amount, "Insufficient");
balances[msg.sender] -= amount;
balances[to] += amount;
}
// Emergency controls
function pause() external onlyOwner {
_pause();
// Emit event, notify monitoring systems
}
function unpause() external onlyOwner {
_unpause();
}
/// @notice Emergency withdrawal bypasses normal logic (owner only, when paused)
function emergencyWithdrawAll() external onlyOwner {
require(paused(), "Must be paused");
(bool success, ) = owner().call{value: address(this).balance}("");
require(success, "Failed");
}
}
When to Use
- Any contract holding significant value (DeFi protocols, token contracts, bridges).
- Contracts in early deployment stages where rapid response capability is critical.
- As part of a graduated security response plan (pause -> investigate -> fix -> unpause or migrate).
When NOT to Use
- Fully decentralized, trustless contracts where no entity should have pause authority. Pause capability is a form of centralization.
- After a protocol has been battle-tested for years and the community decides to renounce pause authority (a governance decision).
- Consider: who holds the pause key? A single EOA is a risk. A multisig with a time-delay is more appropriate for production.
F.9 Rate Limiting
Problem
Certain operations should be restricted in frequency to prevent abuse, manage risk, or enforce cooldown periods. Without rate limiting, an attacker could drain a faucet, spam governance proposals, or exploit a vulnerability faster than responders can react.
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract RateLimitedVault {
mapping(address => uint256) public balances;
mapping(address => uint256) public lastWithdrawalTime;
mapping(address => uint256) public dailyWithdrawn;
mapping(address => uint256) public dailyResetTime;
uint256 public constant COOLDOWN = 1 hours;
uint256 public constant DAILY_LIMIT = 10 ether;
uint256 public constant DAY = 24 hours;
error CooldownActive(uint256 remainingSeconds);
error DailyLimitExceeded(uint256 requested, uint256 remaining);
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
// Check cooldown
uint256 timeSince = block.timestamp - lastWithdrawalTime[msg.sender];
if (timeSince < COOLDOWN) {
revert CooldownActive(COOLDOWN - timeSince);
}
// Check and reset daily limit
if (block.timestamp >= dailyResetTime[msg.sender] + DAY) {
dailyWithdrawn[msg.sender] = 0;
dailyResetTime[msg.sender] = block.timestamp;
}
uint256 remaining = DAILY_LIMIT - dailyWithdrawn[msg.sender];
if (amount > remaining) {
revert DailyLimitExceeded(amount, remaining);
}
// Standard checks
require(balances[msg.sender] >= amount, "Insufficient balance");
// Update rate limit state
lastWithdrawalTime[msg.sender] = block.timestamp;
dailyWithdrawn[msg.sender] += amount;
// CEI: state updated, now interact
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
/// @notice View remaining daily allowance
function remainingDailyAllowance(address user) external view returns (uint256) {
if (block.timestamp >= dailyResetTime[user] + DAY) {
return DAILY_LIMIT;
}
return DAILY_LIMIT - dailyWithdrawn[user];
}
/// @notice View seconds until next withdrawal is allowed
function cooldownRemaining(address user) external view returns (uint256) {
uint256 elapsed = block.timestamp - lastWithdrawalTime[user];
if (elapsed >= COOLDOWN) return 0;
return COOLDOWN - elapsed;
}
}
Rate Limiting Strategies
| Strategy | Mechanism | Use Case |
|---|---|---|
| Cooldown timer | Minimum time between actions | Faucets, withdrawals |
| Daily/hourly cap | Maximum amount per time window | Withdrawal limits, minting |
| Per-block limit | Maximum actions per block | Anti-flash-loan, anti-MEV |
| Exponential backoff | Increasing delay after each action | Anti-spam for governance proposals |
| Stake-weighted | Limit proportional to staked amount | Sybil-resistant resource allocation |
When to Use
- Withdrawal functions on contracts holding large treasuries.
- Faucets and airdrop claims.
- Governance proposal creation (prevent spam proposals).
- Bridge contracts (limit damage from potential exploits).
When NOT to Use
- High-frequency DeFi operations where rate limiting would harm legitimate users (e.g., arbitrage bots providing market efficiency).
- When the gas cost of the operation already serves as a natural rate limiter.
- When the rate limit logic adds significant gas overhead relative to the protected operation.
Pattern Selection Guide
The following table maps common contract scenarios to the recommended patterns:
| Scenario | Primary Patterns | Secondary Patterns |
|---|---|---|
| Token contract (ERC-20) | Guard Check, Access Control | Pausable, Proxy (if upgradeable) |
| DeFi lending pool | CEI, Pull Over Push, Pausable | Rate Limiting, Proxy |
| NFT marketplace | Factory, State Machine, CEI | Guard Check, Access Control |
| DAO treasury | Access Control (roles), Rate Limiting | Pausable, Guard Check |
| Upgradeable protocol | Proxy (UUPS), Access Control | Pausable, Guard Check |
| Crowdfunding | State Machine, Pull Over Push | Guard Check, Pausable |
| DEX / AMM | CEI, Guard Check, Factory | Pausable, Rate Limiting |
| Bridge contract | CEI, Rate Limiting, Pausable | Access Control, Guard Check |
Composing Patterns
These patterns are not mutually exclusive. Production contracts typically combine several:
contract ProductionVault is
Initializable, // Proxy pattern (constructor replacement)
UUPSUpgradeable, // Proxy pattern (upgrade logic)
OwnableUpgradeable, // Access Control
PausableUpgradeable, // Emergency Stop
ReentrancyGuardUpgradeable // CEI reinforcement
{
// Guard Check: custom errors
error InvalidAmount();
error DailyLimitExceeded();
// Rate Limiting: state variables
mapping(address => uint256) public lastWithdrawal;
function withdraw(uint256 amount)
external
whenNotPaused // Emergency Stop
nonReentrant // CEI guard
{
// Guard Check
if (amount == 0) revert InvalidAmount();
// Rate Limiting
require(block.timestamp - lastWithdrawal[msg.sender] > 1 hours);
lastWithdrawal[msg.sender] = block.timestamp;
// CEI: Effects then Interactions
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Failed");
}
}
These nine patterns form the foundation of secure smart contract development. Chapter 14 introduces them in the context of progressive contract-building exercises. Chapters 19-22 demonstrate their application in DeFi protocols, token contracts, and NFT systems. For audit checklists that verify pattern adherence, see Chapter 35.