Design Patterns In Solidity
Jamiebones
Posted on January 16, 2022
Design pattern are the common patterns that could be applied when designing a contract in Solidity. Some commonly applied patterns in smart contract are :
- The withdrawal pattern to withdraw ether from a contract
- The factory contract pattern to create new contracts
- The Iterable map pattern that allows looping over a mapping.
- The tight variable packaging to reduce gas consumption when using structs.
We will discuss about the following design patterns in this post:
-
Pattern useful for withdrawing of ether/token
-
Restrictive pattern used to restrict access to functions
-
Pattern that incorporates an emergency stop in a smart contract
-
This pattern is used for creating child contracts.
-
Patterns that gives the ability to loop over a mapping
data structure in Solidity.
Withdrawal pattern
The withdrawal pattern is also referred as a pull-over-push pattern. This pattern allows ether or token to be removed from a contract by pull instead of push.
A smart contract can contain ether and you might want to distribute the ether to different account. It is better you allow the owner of the ether or token pull it themselves instead of pushing the ether to their address.
Sample code:
contract TokenBank {
addresss[] internal investors;
mapping(address => uint256) internal balances;
//function saves an investor address on an array
function registerInvestor(address _investor) public onlyOnwer {
require(_investor != address(0) );
investors.push(_investor);
}
function calulateDividendAccured(address _investor) internal returns (uint256) {
//perform your calculations here and return the dividends
}
//bad practice
function distributeDividends() public onlyOwner {
for (uint i=0; i < investors.length; i++ ) {
uint amount = calulateDividendAccured(investors[i]);
//amount is what due to the investor
balances[investor[i]] = 0;
investors[i].transfer(amount); //pushing ether to address
}
}
}
The example above is a contract where investors address are saved in an address [] and dividends are calculated and withdrawn from the contract and paid to the investor.
The problem with this pattern is that :
we are looping over an unbounded array whose size might grow very big with time and we are also changing the state variables which might exceed the block gas limits and makes the transactions fail, causing the locking of ether in the contract.
the contract owner is saddled with the responsibilities of paying for the gas consumed by the transaction. Why not outsource that responsibility to the investor, who wants to withdraw dividends.
The withdrawal pattern specifies that you create a function which the user can call when they want to withdraw their dividends. By this way, you avoid looping over an array that could easily grow out of bounds
//Good practice
function claimDividend() public {
uint amount = calulateDividendAccured[msg.sender];
require (amount > 0, "No dividends to claim");
//set their balance to zero to prevent reentrancy attack
balances[msg.sender] = 0;
msg.sender.transfer(amount); //pull ether from the
contract
}
When to use the withdrawal pattern
- You are sending ether/token to multiple accounts
- You want to avoid paying transaction fee as the initiator of the transaction pays the fees.
Access restriction pattern
This pattern as the name entails is one that is used when restriction is necessary on the contract. It consist of the use of modifiers
to restrict access to some functions on a contract.
sample code
contract LibraryManagementControl {
address public owner;
address public libarian;
constructor(){
owner = msg.sender;
}
function appointLibarian( address _libAddress) public onlyOwner {
libarian = _libAddress;
}
modifier onlyLibarian {
require(msg.sender == libarian );
_;
}
modifier onlyOwner {
require(msg.sender == owner );
_;
}
modifier authorized {
require( msg.sender == owner || msg.sender == libarian );
_;
}
function setUpLibary() public onlyLibarian {
//code to set up a library
}
}
The code above is a simple contract that sets up a library. The person's that deploys the contract becomes the contract owner and he is authorized to appoint a librarian. The pattern makes use of access modifiers to restrict some certain functions in the contract from being called by those not authorized.
When to use
- some functions are only to be executed from certain roles
- you want to improve the security of the contracts from unauthorized function calls.
Emergency stop pattern
This pattern incorporates the ability to pause all function execution in the contract in the case of something going wrong. Since contracts are immutable once deployed; if there are bugs in the contract the pause
functionality can be activated to prevent further damage caused.
The emergency pause functionality should be in the control of the owner or an authorized address. The Emergency stop pattern should be avoided at all means as it is against the spirit of decentralisation but in those instance where a centralized control is required, it could be a worthy pattern to incorporate in your contract.
Sample code :
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract MerchantBank {
address payable public owner;
bool paused = false;
constructor(){
owner = payable(msg.sender);
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
modifier isPaused {
require(paused);
_;
}
modifier notPaused {
require(! paused);
_;
}
function pauseContract() public onlyOwner notPaused {
paused = true;
}
function unpauseContract() public onlyOwner isPaused {
paused = false;
}
function depositEther() public notPaused {
//logic to deposit ether into the contract
}
function emergencyWithdrawal() public isPaused {
//transfer the ether out fast before more damage is done
owner.transfer(address(this).balance);
}
}
Our sample code has a contract called MerchantBank
which the owner of the contract is the person that deploys it. Access modifiers are used to restrict access. The contract has a function called depositEther
which can only be called when the function is not in a paused state. The pause state can be activated by the contract owner and when the contract is paused users can no longer deposits ether.
The contract also has an emergencyWithdrawal
function that can be triggered by the owner if the contract is paused as a result of bugs in the code and the ether in the contract can safely be transferred elsewhere.
When to use
- You want the ability to pause the contract as a result of unwanted situation
- In case of failure you want the ability to stop state corruption.
- You are a centralized entity.
Factory creation pattern
This pattern is used for the creation of child contract from a parent that acts as a factory. The factory contract has an array of address where all the child contracts created are stored. This pattern is very common in Solidity.
Sample code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract OTokenFactory {
address [] public otokenAddress;
function createNewOToken(address _asset, address _collateral, address _strikePrice, uint256 _expiry ) public returns (address) {
address otoken = address (new OToken(_asset, _collateral, _expiry,
_strikePrice));
otokenAddress.push(otoken);
return otoken;
}
}
contract OToken {
address public asset;
address public collateral;
address public strikePrice;
uint256 public expiry;
constructor(address _asset, address _collateral, uint256
_expiry, address _strikePrice ) {
asset = _asset;
collateral = _collateral;
strikePrice = _strikePrice;
expiry = _expiry;
}
}
The code above is used for the creation of oToken. The contract OTokenFactory is deployed and it is used to create new child OToken contract. When the function createNewOToken
is called, parameters that is used for creating a new OToken is passed to it.
The function uses the new
keyword to spawn a new instance of OToken
contract. The result of this line below :
address otoken = address (new OToken(_asset, _collateral, _expiry,
_strikePrice));
otokenAddress.push(otoken);
is an address, that is stored in an array of addresses that depicts the address of the new created OToken contract.
When to use
- You need a new contract for each request.
Iterable map pattern
The iterable map pattern allows you to iterate over mapping entries. Solidity mapping provides no way to iterate over them. There are some cases where you will need to iterate over a mapping. This pattern is well situated for such cases.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract DepositContract {
mapping(address => uint256) public balances;
address[] public accountHolders;
function deposit() payable public {
require(msg.value > 0);
bool exists = balances[msg.sender] != 0;
if ( !exists ){
accountHolders.push(msg.sender);
}
balances[msg.sender] += msg.value;
}
struct AccountDetails {
address accountAddress;
uint256 amount;
}
function getAccountHolders() public view returns (AccountDetails[] memory ) {
AccountDetails[] memory accounts = new AccountDetails[](accountHolders.length);
for (uint256 i=0; i < accounts.length; i++ ) {
address _currentAddress = accountHolders[i];
uint256 _amount= balances[_currentAddress];
accounts[i] = AccountDetails(
_currentAddress,
_amount
);
}
return accounts;
}
}
In this pattern we added the ability to loop over our data in the mapping data structure. The contract above called DepositContract
consists of a mapping that links the address to the amount deposited by each address. Normally without this pattern, we could only pull out the value deposited by supplying the address.
The deposit
function that takes care of the amount deposited by the user stores that value in a state variable called balances
. The function also stores the address of the depositor into an address array called accountHolders
. The code checks and ensures that only unique address are saved in the accountHolders
array.
There is another function called getAccountHolders
and it's purpose is to display the address and the value owned by each address. The function is a view function which does not require a transaction, thereby saving on gas. We defined a struct that contains an address and a uint256 variables, used for saving the account details.
We created an array of AccountDetails
struct in memory called accounts
that will hold each account details. We initialized the value of accounts to the length of the accountHolders
array (contains unique address of account holders). As we loop through the accountHolders
array we retrieve the saved address which we plug into the balances
mapping to retrieve the amount saved by that address. These value and the address is placed in a AccountDetails
struct and pushed into the accounts
array that holds the array of AccountDetails.
As we can see, this pattern allows us to iterate over all values in a mapping.
When to use
- You need iterable behaviours over Solidity mappings
- The items contain in the mappings are not that many.
- You would want to filter some data out of the mapping and get the result via a view function.
Summary
There are many more patterns in the wild. This is just a scratch on what is available. I hope you have been able to learn a thing or two.
Thanks for reading..
Happy coding!
Posted on January 16, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.