Lesson 24 of 34
In Progress

Common Solidity Design Patterns

Design patterns are a common concept in software engineering that provide a standardized way of solving common problems. In the world of Solidity, design patterns are particularly important as they can help ensure the reliability and security of smart contracts. In this article, we’ll explore some of the most common design patterns used in Solidity and how they can be applied to your smart contracts.

Factory Pattern

The factory pattern is a creational design pattern that allows you to create multiple instances of a contract. This can be useful when you want to create multiple instances of the same contract, each with its own unique state.

To implement the factory pattern in Solidity, you can create a factory contract that has a function to create new instances of a target contract. The factory contract can then store a list of all the created instances and provide a way to access them.

Here’s an example of a factory contract that creates instances of a token contract:

pragma solidity ^0.5.0;

contract TokenFactory {
  address[] public tokens;

  function createToken() public {
    address newToken = new Token();
    tokens.push(newToken);
  }
}

contract Token {
  // Contract code goes here...
}

Singleton Pattern

The singleton pattern is a creational design pattern that ensures that there is only one instance of a contract. This can be useful when you want to ensure that there is only one central contract that controls certain global state.

To implement the singleton pattern in Solidity, you can include a private variable in your contract that stores the address of the singleton instance. You can then check this variable in the contract’s constructor to ensure that there isn’t already an instance of the contract.

Here’s an example of a singleton contract that tracks the total supply of a token:

pragma solidity ^0.5.0;

contract CentralBank {
  address private _singletonAddress;
  uint private _totalSupply;

  function CentralBank() public {
    // Ensure there is only one instance of this contract
    require(_singletonAddress == address(0));
    _singletonAddress = address(this);
  }

  function totalSupply() public view returns (uint) {
    return _totalSupply;
  }

  function issue(uint amount) public {
    require(msg.sender == _singletonAddress);
    _totalSupply += amount;
  }
}

Observer Pattern

The observer pattern is a behavioral design pattern that allows multiple contracts to subscribe to events emitted by another contract. This can be useful when you want to allow multiple contracts to react to changes in another contract’s state.

To implement the observer pattern in Solidity, you can create an event emitter contract that emits events and a separate event observer contract that listens for those events. The observer contract can then have a function to handle the events when they are emitted.

Here’s an example of an event emitter contract and an event observer contract:

pragma solidity ^0.5.0;

// Deploy an instance of the event emitter contract
EventEmitter emitter = new EventEmitter();

// Deploy an instance of the event observer contract, passing in the address of the event emitter contract
EventObserver observer = new EventObserver(emitter.address);

// Emit an event from the event emitter contract
emitter.emitEvent(123);

// The event observer contract will automatically handle the event

State Machine Pattern

The state machine pattern is a behavioral design pattern that allows a contract to have multiple states and transition between those states based on certain actions. This can be useful when you want to model complex behavior in your smart contracts.

To implement the state machine pattern in Solidity, you can create an enumeration of all the possible states and a private variable to store the current state. You can then include functions in the contract to transition between states based on certain conditions.

Here’s an example of a state machine contract that has three states: created, active, and inactive. The contract has functions to activate and deactivate the contract based on its current state:

pragma solidity ^0.5.0;

enum State { Created, Active, Inactive }

contract StateMachine {
  State private currentState;

  function StateMachine() public {
    currentState = State.Created;
  }

  function activate() public {
    require(currentState == State.Created || currentState == State.Inactive);
    currentState = State.Active;
  }

  function deactivate() public {
    require(currentState == State.Active);
    currentState = State.Inactive;
  }
}

Conclusion

In this article, we explored some of the most common design patterns used in Solidity. By using these design patterns in your smart contracts, you can ensure the reliability and security of your contracts and model complex behavior.

Exercises

To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.

Implement a state machine contract that has three states: created, approved, and rejected. The contract should have functions to approve and reject the contract based on its current state.

pragma solidity ^0.5.0;

enum State { Created, Approved, Rejected }

contract StateMachine {
  State private currentState;

  function StateMachine() public {
    currentState = State.Created;
  }

  function approve() public {
    require(currentState == State.Created);
    currentState = State.Approved;
  }

  function reject() public {
    require(currentState == State.Created);
    currentState = State.Rejected;
  }
}

Write a contract that uses the observer pattern to listen for events emitted by another contract. The contract should have a function to handle the event and update a state variable based on the event data.

pragma solidity ^0.5.0;

contract EventObserver {
  uint public eventCount;

  function handleEvent(uint value) public {
    eventCount++;
  }

  function updateState(uint newState) public {
    emit StateUpdated(newState);
  }

  event StateUpdated(uint newState);
}

contract EventEmitter {
  EventObserver observer;

  function EventEmitter(address _observerAddress) public {
    observer = EventObserver(_observerAddress);
  }

  function emitEvent(uint value) public {
    observer.handleEvent(value);
  }

  function updateState(uint newState) public {
    observer.updateState(newState);
  }
}

Write a contract that uses the factory pattern to create instances of another contract. The contract should have a function to create new instances and a mapping to store the addresses of the created instances.

pragma solidity ^0.5.0;

contract Factory {
  mapping(uint => address) public createdContracts;

  function createContract() public {
    uint id = createdContracts.length;
    createdContracts[id] = new MyContract();
  }
}

contract MyContract {
  uint public id;

  function MyContract() public {
    id = msg.sender.balance;
  }
}

Write a contract that uses the proxy pattern to delegate calls to another contract. The contract should have a function to set the address of the delegate contract and a fallback function to delegate calls to the delegate contract.

pragma solidity ^0.5.0;

contract Proxy {
  address public delegate;

  function setDelegate(address _delegate) public {
    delegate = _delegate;
  }

  function() external payable {
    delegate.call(msg.data);
  }
}

contract Delegate {
  function doSomething() public {
    // Do something
  }
}

Write a contract that uses the singleton pattern to ensure there is only one instance of the contract. The contract should have a function to check if an instance already exists and a fallback function to create a new instance if one does not exist.

pragma solidity ^0.5.0;

contract Singleton {
  address public instance;

  function checkInstance() public view returns (bool) {
    return address(this).balance > 0;
  }

  function() external payable {
    require(checkInstance() == false);
    instance = msg.sender;
  }
}