Blockchain
Smart Contract Patterns
In March 2023, the Euler Finance protocol lost $197 million in a single transaction. The cause - a violation of one design pattern. A month later, Compound narrowly avoided losing $80M thanks to another pattern - Timelock gave the community 48 hours to detect a malicious proposal. Smart contracts manage billions of dollars with no ability to roll back, and the only thing standing between user funds and catastrophe is architectural patterns refined through years of industry mistakes.
- **Uniswap V2 Factory** created over 300,000 trading pairs through a single Factory contract. CREATE2 allows computing any pair's address off-chain without querying the blockchain - saving gas on every swap
- **USDC, Aave, Compound** - all use the Proxy pattern to upgrade logic. When Circle discovered a vulnerability in USDC, they updated the implementation through a proxy without interrupting operation of the stablecoin with a $30B+ market cap
- **The DAO (2016, $60M), Euler (2023, $197M), Curve (2023, $70M)** - all these hacks happened due to violations of the Checks-Effects-Interactions pattern. ReentrancyGuard from OpenZeppelin, costing 20,000 gas, could have prevented each one of them
Предварительные знания
Factory: Creating Contracts from Contracts
In traditional programming, the Factory pattern creates objects. In Solidity it does something more powerful - it **creates new contracts directly from another contract's code**. Each created contract gets its own address, storage, and balance. The Factory contract serves as a single entry point: it keeps a registry of all spawned contracts and guarantees uniform creation.
The EVM provides two opcodes for contract creation: **CREATE** computes the address from `keccak256(sender, nonce)` - each call yields a new address. **CREATE2** (EIP-1014) computes the address from `keccak256(0xff, sender, salt, bytecodeHash)` - the address is **deterministic** and known in advance, even before deployment. This allows sending funds to a contract address that doesn't yet exist.
Deploying a full contract via Factory is expensive - from 200,000 to 1,000,000+ gas. For situations where hundreds or thousands of identical contracts need to be created, there is the **Clone Pattern (EIP-1167)**: instead of copying the entire bytecode, a minimal proxy (only 45 bytes) is created that forwards all calls to a single implementation contract via DELEGATECALL.
**Uniswap V2** uses CREATE2 in its Factory so anyone can compute a pair's address off-chain: `address(uint160(uint256(keccak256(abi.encodePacked(hex"ff", factory, salt, initCodeHash)))))`. The router computes the pair address without a single blockchain call - saving gas on every swap.
What is the main advantage of the Clone Pattern (EIP-1167) compared to a regular Factory that deploys a full contract?
Proxy: Upgradeable Contracts
Smart contract code in Ethereum is **immutable**: once deployed, the bytecode cannot be changed. But what if you find a bug? Or need to add a new feature? The **Proxy pattern** solves this problem: users interact with a proxy contract that **delegates all calls** to a separate implementation contract. To upgrade the logic, simply point the proxy at a new implementation - the address, balance, and user data stay in place.
The key mechanism is the **DELEGATECALL** opcode: it executes the target contract's code but in the context of the caller (proxy). This means `msg.sender`, `msg.value`, and all storage belong to the proxy, while the implementation only defines the logic. Here is a minimal implementation:
**Storage collision** is the main danger of the proxy pattern. The proxy and the implementation share one storage space. If the proxy stores `address impl` in slot 0, and the implementation expects `uint256 totalSupply` in slot 0 - they will overwrite each other's data. **EIP-1967** solves this by placing proxy service data in pseudo-random slots (`keccak256(...) - 1`) that practically cannot overlap with regular variables.
The proxy pattern is a broad topic with many variants (Transparent Proxy, UUPS, Beacon Proxy, Diamond). This lesson covers the basic principle. A deep dive into upgradeable contracts is in lesson **bc-37-upgradeable**.
A proxy contract uses DELEGATECALL to call an implementation. Where is the data (state variables) written by the implementation code stored?
Pull Payment: Safe Fund Distribution
Imagine an auction: 100 participants placed bids, and when the winner is determined, the contract must return funds to the 99 losers. The naive approach - iterate with a loop and call `transfer()` for each one. But what if one of the recipients is a contract whose `receive()` always calls `revert()`? The entire loop reverts, and **nobody receives any funds**. One attacker blocks payouts for everyone. This is called **Denial of Service (DoS) via revert**.
**Pull Payment** ("recipients claim themselves") flips the model: instead of sending funds, the contract records the **amount owed** in an internal escrow, and each recipient calls `withdraw()` on their own. If one recipient doesn't claim their funds - that's their problem, it doesn't affect the others.
Pull Payment is not only a DoS protection. It also reduces the **attack surface for reentrancy**: instead of sending ETH inside complex business logic (auction, profit distribution), the only point of ETH transfer is the `withdraw()` function, which is easier to audit and protect.
An auction contract uses the Push pattern: it loops through transfer() calls to return bids to 50 participants. One of the participants is a contract with receive() { revert(); }. What will happen?
Access Control: Permission Management
A smart contract is public code: **anyone** can call any `external`/`public` function. Without Access Control, an attacker can withdraw funds, pause the contract, or transfer ownership. Access control patterns define rules: *who* can call *which* function and *under what conditions*.
The simplest option is **Ownable** (a single owner). But real protocols need granular permissions: one address can pause, another can manage the treasury, a third can update parameters. For this, OpenZeppelin provides **AccessControl** - a role-based model.
**Ownable** without a two-step transfer (`transferOwnership`) is a common cause of lost contracts. If the owner transfers rights to the wrong address (a typo), the contract is lost forever. **Ownable2Step** from OpenZeppelin requires the new owner to explicitly confirm acceptance - `acceptOwnership()`.
A DeFi protocol uses Ownable (one owner). The team wants pause operations to be done quickly (security team), while treasury withdrawals go through a 48-hour delay. Which pattern fits best?
Guard Check: Protecting Invariants
The last line of defense in a smart contract is **guard patterns**: mechanisms that check preconditions, postconditions, and invariants at every step of execution. The central principle is **Checks-Effects-Interactions (CEI)**, the order of operations whose violation has cost the industry hundreds of millions of dollars in reentrancy attacks.
Solidity provides three mechanisms for checks: `require()`, `revert()`, and `assert()`. Since version 0.8.26 it is recommended to use **custom errors** instead of string messages - they save up to 50% gas on revert:
OpenZeppelin provides ready-made guard modifiers for common protection scenarios:
**Circuit Breaker** (Pausable) is an emergency stop pattern. On detecting an anomaly (unusual withdrawal volume, suspicious transactions), an authorized address pauses the contract. All critical functions with the `whenNotPaused` modifier stop working. This gave **Euler Finance** time to block further fund loss during the 2023 exploit.
The Checks-Effects-Interactions (CEI) pattern fully protects against reentrancy, so ReentrancyGuard is not needed
CEI is a necessary but not always sufficient protection. In complex contracts with multiple functions, cross-function reentrancy is possible: an attacker calls function A, which triggers a callback, and from the callback calls function B that uses state that hasn't been updated yet. ReentrancyGuard (mutex) protects the entire contract, blocking any re-entry
Developers often rely only on CEI, forgetting about cross-function reentrancy. For example, withdraw() follows CEI, but getBalance() reads state that another function hasn't updated yet. The combination of CEI + ReentrancyGuard + Pausable is the standard protection stack in production contracts used by Aave, Compound, and Uniswap.
A contract calls payable(msg.sender).call{value: amount}("") BEFORE updating the balance in storage (balances[msg.sender] -= amount). What attack does this enable?
Key Ideas
- **Factory pattern** creates contracts from contracts. CREATE2 gives deterministic addresses, while Clone Pattern (EIP-1167) reduces deployment cost from 200,000+ to 37,000 gas - critical when creating identical contracts at scale
- **Proxy pattern** separates data from logic via DELEGATECALL: proxy stores state, implementation stores code. An upgrade = changing the pointer to the implementation. The main danger is storage collision, solved by EIP-1967
- **Pull Payment** flips the payout model: the contract doesn't send funds (Push), it lets recipients claim themselves (Pull). One malicious receive() cannot block payouts for everyone else
- **AccessControl** provides granular roles instead of a single owner. PAUSER_ROLE for quick response, TREASURY_ROLE through Timelock for transparency - every protocol needs multi-level access
- **Checks-Effects-Interactions + ReentrancyGuard + Pausable** - triple protection against reentrancy and emergency situations. Violating the CEI pattern has cost the industry hundreds of millions of dollars - from The DAO to Euler Finance
Related Topics
Smart contract patterns connect the Solidity foundation with advanced topics in security and upgradeability:
- Solidity: Language Basics — Patterns are built on the Solidity foundation - data types, functions, modifiers, and inheritance
- Upgradeable Contracts — The Proxy pattern from this lesson is an introduction. Transparent Proxy, UUPS, Beacon, and Diamond are covered in depth
- ERC Standards — ERC-20, ERC-721, and ERC-1155 actively use all patterns from this lesson: AccessControl for mint/burn, Pull Payment for royalty distribution
- Security: Reentrancy — Guard Check and the CEI pattern are the first line of defense. Deep dive into reentrancy attacks including cross-function and read-only reentrancy
Вопросы для размышления
- Factory + Clone (EIP-1167) dramatically reduces deployment cost, but all clones delegate calls to one implementation. What risks does this create if a critical bug is found in the implementation?
- The Proxy pattern makes a contract upgradeable, but upgradeability conflicts with the idea of immutable code on a blockchain. How do you find the balance between safety (ability to fix a bug) and trust (the user knows the code won't change)?
- Pull Payment shifts responsibility to the recipient: they must call withdraw() themselves. How does this affect UX? How do DeFi protocols handle the problem of "forgotten" funds in escrow?