Smart contracts are self-executing programs that run on the blockchain, with the terms of an agreement directly written into code. As with any software development, applying well-established design patterns to smart contracts helps create more secure, maintainable, and efficient code.
In this first part, we’ll explore some fundamental design patterns that every Solidity developer should know. These patterns address common challenges in smart contract development, particularly around security and access control.
Why Design Patterns Matter in Smart Contracts
Smart contracts are immutable once deployed (unless using upgrade patterns), operate with real financial value, and are visible to everyone on the blockchain. This makes security and correctness absolutely critical — a single bug can lead to the permanent loss of funds, as we’ve seen with the infamous DAO hack.
Access Restriction Pattern
One of the most fundamental patterns in smart contract development is restricting access to certain functions. Only authorized addresses should be able to call sensitive functions.
pragma solidity ^0.8.0;
contract AccessControl {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
constructor() {
owner = msg.sender;
}
function sensitiveFunction() public onlyOwner {
// Only the owner can call this
}
}
Checks-Effects-Interactions Pattern
This pattern is crucial for preventing reentrancy attacks — one of the most common and dangerous vulnerabilities in smart contracts. The rule is simple: always perform all checks first, then update state, then interact with external contracts.
contract SafeWithdraw {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) public {
// 1. CHECK: Verify conditions
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. EFFECTS: Update state
balances[msg.sender] -= amount;
// 3. INTERACTIONS: External calls last
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Pull Payment Pattern
Instead of pushing payments to users (which can fail for various reasons), this pattern allows users to pull (withdraw) their payments themselves. This is more secure and gas-efficient.
contract PullPayment {
mapping(address => uint256) private payments;
function makePayment(address recipient, uint256 amount) internal {
payments[recipient] += amount;
}
function withdrawPayment() public {
uint256 payment = payments[msg.sender];
require(payment > 0, "No payment available");
payments[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: payment}("");
require(success, "Transfer failed");
}
}
Emergency Stop (Circuit Breaker) Pattern
This pattern allows contract owners to pause contract functionality in case of a discovered vulnerability or attack.
contract EmergencyStop {
bool public stopped = false;
address public owner;
modifier stopInEmergency() {
require(!stopped, "Contract is stopped");
_;
}
modifier onlyInEmergency() {
require(stopped, "Not in emergency");
_;
}
function stopContract() public {
require(msg.sender == owner);
stopped = true;
}
function resumeContract() public {
require(msg.sender == owner);
stopped = false;
}
}
Conclusion
These four patterns form the foundation of secure smart contract development. In Part 2, we’ll explore more advanced patterns including the Proxy Pattern for upgradeable contracts, the Factory Pattern for deploying multiple contracts, and the State Machine Pattern for managing complex contract states.
Remember: in blockchain development, security is not an afterthought — it must be baked in from the beginning. These patterns represent the accumulated wisdom of the smart contract development community, learned sometimes through painful (and expensive) lessons.