Statements
The bodies of rules, functions, and hooks in CVL are made up of statements. Statements describe the steps that are simulated by the Prover when evaluating a rule.
Statements in CVL are similar to statements in Solidity, although there are some differences; see Solidity-like Statements. This document lists the available CVL commands.
Syntax
The syntax for statements in CVL is given by the following EBNF grammar:
block ::= statement { statement }
statement ::= type id [ "=" expr ] ";"
| "require" expr ";"
| "static_require" expr ";"
| "assert" expr [ "," string ] ";"
| "static_assert" expr [ "," string ] ";"
| "satisfy" expr [ "," string ] ";"
| "requireInvariant" id "(" exprs ")" ";"
| lhs "=" expr ";"
| "if" expr statement [ "else" statement ]
| "{" block "}"
| "return" [ expr ] ";"
| function_call ";"
| "reset_storage" expr ";"
| "havoc" id [ "assuming" expr ] ";"
lhs ::= id [ "[" expr "]" ] [ "," lhs ]
See Basic Syntax for the id
and string
productions. See Types for
the type
production. See Expressions for the expr
and function_call
productions.
Variable declarations
Unlike undefined variables in most programming languages, undefined variables in CVL are a centrally important language feature. If a variable is declared but not defined, the Prover will generate models with every possible value of the undefined variable.
Undefined variables in CVL behave the same way as rule parameters.
When the Prover reports a counterexample that violates a rule, the values of the variables declared in the rule are displayed in the report. Variables declared in CVL functions are not currently visible in the report.
assert
and require
The assert
and require
commands are similar to the corresponding statements
in Solidity. The require
statement is used to specify the preconditions for
a rule, while the assert
statement is used to specify the expected behavior
of contract functions.
During verification, the Prover will ignore any model that causes the
require
expressions to evaluate to false. Unlike Solidity, the require
statement does not contain a descriptive message, because the Prover will never
consider an example where the require
statement evaluates to false
.
The assert
statements define the expected behavior of contract functions. If
it is possible to generate a model that causes the assert
expression to
evaluate to false
, the Prover will construct one of them and report a
violation.
Assert conditions may be followed by a message string describing the condition; this message will be included in the reported violation.
Note
Unlike Solidity’s assert
and require
, the CVL syntax for assert
and
require
does not require parentheses around the expression and message.
Examples
rule withdraw_succeeds {
env e; // env represents the bytecode environment passed on every call
// invoke function withdraw and assume that it does not revert
bool success = withdraw(e); // e is passed as an additional argument
assert success, "withdraw must succeed"; // verify that withdraw succeeded
}
rule totalFundsAfterDeposit(uint256 amount) {
env e;
deposit(e, amount);
uint256 userFundsAfter = getFunds(e, e.msg.sender);
uint256 totalAfter = getTotalFunds(e);
// Verify that the total funds of the system is at least the current funds of the msg.sender.
assert totalAfter >= userFundsAfter;
}
satisfy
statements
A satisfy
statement is used to check that the rule can be executed in such a
way that the satisfy
statement is reached and that its condition is fulfilled.
We require that each rule ends with either a satisfy
statement or an assert
statement.
See Producing Positive Examples for an example demonstrating the satisfy
command.
For each satisfy
statement that is checked successfully, the Certora verifier
will produce a witness for a valid execution of the rule. It will show an
execution trace containing values for each input variable and each state
variable where all require
and satisfy
statements are executed successfully.
In case there is no such execution, for example if the require
statements are
already inconsistent or if a Solidity function always reverts, the rule will
show as “Violated”.
If the rule contains multiple satisfy
statements, then all executed satisfy
statements must hold. However, a satisfy
statement on a conditional branch
that is not executed does not need to hold.
If at least one satisfy
statement is not satisfiable, an error is reported.
If all satisfy
statements can be fulfilled on at least one path, the rule
succeeds.
Note
A success only guarantees that there is some satisfying execution starting in some arbitrary state. It is not possible to check that every possible starting state has an execution that satisfies the condition.
Note
satisfy
statements are never checked in the same sub-rule with assert
statements,
and they are always checked under optimistic assumptions.
This means that rules without any explicit assert
statements will not check the
pessimistic assertions, i.e., the implicit assertions that we insert
in rules with assert
statements when at least one of the “optimistic” flags
is not set. In order to trigger creation of a sub-rule that contains these
checks, users can insert an assert true
statement into the rule. This assert
itself will never be violated, but the sub-rule we create for it will contain
all the pessimistic assertions for the program.
requireInvariant
statements
requireInvariant
is shorthand for require
of the expression of the invariant where the invariant parameters have to be substituted with the values/ variables for which the invariant should hold.
Note
requireInvariant
is always safe for invariants that have been proved, even in
preserved
blocks; see Invariants and induction for a detailed explanation.
Havoc Statements
Havoc statements introduce non-determinism into the contract execution, allowing the SMT solver to choose random values for specific variables. Havoc statements are helpful for modeling uncertainty and verifying a wider range of possible scenarios.
Syntax
The syntax for a havoc
statement is as follows:
havoc identifier [ assuming condition ];
identifier
: The variable or expression for which non-deterministic values will be chosen.condition
: An optional condition that restricts the possible values for the havoc variable.
Usage
Basic Havoc
The basic use of a havoc statement involves introducing non-deterministic values for a specific variable. This is useful when the exact value of a variable is unknown or when exploring various scenarios.
Example:
uint256 x;
havoc x;
In this example, the value of variable x
is chosen randomly by the SMT solver.
Note: The havoc statement is not really necessary as unassigned values are havoc by default.
Havoc with Condition
Havoc statements can include a condition that restricts the possible values for the havoc variable. This allows for more fine-grained control over the non-deterministic choices made by the SMT solver.
Example:
uint256 y;
havoc y assuming y > 10;
In this example, the havoc statement introduces non-deterministic values for variable y
, but only values greater than 10 are considered valid.
Note: The above is equivalent to:
uint256 y;
require y > 0;
Two-State Contexts: @old
and @new
Two-state contexts, denoted by @old
and @new
, are essential when dealing with havoc statements. They provide a mechanism to reference the old and new states of a variable within the havoc statement, allowing for more nuanced control over the non-deterministic choices.
Example:
havoc sumAllBalance assuming sumAllBalance@new() == sumAllBalance@old() + balance - old_balance;
In the given example, the havoc statement introduces non-deterministic values for the variable sumAllBalance
. The assuming clause adds a condition: the new state of sumAllBalance
should be the old state plus the change in the balance variable.
sumAllBalance@new()
: Value in the updated state.
sumAllBalance@old()
: Value in the previous state.
balance - old_balance
: Change in the balance variable.
Note
Hooks will not be triggered for havoc statements. That is, if there is a hook defined on load, or store, of the sumAllBalance
variable,
it will not be triggered from the havoc statement.
Advanced Usage: havoc assuming
The havoc assuming
construct allows introducing non-deterministic choices for variables while imposing specific conditions. This can be particularly useful for modeling complex scenarios where certain constraints must be satisfied.
Example:
ghost uint256 a;
ghost uint256 b;
rule example(){
havoc a assuming a@new < b;
havoc b assuming a + b@new == 100;
assert a < b && a + b == 100;
}
In this example, havoc statements are used to introduce non-deterministic values for ghosts a
and b
while ensuring that a
is less than b
and their sum is equal to 100.
Conclusion
Havoc statements play a critical role in making CVL specifications more expressive and capable of handling uncertainty. They widen the coverage of possible contract behaviors making verification more robust and comprehensive. Understanding two-state contexts (@old
and @new
) and the havoc assuming
construct is useful for harnessing the full power of CVL, in particular when combined with ghosts.
Solidity-like Statements
Solidity-like statements provide a familiar syntax for expressing conditions and behaviors similar to Solidity, These statements enhance the readability and ease of writing specifications by adopting a syntax that resembles Solidity.
1. Assert Statement
Syntax:
assert condition;
Usage:
The assert
statement is used to assert a condition that must be true during the execution of the contract. If the condition evaluates to false, it will trigger a verification failure.
Example:
uint256 balance;
assert balance > 0;
In this example, the assert
statement ensures that the balance variable is positive.
2. Require Statement
Syntax:
require condition;
Usage:
The require
statement is similar to the assert
statement but is used for expressing preconditions that must be satisfied for the execution to continue. Values that make the condition evaluate to false will not be considered as violations of a later assert
statement or witnesses to a later satisfy
statement.
Example:
uint256 amount;
require amount > 0;
satisfy amount >= 0;
Here, the require
statement ensures that the amount
must be greater than zero. This means there cannot be a witness of the satisfy
command with amount
equal to zero.
3. Modeling Reverts in Solidity Calls
The default method of calling Solidity functions within CVL is to assume they do not revert.
This behavior can be adjusted with the @withrevert
modifier.
After every Solidity call, even if it is not marked with @withrevert
, a builtin variable called lastReverted
is updated according to whether the Solidity call reverted or not.
Note: For calls without @withrevert
, lastReverted
is automatically set to to false.
Syntax:
f@withrevert(args);
assert !lastReverted;
In this example, we call to f
without pruning the reverting paths, and then we assert that the call to f
did not revert on any given input.
Example:
uint256 limit = 100;
uint256 value;
require value > limit;
Deposit@withrevert(value);
assert lastReverted, "Expected revert when value exceeds limit";
In this example, the @withrevert
modifier is applied to the Deposit
function call, which is expected to revert if the value
exceeds the specified limit
. The assert
statement checks whether lastReverted
is true, ensuring that the contract execution does revert as anticipated when the condition is violated. The error message in the assert
provides additional context about the expectation.
4. Return Statement
Syntax:
return expression;
Usage:
The return
statement is used to terminate the execution of a function and return a value. It can only be used in functions to specify the value to be returned.
Example:
function calculateSum(uint256 a, uint256 b) returns (uint256) {
return a + b;
}
This example defines a function calculateSum
that takes two parameters and returns their sum.
Conclusion
Solidity-like statements in CVL simplify the process of writing specifications by using a syntax that closely resembles Solidity. These statements align with the familiar patterns and structures used in Solidity smart contracts, making it easier for developers and auditors to express and verify the desired behaviors and conditions in a contract. Understanding and using these statements contributes to more readable and expressive CVL specifications.