Lesson 29 of 34
In Progress

Advanced Contract Patterns

As you become more comfortable with Solidity and smart contract development, you may find yourself wanting to use more advanced design patterns to build more complex and powerful contracts. In this chapter, we’ll explore some common patterns that can help you take your contract development to the next level.

State Machines:

A state machine is a design pattern that allows you to model the different states that a contract can be in, and the transitions between those states. This can be especially useful for contracts that have complex logic or need to handle multiple scenarios.

To implement a state machine in Solidity, you’ll need to define an enum that represents the different states that your contract can be in. You’ll also need to define a state variable that stores the current state of the contract. Here’s an example of a simple state machine contract:

pragma solidity ^0.6.0;

enum ContractState {
  INITIALIZED,
  ACTIVE,
  PAUSED,
  CLOSED
}

ContractState public state;

function setState(ContractState _state) public {
  require(_state != state, "State is already set to this value");
  state = _state;
}

In this example, the contract has a state variable called state that can be one of four values: INITIALIZED, ACTIVE, PAUSED, or CLOSED. The setState function allows you to change the state of the contract, but only if the new state is different from the current state.

Dependency Injection:

Dependency injection is a design pattern that allows you to decouple different components of your contract by injecting their dependencies (e.g. other contracts or libraries) at runtime. This can make it easier to test your contracts and make changes to their dependencies without affecting the rest of the contract.

To use dependency injection in your Solidity contracts, you can define a contract interface that specifies the functions and variables that the dependency must implement, and then pass an instance of that dependency to your contract as a constructor argument. Here’s an example of a contract that uses dependency injection:

pragma solidity ^0.6.0;

interface Token {
  function balanceOf(address _owner) external view returns (uint);
  function transfer(address _to, uint _value) external;
}

contract MyContract {
  Token public token;

  constructor(Token _token) public {
    token = _token;
  }

  function transferTokens(address _to, uint _value) public {
    require(token.balanceOf(msg.sender) >= _value, "Not enough tokens");
    token.transfer(_to, _value);
  }
}

In this example, the MyContract contract depends on a contract that implements the Token interface. When an instance of MyContract is created, it receives an instance of the Token contract as a constructor argument. The MyContract contract can then use the token variable to call functions on the Token contract.

Circuit Breakers

Circuit breakers are a common pattern used in smart contracts to allow for emergency shutdowns in case of unexpected errors or attacks. They allow the contract owner or a designated authority to pause the contract’s functions in order to fix any issues or vulnerabilities.

To implement a circuit breaker in Solidity, you can use a boolean flag to indicate whether the contract is paused or not. Then, you can use this flag to modify the behavior of your contract’s functions. For example:

pragma solidity ^0.6.0;

contract CircuitBreaker {
    bool public paused;

    constructor() public {
        paused = false;
    }

    function pause() public {
        require(!paused, "The contract is already paused.");
        paused = true;
    }

    function unpause() public {
        require(paused, "The contract is not paused.");
        paused = false;
    }

    function doSomething() public {
        require(!paused, "The contract is paused.");
        // Do something
    }
}

In this example, the pause() function can be called by the contract owner or a designated authority to set the paused flag to true. The unpause() function can then be used to set the flag back to false. The doSomething() function is then modified to check the value of the paused flag before executing, ensuring that it cannot be called while the contract is paused.

It’s important to note that circuit breakers should only be used as a last resort, as they can disrupt the normal operation of the contract. They should also be implemented with caution, as they can potentially be exploited if not implemented properly.

Upgradability:

One of the challenges of smart contract development is that once a contract is deployed to the Ethereum network, it’s difficult to change. This can make it hard to fix bugs or add new features to your contracts.

To solve this problem, you can use the upgradability pattern to build contracts that can be easily updated without requiring a new deployment. There are several ways to implement upgradability in Solidity, but one common approach is to use a proxy contract that delegates calls to an implementation contract. The proxy contract can be updated to delegate calls to a new implementation contract, allowing you to make changes to the contract logic without affecting the contract’s address or its stored data.

Here’s an example of a simple proxy contract:

pragma solidity ^0.6.0;

contract MyContract {
  function execute(bytes calldata _data) external;
}

contract MyContractProxy {
  MyContract public implementation;

  constructor(MyContract _implementation) public {
    implementation = _implementation;
  }

  function execute(bytes calldata _data) external {
    implementation.execute(_data);
  }
}

In this example, the MyContractProxy contract has a public variable called implementation that stores an instance of the MyContract contract. The execute function simply delegates the call to the execute function of the implementation contract. To update the contract, you would deploy a new instance of the MyContract contract and set the implementation variable of the MyContractProxy contract to the new instance.

Delegation:

The delegation pattern allows you to delegate certain functions or responsibilities to another contract or address. This can be useful in situations where you want to outsource part of the contract logic to another contract, or allow a third party to perform certain actions on your behalf.

To implement delegation in Solidity, you can define a function that takes an address or contract as an argument and calls a function on that address or contract. Here’s an example of a contract that uses delegation:

pragma solidity ^0.6.0;

contract MyContract {
  function execute(address _delegate, bytes calldata _data) external {
    require(_delegate.call(_data), "Delegate call failed");
  }
}

In this example, the execute function takes an address as an argument and calls the call function on that address, passing in the _data argument. This allows the contract to delegate the execution of the _data to the specified address.

Conclusion:

Using advanced design patterns like state machines, dependency injection, circuit breakers, upgradability, and delegation can help you build more robust and flexible smart contracts. While these patterns can be useful in certain situations, it’s important to carefully consider their trade-offs and ensure that you are using them in a way that is appropriate for your specific use case.

Exercises

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

Write a Solidity contract that includes a circuit breaker. The circuit breaker should have a pause() function that can be called by the contract owner, and a doSomething() function that is modified to check the value of the circuit breaker before executing.

pragma solidity ^0.6.0;

contract CircuitBreakerExercise {
    bool public paused;

    constructor() public {
        paused = false;
    }

    function pause() public {
        require(!paused, "The contract is already paused.");
        paused = true;
    }

    function doSomething() public {
        require(!paused, "The contract is paused.");
        // Do something
    }
}

Modify the contract from Exercise 1 to include an unpause() function that can be called by the contract owner to set the circuit breaker back to false.

pragma solidity ^0.6.0;

contract CircuitBreakerExercise {
    bool public paused;

    constructor() public {
        paused = false;
    }

    function pause() public {
        require(!paused, "The contract is already paused.");
        paused = true;
    }

    function unpause() public {
        require(paused, "The contract is not paused.");
        paused = false;
    }

    function doSomething() public {
        require(!paused, "The contract is paused.");
        // Do something
    }
}

Modify the contract from Exercise 2 to include a doSomethingElse() function that is also modified to check the value of the circuit breaker before executing.

pragma solidity ^0.6.0;

contract CircuitBreakerExercise {
    bool public paused;

    constructor() public {
        paused = false;
    }

    function pause() public {
        require(!paused, "The contract is already paused.");
        paused = true;
    }

    function unpause() public {
        require(paused, "The contract is not paused.");
        paused = false;
    }

    function doSomething() public {
        require(!paused, "The contract is paused.");
        // Do something
    }

    function doSomethingElse() public {
        require(!paused, "The contract is paused.");
        // Do something else
    }
}

Write a Solidity contract that implements a circuit breaker pattern. The contract should have a public function called emergencyShutdown that sets a boolean value called shutdown to true. The contract should also have a public function called resume that sets the shutdown value to false. The contract should have a public function called isShutdown that returns the value of shutdown.

pragma solidity ^0.7.0;

contract CircuitBreaker {
    bool public shutdown;

    function emergencyShutdown() public {
        shutdown = true;
    }

    function resume() public {
        shutdown = false;
    }

    function isShutdown() public view returns (bool) {
        return shutdown;
    }
}

Write a Solidity contract that uses a circuit breaker pattern to stop a function from being called if the shutdown value is true. The contract should have a public function called doImportantWork that increments a value called counter by 1. The contract should have a public function called getCounter that returns the value of counter.

pragma solidity ^0.7.0;

contract CircuitBreaker {
    bool public shutdown;
    uint public counter;

    function doImportantWork() public {
        require(!shutdown, "The contract is in shutdown mode and cannot perform this action.");
        counter++;
    }

    function getCounter() public view returns (uint) {
        return counter;
    }
}