Stainless Smart Contracts

Overview

Stainless now has experimental support for formally verifying smart contracts, and compiling them to Solidity.

Installation

Development happens on the smart repository, a fork of Stainless which is frequently rebased on top of Stainless’s master. You can follow the Stainless installation guide by changing the repository:

$ git clone https://github.com/epfl-lara/smart.git
Cloning into 'smart'...
// ...
$ cd smart
$ sbt clean universal:stage
// takes about 1 minute

You can then create a symbolic link (e.g. for Linux & Mac OS-X) to have access to a stainless command-line (assuming ~/bin is in your path).

$ ln -s /path/to/smart/frontends/scalac/target/universal/stage/bin/stainless-scalac ~/bin/stainless

Formal Verification of Smart Contracts

Let us have a look at MinimumToken which implements a token with just a transfer function. From the smart repository, issue the following commands:

$ cd frontends/benchmarks/smartcontracts/valid/MinimumToken
$ stainless *.scala

After 30 seconds or so, Stainless should report that all verification conditions are valid. What do these correspond to? The file MinimumToken.scala defines a token with a transferFrom function.

def transferFrom(from: Address, to: Address, amount: Uint256): Unit = {
  require(contractInvariant(this))

  // input validation at runtime
  dynRequire(to != Address(0))
  dynRequire(from != to)
  dynRequire(amount <= balanceOf(from))

  // ghost code to update the list of participants
  ghost {
    addParticipant(from)
    addParticipant(to)
  }

  // balanceOf mapping before any update
  @ghost val b0 = Mapping.duplicate(balanceOf)

  // code to remove balance from `from` address
  balanceOf set (from, balanceOf(from) - amount)

  // balanceOf mapping before after the first update, before the second update
  @ghost val b1 = Mapping.duplicate(balanceOf)

  // code to add balance to recipient `to`
  balanceOf set (to, balanceOf(to) + amount)

  // proof that the sum of balances stays equal to `total`
  assert((
    sumBalances(participants, balanceOf)                                    ==| balancesUpdatedLemma(participants, b1, to, b1(to) + amount) |
    sumBalances(participants, b1) - b1(to) + (b1(to) + amount)              ==| trivial |
    sumBalances(participants, b1) + amount                                  ==| (balancesUpdatedLemma(participants, b0, from, b0(from) - amount) && sumBalances(participants, b1) == sumBalances(participants, b0) - b0(from) + (b0(from) - amount)) |
    sumBalances(participants, b0) - b0(from) + (b0(from) - amount) + amount ==| ((b0(from) - amount) + amount == b0(from)) |
    sumBalances(participants, b0) - b0(from) + b0(from)                     ==| trivial |
    sumBalances(participants, b0)                                           ==| trivial |
    total
  ).qed)

} ensuring { _ =>
  contractInvariant(this)
}

Ignoring the body of the function for a while, the require and ensuring annotations (pre and post-conditions) ask Stainless to show that, regardless with which arguments the transferFrom function is called, as long as the contract invariant holds before the function call, then it will still hold after the function call.

The contractInvariant function is defined in the file MinimumTokenInvariant.scala.

def contractInvariant(contract: MinimumToken): Boolean = {
  distinctAddresses(contract.participants) &&
  sumBalances(contract.participants, contract.balanceOf) == contract.total &&
  forall((x: Address) =>
    (contract.balanceOf(x) != Uint256.ZERO) ==>
    contract.participants.contains(x)
  )
}

It states that all addresses that appear in the (ghost) variable participants are distinct, that the sum of all balances of participants equals to total, and that all addresses with a non-zero balance appear in the list of participants.

Showing that this invariant holds after the updates that happens in the transferFrom function requires some work. Some lemmas that are used to relate the sum of all balances before and after updates are stated and proven in the MinimumTokenInvariant.scala file. In the transferFrom function, we then invoke the lemmas using assertions that will be used for verification. These ghost expressions are ignored during compilation.

The ==| and | notations are defined in stainless.equations. They enable to prove that two expressions are equal by detailing the sequence of intermediary steps, while providing evidence for each step (or trivial if not evidence is required).

MinimumToken is not so useful as is, since there is no way to create tokens. As an exercise, the reader may try to add a function for minting tokens, and prove that this function maintains contractInvariant. Additionally, we can add a custom constructor to this contract by adding a function called constructor which will be translated to a constructor in Solidity during compilation.

Compilation to Solidity

The MinimumToken example can be compiled to Solidity using the following command (still in the MinimumToken folder):

$ stainless --solidity *.scala

It will produce the following Solidity code (in the file MinimumToken.sol), which can be compiled by the Solidity compiler to Ethereum Virtual Machine bytecode.

function transferFrom (address from, address to, uint256 amount) public {
    require(!(to == address(0)), "error");
    require(!(from == to), "error");
    require(amount <= balanceOf[from], "error");
    balanceOf[to] = balanceOf[to] + amount;
    balanceOf[from] = balanceOf[from] - amount;
}

All ghost expressions have been eliminated, and only the dynamic requires (dynRequire) and the code that updates the balances remain.

Features

Ghost code

Ghost code which is annotated with the @ghost annotation is ignored when compiling the smart contracts to Solidity.

Static and Dynamic Checks

Importing stainless.lang.StaticChecks._ provides the keywords assert and require which trigger the creation of verification conditions. These expressions are ghost will not be compiled to Solidity, which allows you to save on gas cost once your contracts are deployed.

On the other hand, importing stainless.smartcontracts._ gives you the keywords dynAssert and dynRequire which do not trigger the creation of verification conditions, and which do get compiled to Solidity (respectively to assert and require) to get runtime checks.

Remark: Beware if you use require on external functions, as these will not appear in the compiled Solidity code. If you want both static verification conditions (from internal calls) and dynamic checks at runtime, you can use both require and dynRequire as follows.

def f() = {
  require(condition)
  dynRequire(condition)
  // rest of the code
}

Strict Arithmetic

The --strict-arithmetic mode makes Stainless add verification conditions (VCs) that check that arithmetic operations do not overflow. For instance, when the mode is active, writing a + b if a and b are uint256̀ will create a VC stating that a + b must be greater or equal to a, and Stainless will report whether this VC is valid or not (or unknown).

Development

The smart repository is in active development and you should expect many (possibly backward-incompatible) changes as we implement new features. Here is a list of things that we are working on, or plan to work on in the near future:

  • Conversion from Address to Contract.
  • Direct compilation to EVM bytecode and other backends.
  • Inheritance between contracts (case classes will be replaced by traits).
  • Fallback functions.
  • More uintX types (only uint8 and uint256 are supported for the moment).
  • @internal and @external annotations for functions, to denote functions that can only be accessed from the inside or outside, respectively.
  • For loops (at the moment, while loops or recursive functions can be used instead).

If you would love to a see a feature which is not listed here, please open an issue in the smart repository.

Known Issues

  • Your code must contain a case class that extends the Contract class (from stainless.smartcontracts), otherwise you will get an exception during verification.
  • For readability, the compiler to Solidity currently print the names of the variables as they appear in your Stainless source code. As such, you should avoid using two variables with the same name in the same scope.

Reporting Issues

As you start experimenting with your own smart contracts in Stainless, you may encounter bugs with verification and compilation. These can be reported in the smart repository. You may also get help in the gitter channel.