A Solidity Implementation of the State Machine Design Pattern

Writing smart contracts is scary. They handle real money, and forgetting to add a single keyword, or misordering 2 seemingly-interchangeable lines of code can result in the loss of millions of dollars. Yes, you can write tests and have professionals audit your code, but if the structure and functionality of your contracts start getting complicated, then there is still a good chance that they may miss something. You can’t be 100% certain that your code is safe.

Fortunately, a plethora of best practices, known security flaws and design patterns exist, that can help you minimize the risks. At Token Foundry, one of the ways we help to secure our token sales is by using a programming design pattern known as the “State Machine” pattern.

If you’ve been programming for a while, you may well already be aware of this pattern. The purpose of this article is to explain:

  • What the State Machine pattern is and when it should be used
  • A breakdown of the State Machine we have developed at Token Foundry, how to use it yourself, and how it actually works
  • How this pattern can help to ensure the security of your contracts
  • How we’re hoping to further develop our state machine in the future

What is the State Machine Design Pattern?

The State Machine pattern splits up the functionality of a program into a number of different “states”. At any given point in time, the program is in one and only one state, during which only state-specific functionality is possible. The program can transition between these states in a pre-defined way. For example, a program can require transitions to be manually triggered, or can automatically transition between states. At Token Foundry we define these automatic transitions using state start conditions, which can include (but aren't limited to):

  • A variable now takes some desired value
  • A specific time has been reached
  • A required event or function call has occured

On entering the new state, variable value changes may occur, or certain functionality may be carried out automatically. The functions that must be executed on entering a new state are callback functions.

When Should the Pattern Be Used?

The State Machine design pattern isn’t suitable for use in all programs or smart contracts. Systems that are well suited to the pattern should be easily broken down into distinct stages, where different behavior occurs, or different functionality is permitted. These stages of the system are represented by states in a state machine, and should occur one after the next over a period of time.

For example, when revealing information on-chain, it is common-place for all parties to commit the hash of their information before all revealing the actual values. One example of this is in voting — the contract may need states as follows:

  • Registration — voters can register with the contract to later vote
  • Commitment of votes — hashes of voters’ chosen options are committed
  • Revealing of votes — voters now reveal their vote (which matches their hash)
  • Voting is over — no more input from voters is allowed

The start conditions for these states could trigger transitions once a certain number of voters have registered or once a certain amount of time has passed.

A Breakdown of Token Foundry’s State Machine and How to Use it Yourself

At Token Foundry, we’ve created some smart contracts that allow developers to easily implement a linear (for now) state machine pattern. Our StateMachine contracts and tests are open-source, and can be found on the Token Foundry GitHub for anyone to read, test and use.

Our implementation allows an arbitrary number of states to be defined, along with an arbitrary number of start conditions and callback functions for each state.

We provide two contracts: StateMachine.sol and TimedStateMachine.sol. The first of these is the base pattern implementation, and the second is an extension which enables timestamp-based start conditions for states.

The basic idea for setting up your state machine can be split into a few simple steps:

  1. Identify the high-level states that your machine will have.

They get defined as constant values in the contract. For example, in a simple token sale, you could have:

bytes32 constant FREEZE = "freeze";
bytes32 constant IN_PROGRESS = "inProgress";
bytes32 constant ENDED = "ended"

These states must be passed to the function setStates(bytes32[] states) to set up the state machine. This should usually be done in the constructor of your contract.

2. Define which functions will be permitted in each state.

This is also recommended to be carried out in the constructor of the contract, so that any disallowed functions are set as such from the beginning. For example, continuing the above example, we would only want a contributor to be able to buy tokens during our IN_PROGRESS state.

In the constructor we put:
allowFunction(IN_PROGRESS, this.buy.selector);

This sets function buy as only being permitted within IN_PROGRESS - if the state machine is in FREEZE or ENDED, then buy cannot be executed. More on how this works later.

3. Define any state’s start conditions and callback functions

Start condition and callback functions must be defined within your smart contract, and must be added to the relevant states when contructing your state machine.

Start conditions must take the following form, where bytes32 is the state ID (e.g. constant FREEZE):
function exampleStartCondition(bytes32) internal returns(bool) {...}
The callbacks automatically executed on entering a state take a different form:
function exampleCallback() internal {...}

These are then set for the relevant states as follows:

addStartCondition(ENDED, hasSaleSoldOut);
addCallback(ENDED, transferMoneyToTeam);

To make timestamp-triggered transitions simpler, we also defined a TimedStateMachine. This contract has a timestamp-triggered start condition pre-defined, and uses the following function to easily enable these transitions to be added to a state machine:
function setStateStartTime(bytes32 stateId, uint256 timestamp) internal

So how does this all work?

In our StateMachine contract, we have a modifier named checkAllowed. Which is defined as follows:

modifier checkAllowed {
conditionalTransitions();
require(states[currentStateId].allowedFunctions[msg.sig]);
_;
}

When a function is defined using the checkAllowed modifier, the function conditionalTransitions() is first executed. conditionalTransitions() checks each of the start conditions of the subsequent state, and if any of these are true transitions into said state. This process repeats until the state machine is in the correct current state. The next line then requires that the function is permitted to be executed in the current state.

For example, let’s say we have a state machine in state A, and that state B has a start condition of time >= 10pm. If a function onlyInStateA marked checkAllowed is called at 10.05pm, conditionalTransitions will see that (time >= 10pm) == true, and transition the machine into state B automatically - calling any necessary callbacks at this time. It will then see that the machine is in fact in state B and not execute onlyInStateA.

How our State Machine Helps Improve Code Security, Reasoning and Manageability

Using design patterns when programming — including the state machine pattern — breaks complex ideas down into a more simply understood constructs. A system redesigned as a state machine allows states to be individually tested and reasoned about, without risking interference from other states and their behaviour.

State Machine constructs also enable the flow clarity of a system to increase. Such simplicity and clarity in smart contracts is key, especially when they handle potentially large sums of money.

Limitations and Future Plans

Currently, our implementation only allows for linear state machines (each state can only have 1 outgoing transition). This works well for our sale contracts, which have a simple flow and are only meant to live for a short period of time. However, if your system requires more complex features, this implementation may not be enough. Examples of such features are reversing a transition, branching flows or having cycles.

We are currently working on refactoring this project in order to support non-linear machine structures and cycles, in (hopefully) the not-too distant future.

We really appreciate all questions, queries and feedback on the code that we write, so please feel free to reach out.

Alice Henshaw 
Solidity Engineer @ Token Foundry
www.tokenfoundry.com