Design Patterns for Smart Contracts

Posted on

Smart contract design patterns are essential to building secure and efficient blockchain applications. By following established design patterns, developers can ensure their smart contracts are reliable, scalable, and easier to maintain.

In this article, we’ll explore some common smart contract design patterns and provide examples of their implementations. Understanding smart contract design patterns is crucial for creating successful decentralized applications, whether you’re a seasoned developer or just getting started in blockchain development.

Common Design Patterns

  1. Factory Pattern – A smart contract that creates and deploys other smart contracts.
  2. Singleton Pattern – This design pattern ensures that an entity can only be instantiated once.
  3. State Machine Pattern – A smart contract that represents a state machine with a finite set of states and transitions between them.
  4. Oracle Pattern – A smart contract that serves data from an external source.
  5. Proxy Pattern – A smart contract that allows upgrades and maintenance—within certain parameters—for another one, without affecting its state and providing an interface for its access.
  6. Check-Effects-Interactions Pattern – This design pattern determines the order in which certain types of statements must be called within smart contract functions.

Factory Pattern

Here’s an example of the Factory Pattern in a smart contract:

pragma solidity 0.8.4;

contract MyContract {
    uint256 public data;
    
    constructor(uint256 _data) {
        data = _data;
    }
}

contract MyContractFactory {
    mapping(address => address) public myContracts;
    
    function createMyContract(uint256 _data) public {
        address newContract = address(new MyContract(_data));
        myContracts[msg.sender] = newContract;
    }
}

In this example, MyContract is the contract with the desired functionality, and MyContractFactory is the Factory contract that creates instances of MyContract. When createMyContract is called with a given _data parameter, it creates a new instance of MyContract with that data and assigns it to the caller’s address in the myContracts mapping.

If you want to create multiple instances of the same contract and you’re looking for a way to keep track of them and make their management easier, this is a useful pattern. In this case, each instance is kept track of according to the createMyContract caller.

The disadvantage here is that any single address can only have at most have 1 instance registered in the factory contract state, although all instances will continue to exist in the blockchain and could eventually be tracked down.

Singleton Pattern

Classic software development dictates that the Singleton pattern ensures only one instance of a class exists and provides a global point of access to it. In Solidity, this isn’t really possible like it is in other languages.

Since the blockchain is public, no-one can really control who deploys what, as long as they have the funds. This means that there’s no surefire way of guaranteeing that a specific smart contract (meaning a particular, exact string of bytecode) isn’t deployed n times.

The closes thing to this is EIP-2470, which shouldn’t be used as it is officially marked as stagnant. What it aims to do, rather than having a contract deployed only once per chain (which isn’t really possible, as stated) is to have the same address for a contract on any chain. In other words, it tries to make the address of a contract—one known to a client—predictable on any given network.

State Machine Pattern

The State Machine pattern models behavior based on the state of an object, an entity, or in this case, a smart contract.

Let’s look at a simplified example of its implementation in Solidity.

pragma solidity 0.8.4;

contract VoteTally {
    enum State { Created, InProgress, Completed }

    address public owner;
    State public state;
    uint256 public votes;

    constructor() {
        owner = msg.sender;
        state = State.Created;
    }

    function start() public {
        require(msg.sender == owner, "must be owner to start");
        require(state == State.Created);
        state = State.InProgress;
    }

    function vote() external {
        require(state == State.InProgress, "must be in progress to vote");
        votes++;
    }

    function finish() public {
        require(msg.sender == owner, "must be owner to finish");
        require(state == State.InProgress, "must be in progress to finish");
        state = State.Completed;
    }
}

This contract implements a straightforward vote tally. It has 3 states:

  1. Created, which is its default state.
  2. In Progress, which is the only state in which votes—which are cast publicly—can be submitted.
  3. Finished, which is its final state, symbolizing voting has finished.

Only the owner (which is the deployer account) can change state for the contract. In this particular case, the contract cannot move back to one of its previous states once it has transitioned into a new one.

Other arbitrary states and their transitions can be modeled after different amounts of enums, as well as privileges.

See also  Web3.js vs Ethers.js: Picking the Right Ethereum Library for Your DApp

Oracle Pattern

On some occasions, there is data that cannot be deduced, calculated or obtained directly from the blockchain. In others, it’s a good idea in terms of security not to have this data deduced on-chain. In these cases, the Oracle pattern can be applied.

This pattern is based on delegating the source of some specific information or data to a smart contract, which is trusted by the callers and can arbitrarily retrieve and set information from off-chain sources.

Typically, Oracles will provide some sort of off-chain proof of their authority or trustfulness, but it’s ultimately up to the callers whether to trust them or not. Some examples of popular Oracles are ChainLink, Witnet and UMA.

Here’s a simple example of how this can be implemented in Solidity:

pragma solidity 0.8.4;

interface IOracle {
    function getPrice() external view returns (uint);
}

contract MyContract {
    IOracle private oracle;

    constructor(address oracleAddress) {
        oracle = IOracle(oracleAddress);
    }

    function doSomething() external {
        uint price = oracle.getPrice();
        // Do something with the price
    }
}

contract MyOracle is IOracle {
    address public admin;
    uint256 private price;

    constructor(){
        admin = msg.sender;
    }

    function getPrice() external view override returns (uint) {
        return price;
    }
    
    function setPrice(uint256 _price) external {
        require(msg.sender == admin, "only admin can set price");
        // set the price from an arbitrary but trusted source, represented by the admin address
        price = _price;
    }
}

In this example, the deployer account for the oracle is the account that is allowed to arbitrarily set the price variable. Then, contracts like MyContract place their trust in the oracle contract and use whatever value has been set there for that variable.

Proxy Pattern

Smart contracts are unchangeable, but software quality often requires source code updates to produce better versions. Although blockchain technology benefits from immutability, some level of changeability is necessary for fixing errors and improving products.

The Proxy Pattern in Solidity enables developers to modify smart contracts without compromising the integrity of the blockchain network. A proxy contract serves as an intermediary between users and the actual contract, enabling upgrades without disrupting the existing contract’s address.

There is significant technical complexity in terms of implementation of such a pattern in blockchain development. Therefore, using proven and widely adopted solutions is recommended here, rather than coding your own implementation.

OpenZeppelin Upgrades provide an “easy to use, simple, robust, and opt-in upgrade mechanism for smart contracts that can be controlled by any type of governance, be it a multi-sig wallet, a simple address or a complex DAO“.

Their solution is extensively used, and its in-depth documentation is available in their Proxy Upgrade Pattern site.

Checks-Effects-Interactions

This pattern is about performing verifications first (requires, permissions), then updating state variables for the contract (such as a balance) and finally interacting with external elements like other contracts.

Here’s an example:

function withdraw() public {
    // checks first
    require(balances[msg.sender] > 0, "insufficient balance");
    // effects second
    balances[msg.sender] = 0;
    // interactions last
    (bool success, ) = msg.sender.call{value: balances[msg.sender]}("");
    require(success, "transfer failed");
}

The objective of this pattern is to avoid re-entrancy vulnerabilities. You can find more information about this pattern, attack vector and how to mitigate its impact on our Preventing re-entrancy attacks in Solidity article.

Best Practices for Implementing Patterns

  1. Use established patterns: don’t reinvent the wheel. Established design patterns which have been tested and proven to work should generally be favored over other code.
  2. Keep it simple: don’t over-complicate your smart contract. Keep it simple and easy to read.
  3. Test your code: always test your code thoroughly to ensure that it works as intended. This includes both unit or integration tests, as well as end-user tests.
  4. Use secure coding practices: follow secure coding practices to ensure that your smart contract is not vulnerable to attacks. Security audits as well as static analysis tools like Slither will come in handy for this.
  5. Document your code: documenting your code will make it easy for other developers to understand it and collaborate on it.

Conclusion

In summary, design patterns are helpful tools for blockchain developers who intend to write smart contract code. By using established patterns and following best practices, developers can create secure and efficient smart contracts that automate business processes and reduce costs. Whether you’re a beginner or an experienced blockchain developer, understanding design patterns is crucial to success in smart contract development.

Posted in Blockchain, Smart Contract, SolidityTagged ,

Martin Liguori
linkedin logo
twitter logo
instagram logo
By Martin Liguori
I have been working on IT for more than 20 years. Engineer by profession graduated from the Catholic University of Uruguay, and I believe that teamwork is one of the most important factors in any project and/or organization. I consider having the knowledge both developing software and leading work teams and being able to achieve their autonomy. I consider myself a pro-active, dynamic and passionate person for generating disruptive technological solutions in order to improve people's quality of life. I have helped companies achieve much more revenue through the application of decentralized disruptive technologies, being a specialist in these technologies. If you want to know more details about my educational or professional journey, I invite you to review the rest of my profile or contact me at martin@infuy.com