Rules
Rules (along with Invariants) are the main entry points for the Prover. A rule defines a sequence of commands that should be simulated during verification.
When the Prover is invoked with the --verify option, it generates a report for each rule and invariant present in the spec file (as well as any imported rules).
You can find examples for rules in the Certora Prover and CVL Examples Repository. For example, the specs in /CVLByExample/Teams/ demonstrate some of these features.
Syntax
The syntax for rules is given by the following EBNF grammar:
rule ::= [ "rule" ]
id
[ "(" [ params ] ")" ]
[ "filtered" "{" id "->" expression { "," id "->" expression } "}" ]
[ "description" string ]
[ "good_description" string ]
block
params ::= cvl_type [ id ] { "," cvl_type [ id ] }
See Basic Syntax for the id
and string
productions; see Expressions for the expression
production; see Types for the cvl_type
production.
Overview
A rule defines a sequence of commands that should be simulated during verification. These commands may be non-deterministic: they may contain unassigned variables whose value is not specified. The state of storage at the beginning of a rule is also unspecified. Rules may also be declared with a set of parameters; these parameters are treated the same way as undeclared variables.
In principal, the Prover will generate every possible combination of values for the undefined variables, and simulate the commands in the rule using those values. A particular combination of values is referred to as an example or a model. There are often an infinite number of models for a given rule; see How rules are verified for a brief explanation of how the Prover considers all of them.
If a rule contains a require
statement that fails on a particular example,
the example is ignored. Of the remaining examples, the Prover checks that all
of the assert
statements evaluate to true. If all of the assert
statements
evaluate to true on every example, the rule passes. Otherwise, the Prover will
output a specific counterexample that causes the assertions to fail.
-
/// `deposit` must increase the pool's underlying asset balance rule integrityOfDeposit { mathint balance_before = underlyingBalance(); env e; uint256 amount; safeAssumptions(_, e); deposit(e, amount); mathint balance_after = underlyingBalance(); assert balance_after == balance_before + amount, "deposit must increase the underlying balance of the pool"; }
Caution
assert
statements in contract code are handled differently from assert
statements in rules.
An assert
statement in Solidity causes the transaction to revert, in the same
way that a require
statement in Solidity would. By default, examples that
cause contract functions to revert are ignored by the prover, and these examples will not be reported as counterexamples.
The --multi_assert_check option causes assertions in the contract code to be reported as counterexamples.
Parametric rules
Rules that contain undefined method
variables are sometimes called
parametric rules. See The method and calldataarg types for more details about
how to use method variables.
Undefined variables of the method
type are treated slightly differently from
undefined variables of other types. If a rule uses one or more undefined
method
variables, the Prover will generate a separate report for each method
(or combination of methods).
In particular, the Prover will generate a separate counterexample for each method that violates the rule, and will indicate if some contract methods always satisfy the rule.
You can request that the Prover only run with specific methods using the --method <method_signature> and --parametric_contracts <contract_name> ... command line arguments. The set of methods can also be restricted using rule filters. The Prover will automatically skip any methods that have `DELETE` summaries.
If you wish to only invoke methods on a certain contract, you can call the
method
variable with an explicit receiver contract. The receiver must be a
contract variable (either currentContract or a variable introduced with a
using
statement). For example, the following will only verify the rule r
on methods of the contract example
:
using Example as example;
rule r {
method f; env e; calldataarg args;
example.f(e,args);
...
}
It is an error to call the same method
variable on two different contracts.
rule sanity(method f) {
env e;
calldataarg args;
f(e,args);
assert false;
}
Filters
A rule declaration may have a filtered
block after the rule parameters.
Rule filters allow you to prevent verification of parametric rules on certain
methods. This can be less computationally expensive than using a require
statement to ignore counterexamples for a method.
The filtered
block consists of zero or more filters of the form var -> expr
.
var
must match one of the method
parameters to the rule, and expr
must be
a boolean expression that may refer to the variable var
. The filter
expression may not refer to other method parameters or any variables defined in
the rule.
Before verifying that a method m
satisfies a parametric rule, the expr
is
evaluated with var
bound to a method
object. This allows expr
to refer
to the fields of var
, such as var.selector
and var.isView
. See
The method and calldataarg types for a list of the fields available on method
objects.
For example, the following rule has two filters. The rule will only be
verified with f
instantiated by a view method, and g
instantiated by a
method other than exampleMethod(uint,uint)
or otherExample(address)
:
rule r(method f, method g) filtered {
f -> f.isView,
g -> g.selector != exampleMethod(uint,uint).selector
&& g.selector != otherExample(address).selector
} {
// rule body
...
}
See The method and calldataarg types for a list of the fields of the method
type.
Multiple assertions
Rules may contain multiple assertions. By default, if any assertion fails, the Prover will report that the entire rule failed and give a counterexample that causes one of the assertions to fail.
Occasionally it is useful to consider different assert statements in a rule
separately. With the --multi_assert_check option, the Prover will try
to generate separate counterexamples for each assert
statement. The
counterexamples generated for a particular assert
statement will pass all
earlier assert
statements.
Rule descriptions
Rules may be annotated by writing description
and/or good_description
before
the method body, followed by a string. These strings are displayed in the
verification report.
How rules are verified
While verifying a rule, the Prover does not actually enumerate every possible example and run the rule on the example. Instead, the Prover translates the contract code and the rule into a logical formula with logical variables representing the unspecified variables from the rule.
The logical formula is designed so that if a particular example satisfies the
requirements and also causes an assertion to fail, then the formula will
evaluate to true
on that example; otherwise the formula will evaluate
to false.
The Prover then uses off-the-shelf software called an SMT solver to determine
whether there are any examples that cause the formula to evaluate to true. If
there are, the SMT solver provides an example to the Prover, which then
translates it into an example for the user. If the SMT solver reports that the
formula is unsatisfiable, then we are guaranteed that whenever the require
statements are true, the assert
statements are also true.