Module 10: Design Patterns
Module 10: Design Patterns in Solidity
10.1 Introduction to Design Patterns
Design patterns are reusable solutions to common problems in software design. In Solidity, design patterns help address challenges specific to blockchain development, such as immutability, gas costs, and security concerns.
Why Use Design Patterns?
- Reusability: Avoid reinventing the wheel by using proven solutions
- Maintainability: Make code easier to understand and maintain
- Security: Implement patterns that have been vetted for security issues
- Gas Efficiency: Optimize gas usage with established patterns
- Interoperability: Create contracts that work well with other contracts
Categories of Design Patterns
In Solidity, design patterns can be categorized into:
- Behavioral Patterns: How contracts interact with each other
- Security Patterns: How to secure contracts against attacks
- Economic Patterns: How to handle economic incentives
- Upgradeability Patterns: How to update immutable contracts
- Access Control Patterns: How to manage permissions
10.2 Behavioral Patterns
Behavioral patterns define how contracts interact with each other and how they handle operations.
Guard Check Pattern
The Guard Check pattern validates inputs and conditions before executing the main logic:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract GuardCheck {
mapping(address => uint256) private balances;
// Using Guard Check pattern
function transfer(address to, uint256 amount) public {
// Guards first
require(to != address(0), "Cannot transfer to zero address");
require(amount > 0, "Amount must be greater than zero");
require(balances[msg.sender] >= amount, "Insufficient balance");
// Main logic after all checks pass
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
State Machine Pattern
The State Machine pattern models contracts that transition between different states:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Auction {
enum State { Created, Bidding, Ended, Canceled }
State public state;
address public highestBidder;
uint256 public highestBid;
constructor() {
state = State.Created;
}
modifier inState(State _state) {
require(state == _state, "Invalid state");
_;
}
function startBidding() public inState(State.Created) {
state = State.Bidding;
}
function bid() public payable inState(State.Bidding) {
require(msg.value > highestBid, "Bid not high enough");
highestBidder = msg.sender;
highestBid = msg.value;
}
function endAuction() public inState(State.Bidding) {
state = State.Ended;
// Transfer funds to seller
}
function cancelAuction() public inState(State.Bidding) {
state = State.Canceled;
// Refund highest bidder
}
}
Oracle Pattern
The Oracle pattern allows contracts to access external data:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Oracle {
address public owner;
uint256 public price;
event PriceUpdated(uint256 price);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function updatePrice(uint256 _price) public onlyOwner {
price = _price;
emit PriceUpdated(_price);
}
}
contract Consumer {
Oracle public oracle;
constructor(address _oracle) {
oracle = Oracle(_oracle);
}
function getPrice() public view returns (uint256) {
return oracle.price();
}
function calculateValue(uint256 amount) public view returns (uint256) {
return amount * oracle.price();
}
}
Commit-Reveal Pattern
The Commit-Reveal pattern prevents front-running by separating the commitment and revelation of actions:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract CommitReveal {
struct Commitment {
bytes32 commit;
uint256 blockNumber;
bool revealed;
}
mapping(address => Commitment) public commitments;
// Step 1: User commits a hash (action + salt)
function commit(bytes32 commitment) public {
commitments[msg.sender] = Commitment({
commit: commitment,
blockNumber: block.number,
revealed: false
});
}
// Step 2: User reveals the action and salt
function reveal(string memory action, bytes32 salt) public {
Commitment storage commitment = commitments[msg.sender];
require(commitment.commit != bytes32(0), "No commitment found");
require(!commitment.revealed, "Already revealed");
require(block.number > commitment.blockNumber, "Cannot reveal in same block");
bytes32 computedCommit = keccak256(abi.encodePacked(action, salt, msg.sender));
require(computedCommit == commitment.commit, "Invalid reveal");
commitment.revealed = true;
// Process the revealed action
processAction(action);
}
function processAction(string memory action) internal {
// Process the action based on its content
}
}
10.3 Security Patterns
Security patterns help protect contracts against various attacks and vulnerabilities.
Checks-Effects-Interactions Pattern
The Checks-Effects-Interactions pattern prevents reentrancy attacks by performing all state changes before external calls:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract ChecksEffectsInteractions {
mapping(address => uint256) private balances;
function withdraw(uint256 amount) public {
// 1. Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
// 2. Effects (state changes)
balances[msg.sender] -= amount;
// 3. Interactions (external calls)
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
}
Emergency Stop (Circuit Breaker) Pattern
The Emergency Stop pattern allows pausing a contract in case of emergencies:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract EmergencyStop {
address public owner;
bool public stopped = false;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier whenNotStopped() {
require(!stopped, "Contract is stopped");
_;
}
modifier whenStopped() {
require(stopped, "Contract is not stopped");
_;
}
function toggleContractStopped() public onlyOwner {
stopped = !stopped;
}
function deposit() public payable whenNotStopped {
// Deposit logic
}
function withdraw() public whenNotStopped {
// Withdraw logic
}
// Emergency function that can be called even when stopped
function emergencyWithdraw() public onlyOwner whenStopped {
// Emergency withdraw logic
}
}
Rate Limiting Pattern
The Rate Limiting pattern prevents abuse by limiting the frequency or amount of operations:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract RateLimiter {
uint256 public constant RATE_LIMIT = 1 ether;
uint256 public constant TIME_PERIOD = 1 days;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public withdrawnAmount;
function withdraw(uint256 amount) public {
// Check if a new period has started
if (block.timestamp >= lastWithdrawTime[msg.sender] + TIME_PERIOD) {
// Reset for new period
withdrawnAmount[msg.sender] = 0;
lastWithdrawTime[msg.sender] = block.timestamp;
}
// Check rate limit
require(withdrawnAmount[msg.sender] + amount <= RATE_LIMIT, "Rate limit exceeded");
// Update state
withdrawnAmount[msg.sender] += amount;
// Perform withdrawal
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
function deposit() public payable {
// Deposit logic
}
}
Secure Ether Transfer Pattern
The Secure Ether Transfer pattern ensures safe Ether transfers:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract SecureEtherTransfer {
// Pull over push pattern
mapping(address => uint256) public pendingWithdrawals;
function deposit() public payable {
// Deposit logic
}
// Unsafe: Might fail if recipient is a contract that rejects Ether
function unsafeTransfer(address payable to, uint256 amount) public {
to.transfer(amount); // Limited to 2300 gas
}
// Safer: Uses call with more gas, but check return value
function saferTransfer(address payable to, uint256 amount) public {
(bool success, ) = to.call{value: amount}("");
require(success, "Transfer failed");
}
// Safest: Pull over push pattern
function createPendingWithdrawal(address to, uint256 amount) public {
pendingWithdrawals[to] += amount;
}
function withdraw() public {
uint256 amount = pendingWithdrawals[msg.sender];
require(amount > 0, "No pending withdrawal");
// Update state before transfer
pendingWithdrawals[msg.sender] = 0;
// Transfer
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
10.4 Economic Patterns
Economic patterns deal with incentives, tokens, and financial aspects of contracts.
Token Pattern
The Token pattern implements a transferable asset:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Token {
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
uint256 public totalSupply;
string public name;
string public symbol;
uint8 public decimals;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
decimals = _decimals;
totalSupply = _initialSupply;
balances[msg.sender] = _initialSupply;
}
function transfer(address to, uint256 value) public returns (bool) {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= value;
balances[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
function approve(address spender, uint256 value) public returns (bool) {
allowances[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transferFrom(address from, address to, uint256 value) public returns (bool) {
require(balances[from] >= value, "Insufficient balance");
require(allowances[from][msg.sender] >= value, "Insufficient allowance");
balances[from] -= value;
balances[to] += value;
allowances[from][msg.sender] -= value;
emit Transfer(from, to, value);
return true;
}
}
Subscription Pattern
The Subscription pattern implements recurring payments:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Subscription {
struct SubscriptionPlan {
uint256 price;
uint256 duration; // in seconds
}
struct UserSubscription {
uint256 planId;
uint256 startTime;
uint256 endTime;
bool active;
}
mapping(uint256 => SubscriptionPlan) public plans;
mapping(address => UserSubscription) public subscriptions;
uint256 public nextPlanId = 1;
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function createPlan(uint256 price, uint256 duration) public onlyOwner {
plans[nextPlanId] = SubscriptionPlan({
price: price,
duration: duration
});
nextPlanId++;
}
function subscribe(uint256 planId) public payable {
SubscriptionPlan storage plan = plans[planId];
require(plan.price > 0, "Plan does not exist");
require(msg.value >= plan.price, "Insufficient payment");
UserSubscription storage sub = subscriptions[msg.sender];
// If already subscribed, extend the subscription
if (sub.active && sub.endTime > block.timestamp) {
sub.endTime += plan.duration;
} else {
// New subscription
sub.planId = planId;
sub.startTime = block.timestamp;
sub.endTime = block.timestamp + plan.duration;
sub.active = true;
}
// Refund excess payment
if (msg.value > plan.price) {
payable(msg.sender).transfer(msg.value - plan.price);
}
}
function cancelSubscription() public {
UserSubscription storage sub = subscriptions[msg.sender];
require(sub.active, "No active subscription");
sub.active = false;
}
function isSubscribed(address user) public view returns (bool) {
UserSubscription storage sub = subscriptions[user];
return sub.active && sub.endTime > block.timestamp;
}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
}
Staking Pattern
The Staking pattern allows users to lock tokens and earn rewards:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
contract Staking {
IERC20 public stakingToken;
IERC20 public rewardToken;
uint256 public rewardRate = 100; // Tokens per second
uint256 public lastUpdateTime;
uint256 public rewardPerTokenStored;
mapping(address => uint256) public userRewardPerTokenPaid;
mapping(address => uint256) public rewards;
mapping(address => uint256) public balances;
uint256 public totalSupply;
constructor(address _stakingToken, address _rewardToken) {
stakingToken = IERC20(_stakingToken);
rewardToken = IERC20(_rewardToken);
}
modifier updateReward(address account) {
rewardPerTokenStored = rewardPerToken();
lastUpdateTime = block.timestamp;
if (account != address(0)) {
rewards[account] = earned(account);
userRewardPerTokenPaid[account] = rewardPerTokenStored;
}
_;
}
function rewardPerToken() public view returns (uint256) {
if (totalSupply == 0) {
return rewardPerTokenStored;
}
return rewardPerTokenStored + (
((block.timestamp - lastUpdateTime) * rewardRate * 1e18) / totalSupply
);
}
function earned(address account) public view returns (uint256) {
return (
(balances[account] * (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18
) + rewards[account];
}
function stake(uint256 amount) public updateReward(msg.sender) {
require(amount > 0, "Cannot stake 0");
totalSupply += amount;
balances[msg.sender] += amount;
stakingToken.transferFrom(msg.sender, address(this), amount);
}
function withdraw(uint256 amount) public updateReward(msg.sender) {
require(amount > 0, "Cannot withdraw 0");
totalSupply -= amount;
balances[msg.sender] -= amount;
stakingToken.transfer(msg.sender, amount);
}
function getReward() public updateReward(msg.sender) {
uint256 reward = rewards[msg.sender];
if (reward > 0) {
rewards[msg.sender] = 0;
rewardToken.transfer(msg.sender, reward);
}
}
function exit() public {
withdraw(balances[msg.sender]);
getReward();
}
}
10.5 Upgradeability Patterns
Upgradeability patterns allow contracts to be updated despite blockchain immutability.
Proxy Pattern
The Proxy pattern separates the contract interface from its implementation:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
// Interface for the implementation contract
interface IImplementation {
function getValue() external view returns (uint256);
function setValue(uint256 _value) external;
}
// Proxy contract that delegates calls to the implementation
contract Proxy {
address public implementation;
address public admin;
constructor(address _implementation) {
implementation = _implementation;
admin = msg.sender;
}
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}
function upgrade(address _newImplementation) public onlyAdmin {
implementation = _newImplementation;
}
// Fallback function that delegates all calls to the implementation
fallback() external payable {
address _impl = implementation;
assembly {
// Copy msg.data
calldatacopy(0, 0, calldatasize())
// Call implementation
let result := delegatecall(gas(), _impl, 0, calldatasize(), 0, 0)
// Copy return data
returndatacopy(0, 0, returndatasize())
// Return or revert
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
// Receive function to accept Ether
receive() external payable {}
}
// Implementation contract
contract Implementation is IImplementation {
uint256 private value;
function getValue() external view override returns (uint256) {
return value;
}
function setValue(uint256 _value) external override {
value = _value;
}
}
// Updated implementation with additional functionality
contract ImplementationV2 is IImplementation {
uint256 private value;
function getValue() external view override returns (uint256) {
return value;
}
function setValue(uint256 _value) external override {
value = _value;
}
function increment() external {
value += 1;
}
}
Data Separation Pattern
The Data Separation pattern separates data from logic:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
// Data contract that stores the state
contract DataContract {
address public owner;
address public logicContract;
uint256 public value;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier onlyLogic() {
require(msg.sender == logicContract, "Not logic contract");
_;
}
function setLogicContract(address _logicContract) public onlyOwner {
logicContract = _logicContract;
}
function setValue(uint256 _value) public onlyLogic {
value = _value;
}
}
// Logic contract that implements the business logic
contract LogicContract {
DataContract public dataContract;
constructor(address _dataContract) {
dataContract = DataContract(_dataContract);
}
function setValue(uint256 _value) public {
dataContract.setValue(_value);
}
function getValue() public view returns (uint256) {
return dataContract.value();
}
}
// Updated logic contract
contract LogicContractV2 {
DataContract public dataContract;
constructor(address _dataContract) {
dataContract = DataContract(_dataContract);
}
function setValue(uint256 _value) public {
dataContract.setValue(_value);
}
function getValue() public view returns (uint256) {
return dataContract.value();
}
function increment() public {
uint256 currentValue = dataContract.value();
dataContract.setValue(currentValue + 1);
}
}
Eternal Storage Pattern
The Eternal Storage pattern uses a generic key-value store for data:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
// Eternal storage contract
contract EternalStorage {
address public owner;
address public logicContract;
mapping(bytes32 => uint256) private uintStorage;
mapping(bytes32 => string) private stringStorage;
mapping(bytes32 => address) private addressStorage;
mapping(bytes32 => bytes) private bytesStorage;
mapping(bytes32 => bool) private boolStorage;
mapping(bytes32 => int256) private intStorage;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier onlyLogic() {
require(msg.sender == logicContract, "Not logic contract");
_;
}
function setLogicContract(address _logicContract) public onlyOwner {
logicContract = _logicContract;
}
// Uint storage
function getUint(bytes32 key) public view returns (uint256) {
return uintStorage[key];
}
function setUint(bytes32 key, uint256 value) public onlyLogic {
uintStorage[key] = value;
}
// String storage
function getString(bytes32 key) public view returns (string memory) {
return stringStorage[key];
}
function setString(bytes32 key, string memory value) public onlyLogic {
stringStorage[key] = value;
}
// Address storage
function getAddress(bytes32 key) public view returns (address) {
return addressStorage[key];
}
function setAddress(bytes32 key, address value) public onlyLogic {
addressStorage[key] = value;
}
// Bool storage
function getBool(bytes32 key) public view returns (bool) {
return boolStorage[key];
}
function setBool(bytes32 key, bool value) public onlyLogic {
boolStorage[key] = value;
}
}
// Logic contract using eternal storage
contract TokenLogic {
EternalStorage public eternalStorage;
// Storage keys
bytes32 constant TOTAL_SUPPLY = keccak256("TOTAL_SUPPLY");
bytes32 constant OWNER = keccak256("OWNER");
constructor(address _eternalStorage) {
eternalStorage = EternalStorage(_eternalStorage);
eternalStorage.setAddress(OWNER, msg.sender);
}
modifier onlyOwner() {
require(eternalStorage.getAddress(OWNER) == msg.sender, "Not owner");
_;
}
function balanceOf(address account) public view returns (uint256) {
return eternalStorage.getUint(keccak256(abi.encodePacked("BALANCE", account)));
}
function transfer(address to, uint256 amount) public returns (bool) {
address from = msg.sender;
bytes32 fromBalanceKey = keccak256(abi.encodePacked("BALANCE", from));
bytes32 toBalanceKey = keccak256(abi.encodePacked("BALANCE", to));
uint256 fromBalance = eternalStorage.getUint(fromBalanceKey);
require(fromBalance >= amount, "Insufficient balance");
eternalStorage.setUint(fromBalanceKey, fromBalance - amount);
eternalStorage.setUint(toBalanceKey, eternalStorage.getUint(toBalanceKey) + amount);
return true;
}
function mint(address to, uint256 amount) public onlyOwner {
bytes32 toBalanceKey = keccak256(abi.encodePacked("BALANCE", to));
eternalStorage.setUint(toBalanceKey, eternalStorage.getUint(toBalanceKey) + amount);
eternalStorage.setUint(TOTAL_SUPPLY, eternalStorage.getUint(TOTAL_SUPPLY) + amount);
}
}
10.6 Access Control Patterns
Access control patterns manage permissions and roles in contracts.
Ownership Pattern
The Ownership pattern restricts access to certain functions to a single owner:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract Owned {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "New owner is the zero address");
owner = newOwner;
}
}
contract MyContract is Owned {
uint256 public value;
function setValue(uint256 _value) public onlyOwner {
value = _value;
}
}
Role-Based Access Control Pattern
The Role-Based Access Control pattern allows different roles with different permissions:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract AccessControl {
// Role => Address => Has role
mapping(bytes32 => mapping(address => bool)) private roles;
// Events
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
// Role constants
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
constructor() {
_grantRole(ADMIN_ROLE, msg.sender);
}
modifier onlyRole(bytes32 role) {
require(hasRole(role, msg.sender), "AccessControl: account does not have role");
_;
}
function hasRole(bytes32 role, address account) public view returns (bool) {
return roles[role][account];
}
function grantRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
_grantRole(role, account);
}
function revokeRole(bytes32 role, address account) public onlyRole(ADMIN_ROLE) {
_revokeRole(role, account);
}
function _grantRole(bytes32 role, address account) internal {
if (!hasRole(role, account)) {
roles[role][account] = true;
emit RoleGranted(role, account, msg.sender);
}
}
function _revokeRole(bytes32 role, address account) internal {
if (hasRole(role, account)) {
roles[role][account] = false;
emit RoleRevoked(role, account, msg.sender);
}
}
}
contract Token is AccessControl {
mapping(address => uint256) public balances;
uint256 public totalSupply;
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
totalSupply += amount;
balances[to] += amount;
}
function burn(address from, uint256 amount) public onlyRole(BURNER_ROLE) {
require(balances[from] >= amount, "Insufficient balance");
balances[from] -= amount;
totalSupply -= amount;
}
}
Multi-Signature Pattern
The Multi-Signature pattern requires multiple approvals for actions:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
contract MultiSigWallet {
event Deposit(address indexed sender, uint amount);
event Submit(uint indexed txId);
event Approve(address indexed owner, uint indexed txId);
event Revoke(address indexed owner, uint indexed txId);
event Execute(uint indexed txId);
struct Transaction {
address to;
uint value;
bytes data;
bool executed;
}
address[] public owners;
mapping(address => bool) public isOwner;
uint public required;
Transaction[] public transactions;
mapping(uint => mapping(address => bool)) public approved;
modifier onlyOwner() {
require(isOwner[msg.sender], "Not owner");
_;
}
modifier txExists(uint _txId) {
require(_txId < transactions.length, "Transaction does not exist");
_;
}
modifier notApproved(uint _txId) {
require(!approved[_txId][msg.sender], "Transaction already approved");
_;
}
modifier notExecuted(uint _txId) {
require(!transactions[_txId].executed, "Transaction already executed");
_;
}
constructor(address[] memory _owners, uint _required) {
require(_owners.length > 0, "Owners required");
require(_required > 0 && _required <= _owners.length, "Invalid required number of owners");
for (uint i = 0; i < _owners.length; i++) {
address owner = _owners[i];
require(owner != address(0), "Invalid owner");
require(!isOwner[owner], "Owner not unique");
isOwner[owner] = true;
owners.push(owner);
}
required = _required;
}
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
function submit(address _to, uint _value, bytes calldata _data) external onlyOwner {
transactions.push(Transaction({
to: _to,
value: _value,
data: _data,
executed: false
}));
emit Submit(transactions.length - 1);
}
function approve(uint _txId) external onlyOwner txExists(_txId) notApproved(_txId) notExecuted(_txId) {
approved[_txId][msg.sender] = true;
emit Approve(msg.sender, _txId);
}
function revoke(uint _txId) external onlyOwner txExists(_txId) notExecuted(_txId) {
require(approved[_txId][msg.sender], "Transaction not approved");
approved[_txId][msg.sender] = false;
emit Revoke(msg.sender, _txId);
}
function execute(uint _txId) external txExists(_txId) notExecuted(_txId) {
require(_getApprovalCount(_txId) >= required, "Approvals below required");
Transaction storage transaction = transactions[_txId];
transaction.executed = true;
(bool success, ) = transaction.to.call{value: transaction.value}(transaction.data);
require(success, "Transaction failed");
emit Execute(_txId);
}
function _getApprovalCount(uint _txId) private view returns (uint count) {
for (uint i = 0; i < owners.length; i++) {
if (approved[_txId][owners[i]]) {
count++;
}
}
}
}
10.7 Practical Example: Decentralized Exchange
Let's create a decentralized exchange that incorporates multiple design patterns:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);
}
contract DecentralizedExchange {
// Access Control Pattern
address public owner;
mapping(address => bool) public operators;
// State Machine Pattern
enum OrderStatus { Open, Filled, Cancelled }
// Data Structures
struct Order {
uint256 id;
address maker;
address tokenGive;
address tokenGet;
uint256 amountGive;
uint256 amountGet;
uint256 timestamp;
OrderStatus status;
}
// Storage
mapping(uint256 => Order) public orders;
uint256 public nextOrderId = 1;
// Emergency Stop Pattern
bool public stopped = false;
// Rate Limiting Pattern
mapping(address => uint256) public lastOrderTime;
uint256 public constant ORDER_COOLDOWN = 1 minutes;
// Events
event OrderCreated(
uint256 indexed id,
address indexed maker,
address tokenGive,
address tokenGet,
uint256 amountGive,
uint256 amountGet,
uint256 timestamp
);
event OrderFilled(
uint256 indexed id,
address indexed filler,
uint256 timestamp
);
event OrderCancelled(
uint256 indexed id,
uint256 timestamp
);
event OperatorUpdated(
address indexed operator,
bool status
);
event EmergencyStop(
bool stopped
);
// Custom errors
error Unauthorized();
error InvalidOrder();
error OrderNotOpen();
error InsufficientBalance();
error InsufficientAllowance();
error RateLimitExceeded();
error ContractStopped();
constructor() {
owner = msg.sender;
operators[msg.sender] = true;
}
// Access Control Pattern
modifier onlyOwner() {
if (msg.sender != owner) {
revert Unauthorized();
}
_;
}
modifier onlyOperator() {
if (!operators[msg.sender]) {
revert Unauthorized();
}
_;
}
// Emergency Stop Pattern
modifier whenNotStopped() {
if (stopped) {
revert ContractStopped();
}
_;
}
// Rate Limiting Pattern
modifier rateLimited() {
if (block.timestamp < lastOrderTime[msg.sender] + ORDER_COOLDOWN) {
revert RateLimitExceeded();
}
_;
}
// Owner functions
function setOperator(address operator, bool status) external onlyOwner {
operators[operator] = status;
emit OperatorUpdated(operator, status);
}
function toggleEmergencyStop() external onlyOwner {
stopped = !stopped;
emit EmergencyStop(stopped);
}
// Main functions
function createOrder(
address tokenGive,
address tokenGet,
uint256 amountGive,
uint256 amountGet
) external whenNotStopped rateLimited {
// Check balances and allowances
IERC20 tokenGiveContract = IERC20(tokenGive);
uint256 balance = tokenGiveContract.balanceOf(msg.sender);
uint256 allowance = tokenGiveContract.allowance(msg.sender, address(this));
if (balance < amountGive) {
revert InsufficientBalance();
}
if (allowance < amountGive) {
revert InsufficientAllowance();
}
// Create order
uint256 orderId = nextOrderId++;
orders[orderId] = Order({
id: orderId,
maker: msg.sender,
tokenGive: tokenGive,
tokenGet: tokenGet,
amountGive: amountGive,
amountGet: amountGet,
timestamp: block.timestamp,
status: OrderStatus.Open
});
// Update rate limit
lastOrderTime[msg.sender] = block.timestamp;
emit OrderCreated(
orderId,
msg.sender,
tokenGive,
tokenGet,
amountGive,
amountGet,
block.timestamp
);
}
function fillOrder(uint256 orderId) external whenNotStopped {
Order storage order = orders[orderId];
// Validate order
if (order.id == 0) {
revert InvalidOrder();
}
if (order.status != OrderStatus.Open) {
revert OrderNotOpen();
}
// Check balances and allowances
IERC20 tokenGetContract = IERC20(order.tokenGet);
uint256 balance = tokenGetContract.balanceOf(msg.sender);
uint256 allowance = tokenGetContract.allowance(msg.sender, address(this));
if (balance < order.amountGet) {
revert InsufficientBalance();
}
if (allowance < order.amountGet) {
revert InsufficientAllowance();
}
// Update order status
order.status = OrderStatus.Filled;
// Execute the trade (Checks-Effects-Interactions Pattern)
IERC20 tokenGiveContract = IERC20(order.tokenGive);
// Transfer tokenGet from filler to maker
tokenGetContract.transferFrom(msg.sender, order.maker, order.amountGet);
// Transfer tokenGive from maker to filler
tokenGiveContract.transferFrom(order.maker, msg.sender, order.amountGive);
emit OrderFilled(orderId, msg.sender, block.timestamp);
}
function cancelOrder(uint256 orderId) external {
Order storage order = orders[orderId];
// Validate order
if (order.id == 0) {
revert InvalidOrder();
}
if (order.status != OrderStatus.Open) {
revert OrderNotOpen();
}
// Only maker or operator can cancel
if (msg.sender != order.maker && !operators[msg.sender]) {
revert Unauthorized();
}
// Update order status
order.status = OrderStatus.Cancelled;
emit OrderCancelled(orderId, block.timestamp);
}
// View functions
function getOrder(uint256 orderId) external view returns (Order memory) {
return orders[orderId];
}
function isOrderOpen(uint256 orderId) external view returns (bool) {
return orders[orderId].status == OrderStatus.Open;
}
}
This decentralized exchange incorporates multiple design patterns:
- Access Control Pattern: Owner and operator roles with different permissions
- State Machine Pattern: Order status transitions (Open → Filled/Cancelled)
- Emergency Stop Pattern: Ability to pause the contract in emergencies
- Rate Limiting Pattern: Cooldown period between orders
- Checks-Effects-Interactions Pattern: Update state before external calls
- Event Pattern: Emit events for all important actions
- Guard Check Pattern: Validate inputs and conditions
10.8 Exercise: Implement a Crowdfunding Contract with Design Patterns
Task: Create a crowdfunding contract that incorporates multiple design patterns.
Solution:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
/**
* @title PatternedCrowdfunding
* @dev A crowdfunding contract that incorporates multiple design patterns
*/
contract PatternedCrowdfunding {
// State Machine Pattern
enum CampaignState { Fundraising, Successful, Failed }
// Data structure
struct Campaign {
address payable creator;
uint256 goal;
uint256 pledged;
uint256 startTime;
uint256 endTime;
CampaignState state;
}
// Storage
mapping(uint256 => Campaign) public campaigns;
mapping(uint256 => mapping(address => uint256)) public pledges;
uint256 public campaignCount;
// Access Control Pattern
address public owner;
// Emergency Stop Pattern
bool public stopped = false;
// Events
event CampaignCreated(
uint256 indexed id,
address indexed creator,
uint256 goal,
uint256 startTime,
uint256 endTime
);
event PledgeAdded(
uint256 indexed campaignId,
address indexed pledger,
uint256 amount
);
event PledgeWithdrawn(
uint256 indexed campaignId,
address indexed pledger,
uint256 amount
);
event CampaignFinalized(
uint256 indexed campaignId,
CampaignState state,
uint256 totalPledged
);
event EmergencyStop(bool stopped);
// Custom errors
error Unauthorized();
error InvalidAmount();
error InvalidDates();
error CampaignNotFound();
error InvalidState();
error ContractStopped();
error DeadlineNotReached();
error TransferFailed();
constructor() {
owner = msg.sender;
}
// Access Control Pattern
modifier onlyOwner() {
if (msg.sender != owner) {
revert Unauthorized();
}
_;
}
// Emergency Stop Pattern
modifier whenNotStopped() {
if (stopped) {
revert ContractStopped();
}
_;
}
// State Machine Pattern
modifier inState(uint256 campaignId, CampaignState state) {
if (campaigns[campaignId].state != state) {
revert InvalidState();
}
_;
}
// Owner functions
function toggleEmergencyStop() external onlyOwner {
stopped = !stopped;
emit EmergencyStop(stopped);
}
// Main functions
function createCampaign(
uint256 goal,
uint256 durationInDays
) external whenNotStopped {
if (goal <= 0) {
revert InvalidAmount();
}
if (durationInDays <= 0) {
revert InvalidDates();
}
uint256 startTime = block.timestamp;
uint256 endTime = startTime + (durationInDays * 1 days);
uint256 campaignId = campaignCount++;
campaigns[campaignId] = Campaign({
creator: payable(msg.sender),
goal: goal,
pledged: 0,
startTime: startTime,
endTime: endTime,
state: CampaignState.Fundraising
});
emit CampaignCreated(
campaignId,
msg.sender,
goal,
startTime,
endTime
);
}
function pledge(uint256 campaignId) external payable whenNotStopped inState(campaignId, CampaignState.Fundraising) {
Campaign storage campaign = campaigns[campaignId];
if (block.timestamp > campaign.endTime) {
revert DeadlineNotReached();
}
if (msg.value <= 0) {
revert InvalidAmount();
}
// Update state (Checks-Effects-Interactions Pattern)
pledges[campaignId][msg.sender] += msg.value;
campaign.pledged += msg.value;
emit PledgeAdded(campaignId, msg.sender, msg.value);
}
function withdrawPledge(uint256 campaignId) external whenNotStopped inState(campaignId, CampaignState.Fundraising) {
Campaign storage campaign = campaigns[campaignId];
uint256 pledgedAmount = pledges[campaignId][msg.sender];
if (pledgedAmount <= 0) {
revert InvalidAmount();
}
// Update state before transfer (Checks-Effects-Interactions Pattern)
pledges[campaignId][msg.sender] = 0;
campaign.pledged -= pledgedAmount;
// Transfer funds
(bool success, ) = payable(msg.sender).call{value: pledgedAmount}("");
if (!success) {
revert TransferFailed();
}
emit PledgeWithdrawn(campaignId, msg.sender, pledgedAmount);
}
function finalizeCampaign(uint256 campaignId) external whenNotStopped inState(campaignId, CampaignState.Fundraising) {
Campaign storage campaign = campaigns[campaignId];
if (block.timestamp <= campaign.endTime) {
revert DeadlineNotReached();
}
if (campaign.pledged >= campaign.goal) {
campaign.state = CampaignState.Successful;
// Transfer funds to creator
(bool success, ) = campaign.creator.call{value: campaign.pledged}("");
if (!success) {
revert TransferFailed();
}
} else {
campaign.state = CampaignState.Failed;
}
emit CampaignFinalized(campaignId, campaign.state, campaign.pledged);
}
function claimRefund(uint256 campaignId) external whenNotStopped inState(campaignId, CampaignState.Failed) {
uint256 pledgedAmount = pledges[campaignId][msg.sender];
if (pledgedAmount <= 0) {
revert InvalidAmount();
}
// Update state before transfer (Checks-Effects-Interactions Pattern)
pledges[campaignId][msg.sender] = 0;
// Transfer funds
(bool success, ) = payable(msg.sender).call{value: pledgedAmount}("");
if (!success) {
revert TransferFailed();
}
emit PledgeWithdrawn(campaignId, msg.sender, pledgedAmount);
}
// View functions
function getCampaign(uint256 campaignId) external view returns (Campaign memory) {
return campaigns[campaignId];
}
function getPledge(uint256 campaignId, address pledger) external view returns (uint256) {
return pledges[campaignId][pledger];
}
function isCampaignSuccessful(uint256 campaignId) external view returns (bool) {
return campaigns[campaignId].state == CampaignState.Successful;
}
function isCampaignFailed(uint256 campaignId) external view returns (bool) {
return campaigns[campaignId].state == CampaignState.Failed;
}
function isCampaignOpen(uint256 campaignId) external view returns (bool) {
Campaign storage campaign = campaigns[campaignId];
return campaign.state == CampaignState.Fundraising && block.timestamp <= campaign.endTime;
}
}
This crowdfunding contract incorporates multiple design patterns:
- State Machine Pattern: Campaign states (Fundraising → Successful/Failed)
- Access Control Pattern: Owner role for emergency functions
- Emergency Stop Pattern: Ability to pause the contract in emergencies
- Checks-Effects-Interactions Pattern: Update state before external calls
- Guard Check Pattern: Validate inputs and conditions
- Pull over Push Pattern: Refunds are claimed by users rather than automatically sent
- Event Pattern: Emit events for all important actions
Conclusion
Design patterns are essential tools for Solidity developers. They provide proven solutions to common problems, making your contracts more secure, maintainable, and gas-efficient.
Key takeaways from this module:
- Behavioral patterns like Guard Check and State Machine help structure contract interactions.
- Security patterns like Checks-Effects-Interactions and Emergency Stop protect against attacks.
- Economic patterns like Token and Staking implement financial incentives.
- Upgradeability patterns like Proxy and Eternal Storage allow contracts to be updated.
- Access Control patterns like Role-Based Access Control and Multi-Signature manage permissions.
By understanding and applying these design patterns, you can create more robust and effective smart contracts.
In the next module, we'll explore advanced smart contract development techniques, including integration with oracles, cross-chain communication, and more.