mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
671 lines
29 KiB
ReStructuredText
671 lines
29 KiB
ReStructuredText
.. _security_considerations:
|
|
|
|
#######################
|
|
Security Considerations
|
|
#######################
|
|
|
|
While it is usually quite easy to build software that works as expected,
|
|
it is much harder to check that nobody can use it in a way that was **not** anticipated.
|
|
|
|
In Solidity, this is even more important because you can use smart contracts
|
|
to handle tokens or, possibly, even more valuable things. Furthermore, every
|
|
execution of a smart contract happens in public and, in addition to that,
|
|
the source code is often available.
|
|
|
|
Of course you always have to consider how much is at stake:
|
|
You can compare a smart contract with a web service that is open to the
|
|
public (and thus, also to malicious actors) and perhaps even open source.
|
|
If you only store your grocery list on that web service, you might not have
|
|
to take too much care, but if you manage your bank account using that web service,
|
|
you should be more careful.
|
|
|
|
This section will list some pitfalls and general security recommendations but
|
|
can, of course, never be complete. Also, keep in mind that even if your smart
|
|
contract code is bug-free, the compiler or the platform itself might have a
|
|
bug. A list of some publicly known security-relevant bugs of the compiler can
|
|
be found in the :ref:`list of known bugs<known_bugs>`, which is also
|
|
machine-readable. Note that there is a bug bounty program that covers the code
|
|
generator of the Solidity compiler.
|
|
|
|
As always, with open source documentation, please help us extend this section
|
|
(especially, some examples would not hurt)!
|
|
|
|
NOTE: In addition to the list below, you can find more security recommendations and best practices
|
|
`in Guy Lando's knowledge list <https://github.com/guylando/KnowledgeLists/blob/master/EthereumSmartContracts.md>`_ and
|
|
`the Consensys GitHub repo <https://consensys.github.io/smart-contract-best-practices/>`_.
|
|
|
|
********
|
|
Pitfalls
|
|
********
|
|
|
|
Private Information and Randomness
|
|
==================================
|
|
|
|
Everything you use in a smart contract is publicly visible, even
|
|
local variables and state variables marked ``private``.
|
|
|
|
Using random numbers in smart contracts is quite tricky if you do not want
|
|
miners to be able to cheat.
|
|
|
|
Re-Entrancy
|
|
===========
|
|
|
|
Any interaction from a contract (A) with another contract (B) and any transfer
|
|
of Ether hands over control to that contract (B). This makes it possible for B
|
|
to call back into A before this interaction is completed. To give an example,
|
|
the following code contains a bug (it is just a snippet and not a
|
|
complete contract):
|
|
|
|
::
|
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity >=0.4.0 <0.7.0;
|
|
|
|
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
|
|
contract Fund {
|
|
/// @dev Mapping of ether shares of the contract.
|
|
mapping(address => uint) shares;
|
|
/// Withdraw your share.
|
|
function withdraw() public {
|
|
if (msg.sender.send(shares[msg.sender]))
|
|
shares[msg.sender] = 0;
|
|
}
|
|
}
|
|
|
|
The problem is not too serious here because of the limited gas as part
|
|
of ``send``, but it still exposes a weakness: Ether transfer can always
|
|
include code execution, so the recipient could be a contract that calls
|
|
back into ``withdraw``. This would let it get multiple refunds and
|
|
basically retrieve all the Ether in the contract. In particular, the
|
|
following contract will allow an attacker to refund multiple times
|
|
as it uses ``call`` which forwards all remaining gas by default:
|
|
|
|
::
|
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity >=0.6.2 <0.7.0;
|
|
|
|
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
|
|
contract Fund {
|
|
/// @dev Mapping of ether shares of the contract.
|
|
mapping(address => uint) shares;
|
|
/// Withdraw your share.
|
|
function withdraw() public {
|
|
(bool success,) = msg.sender.call{value: shares[msg.sender]}("");
|
|
if (success)
|
|
shares[msg.sender] = 0;
|
|
}
|
|
}
|
|
|
|
To avoid re-entrancy, you can use the Checks-Effects-Interactions pattern as
|
|
outlined further below:
|
|
|
|
::
|
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity >=0.4.11 <0.7.0;
|
|
|
|
contract Fund {
|
|
/// @dev Mapping of ether shares of the contract.
|
|
mapping(address => uint) shares;
|
|
/// Withdraw your share.
|
|
function withdraw() public {
|
|
uint share = shares[msg.sender];
|
|
shares[msg.sender] = 0;
|
|
msg.sender.transfer(share);
|
|
}
|
|
}
|
|
|
|
Note that re-entrancy is not only an effect of Ether transfer but of any
|
|
function call on another contract. Furthermore, you also have to take
|
|
multi-contract situations into account. A called contract could modify the
|
|
state of another contract you depend on.
|
|
|
|
Gas Limit and Loops
|
|
===================
|
|
|
|
Loops that do not have a fixed number of iterations, for example, loops that depend on storage values, have to be used carefully:
|
|
Due to the block gas limit, transactions can only consume a certain amount of gas. Either explicitly or just due to
|
|
normal operation, the number of iterations in a loop can grow beyond the block gas limit which can cause the complete
|
|
contract to be stalled at a certain point. This may not apply to ``view`` functions that are only executed
|
|
to read data from the blockchain. Still, such functions may be called by other contracts as part of on-chain operations
|
|
and stall those. Please be explicit about such cases in the documentation of your contracts.
|
|
|
|
Sending and Receiving Ether
|
|
===========================
|
|
|
|
- Neither contracts nor "external accounts" are currently able to prevent that someone sends them Ether.
|
|
Contracts can react on and reject a regular transfer, but there are ways
|
|
to move Ether without creating a message call. One way is to simply "mine to"
|
|
the contract address and the second way is using ``selfdestruct(x)``.
|
|
|
|
- If a contract receives Ether (without a function being called),
|
|
either the :ref:`receive Ether <receive-ether-function>`
|
|
or the :ref:`fallback <fallback-function>` function is executed.
|
|
If it does not have a receive nor a fallback function, the Ether will be
|
|
rejected (by throwing an exception). During the execution of one of these
|
|
functions, the contract can only rely on the "gas stipend" it is passed (2300
|
|
gas) being available to it at that time. This stipend is not enough to modify
|
|
storage (do not take this for granted though, the stipend might change with
|
|
future hard forks). To be sure that your contract can receive Ether in that
|
|
way, check the gas requirements of the receive and fallback functions
|
|
(for example in the "details" section in Remix).
|
|
|
|
- There is a way to forward more gas to the receiving contract using
|
|
``addr.call{value: x}("")``. This is essentially the same as ``addr.transfer(x)``,
|
|
only that it forwards all remaining gas and opens up the ability for the
|
|
recipient to perform more expensive actions (and it returns a failure code
|
|
instead of automatically propagating the error). This might include calling back
|
|
into the sending contract or other state changes you might not have thought of.
|
|
So it allows for great flexibility for honest users but also for malicious actors.
|
|
|
|
- Use the most precise units to represent the wei amount as possible, as you lose
|
|
any that is rounded due to a lack of precision.
|
|
|
|
- If you want to send Ether using ``address.transfer``, there are certain details to be aware of:
|
|
|
|
1. If the recipient is a contract, it causes its receive or fallback function
|
|
to be executed which can, in turn, call back the sending contract.
|
|
2. Sending Ether can fail due to the call depth going above 1024. Since the
|
|
caller is in total control of the call depth, they can force the
|
|
transfer to fail; take this possibility into account or use ``send`` and
|
|
make sure to always check its return value. Better yet, write your
|
|
contract using a pattern where the recipient can withdraw Ether instead.
|
|
3. Sending Ether can also fail because the execution of the recipient
|
|
contract requires more than the allotted amount of gas (explicitly by
|
|
using :ref:`require <assert-and-require>`, :ref:`assert <assert-and-require>`,
|
|
:ref:`revert <assert-and-require>` or because the
|
|
operation is too expensive) - it "runs out of gas" (OOG). If you
|
|
use ``transfer`` or ``send`` with a return value check, this might
|
|
provide a means for the recipient to block progress in the sending
|
|
contract. Again, the best practice here is to use a :ref:`"withdraw"
|
|
pattern instead of a "send" pattern <withdrawal_pattern>`.
|
|
|
|
Callstack Depth
|
|
===============
|
|
|
|
External function calls can fail any time because they exceed the maximum
|
|
call stack of 1024. In such situations, Solidity throws an exception.
|
|
Malicious actors might be able to force the call stack to a high value
|
|
before they interact with your contract.
|
|
|
|
Note that ``.send()`` does **not** throw an exception if the call stack is
|
|
depleted but rather returns ``false`` in that case. The low-level functions
|
|
``.call()``, ``.delegatecall()`` and ``.staticcall()`` behave in the same way.
|
|
|
|
tx.origin
|
|
=========
|
|
|
|
Never use tx.origin for authorization. Let's say you have a wallet contract like this:
|
|
|
|
::
|
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity >=0.5.0 <0.7.0;
|
|
|
|
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
|
|
contract TxUserWallet {
|
|
address owner;
|
|
|
|
constructor() public {
|
|
owner = msg.sender;
|
|
}
|
|
|
|
function transferTo(address payable dest, uint amount) public {
|
|
require(tx.origin == owner);
|
|
dest.transfer(amount);
|
|
}
|
|
}
|
|
|
|
Now someone tricks you into sending Ether to the address of this attack wallet:
|
|
|
|
::
|
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity ^0.6.0;
|
|
|
|
interface TxUserWallet {
|
|
function transferTo(address payable dest, uint amount) external;
|
|
}
|
|
|
|
contract TxAttackWallet {
|
|
address payable owner;
|
|
|
|
constructor() public {
|
|
owner = msg.sender;
|
|
}
|
|
|
|
receive() external payable {
|
|
TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
|
|
}
|
|
}
|
|
|
|
If your wallet had checked ``msg.sender`` for authorization, it would get the address of the attack wallet, instead of the owner address. But by checking ``tx.origin``, it gets the original address that kicked off the transaction, which is still the owner address. The attack wallet instantly drains all your funds.
|
|
|
|
.. _underflow-overflow:
|
|
|
|
Two's Complement / Underflows / Overflows
|
|
=========================================
|
|
|
|
As in many programming languages, Solidity's integer types are not actually integers.
|
|
They resemble integers when the values are small, but behave differently if the numbers are larger.
|
|
For example, the following is true: ``uint8(255) + uint8(1) == 0``. This situation is called
|
|
an *overflow*. It occurs when an operation is performed that requires a fixed size variable
|
|
to store a number (or piece of data) that is outside the range of the variable's data type.
|
|
An *underflow* is the converse situation: ``uint8(0) - uint8(1) == 255``.
|
|
|
|
In general, read about the limits of two's complement representation, which even has some
|
|
more special edge cases for signed numbers.
|
|
|
|
Try to use ``require`` to limit the size of inputs to a reasonable range and use the
|
|
:ref:`SMT checker<smt_checker>` to find potential overflows, or use a library like
|
|
`SafeMath <https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol>`_
|
|
if you want all overflows to cause a revert.
|
|
|
|
Code such as ``require((balanceOf[_to] + _value) >= balanceOf[_to])`` can also help you check if values are what you expect.
|
|
|
|
.. _clearing-mappings:
|
|
|
|
Clearing Mappings
|
|
=================
|
|
|
|
The Solidity type ``mapping`` (see :ref:`mapping-types`) is a storage-only
|
|
key-value data structure that does not keep track of the keys that were
|
|
assigned a non-zero value. Because of that, cleaning a mapping without extra
|
|
information about the written keys is not possible.
|
|
If a ``mapping`` is used as the base type of a dynamic storage array, deleting
|
|
or popping the array will have no effect over the ``mapping`` elements. The
|
|
same happens, for example, if a ``mapping`` is used as the type of a member
|
|
field of a ``struct`` that is the base type of a dynamic storage array. The
|
|
``mapping`` is also ignored in assignments of structs or arrays containing a
|
|
``mapping``.
|
|
|
|
::
|
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity >=0.6.0 <0.7.0;
|
|
|
|
contract Map {
|
|
mapping (uint => uint)[] array;
|
|
|
|
function allocate(uint _newMaps) public {
|
|
for (uint i = 0; i < _newMaps; i++)
|
|
array.push();
|
|
}
|
|
|
|
function writeMap(uint _map, uint _key, uint _value) public {
|
|
array[_map][_key] = _value;
|
|
}
|
|
|
|
function readMap(uint _map, uint _key) public view returns (uint) {
|
|
return array[_map][_key];
|
|
}
|
|
|
|
function eraseMaps() public {
|
|
delete array;
|
|
}
|
|
}
|
|
|
|
Consider the example above and the following sequence of calls: ``allocate(10)``,
|
|
``writeMap(4, 128, 256)``.
|
|
At this point, calling ``readMap(4, 128)`` returns 256.
|
|
If we call ``eraseMaps``, the length of state variable ``array`` is zeroed, but
|
|
since its ``mapping`` elements cannot be zeroed, their information stays alive
|
|
in the contract's storage.
|
|
After deleting ``array``, calling ``allocate(5)`` allows us to access
|
|
``array[4]`` again, and calling ``readMap(4, 128)`` returns 256 even without
|
|
another call to ``writeMap``.
|
|
|
|
If your ``mapping`` information must be deleted, consider using a library similar to
|
|
`iterable mapping <https://github.com/ethereum/dapp-bin/blob/master/library/iterable_mapping.sol>`_,
|
|
allowing you to traverse the keys and delete their values in the appropriate ``mapping``.
|
|
|
|
Minor Details
|
|
=============
|
|
|
|
- Types that do not occupy the full 32 bytes might contain "dirty higher order bits".
|
|
This is especially important if you access ``msg.data`` - it poses a malleability risk:
|
|
You can craft transactions that call a function ``f(uint8 x)`` with a raw byte argument
|
|
of ``0xff000001`` and with ``0x00000001``. Both are fed to the contract and both will
|
|
look like the number ``1`` as far as ``x`` is concerned, but ``msg.data`` will
|
|
be different, so if you use ``keccak256(msg.data)`` for anything, you will get different results.
|
|
|
|
***************
|
|
Recommendations
|
|
***************
|
|
|
|
Take Warnings Seriously
|
|
=======================
|
|
|
|
If the compiler warns you about something, you should better change it.
|
|
Even if you do not think that this particular warning has security
|
|
implications, there might be another issue buried beneath it.
|
|
Any compiler warning we issue can be silenced by slight changes to the
|
|
code.
|
|
|
|
Always use the latest version of the compiler to be notified about all recently
|
|
introduced warnings.
|
|
|
|
Restrict the Amount of Ether
|
|
============================
|
|
|
|
Restrict the amount of Ether (or other tokens) that can be stored in a smart
|
|
contract. If your source code, the compiler or the platform has a bug, these
|
|
funds may be lost. If you want to limit your loss, limit the amount of Ether.
|
|
|
|
Keep it Small and Modular
|
|
=========================
|
|
|
|
Keep your contracts small and easily understandable. Single out unrelated
|
|
functionality in other contracts or into libraries. General recommendations
|
|
about source code quality of course apply: Limit the amount of local variables,
|
|
the length of functions and so on. Document your functions so that others
|
|
can see what your intention was and whether it is different than what the code does.
|
|
|
|
Use the Checks-Effects-Interactions Pattern
|
|
===========================================
|
|
|
|
Most functions will first perform some checks (who called the function,
|
|
are the arguments in range, did they send enough Ether, does the person
|
|
have tokens, etc.). These checks should be done first.
|
|
|
|
As the second step, if all checks passed, effects to the state variables
|
|
of the current contract should be made. Interaction with other contracts
|
|
should be the very last step in any function.
|
|
|
|
Early contracts delayed some effects and waited for external function
|
|
calls to return in a non-error state. This is often a serious mistake
|
|
because of the re-entrancy problem explained above.
|
|
|
|
Note that, also, calls to known contracts might in turn cause calls to
|
|
unknown contracts, so it is probably better to just always apply this pattern.
|
|
|
|
Include a Fail-Safe Mode
|
|
========================
|
|
|
|
While making your system fully decentralised will remove any intermediary,
|
|
it might be a good idea, especially for new code, to include some kind
|
|
of fail-safe mechanism:
|
|
|
|
You can add a function in your smart contract that performs some
|
|
self-checks like "Has any Ether leaked?",
|
|
"Is the sum of the tokens equal to the balance of the contract?" or similar things.
|
|
Keep in mind that you cannot use too much gas for that, so help through off-chain
|
|
computations might be needed there.
|
|
|
|
If the self-check fails, the contract automatically switches into some kind
|
|
of "failsafe" mode, which, for example, disables most of the features, hands over
|
|
control to a fixed and trusted third party or just converts the contract into
|
|
a simple "give me back my money" contract.
|
|
|
|
Ask for Peer Review
|
|
===================
|
|
|
|
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
|
|
*******************
|
|
|
|
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 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<smt_checker>`.
|
|
|
|
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.
|
|
|
|
While the SMTChecker encodes Solidity code into SMT constraints, it contains two
|
|
reasoning engines that use that encoding in different ways.
|
|
|
|
SMT Encoding
|
|
============
|
|
|
|
The SMT encoding tries to be as precise as possible, mapping Solidity types
|
|
and expressions to their closest `SMT-LIB <http://smtlib.cs.uiowa.edu/>`_
|
|
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, bytes, |Array |Arrays |
|
|
|string | | |
|
|
+-----------------------+--------------+-----------------------------+
|
|
|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.
|
|
|
|
For more details on how the SMT encoding works internally, see the paper
|
|
`SMT-based Verification of Solidity Smart Contracts <https://github.com/leonardoalt/text/blob/master/solidity_isola_2018/main.pdf>`_.
|
|
|
|
Model Checking Engines
|
|
======================
|
|
|
|
The SMTChecker module implements two different reasoning engines that use the
|
|
SMT encoding above, a Bounded Model Checker (BMC) and a system of Constrained
|
|
Horn Clauses (CHC). Both engines are currently under development, and have
|
|
different characteristics.
|
|
|
|
Bounded Model Checker (BMC)
|
|
---------------------------
|
|
|
|
The BMC engine analyzes functions in isolation, that is, it does not take the
|
|
overall behavior of the contract throughout many transactions into account when
|
|
analyzing each function. Loops are also ignored in this engine at the moment.
|
|
Internal function calls are inlined as long as they are not recursive, direct
|
|
or indirectly. External function calls are inlined if possible, and knowledge
|
|
that is potentially affected by reentrancy is erased.
|
|
|
|
The characteristics above make BMC easily prone to reporting false positives,
|
|
but it is also lightweight and should be able to quickly find small local bugs.
|
|
|
|
Constrained Horn Clauses (CHC)
|
|
------------------------------
|
|
|
|
The Solidity contract's Control Flow Graph (CFG) is modelled as a system of
|
|
Horn clauses, where the lifecycle of the contract is represented by a loop
|
|
that can visit every public/external function non-deterministically. This way,
|
|
the behavior of the entire contract over an unbounded number of transactions
|
|
is taken into account when analyzing any function. Loops are fully supported
|
|
by this engine. Internal function calls are supported, but external function
|
|
calls are currently unsupported.
|
|
|
|
The CHC engine is much more powerful than BMC in terms of what it can prove,
|
|
and might require more computing resources.
|
|
|
|
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).
|
|
|
|
In the BMC engine, 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.
|
|
|
|
The CHC engine creates nonlinear Horn clauses that use summaries of the called
|
|
functions to support internal function calls. The same approach can and will be
|
|
used for external function calls, but the latter requires more work regarding
|
|
the entire state of the blockchain and is still unimplemented.
|
|
|
|
Complex pure functions are abstracted by an uninterpreted function (UF) over
|
|
the arguments.
|
|
|
|
+-----------------------------------+--------------------------------------+
|
|
|Functions |SMT behavior |
|
|
+===================================+======================================+
|
|
|``assert`` |Verification target |
|
|
+-----------------------------------+--------------------------------------+
|
|
|``require`` |Assumption |
|
|
+-----------------------------------+--------------------------------------+
|
|
|internal |BMC: Inline function call |
|
|
| |CHC: Function summaries |
|
|
+-----------------------------------+--------------------------------------+
|
|
|external |BMC: Inline function call or |
|
|
| |erase knowledge about state variables |
|
|
| |and local storage references. |
|
|
| |CHC: Function summaries and erase |
|
|
| |state knowledge. |
|
|
+-----------------------------------+--------------------------------------+
|
|
|``gasleft``, ``blockhash``, |Abstracted with UF |
|
|
|``keccak256``, ``ecrecover`` | |
|
|
|``ripemd160``, ``addmod``, | |
|
|
|``mulmod`` | |
|
|
+-----------------------------------+--------------------------------------+
|
|
|pure functions without |Abstracted with UF |
|
|
|implementation (external or | |
|
|
|complex) | |
|
|
+-----------------------------------+--------------------------------------+
|
|
|external functions without |BMC: Unsupported |
|
|
|implementation |CHC: Nondeterministic summary |
|
|
+-----------------------------------+--------------------------------------+
|
|
|others |Currently unsupported |
|
|
+-----------------------------------+--------------------------------------+
|
|
|
|
Using abstraction means loss of precise knowledge, but in many cases it does
|
|
not mean loss of proving power.
|
|
|
|
::
|
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity >=0.5.0;
|
|
pragma experimental SMTChecker;
|
|
// This may report a warning if no SMT solver available.
|
|
|
|
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<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.
|
|
|
|
::
|
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
pragma solidity >=0.5.0;
|
|
pragma experimental SMTChecker;
|
|
// This will report a warning
|
|
|
|
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.
|
|
|
|
Real World Assumptions
|
|
======================
|
|
|
|
Some scenarios can be expressed in Solidity and the EVM, but are expected to
|
|
never occur in practice.
|
|
One of such cases is the length of a dynamic storage array overflowing during a
|
|
push: If the ``push`` operation is applied to an array of length 2^256 - 1, its
|
|
length silently overflows.
|
|
However, this is unlikely to happen in practice, since the operations required
|
|
to grow the array to that point would take billions of years to execute.
|