DEV Community

Jamiebones
Jamiebones

Posted on

Design Patterns In Solidity

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:

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
  }

}
}
Enter fullscreen mode Exit fullscreen mode

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
 }

Enter fullscreen mode Exit fullscreen mode

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
}

}

Enter fullscreen mode Exit fullscreen mode

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);
 }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}


Enter fullscreen mode Exit fullscreen mode

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;
}
}

Enter fullscreen mode Exit fullscreen mode

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!

Discussion (2)

Collapse
gitcommitshow profile image
gitcommitshow

Awesome. It would make the article easier to read if you can add the list of all the patterns discussed here on top including one line explanation of each. And then link them to the detailed explanation.

I'd want to come back to it when I'm working on such project.

Collapse
jamiescript profile image
Jamiebones Author

Thanks for the suggestion. I have updated the post.