The Methods Block
The methods
block contains declarations of contract methods. Although CVL is
able to call contract functions even if they are not declared in the methods
block, the methods block allows users to specify additional information about
contract methods, and can help document the expected interface of the contract.
There are two kinds of declarations:
Non-summary declarations document the interface between the specification and the contracts used during verification. Non-summary declarations also support spec reuse by allowing specs written against a complete interface to be checked against a contract that only implements part of the interface.
Summary declarations are used to replace calls to methods having the given signature with something that is simpler for the Prover to reason about. Summaries allow the Prover to reason about external contracts whose code is unavailable. They can also be useful to simplify the code being verified to circumvent timeouts.
Caution
Summary declarations change the way that some function calls are interpreted,
and are therefore unsound (with the exception of HAVOC_ALL
summaries
which are always sound, and NONDET
summaries which are sound for view
functions).
Contents
Syntax
The syntax for the methods
block is given by the following EBNF grammar:
methods ::= "methods" "{" { method_spec } "}"
method_spec ::= ( sighash | [ id "." ] id "(" evm_params ")" )
[ "returns" types ]
[ "envfree" ]
[ "=>" method_summary [ "UNRESOLVED" | "ALL" ] ]
[ ";" ]
evm_param ::= evm_type [ id ]
types ::= cvl_type { "," cvl_type }
| "(" [ evm_type [ id ] { "," evm_type [ id ] } ] ")"
method_summary ::= "ALWAYS" "(" value ")"
| "CONSTANT"
| "PER_CALLEE_CONSTANT"
| "NONDET"
| "HAVOC_ECF"
| "HAVOC_ALL"
| "DISPATCHER" [ "(" ( "true" | "false" ) ")" ]
| "AUTO"
| id "(" [ id { "," id } ] ")"
See Types for the evm_type
and cvl_type
productions. See Basic Syntax
for the id
production. See Statements for the block
production, and
Expressions for the expression
production.
Entries in the methods
block
Each entry in the methods block denotes either the sighash or the type signature
for a contract method. Methods of contracts that are introduced by using statements can also be described by prefixing the method name with
the contract variable name. For example, if contract C
is introduced by the
statement using C as c
, then the method f(uint)
of contract c
can be
referred to as c.f(uint)
.
It is possible for a method signature to appear in the methods
block but not
in the contract being verified. In this case, the Prover will skip any rules
that mention the missing method, rather than reporting an error. This behavior
allows reusing specifications on contracts that only support part of an
interface: only the supported methods will be verified.
Following the method signature is an optional returns
clause. If a method
declaration contains a returns
clause, the declared return type must match
the contract method’s return type. If the returns
clause is omitted, the
return type is taken from the contract method’s return type.
Following the returns
clause is an optional envfree
tag. Marking a method
with envfree
has two effects. First, calls to the method
from CVL do not need to explicitly pass an environment value as the
first argument. Second, the Prover will verify that the method implementation
in the contract being verified does not depend on any of the environment
variables. The results of this check are displayed on the verification report
as separate rules called envfreeFuncsStaticCheck
and
envfreeFuncsAreNonpayable
1.
Finally, the method entry may contain an optional summarization (indicated by
=>
followed by the summary type and an optional application policy). A
summarized declaration indicates that the Prover should replace some calls to
the summarized function by an approximation. This is an important technique
for working around Prover timeouts and also for working with external contracts
whose implementation is not fixed at verification time2.
The summary type determines what type of approximation is used to replace the function calls. The available types are described in the following sections:
The application policy determines which function calls are replaced by approximations. See Which function calls are summarized for details.
Todo
Some of the method summary types are unsupported for methods having certain argument or return types. The exact limitations are currently undocumented.
Which function calls are summarized
To decide whether to summarize a given internal or external function call, the Prover first determines whether it matches any of the declarations in the methods block, and then uses the declaration and the calling context to determine whether the call should be replaced by an approximation.
To determine whether a call matches a declaration, the tool computes an ABI
signature for both the call and the method summary. This ABI signature may be
simpler than the declaration in Solidity or the methods
block, because
ABI signatures are less expressive than Solidity type signatures. In
particular, structs are converted into tuples and location annotations such as
memory
or calldata
are dropped. If there are multiple internal functions or
multiple method summaries that are converted to the same summarized ABI
signature, the Prover will report an error.
Method summaries match all calls with the matching ABI signature, including internal methods and external methods on all contracts. There is currently no way to apply different summaries to different contracts or to summarize some calls and not others to methods with the same ABI signature. For this reason, it is not possible to specify a summary for a method that is qualified by a contract name.
To determine whether a function call is replaced by an approximation, the
Prover considers the context in which the function is called in addition to the
application policy for its signature. If present, the application policy must
be either ALL
or UNRESOLVED
; the default policy is ALL
with the exception
of DISPATCHER
summaries, which have a default of UNRESOLVED
. The decision
to replace a call by an approximation is made as follows:
If the function is called from CVL rather than from contract code then it is never replaced by a summary.
If the code for the function is known at verification time, either because it is a method of
currentContract
or because the receiver contract is linked, then the function is only summarized if the resolution type isALL
.If the code for the function is not known at verification time, then the function call must be summarized. If no summary is given, the default summary type is AUTO, whose behavior is determined by the type of function call. In this case, the verification report will contain a contract call resolution warning.
Todo
The @dontsummarize
tag on method calls is currently undocumented but likely
affects the summarization behavior. See Calling contract functions.
Summary types
View summaries: ALWAYS
, CONSTANT
, PER_CALLEE_CONSTANT
, and NONDET
These four summary types treat the summarized methods as view methods: the summarized methods are replaced by approximations that do not update the state of any contract (aside from any balances transferred with the method call itself). They differ in the assumptions made about the return value:
The
ALWAYS(v)
approximation assumes that the method always returnsv
The
CONSTANT
approximation assumes that all calls to methods with the given signature always return the same result. If the summarized method is expected to return multiple results, the approximation returns the correct number of values.The
PER_CALLEE_CONSTANT
approximation assumes that all calls to the method on a given receiver contract must return the same result, but that the returned value may be different for different receiver contracts. If the summarized method is expected to return multiple results, the approximation returns the correct number of values.The
NONDET
approximation makes no assumptions about the return values; each call to the summarized method may return a different result. The number of returned values is not assumed to match the requested number, unless --settings -optimisticReturnsize is specified.
Havoc summaries: HAVOC_ALL
and HAVOC_ECF
The most conservative summary type is HAVOC_ALL
. This summary makes no
assumptions at all about the called function: it is allowed to have arbitrary
side effects on the state of any contract (including the calling contract), and
may return any value. It can also change any contract’s ETH balance in an
arbitrary way. In effect, calling a method that is summarized by HAVOC_ALL
obliterates all knowledge that the Prover has about the state of the contract
before the call.
The HAVOC_ALL
approximation is sound, but it can be overly
restrictive in practice. In reality, a contract’s state cannot be changed in
arbitrary ways, but only according to the contract’s methods. However, the
Prover does not currently have support for more fine-grained reasoning about
the side effects of unknown methods.
A useful middle ground is the HAVOC_ECF
summary type. A HAVOC_ECF
summarization for a method encodes the assumption that the called method is not
reentrant. This summarization approximates a method call by assuming it can
have arbitrary effects on contracts other than the contract being verified, but
that it can neither change the current contract’s state nor decrease its ETH
balance (aside from value transferred by the method call itself).
The Prover makes no assumptions about the return value of a havoc summary. For methods that return multiple values, the approximations are allowed to return the incorrect number of results. In most cases, this will cause the calling method to revert. If you want to ignore this particular revert condition, you can pass the --settings -optimisticReturnsize option.
DISPATCHER
summaries
The DISPATCHER
summary type provides a useful approximation for methods of
interfaces that are implemented by multiple contracts. For example, the
methods defined by the ERC20 specification are often summarized using the
DISPATCHER
summary type.
If a function with a DISPATCHER
summary is called, the Prover will assume
that the receiver of the call is one of the known contract implementations
containing the given signature; the call will then behave the same way that a
normal method call on the receiver would. The Prover will consider examples
with every possible implementing contract, but multiple DISPATCHER
method
calls on the same receiver address in the same example will use the same
receiver contract.
The set of contract implementations that the Prover chooses from contains
the set of contracts passed as arguments to the CLI.
In addition, the Prover may consider an unknown target contract whose methods
are all interpreted using the AUTO summary. The presence
of the unknown contract is determined by the optional boolean argument to the
DISPATCHER
summary:
With
DISPATCHER(false)
or justDISPATCHER
, the unknown contract is considered as a possibilityWith
DISPATCHER(true)
, only the known contract instances are considered
Note
The most commonly used dispatcher mode is DISPATCHER(true)
, because in almost
all cases DISPATCHER(false)
and AUTO
report the same set of violations.
AUTO
summaries
The behavior of the AUTO
summary depends on the type of call3:
Calls to non-library
view
andpure
methods use theNONDET
approximation: they keep all state unchanged.Normal calls and constructors use the
HAVOC_ECF
approximation: they are assumed to change the state of external contracts arbitrarily but to leave the caller’s state unchanged.Calls to library methods and
delegatecall
s are assumed to change the caller’s storage in an arbitrary way, but are assumed to leave ETH balances and the storage of other contracts unchanged.
Function summaries
Contract methods can also be summarized using CVL Functions or Ghost functions as approximations. Contract calls to the summarized method are replaced by calls to the specified CVL functions.
To use a CVL function or ghost as a summary, use a call to the function in place of the summary type. The function call can only refer directly to the variables defined as arguments in the summary declarations; expressions that combine those variables are not supported.
There are a few restrictions on the functions that can be used as approximations:
Functions used as summaries are not allowed to call contract functions.
Functions used as summaries may not have accept arguments or return values that have struct or array types.
- 1
The effect of payable functions on the contract’s balance depends on the message value, so payable functions also require an
env
.- 2
Because the internal method calls are not explicit in the compiled bytecode, the Prover needs to use heuristics to determine where internal methods are called in order to summarize them. Occasionally, these heuristics are unable to locate an internal method call, and therefore they remain unsummarized. The --settings -showInternalFunctions option can aid in determining whether the Prover was able to identify a specific internal function call or not.
- 3
The behavior of
AUTO
summaries is actually determined by the EVM opcode used to make the call: calls made using theSTATICCALL
opcode use theNONDET
summary, calls usingCALL
orCREATE
opcode use theHAVOC_ECF
summary, and calls using theDELEGATECALL
andCALLCODE
opcodes havoc the current contract only. Modern Solidity versions output opcodes that are consistent with the above description, but older versions behave differently. See State Mutability in the Solidity manual for details.