The Bank
Rules in the Certora Verification Language
The Certora Prover verifies that a smart contract satisfies a set of rules written in a language called Certora Verification Language (CVL). Each rule is checked on all possible inputs. Of course, this is not done by explicitly enumerating the inputs, but rather through symbolic techniques. Rules can check that a public contract method has the correct effects on the contract state or returns the correct value, etc… The syntax for expressing rules somewhat resembles Solidity, but also supports more features that are important for verification.
Consider the following contract interface to implement a simple bank:
pragma solidity >= 0.4.24 < 0.8;
contract Bank {
mapping (address => uint) public funds;
uint public totalFunds;
// get the current fund of an account
function getFunds(address account) public returns (uint)
// get the total funds in the bank
function getTotalFunds() public returns (uint)
// deposit an amount to an account
function deposit(uint amount) public payable
// transfer an amount to an account
function transfer(address account, uint amount) public
// withdraw all amounts
function withdraw() returns (bool) public
}
And here is a rule for verifying that withdraw either reverts or returns true.
rule withdraw_succeeds {
/* The env type represents the EVM parameters passed in every
call (msg.*, tx.*, block.* variables in solidity */
env e;
// Invoke function withdraw and assume it does not revert
/* For non-envfree methods, the environment is passed as the first argument*/
bool success = withdraw(e);
assert success, "withdraw must succeed";
}
The rule calls withdraw with an arbitrary EVM environment (e
) in an arbitrary initial state. It assumes that the function does not revert. The assert command checks that success is true on all potential executions. Notice that each Solidity function has an extra argument, which is the EVM environment.
Parametric rules
To simulate the execution of all functions in the contract, you can define a method argument in the rule and use it in invocation statements. For example:
rule others_can_only_increase_my_balance() {
method f; // an arbitrary function in the contract
env e; // the execution environment
address other;
// Assume the actor and the other address are distinct
require e.msg.sender != other;
// Get the balance of `other` before the invocation
uint256 _balance = getFunds(other);
calldataarg arg; // any argument
f(e, arg); // successful (potentially state-changing!)
// Get the balance of `other` after the invocation
uint256 balance_ = getFunds(other);
assert _balance <= balance_, "Reduced the balance of another address";
}
The Prover verifies that the conditions hold against any function call with any arguments.
Invariants
Invariants are a specification of a condition that should always be true once an operation is concluded. Syntax:
invariant invariantName(args_list) exp
- Assumeexp
holds before execution of any method and verify exp must hold afterwards. The invariant “step” case above is equivalent to a rule:
rule invariantAsRule(method f) {
require exp;
calldataarg arg;
f(e,arg);
assert exp;
}
Unlike a rule, the invariant also checks that exp
holds right after the constructor of the code.
Declaring functions used in the invariant
In the spec file, you need to define the Solidity functions used in the invariants. Here env
ranges over all environments.
methods {
getFunds(address) returns uint256
}
invariant address_zero_cannot_become_an_account(env e)
getFunds(e, 0) == 0
envfree
functions
When a function is not using the environment, it can be declared as envfree
to omit the call’s env
argument. For example, getFunds
is not using any of the environment variables:
methods {
getFunds(address) returns uint256 envfree
}
invariant address_zero_cannot_become_an_account()
getFunds(0) == 0
Understanding the results of the Certora Prover
The Certora Prover produces a table with the verification results as a web
page. For each rule, it either displays a thumbs-up if it formally proved the
rule or displays an input that triggers a violation of the rule. For example,
below is a violation of the rule others_can_only_increase
when simulated on
the transfer function. A call trace demonstrating the violation is shown. It
shows the arguments passed to each simulated function and the resulting return
value (displayed after the slash). This example shows that a transfer of an
amount close to MAX_INT
causes the balance of the recipient account to
decrease.