diff --git a/docs/layout-of-source-files.rst b/docs/layout-of-source-files.rst index 5195a131c..8a0e0736e 100644 --- a/docs/layout-of-source-files.rst +++ b/docs/layout-of-source-files.rst @@ -89,12 +89,12 @@ It is activated for the Ubuntu PPA releases in most versions, but not for solc-js, the Docker images, Windows binaries or the statically-built Linux binaries. -If you use -``pragma experimental SMTChecker;``, then you get additional -safety warnings which are obtained by querying an SMT solver. -The component does not yet support all features of the Solidity language -and likely outputs many warnings. In case it reports unsupported -features, the analysis may not be fully sound. +If you use ``pragma experimental SMTChecker;``, then you get additional +:ref:`safety warnings` which are obtained by querying an +SMT solver. +The component does not yet support all features of the Solidity language and +likely outputs many warnings. In case it reports unsupported features, the +analysis may not be fully sound. .. index:: source file, ! import, module diff --git a/docs/security-considerations.rst b/docs/security-considerations.rst index c3b04f08f..8c1b02473 100644 --- a/docs/security-considerations.rst +++ b/docs/security-considerations.rst @@ -331,6 +331,8 @@ The more people examine a piece of code, the more issues are found. Asking people to review your code also helps as a cross-check to find out whether your code is easy to understand - a very important criterion for good smart contracts. +.. _formal_verification: + ******************* Formal Verification ******************* @@ -344,3 +346,185 @@ Note that formal verification itself can only help you understand the difference between what you did (the specification) and how you did it (the actual implementation). You still need to check whether the specification is what you wanted and that you did not miss any unintended effects of it. + +Solidity implements a formal verification approach based on SMT solving. The +SMTChecker module automatically tries to prove that the code satisfies the +specification given by ``require/assert`` statements. That is, it considers +``require`` statements as assumptions and tries to prove that the conditions +inside ``assert`` statements are always true. If an assertion failure is +found, a counterexample is given to the user, showing how the assertion can be +violated. + +The SMTChecker also checks automatically for arithmetic underflow/overflow, +trivial conditions and unreachable code. +It is currently an experimental feature, therefore in order to use it you need +to enable it via :ref:`a pragma directive`. + +The SMTChecker traverses the Solidity AST creating and collecting program constraints. +When it encounters a verification target, an SMT solver is invoked to determine the outcome. +If a check fails, the SMTChecker provides specific input values that lead to the failure. + +For more details on how the SMT encoding works internally, see the paper +`SMT-based Verification of Solidity Smart Contracts `_. + +Abstraction and False Positives +=============================== + +The SMTChecker implements abstractions in an incomplete and sound way: If a bug +is reported, it might be a false positive introduced by abstractions (due to +erasing knowledge or using a non-precise type). If it determines that a +verification target is safe, it is indeed safe, that is, there are no false +negatives (unless there is a bug in the SMTChecker). + +The SMT encoding tries to be as precise as possible, mapping Solidity types +and expressions to their closest `SMT-LIB `_ +representation, as shown in the table below. + ++-----------------------+--------------+-----------------------------+ +|Solidity type |SMT sort |Theories (quantifier-free) | ++=======================+==============+=============================+ +|Boolean |Bool |Bool | ++-----------------------+--------------+-----------------------------+ +|intN, uintN, address, |Integer |LIA, NIA | +|bytesN, enum | | | ++-----------------------+--------------+-----------------------------+ +|array, mapping |Array |Arrays | ++-----------------------+--------------+-----------------------------+ +|other types |Integer |LIA | ++-----------------------+--------------+-----------------------------+ + +Types that are not yet supported are abstracted by a single 256-bit unsigned integer, +where their unsupported operations are ignored. + +Function calls to the same contract (or base contracts) are inlined when +possible, that is, when their implementation is available. +Calls to functions in other contracts are not inlined even if their code is +available, since we cannot guarantee that the actual deployed code is the same. +Complex pure functions are abstracted by an uninterpreted function (UF) over +the arguments. + ++-----------------------------------+--------------------------------------+ +|Functions |SMT behavior | ++===================================+======================================+ +|``assert`` |Verification target | ++-----------------------------------+--------------------------------------+ +|``require`` |Assumption | ++-----------------------------------+--------------------------------------+ +|internal |Inline function call | ++-----------------------------------+--------------------------------------+ +|external |Inline function call | +| |Erase knowledge about state variables | +| |and local storage references | ++-----------------------------------+--------------------------------------+ +|``gasleft``, ``blockhash``, |Abstracted with UF | +|``keccak256``, ``ecrecover`` | | +|``ripemd160``, ``addmod``, | | +|``mulmod`` | | ++-----------------------------------+--------------------------------------+ +|pure functions without |Abstracted with UF | +|implementation (external or | | +|complex) | | ++-----------------------------------+--------------------------------------+ +|external functions without |Unsupported | +|implementation | | ++-----------------------------------+--------------------------------------+ +|others |Currently unsupported | ++-----------------------------------+--------------------------------------+ + +Using abstraction means loss of precise knowledge, but in many cases it does +not mean loss of proving power. + +:: + + pragma solidity >=0.5.0; + pragma experimental SMTChecker; + + contract Recover + { + function f( + bytes32 hash, + uint8 _v1, uint8 _v2, + bytes32 _r1, bytes32 _r2, + bytes32 _s1, bytes32 _s2 + ) public pure returns (address) { + address a1 = ecrecover(hash, _v1, _r1, _s1); + require(_v1 == _v2); + require(_r1 == _r2); + require(_s1 == _s2); + address a2 = ecrecover(hash, _v2, _r2, _s2); + assert(a1 == a2); + return a1; + } + } + +In the example above, the SMTChecker is not expressive enough to actually +compute ``ecrecover``, but by modelling the function calls as uninterpreted +functions we know that the return value is the same when called on equivalent +parameters. This is enough to prove that the assertion above is always true. + +Abstracting a function call with an UF can be done for functions known to be +deterministic, and can be easily done for pure functions. It is however +difficult to do this with general external functions, since they might depend +on state variables. + +External function calls also imply that any current knowledge that the +SMTChecker might have regarding mutable state variables needs to be erased to +guarantee no false negatives, since the called external function might direct +or indirectly call a function in the analyzed contract that changes state +variables. + +Reference Types and Aliasing +============================= + +Solidity implements aliasing for reference types with the same :ref:`data +location`. +That means one variable may be modified through a reference to the same data +area. +The SMTChecker does not keep track of which references refer to the same data. +This implies that whenever a local reference or state variable of reference +type is assigned, all knowledge regarding variables of the same type and data +location is erased. +If the type is nested, the knowledge removal also includes all the prefix base +types. + +:: + + pragma solidity >=0.5.0; + pragma experimental SMTChecker; + // This will not compile + contract Aliasing + { + uint[] array; + function f( + uint[] memory a, + uint[] memory b, + uint[][] memory c, + uint[] storage d + ) internal view { + require(array[0] == 42); + require(a[0] == 2); + require(c[0][0] == 2); + require(d[0] == 2); + b[0] = 1; + // Erasing knowledge about memory references should not + // erase knowledge about state variables. + assert(array[0] == 42); + // Fails because `a == b` is possible. + assert(a[0] == 2); + // Fails because `c[i] == b` is possible. + assert(c[0][0] == 2); + assert(d[0] == 2); + assert(b[0] == 1); + } + } + +After the assignment to ``b[0]``, we need to clear knowledge about ``a`` since +it has the same type (``uint[]``) and data location (memory). We also need to +clear knowledge about ``c``, since its base type is also a ``uint[]`` located +in memory. This implies that some ``c[i]`` could refer to the same data as +``b`` or ``a``. + +Notice that we do not clear knowledge about ``array`` and ``d`` because they +are located in storage, even though they also have type ``uint[]``. However, +if ``d`` was assigned, we would need to clear knowledge about ``array`` and +vice-versa.