Certora CLI 5.0 Changes
The release of certora-cli
version 5.0 introduces a few small breaking
changes for CVL. These changes improve the coverage for parametric rules and
invariants, disallow Solidity function calls in quantified expressions,
and simplify some rarely-used features. This document explains those changes
and how to work with them.
Note
certora-cli
5.0 also includes several new features, bug fixes, and
performance improvements that are not discussed here; see Prover Release Notes
for more details.
Exhaustive parametric rules
Starting with certora-cli
version 5.0, parametric rules and
invariants will now be checked on the methods of all contracts by
default, instead of just the primary contract.
This change improves the coverage of rules and can catch important errors. For example, an invariant that relates a contract’s total supply to its balance in an underlying token contract could be invalidated by calling methods directly on the underlying token; before this change those violations would not have been detected.
This change can break existing specifications in a few ways:
A property that should hold cannot be verified.
A parametric rule that was never intended to apply to secondary contracts may be violated by methods of those contracts.
Verification may time out on methods of secondary contracts.
The remainder of this section describes how to address these three failure modes.
Fixing properties that should hold
Most parametric rules and all invariants encode general properties of a system that should be impossible for any contract to violate. For example, consider a “solvency” invariant that shows that a vault contract holds enough underlying tokens to cover any withdrawals:
invariant solvency()
underlying.balanceOf(currentContract) >= currentContract.totalSupply();
Any violation of this property would be an important security vulnerability, regardless of whether it is violated by a method of the vault or a method of the underlying token. Therefore, it is important to check that no method of any contract can violate these kinds of properties. However, sometimes verifying these kinds of properties on methods of secondary contracts will require additional work.
Continuing the solvency example, the Prover is likely to find a violation of
the solvency rule in underlying.transferFrom
where the vault contract has
given an allowance to a third party. If a third party has an allowance, it
will be able to reduce the vault’s balance by transferring the vault’s tokens
to itself.
This violation represents an important vulnerability: if the vault mismanages its allowances, then it may become insolvent. This violation shows that the solvency of the vault depends on the correct management of its underlying allowances.
Therefore, to get the rule to pass, we will need to add another invariant stating that the vault manages its allowances correctly:
invariant no_vault_allowance()
underlying.allowance(currentContract) == 0;
We can then use this invariant in the preserved
block for the original
solvency rule. We are also likely to get violations from the case that the
vault contract itself calls methods on the underlying contract, so we rule that
out as well[1]:
invariant solvency()
underlying.balanceOf(currentContract) >= currentContract.totalSupply()
{
preserved with (env e) {
require e.msg.sender != currentContract;
requireInvariant no_vault_allowance();
}
}
There is nothing new about this process of identifying violations and adding
new invariants as necessary; it is the same process you would use for analyzing
any violation. This example just shows that some work may be required when
verifying old specifications with certora-cli
5.0.
The benefit is that by checking methods on secondary contracts, the Prover forces us to consider a previously unstated assumptions about the contract and write invariants that could detect important security vulnerabilities. For this reason, you are encouraged to identify and prove additional invariants to address counterexamples instead of using the filtering techniques described in the following sections.
Filtering properties that should not be checked
Some parametric rules encode properties that are only expected to hold on a specific contract. For example, you might have a rule that ensures that every successful method invocation is correctly authorized:
rule authorization_check(method f)
filtered { f -> f.isView }
{
env e; calldataarg args;
f(e,args);
assert is_authorized(e.msg.sender, f.selector);
}
There is no reason to expect this property to hold on any contract besides the main contract.
To handle cases like these, certora-cli
5.0 introduces two new ways to filter
methods to a specific contract.
The first and simplest way to restrict verification to a specific contract is
to call the method
object with a specific receiver contract:
rule authorization_check(method f)
filtered { f -> f.isView }
{
env e; calldataarg args;
currentContract.f(e,args);
assert is_authorized(e.msg.sender, f.selector);
}
This syntax will add a filter that will only instantiate f
with the methods
of currentContract
. The receiver may be either currentContract
or a
variable introduced by a using
clause.
The second and more flexible way is to use the new contract
property of the
method
variable:
rule authorization_check(method f)
filtered { f -> f.isView && f.contract == currentContract }
{
env e; calldataarg args;
f(e,args);
assert is_authorized(e.msg.sender, f.selector);
}
If f
is a method
variable, f.contract
refers to the contract that contains
the method f
.
Focusing on specific contracts
If you want to focus verification on a specific contract, you can do so using the --parametric_contracts <contract_name> ... option. This option takes a list of contracts and only instantiates parametric rules and invariants on methods of those contracts.
You can use this option to help transition specs to certora-cli
5.0; if C
is the main contract being verified, then passing --parametric_contracts C
will cause
method variables to be instantiated in the same way the would have in older
versions.
Disallow calls to contract functions in quantified expressions
Starting with certora-cli
version 5.0, the Prover no longer supports
making contract method calls in quantified expression bodies by
default.
For example, given the simple contract below, you can no longer
use the method
getI()
in a quantified expression body.
contract example {
uint i;
function getI() public view returns (uint256) {
return i;
}
}
rule there_exists {
// Using getI() in the quantified body will now cause the Prover to
// generate a type-checking error.
require (exists uint256 i . i == getI());
assert false, "Prover will generate an error before this line";
}
In the example rule there_exists
, the Prover will now generate an error similar
to the following:
Error in spec file (test2.spec:8:36): Contract function calls such as getI()
are disallowed inside quantified formulas.
In most simple cases, you can replace contract method calls with either a
{ref}direct storage access <…> or a {ref}ghost getI
simply returns the storage variable i
and you can
change the require
statement in the there_exists
rule to use storage access:
require (exists uint i . i == currentContract.i)
. To use a ghost, declare
the ghost and the hook that populates the ghost with the current value of
the contract variable i
.
ghost uint gI;
hook Sstore i uint256 v {
gI = v;
}
Finally, replace getI
in the require
statement in rule there_exists
with the
ghost variable gI
: require (exists uint i . i == gI)
.
If you must use contract method calls in quantified expressions,
you can still access the old behavior by specifying the
–allow_solidity_calls_in_quantifiers` argument to certoraRun
on the
command line.
Method variable restrictions
Starting with certora-cli
version 5.0, you cannot declare new method variables anywhere except the top-level body of a rule.
Declaring new method
variables inside of if
statements, hook bodies, CVL
function bodies, preserved blocks, and all other contexts are all disallowed.
You can still pass method
-type variables as arguments to CVL functions and
definitions. You can use this feature to rewrite CVL functions that formerly
declared new method
variables.
For example, before certora-cli
5.0, the following CVL function was valid:
function call_arbitrary() {
method f; env e; calldataarg args;
f(e, args);
}
rule r {
call_arbitrary();
assert true;
}
The declaration of f
inside of call_arbitrary
is now disallowed, so f
must
be passed into call_arbitrary
instead of declared within it:
function call_arbitrary(method f) {
env e; calldataarg args;
f(e,args);
}
rule r {
method f;
call_arbitrary(f);
assert true;
}
New DELETE
summary syntax
The syntax of the new `DELETE` keyword in summaries
has changed. Prior to certora-cli
5.0, it was possible to call methods
summarized with DELETE
summaries from spec, and the user had to annotate the
DELETE
modifier to indicate how those calls should be treated.
Starting with certora-cli
5.0, calling methods that have been summarized with
a DELETE
summary is disallowed, and the DELETE
annotation requires no
additional annotation.
CLI changes: New Parametric Contracts Attribute
As mentioned above the attribute parametric_contracts
was added to certora-cli
5.0.
The attribute accepts the parametric contracts as a list of strings.
The attribute can be set as the CLI flag --parametric_contracts
or in a .conf
file.
Example CLI:
certoraRun C1.sol C2.sol C3.sol --parametric_contracts C1 C3 ...
Configuration file:
"files": [ "C1", "C2", "C3"], "parametric_contracts": [ "C1", "C3"], ...
CLI changes: End of CVL1 Deprecation period
With the release of certora-cli
version 5.0, we stop supporting
the CVL1 attributes that were deprecated during the transition to CVL2.
You can find the list of the deprecated attributes here.