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:

  1. Checks — Validate all conditions and revert if any fail.
  2. Effects — Update all state variables.
  3. 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:

  1. Ownable — A single privileged address (the owner). Simple but centralized.
  2. 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.

// 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 require with 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 / custom error: 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.