[SMTChecker] Add a new trusted mode which assumes that code that is

available at compile time is trusted.
This commit is contained in:
Leo Alt
2023-02-06 17:02:33 +01:00
parent f2bf23a067
commit 8d91ccf028
135 changed files with 3156 additions and 235 deletions
+182
View File
@@ -518,6 +518,188 @@ which has the following form:
"source2.sol": ["contract2", "contract3"]
}
Trusted External Calls
======================
By default, the SMTChecker does not assume that compile-time available code
is the same as the runtime code for external calls. Take the following contracts
as an example:
.. code-block:: solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract Ext {
uint public x;
function setX(uint _x) public { x = _x; }
}
contract MyContract {
function callExt(Ext _e) public {
_e.setX(42);
assert(_e.x() == 42);
}
}
When ``MyContract.callExt`` is called, an address is given as the argument.
At deployment time, we cannot know for sure that address ``_e`` actually
contains a deployment of contract ``Ext``.
Therefore, the SMTChecker will warn that the assertion above can be violated,
which is true, if ``_e`` contains another contract than ``Ext``.
However, it can be useful to treat these external calls as trusted, for example,
to test that different implementations of an interface conform to the same property.
This means assuming that address ``_e`` indeed was deployed as contract ``Ext``.
This mode can be enabled via the CLI option ``--model-checker-ext-calls=trusted``
or the JSON field ``settings.modelChecker.extCalls: "trusted"``.
Please be aware that enabling this mode can make the SMTChecker analysis much more
computationally costly.
An important part of this mode is that it is applied to contract types and high
level external calls to contracts, and not low level calls such as ``call`` and
``delegatecall``. The storage of an address is stored per contract type, and
the SMTChecker assumes that an externally called contract has the type of the
caller expression. Therefore, casting an ``address`` or a contract to
different contract types will yield different storage values and can give
unsound results if the assumptions are inconsistent, such as the example below:
.. code-block:: solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
contract D {
constructor(uint _x) { x = _x; }
uint public x;
function setX(uint _x) public { x = _x; }
}
contract E {
constructor() { x = 2; }
uint public x;
function setX(uint _x) public { x = _x; }
}
contract C {
function f() public {
address d = address(new D(42));
// `d` was deployed as `D`, so its `x` should be 42 now.
assert(D(d).x() == 42); // should hold
assert(D(d).x() == 43); // should fail
// E and D have the same interface, so the following
// call would also work at runtime.
// However, the change to `E(d)` is not reflected in `D(d)`.
E(d).setX(1024);
// Reading from `D(d)` now will show old values.
// The assertion below should fail at runtime,
// but succeeds in this mode's analysis (unsound).
assert(D(d).x() == 42);
// The assertion below should succeed at runtime,
// but fails in this mode's analysis (false positive).
assert(D(d).x() == 1024);
}
}
Due to the above, make sure that the trusted external calls to a certain
variable of ``address`` or ``contract`` type always have the same caller
expression type.
It is also helpful to cast the called contract's variable as the type of the
most derived type in case of inheritance.
.. code-block:: solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0;
interface Token {
function balanceOf(address _a) external view returns (uint);
function transfer(address _to, uint _amt) external;
}
contract TokenCorrect is Token {
mapping (address => uint) balance;
constructor(address _a, uint _b) {
balance[_a] = _b;
}
function balanceOf(address _a) public view override returns (uint) {
return balance[_a];
}
function transfer(address _to, uint _amt) public override {
require(balance[msg.sender] >= _amt);
balance[msg.sender] -= _amt;
balance[_to] += _amt;
}
}
contract Test {
function property_transfer(address _token, address _to, uint _amt) public {
require(_to != address(this));
TokenCorrect t = TokenCorrect(_token);
uint xPre = t.balanceOf(address(this));
require(xPre >= _amt);
uint yPre = t.balanceOf(_to);
t.transfer(_to, _amt);
uint xPost = t.balanceOf(address(this));
uint yPost = t.balanceOf(_to);
assert(xPost == xPre - _amt);
assert(yPost == yPre + _amt);
}
}
Note that in function ``property_transfer``, the external calls are
performed on variable ``t``
Another caveat of this mode are calls to state variables of contract type
outside the analyzed contract. In the code below, even though ``B`` deploys
``A``, it is also possible for the address stored in ``B.a`` to be called by
anyone outside of ``B`` in between transactions to ``B`` itself. To reflect the
possible changes to ``B.a``, the encoding allows an unbounded number of calls
to be made to ``B.a`` externally. The encoding will keep track of ``B.a``'s
storage, therefore assertion (2) should hold. However, currently the encoding
allows such calls to be made from ``B`` conceptually, therefore assertion (3)
fails. Making the encoding stronger logically is an extension of the trusted
mode and is under development. Note that the encoding does not keep track of
storage for ``address`` variables, therefore if ``B.a`` had type ``address``
the encoding would assume that its storage does not change in between
transactions to ``B``.
.. code-block:: solidity
pragma solidity >=0.8.0;
contract A {
uint public x;
address immutable public owner;
constructor() {
owner = msg.sender;
}
function setX(uint _x) public {
require(msg.sender == owner);
x = _x;
}
}
contract B {
A a;
constructor() {
a = new A();
assert(a.x() == 0); // (1) should hold
}
function g() public view {
assert(a.owner() == address(this)); // (2) should hold
assert(a.x() == 0); // (3) should hold, but fails due to a false positive
}
}
Reported Inferred Inductive Invariants
======================================
+4
View File
@@ -433,6 +433,10 @@ Input Description
"divModNoSlacks": false,
// Choose which model checker engine to use: all (default), bmc, chc, none.
"engine": "chc",
// Choose whether external calls should be considered trusted in case the
// code of the called function is available at compile-time.
// For details see the SMTChecker section.
"extCalls": "trusted",
// Choose which types of invariants should be reported to the user: contract, reentrancy.
"invariants": ["contract", "reentrancy"],
// Choose whether to output all unproved targets. The default is `false`.