.. _formal_verification: ################################## SMTChecker and Formal Verification ################################## Using formal verification it is possible to perform an automated mathematical proof that your source code fulfills a certain formal specification. The specification is still formal (just as the source code), but usually much simpler. 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 (Satisfiability Modulo Theories) `_ and `Horn `_ solving. The SMTChecker module automatically tries to prove that the code satisfies the specification given by ``require`` and ``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 may be given to the user showing how the assertion can be violated. If no warning is given by the SMTChecker for a property, it means that the property is safe. The other verification targets that the SMTChecker checks at compile time are: - Arithmetic underflow and overflow. - Division by zero. - Trivial conditions and unreachable code. - Popping an empty array. - Out of bounds index access. - Insufficient funds for a transfer. All the targets above are automatically checked by default if all engines are enabled, except underflow and overflow for Solidity >=0.8.7. The potential warnings that the SMTChecker reports are: - `` happens here.``. This means that the SMTChecker proved that a certain property fails. A counterexample may be given, however in complex situations it may also not show a counterexample. This result may also be a false positive in certain cases, when the SMT encoding adds abstractions for Solidity code that is either hard or impossible to express. - `` might happen here``. This means that the solver could not prove either case within the given timeout. Since the result is unknown, the SMTChecker reports the potential failure for soundness. This may be solved by increasing the query timeout, but the problem might also simply be too hard for the engine to solve. To enable the SMTChecker, you must select :ref:`which engine should run`, where the default is no engine. Selecting the engine enables the SMTChecker on all files. .. note:: Prior to Solidity 0.8.4, the default way to enable the SMTChecker was via ``pragma experimental SMTChecker;`` and only the contracts containing the pragma would be analyzed. That pragma has been deprecated, and although it still enables the SMTChecker for backwards compatibility, it will be removed in Solidity 0.9.0. Note also that now using the pragma even in a single file enables the SMTChecker for all files. .. note:: The lack of warnings for a verification target represents an undisputed mathematical proof of correctness, assuming no bugs in the SMTChecker and the underlying solver. Keep in mind that these problems are *very hard* and sometimes *impossible* to solve automatically in the general case. Therefore, several properties might not be solved or might lead to false positives for large contracts. Every proven property should be seen as an important achievement. For advanced users, see :ref:`SMTChecker Tuning ` to learn a few options that might help proving more complex properties. ******** Tutorial ******** Overflow ======== .. code-block:: Solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; contract Overflow { uint immutable x; uint immutable y; function add(uint x_, uint y_) internal pure returns (uint) { return x_ + y_; } constructor(uint x_, uint y_) { (x, y) = (x_, y_); } function stateAdd() public view returns (uint) { return add(x, y); } } The contract above shows an overflow check example. The SMTChecker does not check underflow and overflow by default for Solidity >=0.8.7, so we need to use the command line option ``--model-checker-targets "underflow,overflow"`` or the JSON option ``settings.modelChecker.targets = ["underflow", "overflow"]``. See :ref:`this section for targets configuration`. Here, it reports the following: .. code-block:: text Warning: CHC: Overflow (resulting value larger than 2**256 - 1) happens here. Counterexample: x = 1, y = 115792089237316195423570985008687907853269984665640564039457584007913129639935 = 0 Transaction trace: Overflow.constructor(1, 115792089237316195423570985008687907853269984665640564039457584007913129639935) State: x = 1, y = 115792089237316195423570985008687907853269984665640564039457584007913129639935 Overflow.stateAdd() Overflow.add(1, 115792089237316195423570985008687907853269984665640564039457584007913129639935) -- internal call --> o.sol:9:20: | 9 | return x_ + y_; | ^^^^^^^ If we add ``require`` statements that filter out overflow cases, the SMTChecker proves that no overflow is reachable (by not reporting warnings): .. code-block:: Solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; contract Overflow { uint immutable x; uint immutable y; function add(uint x_, uint y_) internal pure returns (uint) { return x_ + y_; } constructor(uint x_, uint y_) { (x, y) = (x_, y_); } function stateAdd() public view returns (uint) { require(x < type(uint128).max); require(y < type(uint128).max); return add(x, y); } } Assert ====== An assertion represents an invariant in your code: a property that must be true *for all transactions, including all input and storage values*, otherwise there is a bug. The code below defines a function ``f`` that guarantees no overflow. Function ``inv`` defines the specification that ``f`` is monotonically increasing: for every possible pair ``(a, b)``, if ``b > a`` then ``f(b) > f(a)``. Since ``f`` is indeed monotonically increasing, the SMTChecker proves that our property is correct. You are encouraged to play with the property and the function definition to see what results come out! .. code-block:: Solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; contract Monotonic { function f(uint x) internal pure returns (uint) { require(x < type(uint128).max); return x * 42; } function inv(uint a, uint b) public pure { require(b > a); assert(f(b) > f(a)); } } We can also add assertions inside loops to verify more complicated properties. The following code searches for the maximum element of an unrestricted array of numbers, and asserts the property that the found element must be greater or equal every element in the array. .. code-block:: Solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; contract Max { function max(uint[] memory a) public pure returns (uint) { uint m = 0; for (uint i = 0; i < a.length; ++i) if (a[i] > m) m = a[i]; for (uint i = 0; i < a.length; ++i) assert(m >= a[i]); return m; } } Note that in this example the SMTChecker will automatically try to prove three properties: 1. ``++i`` in the first loop does not overflow. 2. ``++i`` in the second loop does not overflow. 3. The assertion is always true. .. note:: The properties involve loops, which makes it *much much* harder than the previous examples, so beware of loops! All the properties are correctly proven safe. Feel free to change the properties and/or add restrictions on the array to see different results. For example, changing the code to .. code-block:: Solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; contract Max { function max(uint[] memory a) public pure returns (uint) { require(a.length >= 5); uint m = 0; for (uint i = 0; i < a.length; ++i) if (a[i] > m) m = a[i]; for (uint i = 0; i < a.length; ++i) assert(m > a[i]); return m; } } gives us: .. code-block:: text Warning: CHC: Assertion violation happens here. Counterexample: a = [0, 0, 0, 0, 0] = 0 Transaction trace: Test.constructor() Test.max([0, 0, 0, 0, 0]) --> max.sol:14:4: | 14 | assert(m > a[i]); State Properties ================ So far the examples only demonstrated the use of the SMTChecker over pure code, proving properties about specific operations or algorithms. A common type of properties in smart contracts are properties that involve the state of the contract. Multiple transactions might be needed to make an assertion fail for such a property. As an example, consider a 2D grid where both axis have coordinates in the range (-2^128, 2^128 - 1). Let us place a robot at position (0, 0). The robot can only move diagonally, one step at a time, and cannot move outside the grid. The robot's state machine can be represented by the smart contract below. .. code-block:: Solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; contract Robot { int x = 0; int y = 0; modifier wall { require(x > type(int128).min && x < type(int128).max); require(y > type(int128).min && y < type(int128).max); _; } function moveLeftUp() wall public { --x; ++y; } function moveLeftDown() wall public { --x; --y; } function moveRightUp() wall public { ++x; ++y; } function moveRightDown() wall public { ++x; --y; } function inv() public view { assert((x + y) % 2 == 0); } } Function ``inv`` represents an invariant of the state machine that ``x + y`` must be even. The SMTChecker manages to prove that regardless how many commands we give the robot, even if infinitely many, the invariant can *never* fail. The interested reader may want to prove that fact manually as well. Hint: this invariant is inductive. We can also trick the SMTChecker into giving us a path to a certain position we think might be reachable. We can add the property that (2, 4) is *not* reachable, by adding the following function. .. code-block:: Solidity function reach_2_4() public view { assert(!(x == 2 && y == 4)); } This property is false, and while proving that the property is false, the SMTChecker tells us exactly *how* to reach (2, 4): .. code-block:: text Warning: CHC: Assertion violation happens here. Counterexample: x = 2, y = 4 Transaction trace: Robot.constructor() State: x = 0, y = 0 Robot.moveLeftUp() State: x = (- 1), y = 1 Robot.moveRightUp() State: x = 0, y = 2 Robot.moveRightUp() State: x = 1, y = 3 Robot.moveRightUp() State: x = 2, y = 4 Robot.reach_2_4() --> r.sol:35:4: | 35 | assert(!(x == 2 && y == 4)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Note that the path above is not necessarily deterministic, as there are other paths that could reach (2, 4). The choice of which path is shown might change depending on the used solver, its version, or just randomly. External Calls and Reentrancy ============================= Every external call is treated as a call to unknown code by the SMTChecker. The reasoning behind that is that even if the code of the called contract is available at compile time, there is no guarantee that the deployed contract will indeed be the same as the contract where the interface came from at compile time. In some cases, it is possible to automatically infer properties over state variables that are still true even if the externally called code can do anything, including reenter the caller contract. .. code-block:: Solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; interface Unknown { function run() external; } contract Mutex { uint x; bool lock; Unknown immutable unknown; constructor(Unknown u) { require(address(u) != address(0)); unknown = u; } modifier mutex { require(!lock); lock = true; _; lock = false; } function set(uint x_) mutex public { x = x_; } function run() mutex public { uint xPre = x; unknown.run(); assert(xPre == x); } } The example above shows a contract that uses a mutex flag to forbid reentrancy. The solver is able to infer that when ``unknown.run()`` is called, the contract is already "locked", so it would not be possible to change the value of ``x``, regardless of what the unknown called code does. If we "forget" to use the ``mutex`` modifier on function ``set``, the SMTChecker is able to synthesize the behaviour of the externally called code so that the assertion fails: .. code-block:: text Warning: CHC: Assertion violation happens here. Counterexample: x = 1, lock = true, unknown = 1 Transaction trace: Mutex.constructor(1) State: x = 0, lock = false, unknown = 1 Mutex.run() unknown.run() -- untrusted external call, synthesized as: Mutex.set(1) -- reentrant call --> m.sol:32:3: | 32 | assert(xPre == x); | ^^^^^^^^^^^^^^^^^ .. _smtchecker_options: ***************************** SMTChecker Options and Tuning ***************************** Timeout ======= The SMTChecker uses a hardcoded resource limit (``rlimit``) chosen per solver, which is not precisely related to time. We chose the ``rlimit`` option as the default because it gives more determinism guarantees than time inside the solver. This options translates roughly to "a few seconds timeout" per query. Of course many properties are very complex and need a lot of time to be solved, where determinism does not matter. If the SMTChecker does not manage to solve the contract properties with the default ``rlimit``, a timeout can be given in milliseconds via the CLI option ``--model-checker-timeout