Introduction
As blockchain technology evolves, businesses and individuals increasingly integrate it into their operations. While blockchain offers unique advantages like transparency and immutability, these features also introduce potential risks. Publicly visible data means anyone can access it, and immutable records mean information—including contract code—cannot be altered once deployed.
To mitigate these risks, developers must prioritize security and maintainability before deploying contracts. Fortunately, years of Solidity development have yielded proven design patterns to address common challenges.
Smart Contract Design Patterns Overview
In 2019, IEEE published a seminal paper titled "Design Patterns for Smart Contracts in the Ethereum Ecosystem," analyzing popular Solidity projects to identify 18 key patterns. These span security, maintainability, lifecycle management, and authorization. Below, we explore the most widely applicable ones.
Security Patterns
1. Checks-Effects-Interaction: Secure State Management Before External Calls
This pattern mandates organizing functions into three sequential steps:
- Checks: Validate inputs (e.g.,
requirestatements). - Effects: Update contract state.
- Interaction: Perform external calls.
Example Fix for Reentrancy Vulnerabilities:
// Vulnerable Version
function addByOne() public {
require(!_adders[msg.sender], "Already added");
_count++;
AdderInterface(msg.sender).notify(); // External call before state finalization
_adders[msg.sender] = true;
}
// Secure Version
function addByOne() public {
require(!_adders[msg.sender], "Already added");
_count++;
_adders[msg.sender] = true; // State finalized first
AdderInterface(msg.sender).notify();
}2. Mutex: Prevent Recursive Attacks
A lock mechanism blocks reentrant calls:
contract Mutex {
bool private locked;
modifier noReentrancy() {
require(!locked, "Reentrancy detected");
locked = true;
_;
locked = false;
}
function sensitiveOperation() public noReentrancy {
// Critical logic
}
}Maintainability Patterns
3. Data Segregation: Decouple Logic from Storage
Separate stable data storage from upgradable business logic:
contract DataRepository {
uint private _data;
function setData(uint data) public { _data = data; }
function getData() public view returns(uint) { return _data; }
}
contract BusinessLogic {
DataRepository private _repo;
function compute() public view returns(uint) {
return _repo.getData() * 20; // Upgradable without data migration
}
}4. Satellite: Modularize Functionality
Isolate features into independent contracts for targeted upgrades:
contract Satellite {
function compute(uint a) public pure returns(uint) { return a * 10; }
}
contract Main {
Satellite private _satellite;
function updateSatellite(address newAddr) public {
_satellite = Satellite(newAddr); // Swap implementations
}
}5. Contract Registry: Centralized Version Tracking
A registry contract manages the latest satellite addresses:
contract Registry {
address public current;
function update(address newAddr) public { current = newAddr; }
}
contract Main {
Registry private _registry;
function useSatellite() public {
Satellite satellite = Satellite(_registry.current());
satellite.compute(5);
}
}Lifecycle Management
6. Mortal: Self-Destruct Mechanism
contract Mortal {
function destroy() public onlyOwner {
selfdestruct(payable(msg.sender));
}
}7. Automatic Deprecation: Time-Limited Contracts
contract ExpiringService {
uint private deadline = block.timestamp + 30 days;
modifier notExpired() {
require(block.timestamp <= deadline, "Contract expired");
_;
}
function service() public notExpired { /* ... */ }
}Authorization Patterns
8. Ownership: Role-Based Access Control
contract Owned {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
constructor() { owner = msg.sender; }
}Action and Control Patterns
9. Commit-Reveal: Privacy-Preserving Transactions
Delay sensitive data disclosure:
contract Voting {
mapping(address => bytes32) commits;
function commit(bytes32 hashedChoice) public {
commits[msg.sender] = hashedChoice;
}
function reveal(string memory choice, string memory salt) public {
require(keccak256(abi.encodePacked(choice, salt)) == commits[msg.sender]);
// Process vote
}
}10. Oracle: Fetch Off-Chain Data
contract WeatherInsurance {
Oracle private _oracle;
function claimPayout() public {
_oracle.query("Rainfall", this.handleResponse);
}
function handleResponse(bytes memory data) public onlyOracle {
// Use rainfall data
}
}FAQ Section
Q1: What’s the most critical security pattern for beginners?
A: Checks-Effects-Interaction—it prevents reentrancy attacks, the most common Solidity vulnerability.
Q2: How do upgradeable contracts handle existing data?
A: Data Segregation stores data permanently in a separate contract, allowing logic upgrades without migration.
Q3: Are design patterns Ethereum-specific?
A: While tailored for Solidity, concepts like modularity (👉 Satellite Pattern) apply across blockchain ecosystems.
Q4: What’s the gas cost implication of using oracles?
A: Oracle calls add overhead. Optimize by batching requests or using decentralized oracle networks like Chainlink.