Fix merge conflicts

state_merge
Nikhil 5 years ago
commit 9373c71703
  1. 2
      .circleci/config.yml
  2. 43
      README.md
  3. 16
      all_tests.sh
  4. 1
      docs/source/index.rst
  5. 6
      docs/source/module-list.rst
  6. 63
      docs/source/mythx-analysis.rst
  7. 2
      docs/source/security-analysis.rst
  8. 2
      mythril/__version__.py
  9. 30
      mythril/analysis/analysis_args.py
  10. 5
      mythril/analysis/call_helpers.py
  11. 14
      mythril/analysis/modules/base.py
  12. 160
      mythril/analysis/modules/delegatecall.py
  13. 6
      mythril/analysis/modules/dependence_on_predictable_vars.py
  14. 34
      mythril/analysis/modules/deprecated_ops.py
  15. 5
      mythril/analysis/modules/dos.py
  16. 113
      mythril/analysis/modules/ether_thief.py
  17. 4
      mythril/analysis/modules/exceptions.py
  18. 39
      mythril/analysis/modules/external_calls.py
  19. 6
      mythril/analysis/modules/integer.py
  20. 6
      mythril/analysis/modules/multiple_sends.py
  21. 35
      mythril/analysis/modules/state_change_external_calls.py
  22. 12
      mythril/analysis/modules/suicide.py
  23. 14
      mythril/analysis/modules/unchecked_retval.py
  24. 100
      mythril/analysis/modules/user_assertions.py
  25. 108
      mythril/analysis/potential_issues.py
  26. 28
      mythril/analysis/report.py
  27. 65
      mythril/analysis/security.py
  28. 66
      mythril/analysis/solver.py
  29. 31
      mythril/analysis/symbolic.py
  30. 16
      mythril/analysis/templates/report_as_markdown.jinja2
  31. 14
      mythril/analysis/templates/report_as_text.jinja2
  32. 6
      mythril/disassembler/disassembly.py
  33. 94
      mythril/ethereum/util.py
  34. 161
      mythril/interfaces/cli.py
  35. 18
      mythril/interfaces/old_cli.py
  36. 57
      mythril/laser/ethereum/call.py
  37. 12
      mythril/laser/ethereum/evm_exceptions.py
  38. 5
      mythril/laser/ethereum/gas.py
  39. 581
      mythril/laser/ethereum/instructions.py
  40. 168
      mythril/laser/ethereum/natives.py
  41. 94
      mythril/laser/ethereum/plugins/implementations/dependency_pruner.py
  42. 21
      mythril/laser/ethereum/plugins/implementations/mutation_pruner.py
  43. 65
      mythril/laser/ethereum/plugins/implementations/plugin_annotations.py
  44. 56
      mythril/laser/ethereum/state/account.py
  45. 2
      mythril/laser/ethereum/state/annotation.py
  46. 2
      mythril/laser/ethereum/state/calldata.py
  47. 4
      mythril/laser/ethereum/state/environment.py
  48. 13
      mythril/laser/ethereum/state/global_state.py
  49. 4
      mythril/laser/ethereum/state/world_state.py
  50. 63
      mythril/laser/ethereum/svm.py
  51. 6
      mythril/laser/ethereum/transaction/symbolic.py
  52. 46
      mythril/laser/ethereum/transaction/transaction_models.py
  53. 26
      mythril/laser/ethereum/util.py
  54. 17
      mythril/laser/smt/__init__.py
  55. 3
      mythril/laser/smt/array.py
  56. 297
      mythril/laser/smt/bitvec.py
  57. 291
      mythril/laser/smt/bitvec_helper.py
  58. 68
      mythril/laser/smt/bitvecfunc.py
  59. 26
      mythril/mythril/mythril_analyzer.py
  60. 53
      mythril/mythril/mythril_disassembler.py
  61. 111
      mythril/mythx/__init__.py
  62. 79
      mythril/solidity/soliditycontract.py
  63. 52
      mythril/support/loader.py
  64. 47
      mythril/support/signatures.py
  65. 8
      requirements.txt
  66. 8
      setup.py
  67. 6
      tests/disassembler_test.py
  68. 72
      tests/instructions/create_test.py
  69. 48
      tests/instructions/extcodehash_test.py
  70. 124
      tests/instructions/static_call_test.py
  71. 27
      tests/laser/Precompiles/test_ec_add.py
  72. 35
      tests/laser/Precompiles/test_elliptic_curves.py
  73. 27
      tests/laser/Precompiles/test_elliptic_mul.py
  74. 61
      tests/laser/Precompiles/test_mod_exp.py
  75. 4
      tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicide0.json
  76. 4
      tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideNotExistingAccount.json
  77. 4
      tests/laser/evm_testsuite/VMTests/vmSystemOperations/suicideSendEtherToMe.json
  78. 165
      tests/laser/smt/bitvecfunc_test.py
  79. 2
      tests/laser/transaction/create_transaction_test.py
  80. 2
      tests/native_test.py
  81. 2
      tests/native_tests.sol
  82. 2
      tox.ini

@ -103,6 +103,8 @@ jobs:
name: Run Edelweiss name: Run Edelweiss
command: | command: |
docker run \ docker run \
-e CIRCLE_BRANCH=$CIRCLE_BRANCH \
-e CIRCLE_SHA1=$CIRCLE_SHA1 \
-e CIRCLE_BUILD_NUM=$CIRCLE_BUILD_NUM \ -e CIRCLE_BUILD_NUM=$CIRCLE_BUILD_NUM \
-e CIRCLE_BUILD_URL=$CIRCLE_BUILD_URL \ -e CIRCLE_BUILD_URL=$CIRCLE_BUILD_URL \
-e CIRCLE_WEBHOOK_URL=$CIRCLE_WEBHOOK_URL \ -e CIRCLE_WEBHOOK_URL=$CIRCLE_WEBHOOK_URL \

@ -32,11 +32,50 @@ Install from Pypi:
$ pip3 install mythril $ pip3 install mythril
``` ```
See the [Wiki](https://github.com/ConsenSys/mythril/wiki/Installation-and-Setup) for more detailed instructions. See the [docs](https://mythril-classic.readthedocs.io/en/master/installation.html) for more detailed instructions.
## Usage ## Usage
Instructions for using Mythril are found on the [Wiki](https://github.com/ConsenSys/mythril/wiki). Run:
```
$ myth analyze <solidity-file>
```
Or:
```
$ myth analyze -a <contract-address>
```
Specify the maximum number of transaction to explore with `-t <number>`. You can also set a timeout with `--execution-timeout <seconds>`. Example ([source code](https://gist.github.com/b-mueller/2b251297ce88aa7628680f50f177a81a#file-killbilly-sol)):
```
==== Unprotected Selfdestruct ====
SWC ID: 106
Severity: High
Contract: KillBilly
Function name: commencekilling()
PC address: 354
Estimated Gas Usage: 574 - 999
The contract can be killed by anyone.
Anyone can kill this contract and withdraw its balance to an arbitrary address.
--------------------
In file: killbilly.sol:22
selfdestruct(msg.sender)
--------------------
Transaction Sequence:
Caller: [CREATOR], data: [CONTRACT CREATION], value: 0x0
Caller: [ATTACKER], function: killerize(address), txdata: 0x9fa299ccbebebebebebebebebebebebedeadbeefdeadbeefdeadbeefdeadbeefdeadbeef, value: 0x0
Caller: [ATTACKER], function: activatekillability(), txdata: 0x84057065, value: 0x0
Caller: [ATTACKER], function: commencekilling(), txdata: 0x7c11da20, value: 0x0
```
Instructions for using Mythril are found on the [docs](https://mythril-classic.readthedocs.io/en/master/).
For support or general discussions please join the Mythril community on [Discord](https://discord.gg/E3YrVtG). For support or general discussions please join the Mythril community on [Discord](https://discord.gg/E3YrVtG).

@ -7,22 +7,6 @@ assert sys.version_info[0:2] >= (3,5), \
"""Please make sure you are using Python 3.5 or later. """Please make sure you are using Python 3.5 or later.
You ran with {}""".format(sys.version)' || exit $? You ran with {}""".format(sys.version)' || exit $?
echo "Checking solc version..."
out=$(solc --version) || {
echo 2>&1 "Please make sure you have solc installed, version 0.4.21 or greater"
}
case $out in
*Version:\ 0.4.2[1-9]* )
echo $out
;;
* )
echo $out
echo "Please make sure your solc version is at least 0.4.21"
exit 1
;;
esac
echo "Checking that truffle is installed..." echo "Checking that truffle is installed..."
if ! which truffle ; then if ! which truffle ; then
echo "Please make sure you have etherum truffle installed (npm install -g truffle)" echo "Please make sure you have etherum truffle installed (npm install -g truffle)"

@ -9,6 +9,7 @@ Welcome to Mythril's documentation!
installation installation
security-analysis security-analysis
analysis-modules analysis-modules
mythx-analysis
mythril mythril

@ -67,3 +67,9 @@ Unchecked Retval
**************** ****************
The `unchecked retval module <https://github.com/ConsenSys/mythril/blob/develop/mythril/analysis/modules/unchecked_retval.py>`_ detects `SWC-104 (Unchecked Call Return Value) <https://smartcontractsecurity.github.io/SWC-registry/docs/SWC-104>`_. The `unchecked retval module <https://github.com/ConsenSys/mythril/blob/develop/mythril/analysis/modules/unchecked_retval.py>`_ detects `SWC-104 (Unchecked Call Return Value) <https://smartcontractsecurity.github.io/SWC-registry/docs/SWC-104>`_.
****************
Unchecked Retval
****************
The `user supplied assertion module <https://github.com/ConsenSys/mythril/blob/develop/mythril/analysis/modules/unchecked_retval.py>`_ detects `SWC-110 (Assert Violation) <https://smartcontractsecurity.github.io/SWC-registry/docs/SWC-110>`_ for user-supplied assertions. User supplied assertions should be log messages of the form: :code:`emit AssertionFailed(string)`.

@ -0,0 +1,63 @@
MythX Analysis
=================
Run :code:`myth pro` with one of the input options described below will run a `MythX analysis <https://mythx.io>`_ on the desired input. This includes a run of Mythril, the fuzzer Harvey, and the static analysis engine Maru and has some false-positive filtering only possible by combining the tool capabilities.
**************
Authentication
**************
In order to authenticate with the MythX API, set the environment variables ``MYTHX_PASSWORD`` and ``MYTHX_ETH_ADDRESS``.
.. code-block:: bash
$ export MYTHX_ETH_ADDRESS='0x0000000000000000000000000000000000000000'
$ export MYTHX_PASSWORD='password'
***********************
Analyzing Solidity Code
***********************
The input format is the same as a regular Mythril analysis.
.. code-block:: bash
$ myth pro ether_send.sol
==== Unprotected Ether Withdrawal ====
SWC ID: 105
Severity: High
Contract: Crowdfunding
Function name: withdrawfunds()
PC address: 730
Anyone can withdraw ETH from the contract account.
Arbitrary senders other than the contract creator can withdraw ETH from the contract account without previously having sent an equivalent amount of ETH to it. This is likely to be a vulnerability.
--------------------
In file: tests/testdata/input_contracts/ether_send.sol:21
msg.sender.transfer(address(this).balance)
--------------------
If an input file contains multiple contract definitions, Mythril analyzes the *last* bytecode output produced by solc. You can override this by specifying the contract name explicitly:
.. code-block:: bash
myth pro OmiseGo.sol:OMGToken
To specify a contract address, use :code:`-a <address>`
****************************
Analyzing On-Chain Contracts
****************************
Analyzing a mainnet contract via INFURA:
.. code-block:: bash
myth pro -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd
Adding the :code:`-l` flag will cause mythril to automatically retrieve dependencies, such as dynamically linked library contracts:
.. code-block:: bash
myth -v4 pro -l -a 0xEbFD99838cb0c132016B9E117563CB41f2B02264

@ -1,7 +1,7 @@
Security Analysis Security Analysis
================= =================
Run :code:`myth -x` with one of the input options described below will run the analysis modules in the `/analysis/modules <https://github.com/ConsenSys/mythril/tree/master/mythril/analysis/modules>`_ directory. Run :code:`myth analyze` with one of the input options described below will run the analysis modules in the `/analysis/modules <https://github.com/ConsenSys/mythril/tree/master/mythril/analysis/modules>`_ directory.
*********************** ***********************
Analyzing Solidity Code Analyzing Solidity Code

@ -4,4 +4,4 @@ This file is suitable for sourcing inside POSIX shell, e.g. bash as well
as for importing into Python. as for importing into Python.
""" """
__version__ = "v0.21.8" __version__ = "v0.21.17"

@ -0,0 +1,30 @@
from mythril.support.support_utils import Singleton
class AnalysisArgs(object, metaclass=Singleton):
"""
This module helps in preventing args being sent through multiple of classes to reach analysis modules
"""
def __init__(self):
self._loop_bound = 3
self._solver_timeout = 10000
def set_loop_bound(self, loop_bound: int):
if loop_bound is not None:
self._loop_bound = loop_bound
def set_solver_timeout(self, solver_timeout: int):
if solver_timeout is not None:
self._solver_timeout = solver_timeout
@property
def loop_bound(self):
return self._loop_bound
@property
def solver_timeout(self):
return self._solver_timeout
analysis_args = AnalysisArgs()

@ -4,6 +4,7 @@ from typing import Union
from mythril.analysis.ops import VarType, Call, get_variable from mythril.analysis.ops import VarType, Call, get_variable
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.natives import PRECOMPILE_COUNT
def get_call_from_state(state: GlobalState) -> Union[Call, None]: def get_call_from_state(state: GlobalState) -> Union[Call, None]:
@ -17,7 +18,7 @@ def get_call_from_state(state: GlobalState) -> Union[Call, None]:
op = instruction["opcode"] op = instruction["opcode"]
stack = state.mstate.stack stack = state.mstate.stack
if op in ("CALL", "CALLCODE", "STATICCALL"): if op in ("CALL", "CALLCODE"):
gas, to, value, meminstart, meminsz, memoutstart, memoutsz = ( gas, to, value, meminstart, meminsz, memoutstart, memoutsz = (
get_variable(stack[-1]), get_variable(stack[-1]),
get_variable(stack[-2]), get_variable(stack[-2]),
@ -28,7 +29,7 @@ def get_call_from_state(state: GlobalState) -> Union[Call, None]:
get_variable(stack[-7]), get_variable(stack[-7]),
) )
if to.type == VarType.CONCRETE and 0 < to.val < 5: if to.type == VarType.CONCRETE and 0 < to.val <= PRECOMPILE_COUNT:
return None return None
if meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE: if meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE:

@ -3,6 +3,7 @@ modules."""
import logging import logging
from typing import List, Set from typing import List, Set
from mythril.analysis.report import Issue from mythril.analysis.report import Issue
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -34,21 +35,14 @@ class DetectionModule:
self.name, self.name,
) )
self.entrypoint = entrypoint self.entrypoint = entrypoint
self._issues = [] # type: List[Issue] self.issues = [] # type: List[Issue]
self._cache = set() # type: Set[int] self.cache = set() # type: Set[int]
@property
def issues(self):
"""
Returns the issues
"""
return self._issues
def reset_module(self): def reset_module(self):
""" """
Resets issues Resets issues
""" """
self._issues = [] self.issues = []
def execute(self, statespace) -> None: def execute(self, statespace) -> None:
"""The entry point for execution, which is being called by Mythril. """The entry point for execution, which is being called by Mythril.

@ -1,79 +1,24 @@
"""This module contains the detection code for insecure delegate call usage.""" """This module contains the detection code for insecure delegate call usage."""
import json
import logging import logging
from copy import copy from typing import List
from typing import List, cast, Dict
from mythril.analysis import solver from mythril.analysis.potential_issues import (
get_potential_issues_annotation,
PotentialIssue,
)
from mythril.analysis.swc_data import DELEGATECALL_TO_UNTRUSTED_CONTRACT from mythril.analysis.swc_data import DELEGATECALL_TO_UNTRUSTED_CONTRACT
from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS
from mythril.laser.ethereum.transaction.transaction_models import ( from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction, ContractCreationTransaction,
) )
from mythril.analysis.report import Issue
from mythril.analysis.modules.base import DetectionModule from mythril.analysis.modules.base import DetectionModule
from mythril.exceptions import UnsatError from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import symbol_factory, UGT from mythril.laser.smt import symbol_factory, UGT
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class DelegateCallAnnotation(StateAnnotation):
def __init__(self, call_state: GlobalState, constraints: List) -> None:
"""
Initialize DelegateCall Annotation
:param call_state: Call state
"""
self.call_state = call_state
self.constraints = constraints
self.return_value = call_state.new_bitvec(
"retval_{}".format(call_state.get_current_instruction()["address"]), 256
)
def _copy__(self):
return DelegateCallAnnotation(self.call_state, copy(self.constraints))
def get_issue(self, global_state: GlobalState, transaction_sequence: Dict) -> Issue:
"""
Returns Issue for the annotation
:param global_state: Global State
:param transaction_sequence: Transaction sequence
:return: Issue
"""
address = self.call_state.get_current_instruction()["address"]
logging.debug(
"[DELEGATECALL] Detected delegatecall to a user-supplied address : {}".format(
address
)
)
description_head = "The contract delegates execution to another contract with a user-supplied address."
description_tail = (
"The smart contract delegates execution to a user-supplied address. Note that callers "
"can execute arbitrary contracts and that the callee contract "
"can access the storage of the calling contract. "
)
return Issue(
contract=self.call_state.environment.active_account.contract_name,
function_name=self.call_state.environment.active_function_name,
address=address,
swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT,
title="Delegatecall Proxy To User-Supplied Address",
bytecode=global_state.environment.code.bytecode,
severity="Medium",
description_head=description_head,
description_tail=description_tail,
transaction_sequence=transaction_sequence,
gas_used=(
global_state.mstate.min_gas_used,
global_state.mstate.max_gas_used,
),
)
class DelegateCallModule(DetectionModule): class DelegateCallModule(DetectionModule):
"""This module detects calldata being forwarded using DELEGATECALL.""" """This module detects calldata being forwarded using DELEGATECALL."""
@ -84,7 +29,7 @@ class DelegateCallModule(DetectionModule):
swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT, swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT,
description="Check for invocations of delegatecall(msg.data) in the fallback function.", description="Check for invocations of delegatecall(msg.data) in the fallback function.",
entrypoint="callback", entrypoint="callback",
pre_hooks=["DELEGATECALL", "RETURN", "STOP"], pre_hooks=["DELEGATECALL"],
) )
def _execute(self, state: GlobalState) -> None: def _execute(self, state: GlobalState) -> None:
@ -93,63 +38,68 @@ class DelegateCallModule(DetectionModule):
:param state: :param state:
:return: :return:
""" """
if state.get_current_instruction()["address"] in self._cache: if state.get_current_instruction()["address"] in self.cache:
return return
issues = self._analyze_state(state) potential_issues = self._analyze_state(state)
for issue in issues:
self._cache.add(issue.address) annotation = get_potential_issues_annotation(state)
self._issues.extend(issues) annotation.potential_issues.extend(potential_issues)
@staticmethod def _analyze_state(self, state: GlobalState) -> List[PotentialIssue]:
def _analyze_state(state: GlobalState) -> List[Issue]:
""" """
:param state: the current state :param state: the current state
:return: returns the issues for that corresponding state :return: returns the issues for that corresponding state
""" """
issues = [] gas = state.mstate.stack[-1]
op_code = state.get_current_instruction()["opcode"] to = state.mstate.stack[-2]
annotations = cast(
List[DelegateCallAnnotation], constraints = [
list(state.get_annotations(DelegateCallAnnotation)), to == ATTACKER_ADDRESS,
) UGT(gas, symbol_factory.BitVecVal(2300, 256)),
state.new_bitvec(
"retval_{}".format(state.get_current_instruction()["address"]), 256
)
== 1,
]
if len(annotations) == 0 and op_code in ("RETURN", "STOP"): for tx in state.world_state.transaction_sequence:
return [] if not isinstance(tx, ContractCreationTransaction):
constraints.append(tx.caller == ATTACKER_ADDRESS)
if op_code == "DELEGATECALL": try:
gas = state.mstate.stack[-1] address = state.get_current_instruction()["address"]
to = state.mstate.stack[-2]
constraints = [ logging.debug(
to == ATTACKER_ADDRESS, "[DELEGATECALL] Detected potential delegatecall to a user-supplied address : {}".format(
UGT(gas, symbol_factory.BitVecVal(2300, 256)), address
] )
)
for tx in state.world_state.transaction_sequence: description_head = "The contract delegates execution to another contract with a user-supplied address."
if not isinstance(tx, ContractCreationTransaction): description_tail = (
constraints.append(tx.caller == ATTACKER_ADDRESS) "The smart contract delegates execution to a user-supplied address. Note that callers "
"can execute arbitrary contracts and that the callee contract "
"can access the storage of the calling contract. "
)
state.annotate(DelegateCallAnnotation(state, constraints)) return [
PotentialIssue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=address,
swc_id=DELEGATECALL_TO_UNTRUSTED_CONTRACT,
bytecode=state.environment.code.bytecode,
title="Delegatecall Proxy To User-Supplied Address",
severity="Medium",
description_head=description_head,
description_tail=description_tail,
constraints=constraints,
detector=self,
)
]
except UnsatError:
return [] return []
else:
for annotation in annotations:
try:
transaction_sequence = solver.get_transaction_sequence(
state,
state.mstate.constraints
+ annotation.constraints
+ [annotation.return_value == 1],
)
issues.append(
annotation.get_issue(
state, transaction_sequence=transaction_sequence
)
)
except UnsatError:
continue
return issues
detector = DelegateCallModule() detector = DelegateCallModule()

@ -74,12 +74,12 @@ class PredictableDependenceModule(DetectionModule):
:param state: :param state:
:return: :return:
""" """
if state.get_current_instruction()["address"] in self._cache: if state.get_current_instruction()["address"] in self.cache:
return return
issues = self._analyze_state(state) issues = self._analyze_state(state)
for issue in issues: for issue in issues:
self._cache.add(issue.address) self.cache.add(issue.address)
self._issues.extend(issues) self.issues.extend(issues)
@staticmethod @staticmethod
def _analyze_state(state: GlobalState) -> list: def _analyze_state(state: GlobalState) -> list:

@ -1,6 +1,8 @@
"""This module contains the detection code for deprecated op codes.""" """This module contains the detection code for deprecated op codes."""
from mythril.analysis.report import Issue from mythril.analysis.potential_issues import (
from mythril.analysis.solver import get_transaction_sequence, UnsatError PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import DEPRECATED_FUNCTIONS_USAGE from mythril.analysis.swc_data import DEPRECATED_FUNCTIONS_USAGE
from mythril.analysis.modules.base import DetectionModule from mythril.analysis.modules.base import DetectionModule
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
@ -32,15 +34,14 @@ class DeprecatedOperationsModule(DetectionModule):
:param state: :param state:
:return: :return:
""" """
if state.get_current_instruction()["address"] in self._cache: if state.get_current_instruction()["address"] in self.cache:
return return
issues = self._analyze_state(state) issues = self._analyze_state(state)
for issue in issues:
self._cache.add(issue.address)
self._issues.extend(issues)
@staticmethod annotation = get_potential_issues_annotation(state)
def _analyze_state(state): annotation.potential_issues.extend(issues)
def _analyze_state(self, state):
""" """
:param state: :param state:
@ -74,27 +75,22 @@ class DeprecatedOperationsModule(DetectionModule):
) )
swc_id = DEPRECATED_FUNCTIONS_USAGE swc_id = DEPRECATED_FUNCTIONS_USAGE
else: else:
return return []
try:
transaction_sequence = get_transaction_sequence( potential_issue = PotentialIssue(
state, state.mstate.constraints
)
except UnsatError:
return
issue = Issue(
contract=state.environment.active_account.contract_name, contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name, function_name=state.environment.active_function_name,
address=instruction["address"], address=instruction["address"],
title=title, title=title,
bytecode=state.environment.code.bytecode, bytecode=state.environment.code.bytecode,
detector=self,
swc_id=swc_id, swc_id=swc_id,
severity="Medium", severity="Medium",
description_head=description_head, description_head=description_head,
description_tail=description_tail, description_tail=description_tail,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), constraints=[],
transaction_sequence=transaction_sequence,
) )
return [issue] return [potential_issue]
detector = DeprecatedOperationsModule() detector = DeprecatedOperationsModule()

@ -7,6 +7,7 @@ from mythril.analysis.swc_data import DOS_WITH_BLOCK_GAS_LIMIT
from mythril.analysis.report import Issue from mythril.analysis.report import Issue
from mythril.analysis.modules.base import DetectionModule from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.solver import get_transaction_sequence, UnsatError from mythril.analysis.solver import get_transaction_sequence, UnsatError
from mythril.analysis.analysis_args import analysis_args
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.annotation import StateAnnotation from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum import util from mythril.laser.ethereum import util
@ -55,7 +56,7 @@ class DosModule(DetectionModule):
:return: :return:
""" """
issues = self._analyze_state(state) issues = self._analyze_state(state)
self._issues.extend(issues) self.issues.extend(issues)
def _analyze_state(self, state: GlobalState) -> List[Issue]: def _analyze_state(self, state: GlobalState) -> List[Issue]:
""" """
@ -90,7 +91,7 @@ class DosModule(DetectionModule):
else: else:
annotation.jump_targets[target] = 1 annotation.jump_targets[target] = 1
if annotation.jump_targets[target] > 2: if annotation.jump_targets[target] > min(2, analysis_args.loop_bound - 1):
annotation.loop_start = address annotation.loop_start = address
elif annotation.loop_start is not None: elif annotation.loop_start is not None:

@ -1,28 +1,28 @@
"""This module contains the detection code for unauthorized ether """This module contains the detection code for unauthorized ether
withdrawal.""" withdrawal."""
import logging import logging
import json
from copy import copy from copy import copy
from mythril.analysis import solver
from mythril.analysis.modules.base import DetectionModule from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.report import Issue from mythril.analysis.potential_issues import (
get_potential_issues_annotation,
PotentialIssue,
)
from mythril.laser.ethereum.transaction.symbolic import ( from mythril.laser.ethereum.transaction.symbolic import (
ATTACKER_ADDRESS, ATTACKER_ADDRESS,
CREATOR_ADDRESS, CREATOR_ADDRESS,
) )
from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.transaction import ContractCreationTransaction
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import UGT, Sum, symbol_factory, BVAddNoOverflow from mythril.laser.ethereum.transaction import ContractCreationTransaction
from mythril.laser.smt.bitvec import If
from mythril.laser.smt import UGT, symbol_factory, UGE
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
DESCRIPTION = """ DESCRIPTION = """
Search for cases where Ether can be withdrawn to a user-specified address. Search for cases where Ether can be withdrawn to a user-specified address.
An issue is reported if: An issue is reported if:
@ -61,90 +61,77 @@ class EtherThief(DetectionModule):
:param state: :param state:
:return: :return:
""" """
if state.get_current_instruction()["address"] in self._cache: if state.get_current_instruction()["address"] in self.cache:
return return
issues = self._analyze_state(state) potential_issues = self._analyze_state(state)
for issue in issues:
self._cache.add(issue.address)
self._issues.extend(issues)
@staticmethod annotation = get_potential_issues_annotation(state)
def _analyze_state(state): annotation.potential_issues.extend(potential_issues)
def _analyze_state(self, state):
""" """
:param state: :param state:
:return: :return:
""" """
state = copy(state)
instruction = state.get_current_instruction() instruction = state.get_current_instruction()
if instruction["opcode"] != "CALL":
return []
address = instruction["address"]
value = state.mstate.stack[-3] value = state.mstate.stack[-3]
target = state.mstate.stack[-2] target = state.mstate.stack[-2]
eth_sent_by_attacker = symbol_factory.BitVecVal(0, 256)
constraints = copy(state.mstate.constraints) constraints = copy(state.mstate.constraints)
"""
Require that the current transaction is sent by the attacker and
that the Ether sent to the attacker's address is greater than the
amount of Ether the attacker sent.
"""
for tx in state.world_state.transaction_sequence: for tx in state.world_state.transaction_sequence:
"""
Constraint: The call value must be greater than the sum of Ether sent by the attacker over all
transactions. This prevents false positives caused by legitimate refund functions.
Also constrain the addition from overflowing (otherwise the solver produces solutions with
ridiculously high call values).
"""
constraints += [BVAddNoOverflow(eth_sent_by_attacker, tx.call_value, False)]
eth_sent_by_attacker = Sum(
eth_sent_by_attacker,
tx.call_value * If(tx.caller == ATTACKER_ADDRESS, 1, 0),
)
""" """
Constraint: All transactions must originate from regular users (not the creator/owner). Constraint: All transactions must originate from regular users (not the creator/owner).
This prevents false positives where the owner willingly transfers ownership to another address. This prevents false positives where the owner willingly transfers ownership to another address.
""" """
if not isinstance(tx, ContractCreationTransaction): if not isinstance(tx, ContractCreationTransaction):
constraints += [tx.caller != CREATOR_ADDRESS] constraints += [tx.caller != CREATOR_ADDRESS]
""" attacker_address_bitvec = symbol_factory.BitVecVal(ATTACKER_ADDRESS, 256)
Require that the current transaction is sent by the attacker and
that the Ether is sent to the attacker's address.
"""
constraints += [ constraints += [
UGT(value, eth_sent_by_attacker), UGE(
state.world_state.balances[state.environment.active_account.address],
value,
)
]
state.world_state.balances[attacker_address_bitvec] += value
state.world_state.balances[state.environment.active_account.address] -= value
constraints += [
UGT(
state.world_state.balances[attacker_address_bitvec],
state.world_state.starting_balances[attacker_address_bitvec],
),
target == ATTACKER_ADDRESS, target == ATTACKER_ADDRESS,
state.current_transaction.caller == ATTACKER_ADDRESS, state.current_transaction.caller == ATTACKER_ADDRESS,
] ]
try: potential_issue = PotentialIssue(
contract=state.environment.active_account.contract_name,
transaction_sequence = solver.get_transaction_sequence(state, constraints) function_name=state.environment.active_function_name,
address=instruction["address"],
issue = Issue( swc_id=UNPROTECTED_ETHER_WITHDRAWAL,
contract=state.environment.active_account.contract_name, title="Unprotected Ether Withdrawal",
function_name=state.environment.active_function_name, severity="High",
address=instruction["address"], bytecode=state.environment.code.bytecode,
swc_id=UNPROTECTED_ETHER_WITHDRAWAL, description_head="Anyone can withdraw ETH from the contract account.",
title="Unprotected Ether Withdrawal", description_tail="Arbitrary senders other than the contract creator can withdraw ETH from the contract"
severity="High", + " account without previously having sent an equivalent amount of ETH to it. This is likely to be"
bytecode=state.environment.code.bytecode, + " a vulnerability.",
description_head="Anyone can withdraw ETH from the contract account.", detector=self,
description_tail="Arbitrary senders other than the contract creator can withdraw ETH from the contract" constraints=constraints,
+ " account without previously having sent an equivalent amount of ETH to it. This is likely to be" )
+ " a vulnerability.",
transaction_sequence=transaction_sequence,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
except UnsatError:
log.debug("[ETHER_THIEF] no model found")
return []
return [issue] return [potential_issue]
detector = EtherThief() detector = EtherThief()

@ -33,8 +33,8 @@ class ReachableExceptionsModule(DetectionModule):
""" """
issues = self._analyze_state(state) issues = self._analyze_state(state)
for issue in issues: for issue in issues:
self._cache.add(issue.address) self.cache.add(issue.address)
self._issues.extend(issues) self.issues.extend(issues)
@staticmethod @staticmethod
def _analyze_state(state) -> list: def _analyze_state(state) -> list:

@ -2,19 +2,23 @@
calls.""" calls."""
from mythril.analysis import solver from mythril.analysis import solver
from mythril.analysis.potential_issues import (
PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import REENTRANCY from mythril.analysis.swc_data import REENTRANCY
from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS from mythril.laser.ethereum.transaction.symbolic import ATTACKER_ADDRESS
from mythril.laser.ethereum.transaction.transaction_models import ( from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction, ContractCreationTransaction,
) )
from mythril.analysis.modules.base import DetectionModule from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.report import Issue
from mythril.laser.smt import UGT, symbol_factory, Or, BitVec from mythril.laser.smt import UGT, symbol_factory, Or, BitVec
from mythril.laser.ethereum.natives import PRECOMPILE_COUNT
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.exceptions import UnsatError from mythril.exceptions import UnsatError
from copy import copy from copy import copy
import logging import logging
import json
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -33,7 +37,7 @@ def _is_precompile_call(global_state: GlobalState):
constraints += [ constraints += [
Or( Or(
to < symbol_factory.BitVecVal(1, 256), to < symbol_factory.BitVecVal(1, 256),
to > symbol_factory.BitVecVal(16, 256), to > symbol_factory.BitVecVal(PRECOMPILE_COUNT, 256),
) )
] ]
@ -64,13 +68,12 @@ class ExternalCalls(DetectionModule):
:param state: :param state:
:return: :return:
""" """
issues = self._analyze_state(state) potential_issues = self._analyze_state(state)
for issue in issues:
self._cache.add(issue.address) annotation = get_potential_issues_annotation(state)
self._issues.extend(issues) annotation.potential_issues.extend(potential_issues)
@staticmethod def _analyze_state(self, state: GlobalState):
def _analyze_state(state):
""" """
:param state: :param state:
@ -82,10 +85,10 @@ class ExternalCalls(DetectionModule):
address = state.get_current_instruction()["address"] address = state.get_current_instruction()["address"]
try: try:
constraints = copy(state.mstate.constraints) constraints = Constraints([UGT(gas, symbol_factory.BitVecVal(2300, 256))])
transaction_sequence = solver.get_transaction_sequence( transaction_sequence = solver.get_transaction_sequence(
state, constraints + [UGT(gas, symbol_factory.BitVecVal(2300, 256))] state, constraints + state.mstate.constraints
) )
# Check whether we can also set the callee address # Check whether we can also set the callee address
@ -98,7 +101,7 @@ class ExternalCalls(DetectionModule):
constraints.append(tx.caller == ATTACKER_ADDRESS) constraints.append(tx.caller == ATTACKER_ADDRESS)
transaction_sequence = solver.get_transaction_sequence( transaction_sequence = solver.get_transaction_sequence(
state, constraints state, constraints + state.mstate.constraints
) )
description_head = "A call to a user-supplied address is executed." description_head = "A call to a user-supplied address is executed."
@ -109,7 +112,7 @@ class ExternalCalls(DetectionModule):
"contract state." "contract state."
) )
issue = Issue( issue = PotentialIssue(
contract=state.environment.active_account.contract_name, contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name, function_name=state.environment.active_function_name,
address=address, address=address,
@ -119,8 +122,8 @@ class ExternalCalls(DetectionModule):
severity="Medium", severity="Medium",
description_head=description_head, description_head=description_head,
description_tail=description_tail, description_tail=description_tail,
transaction_sequence=transaction_sequence, constraints=constraints,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), detector=self,
) )
except UnsatError: except UnsatError:
@ -137,7 +140,7 @@ class ExternalCalls(DetectionModule):
"that the callee contract has been reviewed carefully." "that the callee contract has been reviewed carefully."
) )
issue = Issue( issue = PotentialIssue(
contract=state.environment.active_account.contract_name, contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name, function_name=state.environment.active_function_name,
address=address, address=address,
@ -147,8 +150,8 @@ class ExternalCalls(DetectionModule):
severity="Low", severity="Low",
description_head=description_head, description_head=description_head,
description_tail=description_tail, description_tail=description_tail,
transaction_sequence=transaction_sequence, constraints=constraints,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used), detector=self,
) )
except UnsatError: except UnsatError:

@ -113,7 +113,7 @@ class IntegerOverflowUnderflowModule(DetectionModule):
address = _get_address_from_state(state) address = _get_address_from_state(state)
if address in self._cache: if address in self.cache:
return return
opcode = state.get_current_instruction()["opcode"] opcode = state.get_current_instruction()["opcode"]
@ -331,8 +331,8 @@ class IntegerOverflowUnderflowModule(DetectionModule):
) )
address = _get_address_from_state(ostate) address = _get_address_from_state(ostate)
self._cache.add(address) self.cache.add(address)
self._issues.append(issue) self.issues.append(issue)
detector = IntegerOverflowUnderflowModule() detector = IntegerOverflowUnderflowModule()

@ -45,12 +45,12 @@ class MultipleSendsModule(DetectionModule):
) )
def _execute(self, state: GlobalState) -> None: def _execute(self, state: GlobalState) -> None:
if state.get_current_instruction()["address"] in self._cache: if state.get_current_instruction()["address"] in self.cache:
return return
issues = self._analyze_state(state) issues = self._analyze_state(state)
for issue in issues: for issue in issues:
self._cache.add(issue.address) self.cache.add(issue.address)
self._issues.extend(issues) self.issues.extend(issues)
@staticmethod @staticmethod
def _analyze_state(state: GlobalState): def _analyze_state(state: GlobalState):

@ -1,6 +1,10 @@
from mythril.analysis.potential_issues import (
PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import REENTRANCY from mythril.analysis.swc_data import REENTRANCY
from mythril.analysis.modules.base import DetectionModule from mythril.analysis.modules.base import DetectionModule
from mythril.analysis.report import Issue from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.smt import symbol_factory, UGT, BitVec, Or from mythril.laser.smt import symbol_factory, UGT, BitVec, Or
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.annotation import StateAnnotation from mythril.laser.ethereum.state.annotation import StateAnnotation
@ -32,10 +36,12 @@ class StateChangeCallsAnnotation(StateAnnotation):
new_annotation.state_change_states = self.state_change_states[:] new_annotation.state_change_states = self.state_change_states[:]
return new_annotation return new_annotation
def get_issue(self, global_state: GlobalState) -> Optional[Issue]: def get_issue(
self, global_state: GlobalState, detector: DetectionModule
) -> Optional[PotentialIssue]:
if not self.state_change_states: if not self.state_change_states:
return None return None
constraints = copy(global_state.mstate.constraints) constraints = Constraints()
gas = self.call_state.mstate.stack[-1] gas = self.call_state.mstate.stack[-1]
to = self.call_state.mstate.stack[-2] to = self.call_state.mstate.stack[-2]
constraints += [ constraints += [
@ -50,10 +56,11 @@ class StateChangeCallsAnnotation(StateAnnotation):
try: try:
transaction_sequence = solver.get_transaction_sequence( transaction_sequence = solver.get_transaction_sequence(
global_state, constraints global_state, constraints + global_state.mstate.constraints
) )
except UnsatError: except UnsatError:
return None return None
severity = "Medium" if self.user_defined_address else "Low" severity = "Medium" if self.user_defined_address else "Low"
address = global_state.get_current_instruction()["address"] address = global_state.get_current_instruction()["address"]
logging.debug( logging.debug(
@ -67,7 +74,7 @@ class StateChangeCallsAnnotation(StateAnnotation):
"state change takes place. This can lead to business logic vulnerabilities." "state change takes place. This can lead to business logic vulnerabilities."
) )
return Issue( return PotentialIssue(
contract=global_state.environment.active_account.contract_name, contract=global_state.environment.active_account.contract_name,
function_name=global_state.environment.active_function_name, function_name=global_state.environment.active_function_name,
address=address, address=address,
@ -77,7 +84,8 @@ class StateChangeCallsAnnotation(StateAnnotation):
description_tail=description_tail, description_tail=description_tail,
swc_id=REENTRANCY, swc_id=REENTRANCY,
bytecode=global_state.environment.code.bytecode, bytecode=global_state.environment.code.bytecode,
transaction_sequence=transaction_sequence, constraints=constraints,
detector=detector,
) )
@ -104,12 +112,12 @@ class StateChange(DetectionModule):
) )
def _execute(self, state: GlobalState) -> None: def _execute(self, state: GlobalState) -> None:
if state.get_current_instruction()["address"] in self._cache: if state.get_current_instruction()["address"] in self.cache:
return return
issues = self._analyze_state(state) issues = self._analyze_state(state)
for issue in issues:
self._cache.add(issue.address) annotation = get_potential_issues_annotation(state)
self._issues.extend(issues) annotation.potential_issues.extend(issues)
@staticmethod @staticmethod
def _add_external_call(global_state: GlobalState) -> None: def _add_external_call(global_state: GlobalState) -> None:
@ -139,8 +147,7 @@ class StateChange(DetectionModule):
except UnsatError: except UnsatError:
pass pass
@staticmethod def _analyze_state(self, global_state: GlobalState) -> List[PotentialIssue]:
def _analyze_state(global_state: GlobalState) -> List[Issue]:
annotations = cast( annotations = cast(
List[StateChangeCallsAnnotation], List[StateChangeCallsAnnotation],
@ -171,7 +178,9 @@ class StateChange(DetectionModule):
for annotation in annotations: for annotation in annotations:
if not annotation.state_change_states: if not annotation.state_change_states:
continue continue
vulnerabilities.append(annotation.get_issue(global_state)) issue = annotation.get_issue(global_state, self)
if issue:
vulnerabilities.append(issue)
return vulnerabilities return vulnerabilities
@staticmethod @staticmethod

@ -46,12 +46,12 @@ class SuicideModule(DetectionModule):
:param state: :param state:
:return: :return:
""" """
if state.get_current_instruction()["address"] in self._cache: if state.get_current_instruction()["address"] in self.cache:
return return
issues = self._analyze_state(state) issues = self._analyze_state(state)
for issue in issues: for issue in issues:
self._cache.add(issue.address) self.cache.add(issue.address)
self._issues.extend(issues) self.issues.extend(issues)
@staticmethod @staticmethod
def _analyze_state(state): def _analyze_state(state):
@ -60,9 +60,7 @@ class SuicideModule(DetectionModule):
to = state.mstate.stack[-1] to = state.mstate.stack[-1]
log.debug( log.debug("SUICIDE in function %s", state.environment.active_function_name)
"[SUICIDE] SUICIDE in function " + state.environment.active_function_name
)
description_head = "The contract can be killed by anyone." description_head = "The contract can be killed by anyone."
@ -103,7 +101,7 @@ class SuicideModule(DetectionModule):
) )
return [issue] return [issue]
except UnsatError: except UnsatError:
log.info("[UNCHECKED_SUICIDE] no model found") log.debug("No model found")
return [] return []

@ -55,12 +55,12 @@ class UncheckedRetvalModule(DetectionModule):
:param state: :param state:
:return: :return:
""" """
if state.get_current_instruction()["address"] in self._cache: if state.get_current_instruction()["address"] in self.cache:
return return
issues = self._analyze_state(state) issues = self._analyze_state(state)
for issue in issues: for issue in issues:
self._cache.add(issue.address) self.cache.add(issue.address)
self._issues.extend(issues) self.issues.extend(issues)
def _analyze_state(self, state: GlobalState) -> list: def _analyze_state(self, state: GlobalState) -> list:
instruction = state.get_current_instruction() instruction = state.get_current_instruction()
@ -116,13 +116,9 @@ class UncheckedRetvalModule(DetectionModule):
assert state.environment.code.instruction_list[state.mstate.pc - 1][ assert state.environment.code.instruction_list[state.mstate.pc - 1][
"opcode" "opcode"
] in ["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"] ] in ["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"]
retval = state.mstate.stack[-1] return_value = state.mstate.stack[-1]
# Use Typed Dict after release of mypy 0.670 and remove type ignore
retvals.append( retvals.append(
{ # type: ignore {"address": state.instruction["address"] - 1, "retval": return_value}
"address": state.instruction["address"] - 1,
"retval": retval,
}
) )
return [] return []

@ -0,0 +1,100 @@
"""This module contains the detection code for potentially insecure low-level
calls."""
from mythril.analysis.potential_issues import (
PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import ASSERT_VIOLATION
from mythril.analysis.modules.base import DetectionModule
from mythril.laser.ethereum.state.global_state import GlobalState
import logging
import eth_abi
log = logging.getLogger(__name__)
DESCRIPTION = """
Search for reachable user-supplied exceptions.
Report a warning if an log message is emitted: 'emit AssertionFailed(string)'
"""
assertion_failed_hash = (
0xB42604CB105A16C8F6DB8A41E6B00C0C1B4826465E8BC504B3EB3E88B3E6A4A0
)
class UserAssertions(DetectionModule):
"""This module searches for user supplied exceptions: emit AssertionFailed("Error")."""
def __init__(self):
""""""
super().__init__(
name="External calls",
swc_id=ASSERT_VIOLATION,
description=DESCRIPTION,
entrypoint="callback",
pre_hooks=["LOG1"],
)
def _execute(self, state: GlobalState) -> None:
"""
:param state:
:return:
"""
potential_issues = self._analyze_state(state)
annotation = get_potential_issues_annotation(state)
annotation.potential_issues.extend(potential_issues)
def _analyze_state(self, state: GlobalState):
"""
:param state:
:return:
"""
topic, size, mem_start = state.mstate.stack[-3:]
if topic.symbolic or topic.value != assertion_failed_hash:
return []
message = None
if not mem_start.symbolic and not size.symbolic:
message = eth_abi.decode_single(
"string",
bytes(
state.mstate.memory[
mem_start.value + 32 : mem_start.value + size.value
]
),
).decode("utf8")
description_head = "A user-provided assertion failed."
if message:
description_tail = "A user-provided assertion failed with message '{}'. Make sure the user-provided assertion is correct.".format(
message
)
else:
description_tail = "A user-provided assertion failed. Make sure the user-provided assertion is correct."
address = state.get_current_instruction()["address"]
issue = PotentialIssue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=address,
swc_id=ASSERT_VIOLATION,
title="Assertion Failed",
bytecode=state.environment.code.bytecode,
severity="Medium",
description_head=description_head,
description_tail=description_tail,
constraints=[],
detector=self,
)
return [issue]
detector = UserAssertions()

@ -0,0 +1,108 @@
from mythril.analysis.report import Issue
from mythril.analysis.solver import get_transaction_sequence
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.global_state import GlobalState
class PotentialIssue:
"""Representation of a potential issue"""
def __init__(
self,
contract,
function_name,
address,
swc_id,
title,
bytecode,
detector,
severity=None,
description_head="",
description_tail="",
constraints=None,
):
"""
:param contract: The contract
:param function_name: Function name where the issue is detected
:param address: The address of the issue
:param swc_id: Issue's corresponding swc-id
:param title: Title
:param bytecode: bytecode of the issue
:param detector: The detector the potential issue belongs to
:param gas_used: amount of gas used
:param severity: The severity of the issue
:param description_head: The top part of description
:param description_tail: The bottom part of the description
:param constraints: The non-path related constraints for the potential issue
"""
self.title = title
self.contract = contract
self.function_name = function_name
self.address = address
self.description_head = description_head
self.description_tail = description_tail
self.severity = severity
self.swc_id = swc_id
self.bytecode = bytecode
self.constraints = constraints or []
self.detector = detector
class PotentialIssuesAnnotation(StateAnnotation):
def __init__(self):
self.potential_issues = []
def get_potential_issues_annotation(state: GlobalState) -> PotentialIssuesAnnotation:
"""
Returns the potential issues annotation of the given global state, and creates one if
one does not already exist.
:param state: The global state
:return:
"""
for annotation in state.annotations:
if isinstance(annotation, PotentialIssuesAnnotation):
return annotation
annotation = PotentialIssuesAnnotation()
state.annotate(annotation)
return annotation
def check_potential_issues(state: GlobalState) -> None:
"""
Called at the end of a transaction, checks potential issues, and
adds valid issues to the detector.
:param state: The final global state of a transaction
:return:
"""
annotation = get_potential_issues_annotation(state)
for potential_issue in annotation.potential_issues:
try:
transaction_sequence = get_transaction_sequence(
state, state.mstate.constraints + potential_issue.constraints
)
except UnsatError:
continue
annotation.potential_issues.remove(potential_issue)
potential_issue.detector.cache.add(potential_issue.address)
potential_issue.detector.issues.append(
Issue(
contract=potential_issue.contract,
function_name=potential_issue.function_name,
address=potential_issue.address,
title=potential_issue.title,
bytecode=potential_issue.bytecode,
swc_id=potential_issue.swc_id,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
severity=potential_issue.severity,
description_head=potential_issue.description_head,
description_tail=potential_issue.description_tail,
transaction_sequence=transaction_sequence,
)
)

@ -11,6 +11,7 @@ from mythril.analysis.swc_data import SWC_TO_TITLE
from mythril.support.source_support import Source from mythril.support.source_support import Source
from mythril.support.start_time import StartTime from mythril.support.start_time import StartTime
from mythril.support.support_utils import get_code_hash from mythril.support.support_utils import get_code_hash
from mythril.support.signatures import SignatureDB
from time import time from time import time
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -151,6 +152,30 @@ class Issue:
else: else:
self.source_mapping = self.address self.source_mapping = self.address
def resolve_function_names(self):
""" Resolves function names for each step """
if (
self.transaction_sequence is None
or "steps" not in self.transaction_sequence
):
return
signatures = SignatureDB()
for step in self.transaction_sequence["steps"]:
_hash = step["input"][:10]
try:
sig = signatures.get(_hash)
if len(sig) > 0:
step["name"] = sig[0]
else:
step["name"] = "unknown"
except ValueError:
step["name"] = "unknown"
class Report: class Report:
"""A report containing the content of multiple issues.""" """A report containing the content of multiple issues."""
@ -187,6 +212,7 @@ class Report:
""" """
m = hashlib.md5() m = hashlib.md5()
m.update((issue.contract + str(issue.address) + issue.title).encode("utf-8")) m.update((issue.contract + str(issue.address) + issue.title).encode("utf-8"))
issue.resolve_function_names()
self.issues[m.digest()] = issue self.issues[m.digest()] = issue
def as_text(self): def as_text(self):
@ -212,7 +238,7 @@ class Report:
return {} return {}
logs = [] # type: List[Dict] logs = [] # type: List[Dict]
for exception in self.exceptions: for exception in self.exceptions:
logs += [{"level": "error", "hidden": "true", "msg": exception}] logs += [{"level": "error", "hidden": True, "msg": exception}]
return {"logs": logs} return {"logs": logs}
def as_swc_standard_format(self): def as_swc_standard_format(self):

@ -6,22 +6,28 @@ from mythril.analysis import modules
import pkgutil import pkgutil
import importlib.util import importlib.util
import logging import logging
import os
import sys
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
OPCODE_LIST = [c[0] for _, c in opcodes.items()] OPCODE_LIST = [c[0] for _, c in opcodes.items()]
def reset_callback_modules(): def reset_callback_modules(module_names=(), custom_modules_directory=""):
"""Clean the issue records of every callback-based module.""" """Clean the issue records of every callback-based module."""
modules = get_detection_modules("callback") modules = get_detection_modules("callback", module_names, custom_modules_directory)
for module in modules: for module in modules:
module.detector.reset_module() module.detector.reset_module()
def get_detection_module_hooks(modules, hook_type="pre"): def get_detection_module_hooks(modules, hook_type="pre", custom_modules_directory=""):
hook_dict = defaultdict(list) hook_dict = defaultdict(list)
_modules = get_detection_modules(entrypoint="callback", include_modules=modules) _modules = get_detection_modules(
entrypoint="callback",
include_modules=modules,
custom_modules_directory=custom_modules_directory,
)
for module in _modules: for module in _modules:
hooks = ( hooks = (
module.detector.pre_hooks module.detector.pre_hooks
@ -45,14 +51,13 @@ def get_detection_module_hooks(modules, hook_type="pre"):
return dict(hook_dict) return dict(hook_dict)
def get_detection_modules(entrypoint, include_modules=()): def get_detection_modules(entrypoint, include_modules=(), custom_modules_directory=""):
""" """
:param entrypoint: :param entrypoint:
:param include_modules: :param include_modules:
:return: :return:
""" """
module = importlib.import_module("mythril.analysis.modules.base") module = importlib.import_module("mythril.analysis.modules.base")
module.log.setLevel(log.level) module.log.setLevel(log.level)
@ -60,27 +65,35 @@ def get_detection_modules(entrypoint, include_modules=()):
_modules = [] _modules = []
if not include_modules: for loader, module_name, _ in pkgutil.walk_packages(modules.__path__):
for loader, module_name, _ in pkgutil.walk_packages(modules.__path__): if include_modules and module_name not in include_modules:
continue
if module_name != "base":
module = importlib.import_module("mythril.analysis.modules." + module_name)
module.log.setLevel(log.level)
if module.detector.entrypoint == entrypoint:
_modules.append(module)
if custom_modules_directory:
custom_modules_path = os.path.abspath(custom_modules_directory)
if custom_modules_path not in sys.path:
sys.path.append(custom_modules_path)
for loader, module_name, _ in pkgutil.walk_packages([custom_modules_path]):
if include_modules and module_name not in include_modules:
continue
if module_name != "base": if module_name != "base":
module = importlib.import_module( module = importlib.import_module(module_name, custom_modules_path)
"mythril.analysis.modules." + module_name
)
module.log.setLevel(log.level) module.log.setLevel(log.level)
if module.detector.entrypoint == entrypoint: if module.detector.entrypoint == entrypoint:
_modules.append(module) _modules.append(module)
else:
for module_name in include_modules:
module = importlib.import_module("mythril.analysis.modules." + module_name)
if module.__name__ != "base" and module.detector.entrypoint == entrypoint:
module.log.setLevel(log.level)
_modules.append(module)
log.info("Found %s detection modules", len(_modules)) log.info("Found %s detection modules", len(_modules))
return _modules return _modules
def fire_lasers(statespace, module_names=()): def fire_lasers(statespace, module_names=(), custom_modules_directory=""):
""" """
:param statespace: :param statespace:
@ -91,22 +104,28 @@ def fire_lasers(statespace, module_names=()):
issues = [] issues = []
for module in get_detection_modules( for module in get_detection_modules(
entrypoint="post", include_modules=module_names entrypoint="post",
include_modules=module_names,
custom_modules_directory=custom_modules_directory,
): ):
log.info("Executing " + module.detector.name) log.info("Executing " + module.detector.name)
issues += module.detector.execute(statespace) issues += module.detector.execute(statespace)
issues += retrieve_callback_issues(module_names) issues += retrieve_callback_issues(module_names, custom_modules_directory)
return issues return issues
def retrieve_callback_issues(module_names=()): def retrieve_callback_issues(module_names=(), custom_modules_directory=""):
issues = [] issues = []
for module in get_detection_modules( for module in get_detection_modules(
entrypoint="callback", include_modules=module_names entrypoint="callback",
include_modules=module_names,
custom_modules_directory=custom_modules_directory,
): ):
log.debug("Retrieving results for " + module.detector.name) log.debug("Retrieving results for " + module.detector.name)
issues += module.detector.issues issues += module.detector.issues
reset_callback_modules() reset_callback_modules(
module_names=module_names, custom_modules_directory=custom_modules_directory
)
return issues return issues

@ -4,6 +4,7 @@ from typing import Dict, Tuple, Union
from z3 import sat, unknown, FuncInterp from z3 import sat, unknown, FuncInterp
import z3 import z3
from mythril.analysis.analysis_args import analysis_args
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.constraints import Constraints from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.transaction import BaseTransaction from mythril.laser.ethereum.transaction import BaseTransaction
@ -29,7 +30,7 @@ def get_model(constraints, minimize=(), maximize=(), enforce_execution_time=True
:return: :return:
""" """
s = Optimize() s = Optimize()
timeout = 100000 timeout = analysis_args.solver_timeout
if enforce_execution_time: if enforce_execution_time:
timeout = min(timeout, time_handler.time_remaining() - 500) timeout = min(timeout, time_handler.time_remaining() - 500)
if timeout <= 0: if timeout <= 0:
@ -94,7 +95,7 @@ def get_transaction_sequence(
concrete_transactions = [] concrete_transactions = []
tx_constraints, minimize = _set_minimisation_constraints( tx_constraints, minimize = _set_minimisation_constraints(
transaction_sequence, constraints.copy(), [], 5000 transaction_sequence, constraints.copy(), [], 5000, global_state.world_state
) )
try: try:
@ -102,19 +103,23 @@ def get_transaction_sequence(
except UnsatError: except UnsatError:
raise UnsatError raise UnsatError
min_price_dict = {} # type: Dict[str, int] # Include creation account in initial state
# Note: This contains the code, which should not exist until after the first tx
initial_world_state = transaction_sequence[0].world_state
initial_accounts = initial_world_state.accounts
for transaction in transaction_sequence: for transaction in transaction_sequence:
concrete_transaction = _get_concrete_transaction(model, transaction) concrete_transaction = _get_concrete_transaction(model, transaction)
concrete_transactions.append(concrete_transaction) concrete_transactions.append(concrete_transaction)
caller = concrete_transaction["origin"] min_price_dict = {} # type: Dict[str, int]
value = int(concrete_transaction["value"], 16) for address in initial_accounts.keys():
min_price_dict[caller] = min_price_dict.get(caller, 0) + value min_price_dict[address] = model.eval(
initial_world_state.starting_balances[
if isinstance(transaction_sequence[0], ContractCreationTransaction): symbol_factory.BitVecVal(address, 256)
initial_accounts = transaction_sequence[0].prev_world_state.accounts ].raw,
else: model_completion=True,
initial_accounts = transaction_sequence[0].world_state.accounts ).as_long()
concrete_initial_state = _get_concrete_state(initial_accounts, min_price_dict) concrete_initial_state = _get_concrete_state(initial_accounts, min_price_dict)
@ -133,9 +138,9 @@ def _get_concrete_state(initial_accounts: Dict, min_price_dict: Dict[str, int]):
data["nonce"] = account.nonce data["nonce"] = account.nonce
data["code"] = account.code.bytecode data["code"] = account.code.bytecode
data["storage"] = str(account.storage) data["storage"] = str(account.storage)
data["balance"] = min_price_dict.get(address, 0) data["balance"] = hex(min_price_dict.get(address, 0))
accounts[hex(address)] = data accounts[hex(address)] = data
return accounts return {"accounts": accounts}
def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction): def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction):
@ -147,16 +152,17 @@ def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction):
"%x" % model.eval(transaction.caller.raw, model_completion=True).as_long() "%x" % model.eval(transaction.caller.raw, model_completion=True).as_long()
).zfill(40) ).zfill(40)
input_ = ""
if isinstance(transaction, ContractCreationTransaction): if isinstance(transaction, ContractCreationTransaction):
address = "" address = ""
input_ = transaction.code.bytecode input_ += transaction.code.bytecode
else:
input_ = "".join( input_ += "".join(
[ [
hex(b)[2:] if len(hex(b)) % 2 == 0 else "0" + hex(b)[2:] hex(b)[2:] if len(hex(b)) % 2 == 0 else "0" + hex(b)[2:]
for b in transaction.call_data.concrete(model) for b in transaction.call_data.concrete(model)
] ]
) )
# Create concrete transaction dict # Create concrete transaction dict
concrete_transaction = dict() # type: Dict[str, str] concrete_transaction = dict() # type: Dict[str, str]
@ -170,7 +176,7 @@ def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction):
def _set_minimisation_constraints( def _set_minimisation_constraints(
transaction_sequence, constraints, minimize, max_size transaction_sequence, constraints, minimize, max_size, world_state
) -> Tuple[Constraints, tuple]: ) -> Tuple[Constraints, tuple]:
""" Set constraints that minimise key transaction values """ Set constraints that minimise key transaction values
@ -192,5 +198,21 @@ def _set_minimisation_constraints(
# Minimize # Minimize
minimize.append(transaction.call_data.calldatasize) minimize.append(transaction.call_data.calldatasize)
minimize.append(transaction.call_value) minimize.append(transaction.call_value)
constraints.append(
UGE(
symbol_factory.BitVecVal(1000000000000000000000, 256),
world_state.starting_balances[transaction.caller],
)
)
for account in world_state.accounts.values():
# Lazy way to prevent overflows and to ensure "reasonable" balances
# Each account starts with less than 100 ETH
constraints.append(
UGE(
symbol_factory.BitVecVal(100000000000000000000, 256),
world_state.starting_balances[account.address],
)
)
return constraints, tuple(minimize) return constraints, tuple(minimize)

@ -14,6 +14,7 @@ from mythril.laser.ethereum.strategy.basic import (
BasicSearchStrategy, BasicSearchStrategy,
) )
from mythril.laser.ethereum.natives import PRECOMPILE_COUNT
from mythril.laser.ethereum.transaction.symbolic import ( from mythril.laser.ethereum.transaction.symbolic import (
ATTACKER_ADDRESS, ATTACKER_ADDRESS,
CREATOR_ADDRESS, CREATOR_ADDRESS,
@ -47,7 +48,7 @@ class SymExecWrapper:
dynloader=None, dynloader=None,
max_depth=22, max_depth=22,
execution_timeout=None, execution_timeout=None,
loop_bound=2, loop_bound=3,
create_timeout=None, create_timeout=None,
transaction_count=2, transaction_count=2,
modules=(), modules=(),
@ -55,6 +56,8 @@ class SymExecWrapper:
enable_iprof=False, enable_iprof=False,
disable_dependency_pruning=False, disable_dependency_pruning=False,
run_analysis_modules=True, run_analysis_modules=True,
enable_coverage_strategy=False,
custom_modules_directory="",
): ):
""" """
@ -92,7 +95,8 @@ class SymExecWrapper:
) )
requires_statespace = ( requires_statespace = (
compulsory_statespace or len(get_detection_modules("post", modules)) > 0 compulsory_statespace
or len(get_detection_modules("post", modules, custom_modules_directory)) > 0
) )
if not contract.creation_code: if not contract.creation_code:
self.accounts = {hex(ATTACKER_ADDRESS): attacker_account} self.accounts = {hex(ATTACKER_ADDRESS): attacker_account}
@ -102,6 +106,8 @@ class SymExecWrapper:
hex(ATTACKER_ADDRESS): attacker_account, hex(ATTACKER_ADDRESS): attacker_account,
} }
instruction_laser_plugin = PluginFactory.build_instruction_coverage_plugin()
self.laser = svm.LaserEVM( self.laser = svm.LaserEVM(
dynamic_loader=dynloader, dynamic_loader=dynloader,
max_depth=max_depth, max_depth=max_depth,
@ -111,6 +117,8 @@ class SymExecWrapper:
transaction_count=transaction_count, transaction_count=transaction_count,
requires_statespace=requires_statespace, requires_statespace=requires_statespace,
enable_iprof=enable_iprof, enable_iprof=enable_iprof,
enable_coverage_strategy=enable_coverage_strategy,
instruction_laser_plugin=instruction_laser_plugin,
) )
if loop_bound is not None: if loop_bound is not None:
@ -118,8 +126,8 @@ class SymExecWrapper:
plugin_loader = LaserPluginLoader(self.laser) plugin_loader = LaserPluginLoader(self.laser)
plugin_loader.load(PluginFactory.build_mutation_pruner_plugin()) plugin_loader.load(PluginFactory.build_mutation_pruner_plugin())
plugin_loader.load(PluginFactory.build_instruction_coverage_plugin())
plugin_loader.load(PluginFactory.build_state_merge_plugin()) plugin_loader.load(PluginFactory.build_state_merge_plugin())
plugin_loader.load(instruction_laser_plugin)
# if not disable_dependency_pruning: # if not disable_dependency_pruning:
# plugin_loader.load(PluginFactory.build_dependency_pruner_plugin()) # plugin_loader.load(PluginFactory.build_dependency_pruner_plugin())
@ -131,11 +139,19 @@ class SymExecWrapper:
if run_analysis_modules: if run_analysis_modules:
self.laser.register_hooks( self.laser.register_hooks(
hook_type="pre", hook_type="pre",
hook_dict=get_detection_module_hooks(modules, hook_type="pre"), hook_dict=get_detection_module_hooks(
modules,
hook_type="pre",
custom_modules_directory=custom_modules_directory,
),
) )
self.laser.register_hooks( self.laser.register_hooks(
hook_type="post", hook_type="post",
hook_dict=get_detection_module_hooks(modules, hook_type="post"), hook_dict=get_detection_module_hooks(
modules,
hook_type="post",
custom_modules_directory=custom_modules_directory,
),
) )
if isinstance(contract, SolidityContract): if isinstance(contract, SolidityContract):
@ -198,7 +214,10 @@ class SymExecWrapper:
get_variable(stack[-7]), get_variable(stack[-7]),
) )
if to.type == VarType.CONCRETE and to.val < 5: if (
to.type == VarType.CONCRETE
and 0 < to.val <= PRECOMPILE_COUNT
):
# ignore prebuilts # ignore prebuilts
continue continue

@ -6,15 +6,21 @@
- SWC ID: {{ issue['swc-id'] }} - SWC ID: {{ issue['swc-id'] }}
- Severity: {{ issue.severity }} - Severity: {{ issue.severity }}
- Contract: {{ issue.contract | default("Unknown") }} - Contract: {{ issue.contract | default("Unknown") }}
{% if issue.function %}
- Function name: `{{ issue.function }}` - Function name: `{{ issue.function }}`
{% endif %}
- PC address: {{ issue.address }} - PC address: {{ issue.address }}
{% if issue.min_gas_used or issue.max_gas_used %}
- Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }} - Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }}
{% endif %}
### Description ### Description
{{ issue.description.rstrip() }} {{ issue.description.rstrip() }}
{% if issue.filename and issue.lineno %} {% if issue.filename and issue.lineno %}
In file: {{ issue.filename }}:{{ issue.lineno }} In file: {{ issue.filename }}:{{ issue.lineno }}
{% elif issue.filename %}
In file: {{ issue.filename }}
{% endif %} {% endif %}
{% if issue.code %} {% if issue.code %}
@ -26,13 +32,19 @@ In file: {{ issue.filename }}:{{ issue.lineno }}
{% endif %} {% endif %}
{% if issue.tx_sequence %} {% if issue.tx_sequence %}
### Initial State:
{% for key, value in issue.tx_sequence.initialState.accounts.items() %}
Account: {% if key == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif key == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, balance: {{value.balance}}, nonce:{{value.nonce}}, storage:{{value.storage}}
{% endfor %}
### Transaction Sequence ### Transaction Sequence
{% for step in issue.tx_sequence.steps %} {% for step in issue.tx_sequence.steps %}
{% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %} {% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}
Caller: [CREATOR], data: [CONTRACT CREATION], value: {{ step.value }} Caller: [CREATOR], data: {{ step.input }}, value: {{ step.value }}
{% else %} {% else %}
Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, data: {{ step.input }}, value: {{ step.value }} Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, function: {{ step.name }}, txdata: {{ step.input }}, value: {{ step.value }}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}

@ -4,9 +4,13 @@
SWC ID: {{ issue['swc-id'] }} SWC ID: {{ issue['swc-id'] }}
Severity: {{ issue.severity }} Severity: {{ issue.severity }}
Contract: {{ issue.contract | default("Unknown") }} Contract: {{ issue.contract | default("Unknown") }}
{% if issue.function %}
Function name: {{ issue.function }} Function name: {{ issue.function }}
{% endif %}
PC address: {{ issue.address }} PC address: {{ issue.address }}
{% if issue.min_gas_used or issue.max_gas_used %}
Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }} Estimated Gas Usage: {{ issue.min_gas_used }} - {{ issue.max_gas_used }}
{% endif %}
{{ issue.description }} {{ issue.description }}
-------------------- --------------------
{% if issue.filename and issue.lineno %} {% if issue.filename and issue.lineno %}
@ -19,13 +23,19 @@ In file: {{ issue.filename }}:{{ issue.lineno }}
-------------------- --------------------
{% endif %} {% endif %}
{% if issue.tx_sequence %} {% if issue.tx_sequence %}
Initial State:
{% for key, value in issue.tx_sequence.initialState.accounts.items() %}
Account: {% if key == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif key == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, balance: {{value.balance}}, nonce:{{value.nonce}}, storage:{{value.storage}}
{% endfor %}
Transaction Sequence: Transaction Sequence:
{% for step in issue.tx_sequence.steps %} {% for step in issue.tx_sequence.steps %}
{% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %} {% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}
Caller: [CREATOR], data: [CONTRACT CREATION], value: {{ step.value }} Caller: [CREATOR], data: {{ step.input }}, value: {{ step.value }}
{% else %} {% else %}
Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, data: {{ step.input }}, value: {{ step.value }} Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, function: {{ step.name }}, txdata: {{ step.input }}, value: {{ step.value }}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}

@ -84,10 +84,8 @@ def get_function_info(
# Append with missing 0s at the beginning # Append with missing 0s at the beginning
function_hash = "0x" + instruction_list[index]["argument"][2:].rjust(8, "0") function_hash = "0x" + instruction_list[index]["argument"][2:].rjust(8, "0")
function_names = signature_database.get(function_hash) function_names = signature_database.get(function_hash)
if len(function_names) > 1:
# In this case there was an ambiguous result if len(function_names) > 0:
function_name = "[{}] (ambiguous)".format(", ".join(function_names))
elif len(function_names) == 1:
function_name = function_names[0] function_name = function_names[0]
else: else:
function_name = "_function_" + function_hash function_name = "_function_" + function_hash

@ -3,6 +3,7 @@ solc integration."""
import binascii import binascii
import json import json
import os import os
import solcx
from pathlib import Path from pathlib import Path
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
@ -24,39 +25,44 @@ def safe_decode(hex_encoded_string):
return bytes.fromhex(hex_encoded_string) return bytes.fromhex(hex_encoded_string)
def get_solc_json(file, solc_binary="solc", solc_args=None): def get_solc_json(file, solc_binary="solc", solc_settings_json=None):
""" """
:param file: :param file:
:param solc_binary: :param solc_binary:
:param solc_args: :param solc_settings_json:
:return: :return:
""" """
cmd = [solc_binary, "--standard-json", "--allow-paths", "."]
cmd = [solc_binary, "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"]
settings = json.loads(solc_settings_json) if solc_settings_json else {}
if solc_args: settings.update(
cmd.extend(solc_args.split()) {
if not "--allow-paths" in cmd: "outputSelection": {
cmd.extend(["--allow-paths", "."]) "*": {
else: "": ["ast"],
for i, arg in enumerate(cmd): "*": [
if arg == "--allow-paths": "metadata",
cmd[i + 1] += ",." "evm.bytecode",
"evm.deployedBytecode",
cmd.append(file) "evm.methodIdentifiers",
],
}
}
}
)
input_json = json.dumps(
{
"language": "Solidity",
"sources": {file: {"urls": [file]}},
"settings": settings,
}
)
try: try:
p = Popen(cmd, stdout=PIPE, stderr=PIPE) p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate(bytes(input_json, "utf8"))
stdout, stderr = p.communicate()
ret = p.returncode
if ret != 0:
raise CompilerError(
"Solc experienced a fatal error (code %d).\n\n%s"
% (ret, stderr.decode("UTF-8"))
)
except FileNotFoundError: except FileNotFoundError:
raise CompilerError( raise CompilerError(
"Compiler not found. Make sure that solc is installed and in PATH, or set the SOLC environment variable." "Compiler not found. Make sure that solc is installed and in PATH, or set the SOLC environment variable."
@ -64,10 +70,15 @@ def get_solc_json(file, solc_binary="solc", solc_args=None):
out = stdout.decode("UTF-8") out = stdout.decode("UTF-8")
if not len(out): result = json.loads(out)
raise CompilerError("Compilation failed.")
return json.loads(out) for error in result.get("errors", []):
if error["severity"] == "error":
raise CompilerError(
"Solc experienced a fatal error.\n\n%s" % error["formattedMessage"]
)
return result
def encode_calldata(func_name, arg_types, args): def encode_calldata(func_name, arg_types, args):
@ -107,16 +118,25 @@ def solc_exists(version):
:param version: :param version:
:return: :return:
""" """
solc_binaries = [
os.path.join( solc_binaries = []
os.environ.get("HOME", str(Path.home())), if version.startswith("0.4"):
".py-solc/solc-v" + version, solc_binaries = [
"bin/solc", os.path.join(
) # py-solc setup os.environ.get("HOME", str(Path.home())),
] ".py-solc/solc-v" + version,
if version.startswith("0.5"): "bin/solc",
# Temporary fix to support v0.5.x with Ubuntu PPA setup ) # py-solc setup
solc_binaries.append("/usr/bin/solc") ]
else:
# we are using solc-x for the the 0.5 and higher
solc_binaries = [os.path.join(solcx.__path__[0], "bin", "solc-v" + version)]
for solc_path in solc_binaries: for solc_path in solc_binaries:
if os.path.exists(solc_path): if os.path.exists(solc_path):
return solc_path return solc_path
# Last resort is to use the system installation
default_binary = "/usr/bin/solc"
if os.path.exists(default_binary):
return default_binary

@ -15,7 +15,8 @@ import coloredlogs
import traceback import traceback
import mythril.support.signatures as sigs import mythril.support.signatures as sigs
from argparse import ArgumentParser, Namespace from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
from mythril import mythx
from mythril.exceptions import AddressNotFoundError, CriticalError from mythril.exceptions import AddressNotFoundError, CriticalError
from mythril.mythril import ( from mythril.mythril import (
MythrilAnalyzer, MythrilAnalyzer,
@ -27,12 +28,14 @@ from mythril.__version__ import __version__ as VERSION
ANALYZE_LIST = ("analyze", "a") ANALYZE_LIST = ("analyze", "a")
DISASSEMBLE_LIST = ("disassemble", "d") DISASSEMBLE_LIST = ("disassemble", "d")
PRO_LIST = ("pro", "p")
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
COMMAND_LIST = ( COMMAND_LIST = (
ANALYZE_LIST ANALYZE_LIST
+ DISASSEMBLE_LIST + DISASSEMBLE_LIST
+ PRO_LIST
+ ( + (
"read-storage", "read-storage",
"leveldb-search", "leveldb-search",
@ -41,6 +44,7 @@ COMMAND_LIST = (
"version", "version",
"truffle", "truffle",
"help", "help",
"pro",
) )
) )
@ -63,16 +67,34 @@ def exit_with_error(format_, message):
"sourceType": "", "sourceType": "",
"sourceFormat": "", "sourceFormat": "",
"sourceList": [], "sourceList": [],
"meta": { "meta": {"logs": [{"level": "error", "hidden": True, "msg": message}]},
"logs": [{"level": "error", "hidden": "true", "msg": message}]
},
} }
] ]
print(json.dumps(result)) print(json.dumps(result))
sys.exit() sys.exit()
def get_input_parser() -> ArgumentParser: def get_runtime_input_parser() -> ArgumentParser:
"""
Returns Parser which handles input
:return: Parser which handles input
"""
parser = ArgumentParser(add_help=False)
parser.add_argument(
"-a",
"--address",
help="pull contract from the blockchain",
metavar="CONTRACT_ADDRESS",
)
parser.add_argument(
"--bin-runtime",
action="store_true",
help="Only when -c or -f is used. Consider the input bytecode as binary runtime code, default being the contract creation bytecode.",
)
return parser
def get_creation_input_parser() -> ArgumentParser:
""" """
Returns Parser which handles input Returns Parser which handles input
:return: Parser which handles input :return: Parser which handles input
@ -91,17 +113,6 @@ def get_input_parser() -> ArgumentParser:
metavar="BYTECODEFILE", metavar="BYTECODEFILE",
type=argparse.FileType("r"), type=argparse.FileType("r"),
) )
parser.add_argument(
"-a",
"--address",
help="pull contract from the blockchain",
metavar="CONTRACT_ADDRESS",
)
parser.add_argument(
"--bin-runtime",
action="store_true",
help="Only when -c or -f is used. Consider the input bytecode as binary runtime code, default being the contract creation bytecode.",
)
return parser return parser
@ -146,7 +157,10 @@ def get_utilities_parser() -> ArgumentParser:
:return: Parser which handles utility flags :return: Parser which handles utility flags
""" """
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--solc-args", help="Extra arguments for solc") parser.add_argument(
"--solc-json",
help="Json for the optional 'settings' parameter of solc's standard-json input",
)
parser.add_argument( parser.add_argument(
"--solv", "--solv",
help="specify solidity compiler version. If not present, will try to install it (Experimental)", help="specify solidity compiler version. If not present, will try to install it (Experimental)",
@ -160,7 +174,8 @@ def main() -> None:
rpc_parser = get_rpc_parser() rpc_parser = get_rpc_parser()
utilities_parser = get_utilities_parser() utilities_parser = get_utilities_parser()
input_parser = get_input_parser() runtime_input_parser = get_runtime_input_parser()
creation_input_parser = get_creation_input_parser()
output_parser = get_output_parser() output_parser = get_output_parser()
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Security analysis of Ethereum smart contracts" description="Security analysis of Ethereum smart contracts"
@ -174,8 +189,15 @@ def main() -> None:
analyzer_parser = subparsers.add_parser( analyzer_parser = subparsers.add_parser(
ANALYZE_LIST[0], ANALYZE_LIST[0],
help="Triggers the analysis of the smart contract", help="Triggers the analysis of the smart contract",
parents=[rpc_parser, utilities_parser, input_parser, output_parser], parents=[
rpc_parser,
utilities_parser,
creation_input_parser,
runtime_input_parser,
output_parser,
],
aliases=ANALYZE_LIST[1:], aliases=ANALYZE_LIST[1:],
formatter_class=RawTextHelpFormatter,
) )
create_analyzer_parser(analyzer_parser) create_analyzer_parser(analyzer_parser)
@ -183,10 +205,25 @@ def main() -> None:
DISASSEMBLE_LIST[0], DISASSEMBLE_LIST[0],
help="Disassembles the smart contract", help="Disassembles the smart contract",
aliases=DISASSEMBLE_LIST[1:], aliases=DISASSEMBLE_LIST[1:],
parents=[rpc_parser, utilities_parser, input_parser], parents=[
rpc_parser,
utilities_parser,
creation_input_parser,
runtime_input_parser,
],
formatter_class=RawTextHelpFormatter,
) )
create_disassemble_parser(disassemble_parser) create_disassemble_parser(disassemble_parser)
pro_parser = subparsers.add_parser(
PRO_LIST[0],
help="Analyzes input with the MythX API (https://mythx.io)",
aliases=PRO_LIST[1:],
parents=[utilities_parser, creation_input_parser, output_parser],
formatter_class=RawTextHelpFormatter,
)
create_pro_parser(pro_parser)
read_storage_parser = subparsers.add_parser( read_storage_parser = subparsers.add_parser(
"read-storage", "read-storage",
help="Retrieves storage slots from a given address through rpc", help="Retrieves storage slots from a given address through rpc",
@ -225,7 +262,32 @@ def create_disassemble_parser(parser: ArgumentParser):
:param parser: :param parser:
:return: :return:
""" """
parser.add_argument("solidity_file", nargs="*") # Using nargs=* would the implementation below for getting code for both disassemble and analyze
parser.add_argument(
"solidity_files",
nargs="*",
help="Inputs file name and contract name. Currently supports a single contract\n"
"usage: file1.sol:OptionalContractName",
)
def create_pro_parser(parser: ArgumentParser):
"""
Modify parser to handle mythx analysis
:param parser:
:return:
"""
parser.add_argument(
"solidity_files",
nargs="*",
help="Inputs file name and contract name. \n"
"usage: file1.sol:OptionalContractName file2.sol file3.sol:OptionalContractName",
)
parser.add_argument(
"--full",
help="Run a full analysis. Default: quick analysis",
action="store_true",
)
def create_read_storage_parser(read_storage_parser: ArgumentParser): def create_read_storage_parser(read_storage_parser: ArgumentParser):
@ -292,7 +354,12 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser):
:param analyzer_parser: :param analyzer_parser:
:return: :return:
""" """
analyzer_parser.add_argument("solidity_file", nargs="*") analyzer_parser.add_argument(
"solidity_files",
nargs="*",
help="Inputs file name and contract name. \n"
"usage: file1.sol:OptionalContractName file2.sol file3.sol:OptionalContractName",
)
commands = analyzer_parser.add_argument_group("commands") commands = analyzer_parser.add_argument_group("commands")
commands.add_argument("-g", "--graph", help="generate a control flow graph") commands.add_argument("-g", "--graph", help="generate a control flow graph")
commands.add_argument( commands.add_argument(
@ -346,11 +413,17 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser):
default=86400, default=86400,
help="The amount of seconds to spend on symbolic execution", help="The amount of seconds to spend on symbolic execution",
) )
options.add_argument(
"--solver-timeout",
type=int,
default=10000,
help="The maximum amount of time(in milli seconds) the solver spends for queries from analysis modules",
)
options.add_argument( options.add_argument(
"--create-timeout", "--create-timeout",
type=int, type=int,
default=10, default=10,
help="The amount of seconds to spend on " "the initial contract creation", help="The amount of seconds to spend on the initial contract creation",
) )
options.add_argument( options.add_argument(
"-l", "-l",
@ -360,8 +433,9 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser):
) )
options.add_argument( options.add_argument(
"--no-onchain-storage-access", "--no-onchain-storage-access",
"--no-onchain-access",
action="store_true", action="store_true",
help="turns off getting the data from onchain contracts", help="turns off getting the data from onchain contracts (both loading storage and contract code)",
) )
options.add_argument( options.add_argument(
@ -384,6 +458,16 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser):
action="store_true", action="store_true",
help="Deactivate dependency-based pruning", help="Deactivate dependency-based pruning",
) )
options.add_argument(
"--enable-coverage-strategy",
action="store_true",
help="enable coverage based search strategy",
)
options.add_argument(
"--custom-modules-directory",
help="designates a separate directory to search for custom analysis modules",
metavar="CUSTOM_MODULES_DIRECTORY",
)
def validate_args(args: Namespace): def validate_args(args: Namespace):
@ -410,6 +494,8 @@ def validate_args(args: Namespace):
exit_with_error( exit_with_error(
args.outform, "Invalid -v value, you can find valid values in usage" args.outform, "Invalid -v value, you can find valid values in usage"
) )
if args.command in DISASSEMBLE_LIST and len(args.solidity_files) > 1:
exit_with_error("text", "Only a single arg is supported for using disassemble")
if args.command in ANALYZE_LIST: if args.command in ANALYZE_LIST:
if args.query_signature and sigs.ethereum_input_decoder is None: if args.query_signature and sigs.ethereum_input_decoder is None:
@ -494,15 +580,15 @@ def load_code(disassembler: MythrilDisassembler, args: Namespace):
elif args.__dict__.get("address", False): elif args.__dict__.get("address", False):
# Get bytecode from a contract address # Get bytecode from a contract address
address, _ = disassembler.load_from_address(args.address) address, _ = disassembler.load_from_address(args.address)
elif args.__dict__.get("solidity_file", False): elif args.__dict__.get("solidity_files", False):
# Compile Solidity source file(s) # Compile Solidity source file(s)
if args.command in ANALYZE_LIST and args.graph and len(args.solidity_file) > 1: if args.command in ANALYZE_LIST and args.graph and len(args.solidity_files) > 1:
exit_with_error( exit_with_error(
args.outform, args.outform,
"Cannot generate call graphs from multiple input files. Please do it one at a time.", "Cannot generate call graphs from multiple input files. Please do it one at a time.",
) )
address, _ = disassembler.load_from_solidity( address, _ = disassembler.load_from_solidity(
args.solidity_file args.solidity_files
) # list of files ) # list of files
else: else:
exit_with_error( exit_with_error(
@ -534,6 +620,17 @@ def execute_command(
) )
print(storage) print(storage)
elif args.command in PRO_LIST:
mode = "full" if args.full else "quick"
report = mythx.analyze(disassembler.contracts, mode)
outputs = {
"json": report.as_json(),
"jsonv2": report.as_swc_standard_format(),
"text": report.as_text(),
"markdown": report.as_markdown(),
}
print(outputs[args.outform])
elif args.command in DISASSEMBLE_LIST: elif args.command in DISASSEMBLE_LIST:
if disassembler.contracts[0].code: if disassembler.contracts[0].code:
print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm()) print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm())
@ -552,6 +649,12 @@ def execute_command(
enable_iprof=args.enable_iprof, enable_iprof=args.enable_iprof,
disable_dependency_pruning=args.disable_dependency_pruning, disable_dependency_pruning=args.disable_dependency_pruning,
onchain_storage_access=not args.no_onchain_storage_access, onchain_storage_access=not args.no_onchain_storage_access,
solver_timeout=args.solver_timeout,
requires_dynld=not args.no_onchain_storage_access,
enable_coverage_strategy=args.enable_coverage_strategy,
custom_modules_directory=args.custom_modules_directory
if args.custom_modules_directory
else "",
) )
if not disassembler.contracts: if not disassembler.contracts:
@ -658,12 +761,12 @@ def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None:
config = set_config(args) config = set_config(args)
leveldb_search(config, args) leveldb_search(config, args)
query_signature = args.__dict__.get("query_signature", None) query_signature = args.__dict__.get("query_signature", None)
solc_args = args.__dict__.get("solc_args", None) solc_json = args.__dict__.get("solc_json", None)
solv = args.__dict__.get("solv", None) solv = args.__dict__.get("solv", None)
disassembler = MythrilDisassembler( disassembler = MythrilDisassembler(
eth=config.eth, eth=config.eth,
solc_version=solv, solc_version=solv,
solc_args=solc_args, solc_settings_json=solc_json,
enable_online_lookup=query_signature, enable_online_lookup=query_signature,
) )
if args.command == "truffle": if args.command == "truffle":

@ -44,9 +44,7 @@ def exit_with_error(format_, message):
"sourceType": "", "sourceType": "",
"sourceFormat": "", "sourceFormat": "",
"sourceList": [], "sourceList": [],
"meta": { "meta": {"logs": [{"level": "error", "hidden": True, "msg": message}]},
"logs": [{"level": "error", "hidden": "true", "msg": message}]
},
} }
] ]
print(json.dumps(result)) print(json.dumps(result))
@ -213,6 +211,12 @@ def create_parser(parser: argparse.ArgumentParser) -> None:
default=2, default=2,
help="Maximum number of transactions issued by laser", help="Maximum number of transactions issued by laser",
) )
options.add_argument(
"--solver-timeout",
type=int,
default=10000,
help="The maximum amount of time(in milli seconds) the solver spends for queries from analysis modules",
)
options.add_argument( options.add_argument(
"--execution-timeout", "--execution-timeout",
type=int, type=int,
@ -225,7 +229,10 @@ def create_parser(parser: argparse.ArgumentParser) -> None:
default=10, default=10,
help="The amount of seconds to spend on " "the initial contract creation", help="The amount of seconds to spend on " "the initial contract creation",
) )
options.add_argument("--solc-args", help="Extra arguments for solc") options.add_argument(
"--solc-json",
help="Json for the optional 'settings' parameter of solc's standard-json input",
)
options.add_argument( options.add_argument(
"--phrack", action="store_true", help="Phrack-style call graph" "--phrack", action="store_true", help="Phrack-style call graph"
) )
@ -419,6 +426,7 @@ def execute_command(
enable_iprof=args.enable_iprof, enable_iprof=args.enable_iprof,
disable_dependency_pruning=args.disable_dependency_pruning, disable_dependency_pruning=args.disable_dependency_pruning,
onchain_storage_access=not args.no_onchain_storage_access, onchain_storage_access=not args.no_onchain_storage_access,
solver_timeout=args.solver_timeout,
) )
if args.disassemble: if args.disassemble:
@ -517,7 +525,7 @@ def parse_args(parser: argparse.ArgumentParser, args: argparse.Namespace) -> Non
disassembler = MythrilDisassembler( disassembler = MythrilDisassembler(
eth=config.eth, eth=config.eth,
solc_version=args.solv, solc_version=args.solv,
solc_args=args.solc_args, solc_settings_json=args.solc_json,
enable_online_lookup=args.query_signature, enable_online_lookup=args.query_signature,
) )
if args.truffle: if args.truffle:

@ -4,19 +4,20 @@ parameters for the new global state."""
import logging import logging
import re import re
from typing import Union, List, cast, Callable from typing import Union, List, cast, Callable, Optional
import mythril.laser.ethereum.util as util import mythril.laser.ethereum.util as util
from mythril.laser.ethereum import natives from mythril.laser.ethereum import natives
from mythril.laser.ethereum.gas import OPCODE_GAS from mythril.laser.ethereum.gas import OPCODE_GAS
from mythril.laser.ethereum.state.account import Account from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.natives import PRECOMPILE_COUNT
from mythril.laser.ethereum.state.calldata import ( from mythril.laser.ethereum.state.calldata import (
BaseCalldata, BaseCalldata,
SymbolicCalldata, SymbolicCalldata,
ConcreteCalldata, ConcreteCalldata,
) )
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import BitVec from mythril.laser.smt import BitVec, is_true
from mythril.laser.smt import simplify, Expression, symbol_factory from mythril.laser.smt import simplify, Expression, symbol_factory
from mythril.support.loader import DynLoader from mythril.support.loader import DynLoader
@ -49,7 +50,11 @@ def get_call_parameters(
callee_account = None callee_account = None
call_data = get_call_data(global_state, memory_input_offset, memory_input_size) call_data = get_call_data(global_state, memory_input_offset, memory_input_size)
if int(callee_address, 16) >= 5 or int(callee_address, 16) == 0: if (
isinstance(callee_address, BitVec)
or int(callee_address, 16) > PRECOMPILE_COUNT
or int(callee_address, 16) == 0
):
callee_account = get_callee_account( callee_account = get_callee_account(
global_state, callee_address, dynamic_loader global_state, callee_address, dynamic_loader
) )
@ -84,11 +89,11 @@ def get_callee_address(
except TypeError: except TypeError:
log.debug("Symbolic call encountered") log.debug("Symbolic call encountered")
match = re.search(r"storage_(\d+)", str(simplify(symbolic_to_address))) match = re.search(r"Storage\[(\d+)\]", str(simplify(symbolic_to_address)))
log.debug("CALL to: " + str(simplify(symbolic_to_address))) log.debug("CALL to: " + str(simplify(symbolic_to_address)))
if match is None or dynamic_loader is None: if match is None or dynamic_loader is None:
raise ValueError() return symbolic_to_address
index = int(match.group(1)) index = int(match.group(1))
log.debug("Dynamic contract address at storage index {}".format(index)) log.debug("Dynamic contract address at storage index {}".format(index))
@ -100,8 +105,7 @@ def get_callee_address(
) )
# TODO: verify whether this happens or not # TODO: verify whether this happens or not
except: except:
log.debug("Error accessing contract storage.") return symbolic_to_address
raise ValueError
# testrpc simply returns the address, geth response is more elaborate. # testrpc simply returns the address, geth response is more elaborate.
if not re.match(r"^0x[0-9a-f]{40}$", callee_address): if not re.match(r"^0x[0-9a-f]{40}$", callee_address):
@ -111,7 +115,9 @@ def get_callee_address(
def get_callee_account( def get_callee_account(
global_state: GlobalState, callee_address: str, dynamic_loader: DynLoader global_state: GlobalState,
callee_address: Union[str, BitVec],
dynamic_loader: DynLoader,
): ):
"""Gets the callees account from the global_state. """Gets the callees account from the global_state.
@ -120,28 +126,31 @@ def get_callee_account(
:param dynamic_loader: dynamic loader to use :param dynamic_loader: dynamic loader to use
:return: Account belonging to callee :return: Account belonging to callee
""" """
environment = global_state.environment if isinstance(callee_address, BitVec):
accounts = global_state.accounts if callee_address.symbolic:
return Account(callee_address, balances=global_state.world_state.balances)
else:
callee_address = hex(callee_address.value)[2:]
try: try:
return global_state.accounts[int(callee_address, 16)] return global_state.accounts[int(callee_address, 16)]
except KeyError: except KeyError:
# We have a valid call address, but contract is not in the modules list # We have a valid call address, but contract is not in the modules list
log.debug("Module with address " + callee_address + " not loaded.") log.debug("Module with address %s not loaded.", callee_address)
if dynamic_loader is None: if dynamic_loader is None:
raise ValueError() raise ValueError("dynamic_loader is None")
log.debug("Attempting to load dependency") log.debug("Attempting to load dependency")
try: try:
code = dynamic_loader.dynld(callee_address) code = dynamic_loader.dynld(callee_address)
except ValueError as error: except ValueError as error:
log.debug("Unable to execute dynamic loader because: {}".format(str(error))) log.debug("Unable to execute dynamic loader because: %s", error)
raise error raise error
if code is None: if code is None:
log.debug("No code returned, not a contract account?") log.debug("No code returned, not a contract account?")
raise ValueError() raise ValueError("No code returned")
log.debug("Dependency loaded: " + callee_address) log.debug("Dependency loaded: " + callee_address)
callee_account = Account( callee_account = Account(
@ -151,7 +160,7 @@ def get_callee_account(
dynamic_loader=dynamic_loader, dynamic_loader=dynamic_loader,
balances=global_state.world_state.balances, balances=global_state.world_state.balances,
) )
accounts[callee_address] = callee_account global_state.accounts[int(callee_address, 16)] = callee_account
return callee_account return callee_account
@ -189,10 +198,10 @@ def get_call_data(
) )
uses_entire_calldata = simplify( uses_entire_calldata = simplify(
memory_size - global_state.environment.calldata.calldatasize == 0 memory_size == global_state.environment.calldata.calldatasize
) )
if uses_entire_calldata is True: if is_true(uses_entire_calldata):
return global_state.environment.calldata return global_state.environment.calldata
try: try:
@ -203,18 +212,24 @@ def get_call_data(
] ]
return ConcreteCalldata(transaction_id, calldata_from_mem) return ConcreteCalldata(transaction_id, calldata_from_mem)
except TypeError: except TypeError:
log.debug("Unsupported symbolic calldata offset") log.debug(
"Unsupported symbolic calldata offset %s size %s", memory_start, memory_size
)
return SymbolicCalldata(transaction_id) return SymbolicCalldata(transaction_id)
def native_call( def native_call(
global_state: GlobalState, global_state: GlobalState,
callee_address: str, callee_address: Union[str, BitVec],
call_data: BaseCalldata, call_data: BaseCalldata,
memory_out_offset: Union[int, Expression], memory_out_offset: Union[int, Expression],
memory_out_size: Union[int, Expression], memory_out_size: Union[int, Expression],
) -> Union[List[GlobalState], None]: ) -> Optional[List[GlobalState]]:
if not 0 < int(callee_address, 16) < 5:
if (
isinstance(callee_address, BitVec)
or not 0 < int(callee_address, 16) <= PRECOMPILE_COUNT
):
return None return None
log.debug("Native contract called: " + callee_address) log.debug("Native contract called: " + callee_address)

@ -35,3 +35,15 @@ class OutOfGasException(VmException):
"""A VM exception denoting the current execution has run out of gas.""" """A VM exception denoting the current execution has run out of gas."""
pass pass
class WriteProtection(VmException):
"""A VM exception denoting that a write operation is executed on a write protected environment"""
pass
class ProgramCounterException(VmException):
"""A VM exception denoting an invalid PC value (No stop instruction is reached)."""
pass

@ -180,10 +180,7 @@ OPCODE_GAS = {
"LOG3": (4 * 375, 4 * 375 + 8 * 32), "LOG3": (4 * 375, 4 * 375 + 8 * 32),
"LOG4": (5 * 375, 5 * 375 + 8 * 32), "LOG4": (5 * 375, 5 * 375 + 8 * 32),
"CREATE": (32000, 32000), "CREATE": (32000, 32000),
"CREATE2": ( "CREATE2": (32000, 32000), # TODO: Make the gas values dynamic
32000,
32000,
), # TODO: The gas value is dynamic, to be done while implementing create2
"CALL": (700, 700 + 9000 + 25000), "CALL": (700, 700 + 9000 + 25000),
"NATIVE_COST": calculate_native_gas, "NATIVE_COST": calculate_native_gas,
"CALLCODE": (700, 700 + 9000 + 25000), "CALLCODE": (700, 700 + 9000 + 25000),

@ -6,7 +6,6 @@ import logging
from copy import copy, deepcopy from copy import copy, deepcopy
from typing import cast, Callable, List, Set, Union, Tuple, Any from typing import cast, Callable, List, Set, Union, Tuple, Any
from datetime import datetime from datetime import datetime
from math import ceil
from ethereum import utils from ethereum import utils
from mythril.laser.smt import ( from mythril.laser.smt import (
@ -25,18 +24,24 @@ from mythril.laser.smt import (
Bool, Bool,
Not, Not,
LShR, LShR,
UGE,
) )
from mythril.laser.smt import symbol_factory from mythril.laser.smt import symbol_factory
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.ethereum.state.calldata import ConcreteCalldata, SymbolicCalldata
import mythril.laser.ethereum.util as helper import mythril.laser.ethereum.util as helper
from mythril.laser.ethereum import util from mythril.laser.ethereum import util
from mythril.laser.ethereum.call import get_call_parameters, native_call from mythril.laser.ethereum.call import get_call_parameters, native_call, get_call_data
from mythril.laser.ethereum.evm_exceptions import ( from mythril.laser.ethereum.evm_exceptions import (
VmException, VmException,
StackUnderflowException, StackUnderflowException,
InvalidJumpDestination, InvalidJumpDestination,
InvalidInstruction, InvalidInstruction,
OutOfGasException, OutOfGasException,
WriteProtection,
) )
from mythril.laser.ethereum.gas import OPCODE_GAS from mythril.laser.ethereum.gas import OPCODE_GAS
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
@ -44,8 +49,11 @@ from mythril.laser.ethereum.transaction import (
MessageCallTransaction, MessageCallTransaction,
TransactionStartSignal, TransactionStartSignal,
ContractCreationTransaction, ContractCreationTransaction,
get_next_transaction_id,
) )
from mythril.support.support_utils import get_code_hash
from mythril.support.loader import DynLoader from mythril.support.loader import DynLoader
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -54,6 +62,30 @@ TT256 = 2 ** 256
TT256M1 = 2 ** 256 - 1 TT256M1 = 2 ** 256 - 1
def transfer_ether(
global_state: GlobalState,
sender: BitVec,
receiver: BitVec,
value: Union[int, BitVec],
):
"""
Perform an Ether transfer between two accounts
:param global_state: The global state in which the Ether transfer occurs
:param sender: The sender of the Ether
:param receiver: The recipient of the Ether
:param value: The amount of Ether to send
:return:
"""
value = value if isinstance(value, BitVec) else symbol_factory.BitVecVal(value, 256)
global_state.mstate.constraints.append(
UGE(global_state.world_state.balances[sender], value)
)
global_state.world_state.balances[receiver] += value
global_state.world_state.balances[sender] -= value
class StateTransition(object): class StateTransition(object):
"""Decorator that handles global state copy and original return. """Decorator that handles global state copy and original return.
@ -63,14 +95,18 @@ class StateTransition(object):
if `increment_pc=True`. if `increment_pc=True`.
""" """
def __init__(self, increment_pc=True, enable_gas=True): def __init__(
self, increment_pc=True, enable_gas=True, is_state_mutation_instruction=False
):
""" """
:param increment_pc: :param increment_pc:
:param enable_gas: :param enable_gas:
:param is_state_mutation_instruction: The function mutates state
""" """
self.increment_pc = increment_pc self.increment_pc = increment_pc
self.enable_gas = enable_gas self.enable_gas = enable_gas
self.is_state_mutation_instruction = is_state_mutation_instruction
@staticmethod @staticmethod
def call_on_state_copy(func: Callable, func_obj: "Instruction", state: GlobalState): def call_on_state_copy(func: Callable, func_obj: "Instruction", state: GlobalState):
@ -140,6 +176,13 @@ class StateTransition(object):
:param global_state: :param global_state:
:return: :return:
""" """
if self.is_state_mutation_instruction and global_state.environment.static:
raise WriteProtection(
"The function {} cannot be executed in a static call".format(
func.__name__[:-1]
)
)
new_global_states = self.call_on_state_copy(func, func_obj, global_state) new_global_states = self.call_on_state_copy(func, func_obj, global_state)
new_global_states = [ new_global_states = [
self.accumulate_gas(state) for state in new_global_states self.accumulate_gas(state) for state in new_global_states
@ -172,7 +215,8 @@ class Instruction:
:return: :return:
""" """
# Generalize some ops # Generalize some ops
log.debug("Evaluating {}".format(self.op_code)) log.debug("Evaluating %s at %i", self.op_code, global_state.mstate.pc)
op = self.op_code.lower() op = self.op_code.lower()
if self.op_code.startswith("PUSH"): if self.op_code.startswith("PUSH"):
op = "push" op = "push"
@ -732,51 +776,45 @@ class Instruction:
""" """
state = global_state.mstate state = global_state.mstate
environment = global_state.environment environment = global_state.environment
state.stack.append(environment.calldata.calldatasize)
return [global_state]
@StateTransition() if isinstance(global_state.current_transaction, ContractCreationTransaction):
def calldatacopy_(self, global_state: GlobalState) -> List[GlobalState]: log.debug("Attempt to use CALLDATASIZE in creation transaction")
""" state.stack.append(0)
else:
state.stack.append(environment.calldata.calldatasize)
:param global_state: return [global_state]
:return:
""" @staticmethod
state = global_state.mstate def _calldata_copy_helper(global_state, mstate, mstart, dstart, size):
environment = global_state.environment environment = global_state.environment
op0, op1, op2 = state.stack.pop(), state.stack.pop(), state.stack.pop()
try: try:
mstart = util.get_concrete_int(op0) mstart = util.get_concrete_int(mstart)
except TypeError: except TypeError:
log.debug("Unsupported symbolic memory offset in CALLDATACOPY") log.debug("Unsupported symbolic memory offset in CALLDATACOPY")
return [global_state] return [global_state]
try: try:
dstart = util.get_concrete_int(op1) # type: Union[int, BitVec] dstart = util.get_concrete_int(dstart) # type: Union[int, BitVec]
except TypeError: except TypeError:
log.debug("Unsupported symbolic calldata offset in CALLDATACOPY") log.debug("Unsupported symbolic calldata offset in CALLDATACOPY")
dstart = simplify(op1) dstart = simplify(dstart)
size_sym = False
try: try:
size = util.get_concrete_int(op2) # type: Union[int, BitVec] size = util.get_concrete_int(size) # type: Union[int, BitVec]
except TypeError: except TypeError:
log.debug("Unsupported symbolic size in CALLDATACOPY") log.debug("Unsupported symbolic size in CALLDATACOPY")
size = simplify(op2)
size_sym = True
if size_sym:
size = 320 # The excess size will get overwritten size = 320 # The excess size will get overwritten
size = cast(int, size) size = cast(int, size)
if size > 0: if size > 0:
try: try:
state.mem_extend(mstart, size) mstate.mem_extend(mstart, size)
except TypeError as e: except TypeError as e:
log.debug("Memory allocation error: {}".format(e)) log.debug("Memory allocation error: {}".format(e))
state.mem_extend(mstart, 1) mstate.mem_extend(mstart, 1)
state.memory[mstart] = global_state.new_bitvec( mstate.memory[mstart] = global_state.new_bitvec(
"calldata_" "calldata_"
+ str(environment.active_account.contract_name) + str(environment.active_account.contract_name)
+ "[" + "["
@ -801,12 +839,12 @@ class Instruction:
else simplify(cast(BitVec, i_data) + 1) else simplify(cast(BitVec, i_data) + 1)
) )
for i in range(len(new_memory)): for i in range(len(new_memory)):
state.memory[i + mstart] = new_memory[i] mstate.memory[i + mstart] = new_memory[i]
except IndexError: except IndexError:
log.debug("Exception copying calldata to memory") log.debug("Exception copying calldata to memory")
state.memory[mstart] = global_state.new_bitvec( mstate.memory[mstart] = global_state.new_bitvec(
"calldata_" "calldata_"
+ str(environment.active_account.contract_name) + str(environment.active_account.contract_name)
+ "[" + "["
@ -818,6 +856,22 @@ class Instruction:
) )
return [global_state] return [global_state]
@StateTransition()
def calldatacopy_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
op0, op1, op2 = state.stack.pop(), state.stack.pop(), state.stack.pop()
if isinstance(global_state.current_transaction, ContractCreationTransaction):
log.debug("Attempt to use CALLDATACOPY in creation transaction")
return [global_state]
return self._calldata_copy_helper(global_state, state, op0, op1, op2)
# Environment # Environment
@StateTransition() @StateTransition()
def address_(self, global_state: GlobalState) -> List[GlobalState]: def address_(self, global_state: GlobalState) -> List[GlobalState]:
@ -840,7 +894,11 @@ class Instruction:
""" """
state = global_state.mstate state = global_state.mstate
address = state.stack.pop() address = state.stack.pop()
state.stack.append(global_state.new_bitvec("balance_at_" + str(address), 256))
balance = global_state.world_state.balances[
global_state.environment.active_account.address
]
state.stack.append(balance)
return [global_state] return [global_state]
@StateTransition() @StateTransition()
@ -877,9 +935,31 @@ class Instruction:
state = global_state.mstate state = global_state.mstate
environment = global_state.environment environment = global_state.environment
disassembly = environment.code disassembly = environment.code
state.stack.append(len(disassembly.bytecode) // 2) calldata = global_state.environment.calldata
if isinstance(global_state.current_transaction, ContractCreationTransaction):
# Hacky way to ensure constructor arguments work - Pick some reasonably large size.
no_of_bytes = len(disassembly.bytecode) // 2
if isinstance(calldata, ConcreteCalldata):
no_of_bytes += calldata.size
else:
no_of_bytes += 0x200 # space for 16 32-byte arguments
global_state.mstate.constraints.append(
global_state.environment.calldata.size == no_of_bytes
)
else:
no_of_bytes = len(disassembly.bytecode) // 2
state.stack.append(no_of_bytes)
return [global_state] return [global_state]
@staticmethod
def _sha3_gas_helper(global_state, length):
min_gas, max_gas = cast(Callable, OPCODE_GAS["SHA3_FUNC"])(length)
global_state.mstate.min_gas_used += min_gas
global_state.mstate.max_gas_used += max_gas
StateTransition.check_gas_usage_limit(global_state)
return global_state
@StateTransition(enable_gas=False) @StateTransition(enable_gas=False)
def sha3_(self, global_state: GlobalState) -> List[GlobalState]: def sha3_(self, global_state: GlobalState) -> List[GlobalState]:
""" """
@ -905,10 +985,7 @@ class Instruction:
state.max_gas_used += gas_tuple[1] state.max_gas_used += gas_tuple[1]
return [global_state] return [global_state]
min_gas, max_gas = cast(Callable, OPCODE_GAS["SHA3_FUNC"])(length) Instruction._sha3_gas_helper(global_state, length)
state.min_gas_used += min_gas
state.max_gas_used += max_gas
StateTransition.check_gas_usage_limit(global_state)
state.mem_extend(index, length) state.mem_extend(index, length)
data_list = [ data_list = [
@ -961,34 +1038,6 @@ class Instruction:
global_state.mstate.stack.append(global_state.environment.gasprice) global_state.mstate.stack.append(global_state.environment.gasprice)
return [global_state] return [global_state]
@staticmethod
def _handle_symbolic_args(
global_state: GlobalState, concrete_memory_offset: int
) -> None:
"""
In contract creation transaction with dynamic arguments(like arrays, maps) solidity will try to
execute CODECOPY with code size as len(with_args) - len(without_args) which in our case
would be 0, hence we are writing 10 symbol words onto the memory on the assumption that
no one would use 10 array/map arguments for constructor.
:param global_state: The global state
:param concrete_memory_offset: The memory offset on which symbols should be written
"""
no_of_words = ceil(
min(len(global_state.environment.code.bytecode) / 2, 320) / 32
)
global_state.mstate.mem_extend(concrete_memory_offset, 32 * no_of_words)
for i in range(no_of_words):
global_state.mstate.memory.write_word_at(
concrete_memory_offset + i * 32,
global_state.new_bitvec(
"code_{}({})".format(
concrete_memory_offset + i * 32,
global_state.environment.active_account.contract_name,
),
256,
),
)
@StateTransition() @StateTransition()
def codecopy_(self, global_state: GlobalState) -> List[GlobalState]: def codecopy_(self, global_state: GlobalState) -> List[GlobalState]:
""" """
@ -1001,6 +1050,63 @@ class Instruction:
global_state.mstate.stack.pop(), global_state.mstate.stack.pop(),
global_state.mstate.stack.pop(), global_state.mstate.stack.pop(),
) )
code = global_state.environment.code.bytecode
if code[0:2] == "0x":
code = code[2:]
code_size = len(code) // 2
if isinstance(global_state.current_transaction, ContractCreationTransaction):
# Treat creation code after the expected disassembly as calldata.
# This is a slightly hacky way to ensure that symbolic constructor
# arguments work correctly.
mstate = global_state.mstate
offset = code_offset - code_size
log.debug("Copying from code offset: {} with size: {}".format(offset, size))
if isinstance(global_state.environment.calldata, SymbolicCalldata):
if code_offset >= code_size:
return self._calldata_copy_helper(
global_state, mstate, memory_offset, offset, size
)
else:
# Copy from both code and calldata appropriately.
concrete_code_offset = helper.get_concrete_int(code_offset)
concrete_size = helper.get_concrete_int(size)
code_copy_offset = concrete_code_offset
code_copy_size = (
concrete_size
if concrete_code_offset + concrete_size <= code_size
else code_size - concrete_code_offset
)
code_copy_size = code_copy_size if code_copy_size >= 0 else 0
calldata_copy_offset = (
concrete_code_offset - code_size
if concrete_code_offset - code_size > 0
else 0
)
calldata_copy_size = concrete_code_offset + concrete_size - code_size
calldata_copy_size = (
calldata_copy_size if calldata_copy_size >= 0 else 0
)
[global_state] = self._code_copy_helper(
code=global_state.environment.code.bytecode,
memory_offset=memory_offset,
code_offset=code_copy_offset,
size=code_copy_size,
op="CODECOPY",
global_state=global_state,
)
return self._calldata_copy_helper(
global_state=global_state,
mstate=mstate,
mstart=memory_offset + code_copy_size,
dstart=calldata_copy_offset,
size=calldata_copy_size,
)
return self._code_copy_helper( return self._code_copy_helper(
code=global_state.environment.code.bytecode, code=global_state.environment.code.bytecode,
memory_offset=memory_offset, memory_offset=memory_offset,
@ -1041,9 +1147,9 @@ class Instruction:
@staticmethod @staticmethod
def _code_copy_helper( def _code_copy_helper(
code: str, code: str,
memory_offset: BitVec, memory_offset: Union[int, BitVec],
code_offset: BitVec, code_offset: Union[int, BitVec],
size: BitVec, size: Union[int, BitVec],
op: str, op: str,
global_state: GlobalState, global_state: GlobalState,
) -> List[GlobalState]: ) -> List[GlobalState]:
@ -1089,13 +1195,6 @@ class Instruction:
if code[0:2] == "0x": if code[0:2] == "0x":
code = code[2:] code = code[2:]
if concrete_size == 0 and isinstance(
global_state.current_transaction, ContractCreationTransaction
):
if concrete_code_offset >= len(code) // 2:
Instruction._handle_symbolic_args(global_state, concrete_memory_offset)
return [global_state]
for i in range(concrete_size): for i in range(concrete_size):
if 2 * (concrete_code_offset + i + 1) <= len(code): if 2 * (concrete_code_offset + i + 1) <= len(code):
global_state.mstate.memory[concrete_memory_offset + i] = int( global_state.mstate.memory[concrete_memory_offset + i] = int(
@ -1155,18 +1254,26 @@ class Instruction:
global_state=global_state, global_state=global_state,
) )
@StateTransition @StateTransition()
def extcodehash_(self, global_state: GlobalState) -> List[GlobalState]: def extcodehash_(self, global_state: GlobalState) -> List[GlobalState]:
""" """
:param global_state: :param global_state:
:return: List of global states possible, list of size 1 in this case :return: List of global states possible, list of size 1 in this case
""" """
# TODO: To be implemented world_state = global_state.world_state
address = global_state.mstate.stack.pop() stack = global_state.mstate.stack
global_state.mstate.stack.append( address = Extract(159, 0, stack.pop())
global_state.new_bitvec("extcodehash_{}".format(str(address)), 256)
) if address.symbolic:
code_hash = symbol_factory.BitVecVal(int(get_code_hash(""), 16), 256)
elif address.value not in world_state.accounts:
code_hash = symbol_factory.BitVecVal(0, 256)
else:
addr = "0" * (40 - len(hex(address.value)[2:])) + hex(address.value)[2:]
code = world_state.accounts_exist_or_load(addr, self.dynamic_loader)
code_hash = symbol_factory.BitVecVal(int(get_code_hash(code), 16), 256)
stack.append(code_hash)
return [global_state] return [global_state]
@StateTransition() @StateTransition()
@ -1368,11 +1475,10 @@ class Instruction:
state = global_state.mstate state = global_state.mstate
index = state.stack.pop() index = state.stack.pop()
state.stack.append(global_state.environment.active_account.storage[index]) state.stack.append(global_state.environment.active_account.storage[index])
return [global_state] return [global_state]
@StateTransition() @StateTransition(is_state_mutation_instruction=True)
def sstore_(self, global_state: GlobalState) -> List[GlobalState]: def sstore_(self, global_state: GlobalState) -> List[GlobalState]:
""" """
@ -1381,7 +1487,6 @@ class Instruction:
""" """
state = global_state.mstate state = global_state.mstate
index, value = state.stack.pop(), state.stack.pop() index, value = state.stack.pop(), state.stack.pop()
global_state.environment.active_account.storage[index] = value global_state.environment.active_account.storage[index] = value
return [global_state] return [global_state]
@ -1540,7 +1645,7 @@ class Instruction:
global_state.mstate.stack.append(global_state.new_bitvec("gas", 256)) global_state.mstate.stack.append(global_state.new_bitvec("gas", 256))
return [global_state] return [global_state]
@StateTransition() @StateTransition(is_state_mutation_instruction=True)
def log_(self, global_state: GlobalState) -> List[GlobalState]: def log_(self, global_state: GlobalState) -> List[GlobalState]:
""" """
@ -1555,37 +1660,112 @@ class Instruction:
# Not supported # Not supported
return [global_state] return [global_state]
@StateTransition() def _create_transaction_helper(
self, global_state, call_value, mem_offset, mem_size, create2_salt=None
):
mstate = global_state.mstate
environment = global_state.environment
world_state = global_state.world_state
call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size)
code_raw = []
code_end = call_data.size
for i in range(call_data.size):
if call_data[i].symbolic:
code_end = i
break
code_raw.append(call_data[i].value)
code_str = bytes.hex(bytes(code_raw))
next_transaction_id = get_next_transaction_id()
constructor_arguments = ConcreteCalldata(
next_transaction_id, call_data[code_end:]
)
code = Disassembly(code_str)
caller = environment.active_account.address
gas_price = environment.gasprice
origin = environment.origin
contract_address = None
if create2_salt:
salt = hex(create2_salt)[2:]
salt = "0" * (64 - len(salt)) + salt
addr = hex(caller.value)[2:]
addr = "0" * (40 - len(addr)) + addr
Instruction._sha3_gas_helper(global_state, len(code_str[2:] // 2))
contract_address = int(
get_code_hash("0xff" + addr + salt + get_code_hash(code_str)[2:])[26:],
16,
)
transaction = ContractCreationTransaction(
world_state=world_state,
caller=caller,
code=code,
call_data=constructor_arguments,
gas_price=gas_price,
gas_limit=mstate.gas_limit,
origin=origin,
call_value=call_value,
contract_address=contract_address,
)
raise TransactionStartSignal(transaction, self.op_code, global_state)
@StateTransition(is_state_mutation_instruction=True)
def create_(self, global_state: GlobalState) -> List[GlobalState]: def create_(self, global_state: GlobalState) -> List[GlobalState]:
""" """
:param global_state: :param global_state:
:return: :return:
""" """
# TODO: implement me call_value, mem_offset, mem_size = global_state.mstate.pop(3)
state = global_state.mstate
state.stack.pop(), state.stack.pop(), state.stack.pop() return self._create_transaction_helper(
# Not supported global_state, call_value, mem_offset, mem_size
state.stack.append(0) )
return [global_state]
@StateTransition() @StateTransition()
def create_post(self, global_state: GlobalState) -> List[GlobalState]:
call_value, mem_offset, mem_size = global_state.mstate.pop(3)
call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size)
if global_state.last_return_data:
return_val = symbol_factory.BitVecVal(
int(global_state.last_return_data, 16), 256
)
else:
return_val = symbol_factory.BitVecVal(0, 256)
global_state.mstate.stack.append(return_val)
return [global_state]
@StateTransition(is_state_mutation_instruction=True)
def create2_(self, global_state: GlobalState) -> List[GlobalState]: def create2_(self, global_state: GlobalState) -> List[GlobalState]:
""" """
:param global_state: :param global_state:
:return: :return:
""" """
# TODO: implement me call_value, mem_offset, mem_size, salt = global_state.mstate.pop(4)
state = global_state.mstate
endowment, memory_start, memory_length, salt = ( return self._create_transaction_helper(
state.stack.pop(), global_state, call_value, mem_offset, mem_size, salt
state.stack.pop(),
state.stack.pop(),
state.stack.pop(),
) )
# Not supported
state.stack.append(0) @StateTransition()
def create2_post(self, global_state: GlobalState) -> List[GlobalState]:
call_value, mem_offset, mem_size, salt = global_state.mstate.pop(4)
call_data = get_call_data(global_state, mem_offset, mem_offset + mem_size)
if global_state.last_return_data:
return_val = symbol_factory.BitVecVal(
int(global_state.last_return_data), 256
)
else:
return_val = symbol_factory.BitVecVal(0, 256)
global_state.mstate.stack.append(return_val)
return [global_state] return [global_state]
@StateTransition() @StateTransition()
@ -1605,7 +1785,7 @@ class Instruction:
return_data = state.memory[offset : offset + length] return_data = state.memory[offset : offset + length]
global_state.current_transaction.end(global_state, return_data) global_state.current_transaction.end(global_state, return_data)
@StateTransition() @StateTransition(is_state_mutation_instruction=True)
def suicide_(self, global_state: GlobalState): def suicide_(self, global_state: GlobalState):
""" """
@ -1615,7 +1795,7 @@ class Instruction:
transfer_amount = global_state.environment.active_account.balance() transfer_amount = global_state.environment.active_account.balance()
# Often the target of the suicide instruction will be symbolic # Often the target of the suicide instruction will be symbolic
# If it isn't then we'll transfer the balance to the indicated contract # If it isn't then we'll transfer the balance to the indicated contract
global_state.world_state[target].add_balance(transfer_amount) global_state.world_state.balances[target] += transfer_amount
global_state.environment.active_account = deepcopy( global_state.environment.active_account = deepcopy(
global_state.environment.active_account global_state.environment.active_account
@ -1689,6 +1869,10 @@ class Instruction:
if callee_account is not None and callee_account.code.bytecode == "": if callee_account is not None and callee_account.code.bytecode == "":
log.debug("The call is related to ether transfer between accounts") log.debug("The call is related to ether transfer between accounts")
sender = environment.active_account.address
receiver = callee_account.address
transfer_ether(global_state, sender, receiver, value)
global_state.mstate.stack.append( global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256) global_state.new_bitvec("retval_" + str(instr["address"]), 256)
) )
@ -1706,6 +1890,21 @@ class Instruction:
) )
return [global_state] return [global_state]
if environment.static:
if isinstance(value, int) and value > 0:
raise WriteProtection(
"Cannot call with non zero value in a static call"
)
if isinstance(value, BitVec):
if value.symbolic:
global_state.mstate.constraints.append(
value == symbol_factory.BitVecVal(0, 256)
)
elif value.value > 0:
raise WriteProtection(
"Cannot call with non zero value in a static call"
)
native_result = native_call( native_result = native_call(
global_state, callee_address, call_data, memory_out_offset, memory_out_size global_state, callee_address, call_data, memory_out_offset, memory_out_size
) )
@ -1720,20 +1919,81 @@ class Instruction:
callee_account=callee_account, callee_account=callee_account,
call_data=call_data, call_data=call_data,
call_value=value, call_value=value,
static=environment.static,
) )
raise TransactionStartSignal(transaction, self.op_code) raise TransactionStartSignal(transaction, self.op_code, global_state)
@StateTransition() @StateTransition()
def call_post(self, global_state: GlobalState) -> List[GlobalState]: def call_post(self, global_state: GlobalState) -> List[GlobalState]:
""" """
:param global_state:
:return:
"""
return self.post_handler(global_state, function_name="call")
@StateTransition()
def callcode_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
instr = global_state.get_current_instruction()
environment = global_state.environment
try:
callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters(
global_state, self.dynamic_loader, True
)
if callee_account is not None and callee_account.code.bytecode == "":
log.debug("The call is related to ether transfer between accounts")
sender = global_state.environment.active_account.address
receiver = callee_account.address
transfer_ether(global_state, sender, receiver, value)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
except ValueError as e:
log.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(
e
)
)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
transaction = MessageCallTransaction(
world_state=global_state.world_state,
gas_price=environment.gasprice,
gas_limit=gas,
origin=environment.origin,
code=callee_account.code,
caller=environment.address,
callee_account=environment.active_account,
call_data=call_data,
call_value=value,
static=environment.static,
)
raise TransactionStartSignal(transaction, self.op_code, global_state)
@StateTransition()
def callcode_post(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state: :param global_state:
:return: :return:
""" """
instr = global_state.get_current_instruction() instr = global_state.get_current_instruction()
try: try:
callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters( callee_address, _, _, value, _, memory_out_offset, memory_out_size = get_call_parameters(
global_state, self.dynamic_loader, True global_state, self.dynamic_loader, True
) )
except ValueError as e: except ValueError as e:
@ -1754,7 +2014,6 @@ class Instruction:
) )
global_state.mstate.stack.append(return_value) global_state.mstate.stack.append(return_value)
global_state.mstate.constraints.append(return_value == 0) global_state.mstate.constraints.append(return_value == 0)
return [global_state] return [global_state]
try: try:
@ -1787,11 +2046,10 @@ class Instruction:
return_value = global_state.new_bitvec("retval_" + str(instr["address"]), 256) return_value = global_state.new_bitvec("retval_" + str(instr["address"]), 256)
global_state.mstate.stack.append(return_value) global_state.mstate.stack.append(return_value)
global_state.mstate.constraints.append(return_value == 1) global_state.mstate.constraints.append(return_value == 1)
return [global_state] return [global_state]
@StateTransition() @StateTransition()
def callcode_(self, global_state: GlobalState) -> List[GlobalState]: def delegatecall_(self, global_state: GlobalState) -> List[GlobalState]:
""" """
:param global_state: :param global_state:
@ -1802,8 +2060,20 @@ class Instruction:
try: try:
callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters( callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters(
global_state, self.dynamic_loader, True global_state, self.dynamic_loader
) )
if callee_account is not None and callee_account.code.bytecode == "":
log.debug("The call is related to ether transfer between accounts")
sender = global_state.environment.active_account.address
receiver = callee_account.address
transfer_ether(global_state, sender, receiver, value)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
except ValueError as e: except ValueError as e:
log.debug( log.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(
@ -1821,15 +2091,16 @@ class Instruction:
gas_limit=gas, gas_limit=gas,
origin=environment.origin, origin=environment.origin,
code=callee_account.code, code=callee_account.code,
caller=environment.address, caller=environment.sender,
callee_account=environment.active_account, callee_account=environment.active_account,
call_data=call_data, call_data=call_data,
call_value=value, call_value=environment.callvalue,
static=environment.static,
) )
raise TransactionStartSignal(transaction, self.op_code) raise TransactionStartSignal(transaction, self.op_code, global_state)
@StateTransition() @StateTransition()
def callcode_post(self, global_state: GlobalState) -> List[GlobalState]: def delegatecall_post(self, global_state: GlobalState) -> List[GlobalState]:
""" """
:param global_state: :param global_state:
@ -1839,7 +2110,7 @@ class Instruction:
try: try:
callee_address, _, _, value, _, memory_out_offset, memory_out_size = get_call_parameters( callee_address, _, _, value, _, memory_out_offset, memory_out_size = get_call_parameters(
global_state, self.dynamic_loader, True global_state, self.dynamic_loader
) )
except ValueError as e: except ValueError as e:
log.debug( log.debug(
@ -1878,7 +2149,7 @@ class Instruction:
) )
return [global_state] return [global_state]
# Copy memory # Copy memory
global_state.mstate.mem_extend( global_state.mstate.mem_extend(
memory_out_offset, min(memory_out_size, len(global_state.last_return_data)) memory_out_offset, min(memory_out_size, len(global_state.last_return_data))
) )
@ -1894,7 +2165,7 @@ class Instruction:
return [global_state] return [global_state]
@StateTransition() @StateTransition()
def delegatecall_(self, global_state: GlobalState) -> List[GlobalState]: def staticcall_(self, global_state: GlobalState) -> List[GlobalState]:
""" """
:param global_state: :param global_state:
@ -1902,11 +2173,22 @@ class Instruction:
""" """
instr = global_state.get_current_instruction() instr = global_state.get_current_instruction()
environment = global_state.environment environment = global_state.environment
try: try:
callee_address, callee_account, call_data, value, gas, _, _ = get_call_parameters( callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters(
global_state, self.dynamic_loader global_state, self.dynamic_loader
) )
if callee_account is not None and callee_account.code.bytecode == "":
log.debug("The call is related to ether transfer between accounts")
sender = environment.active_account.address
receiver = callee_account.address
transfer_ether(global_state, sender, receiver, value)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
except ValueError as e: except ValueError as e:
log.debug( log.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( "Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(
@ -1918,36 +2200,43 @@ class Instruction:
) )
return [global_state] return [global_state]
native_result = native_call(
global_state, callee_address, call_data, memory_out_offset, memory_out_size
)
if native_result:
return native_result
transaction = MessageCallTransaction( transaction = MessageCallTransaction(
world_state=global_state.world_state, world_state=global_state.world_state,
gas_price=environment.gasprice, gas_price=environment.gasprice,
gas_limit=gas, gas_limit=gas,
origin=environment.origin, origin=environment.origin,
code=callee_account.code, code=callee_account.code,
caller=environment.sender, caller=environment.address,
callee_account=environment.active_account, callee_account=callee_account,
call_data=call_data, call_data=call_data,
call_value=environment.callvalue, call_value=value,
static=True,
) )
raise TransactionStartSignal(transaction, self.op_code) raise TransactionStartSignal(transaction, self.op_code, global_state)
@StateTransition() @StateTransition()
def delegatecall_post(self, global_state: GlobalState) -> List[GlobalState]: def staticcall_post(self, global_state: GlobalState) -> List[GlobalState]:
""" return self.post_handler(global_state, function_name="staticcall")
:param global_state: def post_handler(self, global_state, function_name: str):
:return:
"""
instr = global_state.get_current_instruction() instr = global_state.get_current_instruction()
try: try:
callee_address, _, _, value, _, memory_out_offset, memory_out_size = get_call_parameters( with_value = function_name is not "staticcall"
global_state, self.dynamic_loader callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters(
global_state, self.dynamic_loader, with_value
) )
except ValueError as e: except ValueError as e:
log.debug( log.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format( "Could not determine required parameters for {}, putting fresh symbol on the stack. \n{}".format(
e function_name, e
) )
) )
global_state.mstate.stack.append( global_state.mstate.stack.append(
@ -1980,7 +2269,7 @@ class Instruction:
) )
return [global_state] return [global_state]
# Copy memory # Copy memory
global_state.mstate.mem_extend( global_state.mstate.mem_extend(
memory_out_offset, min(memory_out_size, len(global_state.last_return_data)) memory_out_offset, min(memory_out_size, len(global_state.last_return_data))
) )
@ -1993,39 +2282,5 @@ class Instruction:
return_value = global_state.new_bitvec("retval_" + str(instr["address"]), 256) return_value = global_state.new_bitvec("retval_" + str(instr["address"]), 256)
global_state.mstate.stack.append(return_value) global_state.mstate.stack.append(return_value)
global_state.mstate.constraints.append(return_value == 1) global_state.mstate.constraints.append(return_value == 1)
return [global_state]
@StateTransition()
def staticcall_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
# TODO: implement me
instr = global_state.get_current_instruction()
try:
callee_address, callee_account, call_data, value, gas, memory_out_offset, memory_out_size = get_call_parameters(
global_state, self.dynamic_loader
)
except ValueError as e:
log.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(
e
)
)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
native_result = native_call(
global_state, callee_address, call_data, memory_out_offset, memory_out_size
)
if native_result:
return native_result
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state] return [global_state]

@ -6,12 +6,20 @@ from typing import List, Union
from ethereum.utils import ecrecover_to_pub from ethereum.utils import ecrecover_to_pub
from py_ecc.secp256k1 import N as secp256k1n from py_ecc.secp256k1 import N as secp256k1n
import py_ecc.optimized_bn128 as bn128
from rlp.utils import ALL_BYTES from rlp.utils import ALL_BYTES
from mythril.laser.ethereum.state.calldata import BaseCalldata, ConcreteCalldata from mythril.laser.ethereum.state.calldata import BaseCalldata, ConcreteCalldata
from mythril.laser.ethereum.util import bytearray_to_int from mythril.laser.ethereum.util import extract_copy, extract32
from ethereum.utils import sha3 from ethereum.utils import (
from mythril.laser.smt import Concat, simplify sha3,
big_endian_to_int,
safe_ord,
zpad,
int_to_big_endian,
encode_int32,
)
from ethereum.specials import validate_point
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -22,35 +30,6 @@ class NativeContractException(Exception):
pass pass
def int_to_32bytes(
i: int
) -> bytes: # used because int can't fit as bytes function's input
"""
:param i:
:return:
"""
o = [0] * 32
for x in range(32):
o[31 - x] = i & 0xFF
i >>= 8
return bytes(o)
def extract32(data: bytearray, i: int) -> int:
"""
:param data:
:param i:
:return:
"""
if i >= len(data):
return 0
o = data[i : min(i + 32, len(data))]
o.extend(bytearray(32 - len(o)))
return bytearray_to_int(o)
def ecrecover(data: List[int]) -> List[int]: def ecrecover(data: List[int]) -> List[int]:
""" """
@ -59,14 +38,14 @@ def ecrecover(data: List[int]) -> List[int]:
""" """
# TODO: Add type hints # TODO: Add type hints
try: try:
byte_data = bytearray(data) bytes_data = bytearray(data)
v = extract32(byte_data, 32) v = extract32(bytes_data, 32)
r = extract32(byte_data, 64) r = extract32(bytes_data, 64)
s = extract32(byte_data, 96) s = extract32(bytes_data, 96)
except TypeError: except TypeError:
raise NativeContractException raise NativeContractException
message = b"".join([ALL_BYTES[x] for x in byte_data[0:32]]) message = b"".join([ALL_BYTES[x] for x in bytes_data[0:32]])
if r >= secp256k1n or s >= secp256k1n or v < 27 or v > 28: if r >= secp256k1n or s >= secp256k1n or v < 27 or v > 28:
return [] return []
try: try:
@ -85,10 +64,10 @@ def sha256(data: List[int]) -> List[int]:
:return: :return:
""" """
try: try:
byte_data = bytes(data) bytes_data = bytes(data)
except TypeError: except TypeError:
raise NativeContractException raise NativeContractException
return list(bytearray(hashlib.sha256(byte_data).digest())) return list(bytearray(hashlib.sha256(bytes_data).digest()))
def ripemd160(data: List[int]) -> List[int]: def ripemd160(data: List[int]) -> List[int]:
@ -120,6 +99,114 @@ def identity(data: List[int]) -> List[int]:
return data return data
def mod_exp(data: List[int]) -> List[int]:
"""
TODO: Some symbolic parts can be handled here
Modular Exponentiation
:param data: Data with <length_of_BASE> <length_of_EXPONENT> <length_of_MODULUS> <BASE> <EXPONENT> <MODULUS>
:return: modular exponentiation
"""
bytes_data = bytearray(data)
baselen = extract32(bytes_data, 0)
explen = extract32(bytes_data, 32)
modlen = extract32(bytes_data, 64)
if baselen == 0:
return [0] * modlen
if modlen == 0:
return []
first_exp_bytes = extract32(bytes_data, 96 + baselen) >> (8 * max(32 - explen, 0))
bitlength = -1
while first_exp_bytes:
bitlength += 1
first_exp_bytes >>= 1
base = bytearray(baselen)
extract_copy(bytes_data, base, 0, 96, baselen)
exp = bytearray(explen)
extract_copy(bytes_data, exp, 0, 96 + baselen, explen)
mod = bytearray(modlen)
extract_copy(bytes_data, mod, 0, 96 + baselen + explen, modlen)
if big_endian_to_int(mod) == 0:
return [0] * modlen
o = pow(big_endian_to_int(base), big_endian_to_int(exp), big_endian_to_int(mod))
return [safe_ord(x) for x in zpad(int_to_big_endian(o), modlen)]
def ec_add(data: List[int]) -> List[int]:
bytes_data = bytearray(data)
x1 = extract32(bytes_data, 0)
y1 = extract32(bytes_data, 32)
x2 = extract32(bytes_data, 64)
y2 = extract32(bytes_data, 96)
p1 = validate_point(x1, y1)
p2 = validate_point(x2, y2)
if p1 is False or p2 is False:
return []
o = bn128.normalize(bn128.add(p1, p2))
return [safe_ord(x) for x in (encode_int32(o[0].n) + encode_int32(o[1].n))]
def ec_mul(data: List[int]) -> List[int]:
bytes_data = bytearray(data)
x = extract32(bytes_data, 0)
y = extract32(bytes_data, 32)
m = extract32(bytes_data, 64)
p = validate_point(x, y)
if p is False:
return []
o = bn128.normalize(bn128.multiply(p, m))
return [safe_ord(c) for c in (encode_int32(o[0].n) + encode_int32(o[1].n))]
def ec_pair(data: List[int]) -> List[int]:
if len(data) % 192:
return []
zero = (bn128.FQ2.one(), bn128.FQ2.one(), bn128.FQ2.zero())
exponent = bn128.FQ12.one()
bytes_data = bytearray(data)
for i in range(0, len(bytes_data), 192):
x1 = extract32(bytes_data, i)
y1 = extract32(bytes_data, i + 32)
x2_i = extract32(bytes_data, i + 64)
x2_r = extract32(bytes_data, i + 96)
y2_i = extract32(bytes_data, i + 128)
y2_r = extract32(bytes_data, i + 160)
p1 = validate_point(x1, y1)
if p1 is False:
return []
for v in (x2_i, x2_r, y2_i, y2_r):
if v >= bn128.field_modulus:
return []
fq2_x = bn128.FQ2([x2_r, x2_i])
fq2_y = bn128.FQ2([y2_r, y2_i])
if (fq2_x, fq2_y) != (bn128.FQ2.zero(), bn128.FQ2.zero()):
p2 = (fq2_x, fq2_y, bn128.FQ2.one())
if not bn128.is_on_curve(p2, bn128.b2):
return []
else:
p2 = zero
if bn128.multiply(p2, bn128.curve_order)[-1] != bn128.FQ2.zero():
return []
exponent *= bn128.pairing(p2, p1, final_exponentiate=False)
result = bn128.final_exponentiate(exponent) == bn128.FQ12.one()
return [0] * 31 + [1 if result else 0]
PRECOMPILE_FUNCTIONS = (
ecrecover,
sha256,
ripemd160,
identity,
mod_exp,
ec_add,
ec_mul,
ec_pair,
)
PRECOMPILE_COUNT = len(PRECOMPILE_FUNCTIONS)
def native_contracts(address: int, data: BaseCalldata) -> List[int]: def native_contracts(address: int, data: BaseCalldata) -> List[int]:
"""Takes integer address 1, 2, 3, 4. """Takes integer address 1, 2, 3, 4.
@ -127,11 +214,10 @@ def native_contracts(address: int, data: BaseCalldata) -> List[int]:
:param data: :param data:
:return: :return:
""" """
functions = (ecrecover, sha256, ripemd160, identity)
if isinstance(data, ConcreteCalldata): if isinstance(data, ConcreteCalldata):
concrete_data = data.concrete(None) concrete_data = data.concrete(None)
else: else:
raise NativeContractException() raise NativeContractException()
return functions[address - 1](concrete_data) return PRECOMPILE_FUNCTIONS[address - 1](concrete_data)

@ -1,7 +1,10 @@
from mythril.laser.ethereum.svm import LaserEVM from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.ethereum.plugins.plugin import LaserPlugin from mythril.laser.ethereum.plugins.plugin import LaserPlugin
from mythril.laser.ethereum.plugins.signals import PluginSkipState from mythril.laser.ethereum.plugins.signals import PluginSkipState
from mythril.laser.ethereum.state.annotation import StateAnnotation from mythril.laser.ethereum.plugins.implementations.plugin_annotations import (
DependencyAnnotation,
WSDependencyAnnotation,
)
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.transaction.transaction_models import ( from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction, ContractCreationTransaction,
@ -9,62 +12,12 @@ from mythril.laser.ethereum.transaction.transaction_models import (
from mythril.exceptions import UnsatError from mythril.exceptions import UnsatError
from mythril.analysis import solver from mythril.analysis import solver
from typing import cast, List, Dict, Set from typing import cast, List, Dict, Set
from copy import copy
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class DependencyAnnotation(StateAnnotation):
"""Dependency Annotation
This annotation tracks read and write access to the state during each transaction.
"""
def __init__(self):
self.storage_loaded = [] # type: List
self.storage_written = {} # type: Dict[int, List]
self.path = [0] # type: List
self.blocks_seen = set() # type: Set[int]
def __copy__(self):
result = DependencyAnnotation()
result.storage_loaded = copy(self.storage_loaded)
result.storage_written = copy(self.storage_written)
result.path = copy(self.path)
result.blocks_seen = copy(self.blocks_seen)
return result
def get_storage_write_cache(self, iteration: int):
if iteration not in self.storage_written:
self.storage_written[iteration] = []
return self.storage_written[iteration]
def extend_storage_write_cache(self, iteration: int, value: object):
if iteration not in self.storage_written:
self.storage_written[iteration] = [value]
elif value not in self.storage_written[iteration]:
self.storage_written[iteration].append(value)
class WSDependencyAnnotation(StateAnnotation):
"""Dependency Annotation for World state
This world state annotation maintains a stack of state annotations.
It is used to transfer individual state annotations from one transaction to the next.
"""
def __init__(self):
self.annotations_stack = []
def __copy__(self):
result = WSDependencyAnnotation()
result.annotations_stack = copy(self.annotations_stack)
return result
def get_dependency_annotation(state: GlobalState) -> DependencyAnnotation: def get_dependency_annotation(state: GlobalState) -> DependencyAnnotation:
""" Returns a dependency annotation """ Returns a dependency annotation
@ -134,6 +87,7 @@ class DependencyPruner(LaserPlugin):
def _reset(self): def _reset(self):
self.iteration = 0 self.iteration = 0
self.calls_on_path = {} # type: Dict[int, bool]
self.sloads_on_path = {} # type: Dict[int, List[object]] self.sloads_on_path = {} # type: Dict[int, List[object]]
self.sstores_on_path = {} # type: Dict[int, List[object]] self.sstores_on_path = {} # type: Dict[int, List[object]]
self.storage_accessed_global = set() # type: Set self.storage_accessed_global = set() # type: Set
@ -166,6 +120,17 @@ class DependencyPruner(LaserPlugin):
else: else:
self.sstores_on_path[address] = [target_location] self.sstores_on_path[address] = [target_location]
def update_calls(self, path: List[int]) -> None:
"""Update the dependency map for the block offsets on the given path.
:param path
:param target_location
"""
for address in path:
if address in self.sstores_on_path:
self.calls_on_path[address] = True
def wanna_execute(self, address: int, annotation: DependencyAnnotation) -> bool: def wanna_execute(self, address: int, annotation: DependencyAnnotation) -> bool:
"""Decide whether the basic block starting at 'address' should be executed. """Decide whether the basic block starting at 'address' should be executed.
@ -175,6 +140,9 @@ class DependencyPruner(LaserPlugin):
storage_write_cache = annotation.get_storage_write_cache(self.iteration - 1) storage_write_cache = annotation.get_storage_write_cache(self.iteration - 1)
if address in self.calls_on_path:
return True
# Skip "pure" paths that don't have any dependencies. # Skip "pure" paths that don't have any dependencies.
if address not in self.sloads_on_path: if address not in self.sloads_on_path:
@ -270,6 +238,13 @@ class DependencyPruner(LaserPlugin):
self.update_sloads(annotation.path, location) self.update_sloads(annotation.path, location)
self.storage_accessed_global.add(location) self.storage_accessed_global.add(location)
@symbolic_vm.pre_hook("CALL")
def call_hook(state: GlobalState):
annotation = get_dependency_annotation(state)
self.update_calls(annotation.path)
annotation.has_call = True
@symbolic_vm.pre_hook("STOP") @symbolic_vm.pre_hook("STOP")
def stop_hook(state: GlobalState): def stop_hook(state: GlobalState):
_transaction_end(state) _transaction_end(state)
@ -293,15 +268,18 @@ class DependencyPruner(LaserPlugin):
for index in annotation.storage_written: for index in annotation.storage_written:
self.update_sstores(annotation.path, index) self.update_sstores(annotation.path, index)
if annotation.has_call:
self.update_calls(annotation.path)
def _check_basic_block(address: int, annotation: DependencyAnnotation): def _check_basic_block(address: int, annotation: DependencyAnnotation):
"""This method is where the actual pruning happens. """This method is where the actual pruning happens.
:param address: Start address (bytecode offset) of the block :param address: Start address (bytecode offset) of the block
:param annotation :param annotation:
""" """
# Don't skip any blocks in the contract creation transaction # Don't skip any blocks in the contract creation transaction
if self.iteration < 1: if self.iteration < 2:
return return
# Don't skip newly discovered blocks # Don't skip newly discovered blocks
@ -338,13 +316,3 @@ class DependencyPruner(LaserPlugin):
annotation.storage_loaded = [] annotation.storage_loaded = []
world_state_annotation.annotations_stack.append(annotation) world_state_annotation.annotations_stack.append(annotation)
log.debug(
"Iteration {}: Adding world state at address {}, end of function {}.\nDependency map: {}\nStorage written: {}".format(
self.iteration,
state.get_current_instruction()["address"],
state.node.function_name,
self.sloads_on_path,
annotation.storage_written[self.iteration],
)
)

@ -1,23 +1,16 @@
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState
from mythril.laser.ethereum.plugins.plugin import LaserPlugin from mythril.laser.ethereum.plugins.plugin import LaserPlugin
from mythril.laser.ethereum.plugins.implementations.plugin_annotations import (
MutationAnnotation,
)
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.ethereum.transaction.transaction_models import ( from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction, ContractCreationTransaction,
) )
from mythril.laser.smt import And, symbol_factory from mythril.laser.smt import And, symbol_factory
class MutationAnnotation(StateAnnotation):
"""Mutation Annotation
This is the annotation used by the MutationPruner plugin to record mutations
"""
pass
class MutationPruner(LaserPlugin): class MutationPruner(LaserPlugin):
"""Mutation pruner plugin """Mutation pruner plugin
@ -42,7 +35,11 @@ class MutationPruner(LaserPlugin):
""" """
@symbolic_vm.pre_hook("SSTORE") @symbolic_vm.pre_hook("SSTORE")
def mutator_hook(global_state: GlobalState): def sstore_mutator_hook(global_state: GlobalState):
global_state.annotate(MutationAnnotation())
@symbolic_vm.pre_hook("CALL")
def call_mutator_hook(global_state: GlobalState):
global_state.annotate(MutationAnnotation()) global_state.annotate(MutationAnnotation())
@symbolic_vm.laser_hook("add_world_state") @symbolic_vm.laser_hook("add_world_state")

@ -0,0 +1,65 @@
from mythril.laser.ethereum.state.annotation import StateAnnotation
from copy import copy
from typing import Dict, List, Set
class MutationAnnotation(StateAnnotation):
"""Mutation Annotation
This is the annotation used by the MutationPruner plugin to record mutations
"""
def __init__(self):
pass
class DependencyAnnotation(StateAnnotation):
"""Dependency Annotation
This annotation tracks read and write access to the state during each transaction.
"""
def __init__(self):
self.storage_loaded = [] # type: List
self.storage_written = {} # type: Dict[int, List]
self.has_call = False # type: bool
self.path = [0] # type: List
self.blocks_seen = set() # type: Set[int]
def __copy__(self):
result = DependencyAnnotation()
result.storage_loaded = copy(self.storage_loaded)
result.storage_written = copy(self.storage_written)
result.has_call = self.has_call
result.path = copy(self.path)
result.blocks_seen = copy(self.blocks_seen)
return result
def get_storage_write_cache(self, iteration: int):
if iteration not in self.storage_written:
self.storage_written[iteration] = []
return self.storage_written[iteration]
def extend_storage_write_cache(self, iteration: int, value: object):
if iteration not in self.storage_written:
self.storage_written[iteration] = [value]
elif value not in self.storage_written[iteration]:
self.storage_written[iteration].append(value)
class WSDependencyAnnotation(StateAnnotation):
"""Dependency Annotation for World state
This world state annotation maintains a stack of state annotations.
It is used to transfer individual state annotations from one transaction to the next.
"""
def __init__(self):
self.annotations_stack = []
def __copy__(self):
result = WSDependencyAnnotation()
result.annotations_stack = copy(self.annotations_stack)
return result

@ -2,8 +2,9 @@
This includes classes representing accounts and their storage. This includes classes representing accounts and their storage.
""" """
import logging
from copy import copy, deepcopy from copy import copy, deepcopy
from typing import Any, Dict, Union, Tuple, cast from typing import Any, Dict, Union, Tuple, Set, cast
from mythril.laser.smt import ( from mythril.laser.smt import (
@ -19,6 +20,28 @@ from mythril.laser.smt import (
from mythril.disassembler.disassembly import Disassembly from mythril.disassembler.disassembly import Disassembly
from mythril.laser.smt import symbol_factory from mythril.laser.smt import symbol_factory
log = logging.getLogger(__name__)
class StorageRegion:
def __getitem__(self, item):
raise NotImplementedError
def __setitem__(self, key, value):
raise NotImplementedError
class ArrayStorageRegion(StorageRegion):
""" An ArrayStorageRegion is a storage region that leverages smt array theory to resolve expressions"""
pass
class IteStorageRegion(StorageRegion):
""" An IteStorageRegion is a storage region that uses Ite statements to implement a storage"""
pass
class Storage: class Storage:
"""Storage class represents the storage of an Account.""" """Storage class represents the storage of an Account."""
@ -37,6 +60,7 @@ class Storage:
self.printable_storage = {} # type: Dict[BitVec, BitVec] self.printable_storage = {} # type: Dict[BitVec, BitVec]
self.dynld = dynamic_loader self.dynld = dynamic_loader
self.storage_keys_loaded = set() # type: Set[int]
self.address = address self.address = address
@staticmethod @staticmethod
@ -51,17 +75,18 @@ class Storage:
def __getitem__(self, item: BitVec) -> BitVec: def __getitem__(self, item: BitVec) -> BitVec:
storage, is_keccak_storage = self._get_corresponding_storage(item) storage, is_keccak_storage = self._get_corresponding_storage(item)
if is_keccak_storage: if is_keccak_storage:
item = self._sanitize(cast(BitVecFunc, item).input_) sanitized_item = self._sanitize(cast(BitVecFunc, item).input_)
value = storage[item] else:
sanitized_item = item
if ( if (
value.value == 0 self.address
and self.address
and item.symbolic is False
and self.address.value != 0 and self.address.value != 0
and item.symbolic is False
and int(item.value) not in self.storage_keys_loaded
and (self.dynld and self.dynld.storage_loading) and (self.dynld and self.dynld.storage_loading)
): ):
try: try:
storage[item] = symbol_factory.BitVecVal( storage[sanitized_item] = symbol_factory.BitVecVal(
int( int(
self.dynld.read_storage( self.dynld.read_storage(
contract_address="0x{:040X}".format(self.address.value), contract_address="0x{:040X}".format(self.address.value),
@ -71,12 +96,12 @@ class Storage:
), ),
256, 256,
) )
self.printable_storage[item] = storage[item] self.storage_keys_loaded.add(int(item.value))
return storage[item] self.printable_storage[item] = storage[sanitized_item]
except ValueError: except ValueError as e:
pass log.debug("Couldn't read storage at %s: %s", item, e)
return simplify(storage[item]) return simplify(storage[sanitized_item])
@staticmethod @staticmethod
def get_map_index(key: BitVec) -> BitVec: def get_map_index(key: BitVec) -> BitVec:
@ -113,15 +138,18 @@ class Storage:
if is_keccak_storage: if is_keccak_storage:
key = self._sanitize(key.input_) key = self._sanitize(key.input_)
storage[key] = value storage[key] = value
if key.symbolic is False:
self.storage_keys_loaded.add(int(key.value))
def __deepcopy__(self, memodict={}): def __deepcopy__(self, memodict=dict()):
concrete = isinstance(self._standard_storage, K) concrete = isinstance(self._standard_storage, K)
storage = Storage( storage = Storage(
concrete=concrete, address=self.address, dynamic_loader=self.dynld concrete=concrete, address=self.address, dynamic_loader=self.dynld
) )
storage._standard_storage = deepcopy(self._standard_storage) storage._standard_storage = deepcopy(self._standard_storage)
storage._map_storage = deepcopy(self._map_storage) storage._map_storage = deepcopy(self._map_storage)
storage.print_storage = copy(self.printable_storage) storage.printable_storage = copy(self.printable_storage)
storage.storage_keys_loaded = copy(self.storage_keys_loaded)
return storage return storage
def __str__(self) -> str: def __str__(self) -> str:

@ -12,6 +12,8 @@ class StateAnnotation:
traverse the state space themselves. traverse the state space themselves.
""" """
# TODO: Remove this? It seems to be used only in the MutationPruner, and
# we could simply use world state annotations if we want them to be persisted.
@property @property
def persist_to_world_state(self) -> bool: def persist_to_world_state(self) -> bool:
"""If this function returns true then laser will also annotate the """If this function returns true then laser will also annotate the

@ -70,7 +70,7 @@ class BaseCalldata:
try: try:
current_index = ( current_index = (
start start
if isinstance(start, Expression) if isinstance(start, BitVec)
else symbol_factory.BitVecVal(start, 256) else symbol_factory.BitVecVal(start, 256)
) )
parts = [] parts = []

@ -22,6 +22,7 @@ class Environment:
callvalue: ExprRef, callvalue: ExprRef,
origin: ExprRef, origin: ExprRef,
code=None, code=None,
static=False,
) -> None: ) -> None:
""" """
@ -32,7 +33,7 @@ class Environment:
:param callvalue: :param callvalue:
:param origin: :param origin:
:param code: :param code:
:param calldata_type: :param static: Makes the environment static.
""" """
# Metadata # Metadata
@ -50,6 +51,7 @@ class Environment:
self.gasprice = gasprice self.gasprice = gasprice
self.origin = origin self.origin = origin
self.callvalue = callvalue self.callvalue = callvalue
self.static = static
def __str__(self) -> str: def __str__(self) -> str:
""" """

@ -9,6 +9,7 @@ from mythril.laser.ethereum.cfg import Node
from mythril.laser.ethereum.state.environment import Environment from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.machine_state import MachineState from mythril.laser.ethereum.state.machine_state import MachineState
from mythril.laser.ethereum.state.annotation import StateAnnotation from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.evm_exceptions import ProgramCounterException
if TYPE_CHECKING: if TYPE_CHECKING:
from mythril.laser.ethereum.state.world_state import WorldState from mythril.laser.ethereum.state.world_state import WorldState
@ -52,6 +53,14 @@ class GlobalState:
self.last_return_data = last_return_data self.last_return_data = last_return_data
self._annotations = annotations or [] self._annotations = annotations or []
def add_annotations(self, annotations: List[StateAnnotation]):
"""
Function used to add annotations to global state
:param annotations:
:return:
"""
self._annotations += annotations
def __copy__(self) -> "GlobalState": def __copy__(self) -> "GlobalState":
""" """
@ -88,6 +97,10 @@ class GlobalState:
""" """
instructions = self.environment.code.instruction_list instructions = self.environment.code.instruction_list
if self.mstate.pc >= len(instructions):
raise ProgramCounterException(
"PC: {} can not be reached.".format(self.mstate.pc)
)
return instructions[self.mstate.pc] return instructions[self.mstate.pc]
@property @property

@ -27,6 +27,7 @@ class WorldState:
""" """
self._accounts = {} # type: Dict[int, Account] self._accounts = {} # type: Dict[int, Account]
self.balances = Array("balance", 256, 256) self.balances = Array("balance", 256, 256)
self.starting_balances = copy(self.balances)
self.node = None # type: Optional['Node'] self.node = None # type: Optional['Node']
self.transaction_sequence = transaction_sequence or [] self.transaction_sequence = transaction_sequence or []
@ -60,6 +61,7 @@ class WorldState:
annotations=new_annotations, annotations=new_annotations,
) )
new_world_state.balances = copy(self.balances) new_world_state.balances = copy(self.balances)
new_world_state.starting_balances = copy(self.starting_balances)
for account in self._accounts.values(): for account in self._accounts.values():
new_world_state.put_account(copy(account)) new_world_state.put_account(copy(account))
new_world_state.node = self.node new_world_state.node = self.node
@ -115,7 +117,7 @@ class WorldState:
concrete_storage=concrete_storage, concrete_storage=concrete_storage,
) )
if balance: if balance:
new_account.set_balance(symbol_factory.BitVecVal(balance, 256)) new_account.add_balance(symbol_factory.BitVecVal(balance, 256))
self.put_account(new_account) self.put_account(new_account)
return new_account return new_account

@ -3,14 +3,18 @@ import logging
from collections import defaultdict from collections import defaultdict
from copy import copy from copy import copy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Callable, Dict, DefaultDict, List, Tuple, Union from typing import Callable, Dict, DefaultDict, List, Tuple, Optional
from mythril.analysis.potential_issues import check_potential_issues
from mythril.laser.ethereum.cfg import NodeFlags, Node, Edge, JumpType from mythril.laser.ethereum.cfg import NodeFlags, Node, Edge, JumpType
from mythril.laser.ethereum.evm_exceptions import StackUnderflowException from mythril.laser.ethereum.evm_exceptions import StackUnderflowException
from mythril.laser.ethereum.evm_exceptions import VmException from mythril.laser.ethereum.evm_exceptions import VmException
from mythril.laser.ethereum.instructions import Instruction from mythril.laser.ethereum.instructions import Instruction
from mythril.laser.ethereum.iprof import InstructionProfiler from mythril.laser.ethereum.iprof import InstructionProfiler
from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState, PluginSkipState from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState, PluginSkipState
from mythril.laser.ethereum.plugins.implementations.plugin_annotations import (
MutationAnnotation,
)
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy
@ -55,6 +59,8 @@ class LaserEVM:
transaction_count=2, transaction_count=2,
requires_statespace=True, requires_statespace=True,
enable_iprof=False, enable_iprof=False,
enable_coverage_strategy=False,
instruction_laser_plugin=None,
) -> None: ) -> None:
""" """
Initializes the laser evm object Initializes the laser evm object
@ -73,6 +79,7 @@ class LaserEVM:
self.total_states = 0 self.total_states = 0
self.dynamic_loader = dynamic_loader self.dynamic_loader = dynamic_loader
# TODO: What about using a deque here?
self.work_list = [] # type: List[GlobalState] self.work_list = [] # type: List[GlobalState]
self.strategy = strategy(self.work_list, max_depth) self.strategy = strategy(self.work_list, max_depth)
self.max_depth = max_depth self.max_depth = max_depth
@ -102,6 +109,13 @@ class LaserEVM:
self.iprof = InstructionProfiler() if enable_iprof else None self.iprof = InstructionProfiler() if enable_iprof else None
if enable_coverage_strategy:
from mythril.laser.ethereum.plugins.implementations.coverage.coverage_strategy import (
CoverageStrategy,
)
self.strategy = CoverageStrategy(self.strategy, instruction_laser_plugin)
log.info("LASER EVM initialized with dynamic loader: " + str(dynamic_loader)) log.info("LASER EVM initialized with dynamic loader: " + str(dynamic_loader))
def extend_strategy(self, extension: ABCMeta, *args) -> None: def extend_strategy(self, extension: ABCMeta, *args) -> None:
@ -200,7 +214,7 @@ class LaserEVM:
for hook in self._stop_sym_trans_hooks: for hook in self._stop_sym_trans_hooks:
hook(self) hook(self)
def exec(self, create=False, track_gas=False) -> Union[List[GlobalState], None]: def exec(self, create=False, track_gas=False) -> Optional[List[GlobalState]]:
""" """
:param create: :param create:
@ -214,6 +228,7 @@ class LaserEVM:
and create and create
and self.time + timedelta(seconds=self.create_timeout) <= datetime.now() and self.time + timedelta(seconds=self.create_timeout) <= datetime.now()
): ):
log.debug("Hit create timeout, returning.")
return final_states + [global_state] if track_gas else None return final_states + [global_state] if track_gas else None
if ( if (
@ -222,6 +237,7 @@ class LaserEVM:
<= datetime.now() <= datetime.now()
and not create and not create
): ):
log.debug("Hit execution timeout, returning.")
return final_states + [global_state] if track_gas else None return final_states + [global_state] if track_gas else None
try: try:
@ -234,8 +250,7 @@ class LaserEVM:
state for state in new_states if state.mstate.constraints.is_possible state for state in new_states if state.mstate.constraints.is_possible
] ]
self.manage_cfg(op_code, new_states) self.manage_cfg(op_code, new_states) # TODO: What about op_code is None?
if new_states: if new_states:
self.work_list += new_states self.work_list += new_states
elif track_gas: elif track_gas:
@ -246,6 +261,7 @@ class LaserEVM:
def _add_world_state(self, global_state: GlobalState): def _add_world_state(self, global_state: GlobalState):
""" Stores the world_state of the passed global state in the open states""" """ Stores the world_state of the passed global state in the open states"""
for hook in self._add_world_state_hooks: for hook in self._add_world_state_hooks:
try: try:
hook(global_state) hook(global_state)
@ -256,11 +272,11 @@ class LaserEVM:
def execute_state( def execute_state(
self, global_state: GlobalState self, global_state: GlobalState
) -> Tuple[List[GlobalState], Union[str, None]]: ) -> Tuple[List[GlobalState], Optional[str]]:
""" """Execute a single instruction in global_state.
:param global_state: :param global_state:
:return: :return: A list of successor states.
""" """
# Execute hooks # Execute hooks
for hook in self._execute_state_hooks: for hook in self._execute_state_hooks:
@ -312,7 +328,11 @@ class LaserEVM:
global_state.transaction_stack global_state.transaction_stack
) + [(start_signal.transaction, global_state)] ) + [(start_signal.transaction, global_state)]
new_global_state.node = global_state.node new_global_state.node = global_state.node
new_global_state.mstate.constraints = global_state.mstate.constraints new_global_state.mstate.constraints = (
start_signal.global_state.mstate.constraints
)
log.debug("Starting new transaction %s", start_signal.transaction)
return [new_global_state], op_code return [new_global_state], op_code
@ -321,11 +341,15 @@ class LaserEVM:
-1 -1
] ]
log.debug("Ending transaction %s.", transaction)
if return_global_state is None: if return_global_state is None:
if ( if (
not isinstance(transaction, ContractCreationTransaction) not isinstance(transaction, ContractCreationTransaction)
or transaction.return_data or transaction.return_data
) and not end_signal.revert: ) and not end_signal.revert:
check_potential_issues(global_state)
end_signal.global_state.world_state.node = global_state.node end_signal.global_state.world_state.node = global_state.node
self._add_world_state(end_signal.global_state) self._add_world_state(end_signal.global_state)
new_global_states = [] new_global_states = []
@ -333,6 +357,19 @@ class LaserEVM:
# First execute the post hook for the transaction ending instruction # First execute the post hook for the transaction ending instruction
self._execute_post_hook(op_code, [end_signal.global_state]) self._execute_post_hook(op_code, [end_signal.global_state])
# Propogate codecall based annotations
if return_global_state.get_current_instruction()["opcode"] in (
"DELEGATECALL",
"CALLCODE",
):
new_annotations = [
annotation
for annotation in global_state.get_annotations(
MutationAnnotation
)
]
return_global_state.add_annotations(new_annotations)
new_global_states = self._end_message_call( new_global_states = self._end_message_call(
copy(return_global_state), copy(return_global_state),
global_state, global_state,
@ -373,6 +410,15 @@ class LaserEVM:
return_global_state.environment.active_account = global_state.accounts[ return_global_state.environment.active_account = global_state.accounts[
return_global_state.environment.active_account.address.value return_global_state.environment.active_account.address.value
] ]
if isinstance(
global_state.current_transaction, ContractCreationTransaction
):
return_global_state.mstate.min_gas_used += (
global_state.mstate.min_gas_used
)
return_global_state.mstate.max_gas_used += (
global_state.mstate.max_gas_used
)
# Execute the post instruction handler # Execute the post instruction handler
new_global_states = Instruction( new_global_states = Instruction(
@ -396,6 +442,7 @@ class LaserEVM:
for state in new_states: for state in new_states:
self._new_node_state(state) self._new_node_state(state)
elif opcode == "JUMPI": elif opcode == "JUMPI":
assert len(new_states) <= 2
for state in new_states: for state in new_states:
self._new_node_state( self._new_node_state(
state, JumpType.CONDITIONAL, state.mstate.constraints[-1] state, JumpType.CONDITIONAL, state.mstate.constraints[-1]

@ -23,7 +23,7 @@ ATTACKER_ADDRESS = 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF
ACTOR_ADDRESSES = [ ACTOR_ADDRESSES = [
symbol_factory.BitVecVal(0xAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFE, 256), symbol_factory.BitVecVal(0xAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFE, 256),
symbol_factory.BitVecVal(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF, 256), symbol_factory.BitVecVal(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF, 256),
symbol_factory.BitVecVal(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEE, 256), symbol_factory.BitVecVal(0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA, 256),
] ]
@ -85,6 +85,8 @@ def execute_contract_creation(
new_account = None new_account = None
for open_world_state in open_states: for open_world_state in open_states:
next_transaction_id = get_next_transaction_id() next_transaction_id = get_next_transaction_id()
# call_data "should" be '[]', but it is easier to model the calldata symbolically
# and add logic in codecopy/codesize/calldatacopy/calldatasize than to model code "correctly"
transaction = ContractCreationTransaction( transaction = ContractCreationTransaction(
world_state=open_world_state, world_state=open_world_state,
identifier=next_transaction_id, identifier=next_transaction_id,
@ -98,7 +100,7 @@ def execute_contract_creation(
code=Disassembly(contract_initialization_code), code=Disassembly(contract_initialization_code),
caller=symbol_factory.BitVecVal(CREATOR_ADDRESS, 256), caller=symbol_factory.BitVecVal(CREATOR_ADDRESS, 256),
contract_name=contract_name, contract_name=contract_name,
call_data=[], call_data=None,
call_value=symbol_factory.BitVecSym( call_value=symbol_factory.BitVecSym(
"call_value{}".format(next_transaction_id), 256 "call_value{}".format(next_transaction_id), 256
), ),

@ -4,7 +4,7 @@ execution."""
import array import array
from copy import deepcopy from copy import deepcopy
from z3 import ExprRef from z3 import ExprRef
from typing import Union, Optional, cast from typing import Union, Optional
from mythril.laser.ethereum.state.calldata import ConcreteCalldata from mythril.laser.ethereum.state.calldata import ConcreteCalldata
from mythril.laser.ethereum.state.account import Account from mythril.laser.ethereum.state.account import Account
@ -12,8 +12,10 @@ from mythril.laser.ethereum.state.calldata import BaseCalldata, SymbolicCalldata
from mythril.laser.ethereum.state.environment import Environment from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.global_state import GlobalState from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState from mythril.laser.ethereum.state.world_state import WorldState
from mythril.disassembler.disassembly import Disassembly from mythril.laser.smt import symbol_factory, UGE, BitVec
from mythril.laser.smt import symbol_factory import logging
log = logging.getLogger(__name__)
_next_transaction_id = 0 _next_transaction_id = 0
@ -43,9 +45,11 @@ class TransactionStartSignal(Exception):
self, self,
transaction: Union["MessageCallTransaction", "ContractCreationTransaction"], transaction: Union["MessageCallTransaction", "ContractCreationTransaction"],
op_code: str, op_code: str,
global_state: GlobalState,
) -> None: ) -> None:
self.transaction = transaction self.transaction = transaction
self.op_code = op_code self.op_code = op_code
self.global_state = global_state
class BaseTransaction: class BaseTransaction:
@ -64,6 +68,7 @@ class BaseTransaction:
code=None, code=None,
call_value=None, call_value=None,
init_call_data=True, init_call_data=True,
static=False,
) -> None: ) -> None:
assert isinstance(world_state, WorldState) assert isinstance(world_state, WorldState)
self.world_state = world_state self.world_state = world_state
@ -99,7 +104,7 @@ class BaseTransaction:
if call_value is not None if call_value is not None
else symbol_factory.BitVecSym("callvalue{}".format(identifier), 256) else symbol_factory.BitVecSym("callvalue{}".format(identifier), 256)
) )
self.static = static
self.return_data = None # type: str self.return_data = None # type: str
def initial_global_state_from_environment(self, environment, active_function): def initial_global_state_from_environment(self, environment, active_function):
@ -115,16 +120,31 @@ class BaseTransaction:
sender = environment.sender sender = environment.sender
receiver = environment.active_account.address receiver = environment.active_account.address
value = environment.callvalue value = (
environment.callvalue
if isinstance(environment.callvalue, BitVec)
else symbol_factory.BitVecVal(environment.callvalue, 256)
)
global_state.world_state.balances[sender] -= value global_state.mstate.constraints.append(
UGE(global_state.world_state.balances[sender], value)
)
global_state.world_state.balances[receiver] += value global_state.world_state.balances[receiver] += value
global_state.world_state.balances[sender] -= value
return global_state return global_state
def initial_global_state(self) -> GlobalState: def initial_global_state(self) -> GlobalState:
raise NotImplementedError raise NotImplementedError
def __str__(self) -> str:
return "{} {} from {} to {:#42x}".format(
self.__class__.__name__,
self.id,
self.caller,
int(str(self.callee_account.address)) if self.callee_account else -1,
)
class MessageCallTransaction(BaseTransaction): class MessageCallTransaction(BaseTransaction):
"""Transaction object models an transaction.""" """Transaction object models an transaction."""
@ -142,6 +162,7 @@ class MessageCallTransaction(BaseTransaction):
self.call_value, self.call_value,
self.origin, self.origin,
code=self.code or self.callee_account.code, code=self.code or self.callee_account.code,
static=self.static,
) )
return super().initial_global_state_from_environment( return super().initial_global_state_from_environment(
environment, active_function="fallback" environment, active_function="fallback"
@ -174,13 +195,18 @@ class ContractCreationTransaction(BaseTransaction):
code=None, code=None,
call_value=None, call_value=None,
contract_name=None, contract_name=None,
contract_address=None,
) -> None: ) -> None:
self.prev_world_state = deepcopy(world_state) self.prev_world_state = deepcopy(world_state)
contract_address = (
contract_address if isinstance(contract_address, int) else None
)
callee_account = world_state.create_account( callee_account = world_state.create_account(
0, concrete_storage=True, creator=caller.value 0, concrete_storage=True, creator=caller.value, address=contract_address
) )
callee_account.contract_name = contract_name callee_account.contract_name = contract_name or callee_account.contract_name
# TODO: set correct balance for new account # init_call_data "should" be false, but it is easier to model the calldata symbolically
# and add logic in codecopy/codesize/calldatacopy/calldatasize than to model code "correctly"
super().__init__( super().__init__(
world_state=world_state, world_state=world_state,
callee_account=callee_account, callee_account=callee_account,
@ -192,7 +218,7 @@ class ContractCreationTransaction(BaseTransaction):
origin=origin, origin=origin,
code=code, code=code,
call_value=call_value, call_value=call_value,
init_call_data=False, init_call_data=True,
) )
def initial_global_state(self) -> GlobalState: def initial_global_state(self) -> GlobalState:

@ -45,7 +45,7 @@ def get_instruction_index(
""" """
index = 0 index = 0
for instr in instruction_list: for instr in instruction_list:
if instr["address"] == address: if instr["address"] >= address:
return index return index
index += 1 index += 1
return None return None
@ -150,3 +150,27 @@ def bytearray_to_int(arr):
for a in arr: for a in arr:
o = (o << 8) + a o = (o << 8) + a
return o return o
def extract_copy(
data: bytearray, mem: bytearray, memstart: int, datastart: int, size: int
):
for i in range(size):
if datastart + i < len(data):
mem[memstart + i] = data[datastart + i]
else:
mem[memstart + i] = 0
def extract32(data: bytearray, i: int) -> int:
"""
:param data:
:param i:
:return:
"""
if i >= len(data):
return 0
o = data[i : min(i + 32, len(data))]
o.extend(bytearray(32 - len(o)))
return bytearray_to_int(o)

@ -1,8 +1,10 @@
from mythril.laser.smt.bitvec import ( from mythril.laser.smt.bitvec import BitVec
BitVec,
from mythril.laser.smt.bitvec_helper import (
If, If,
UGT, UGT,
ULT, ULT,
ULE,
Concat, Concat,
Extract, Extract,
URem, URem,
@ -15,19 +17,20 @@ from mythril.laser.smt.bitvec import (
BVSubNoUnderflow, BVSubNoUnderflow,
LShR, LShR,
) )
from mythril.laser.smt.bitvecfunc import BitVecFunc from mythril.laser.smt.bitvecfunc import BitVecFunc
from mythril.laser.smt.expression import Expression, simplify from mythril.laser.smt.expression import Expression, simplify
from mythril.laser.smt.bool import Bool, is_true, is_false, Or, Not, And from mythril.laser.smt.bool import Bool, is_true, is_false, Or, Not, And
from mythril.laser.smt.array import K, Array, BaseArray from mythril.laser.smt.array import K, Array, BaseArray
from mythril.laser.smt.solver import Solver, Optimize, SolverStatistics from mythril.laser.smt.solver import Solver, Optimize, SolverStatistics
from mythril.laser.smt.model import Model from mythril.laser.smt.model import Model
from mythril.laser.smt.bool import Bool as SMTBool
from typing import Union, Any, Optional, Set, TypeVar, Generic from typing import Union, Any, Optional, Set, TypeVar, Generic
import z3 import z3
Annotations = Optional[Set[Any]] Annotations = Optional[Set[Any]]
T = TypeVar("T", bound=Union[bool.Bool, z3.BoolRef]) T = TypeVar("T", bound=Union[SMTBool, z3.BoolRef])
U = TypeVar("U", bound=Union[BitVec, z3.BitVecRef]) U = TypeVar("U", bound=Union[BitVec, z3.BitVecRef])
@ -105,14 +108,14 @@ class SymbolFactory(Generic[T, U]):
raise NotImplementedError() raise NotImplementedError()
class _SmtSymbolFactory(SymbolFactory[bool.Bool, BitVec]): class _SmtSymbolFactory(SymbolFactory[SMTBool, BitVec]):
""" """
An implementation of a SymbolFactory that creates symbols using An implementation of a SymbolFactory that creates symbols using
the classes in: mythril.laser.smt the classes in: mythril.laser.smt
""" """
@staticmethod @staticmethod
def Bool(value: "__builtins__.bool", annotations: Annotations = None) -> bool.Bool: def Bool(value: "__builtins__.bool", annotations: Annotations = None) -> SMTBool:
""" """
Creates a Bool with concrete value Creates a Bool with concrete value
:param value: The boolean value :param value: The boolean value
@ -120,7 +123,7 @@ class _SmtSymbolFactory(SymbolFactory[bool.Bool, BitVec]):
:return: The freshly created Bool() :return: The freshly created Bool()
""" """
raw = z3.BoolVal(value) raw = z3.BoolVal(value)
return Bool(raw, annotations) return SMTBool(raw, annotations)
@staticmethod @staticmethod
def BitVecVal(value: int, size: int, annotations: Annotations = None) -> BitVec: def BitVecVal(value: int, size: int, annotations: Annotations = None) -> BitVec:

@ -8,7 +8,8 @@ default values over a certain range.
from typing import cast from typing import cast
import z3 import z3
from mythril.laser.smt.bitvec import BitVec, If from mythril.laser.smt.bitvec import BitVec
from mythril.laser.smt.bitvec_helper import If
from mythril.laser.smt.bool import Bool from mythril.laser.smt.bool import Bool

@ -1,10 +1,11 @@
"""This module provides classes for an SMT abstraction of bit vectors.""" """This module provides classes for an SMT abstraction of bit vectors."""
from typing import Union, overload, List, Set, cast, Any, Optional, Callable from operator import lshift, rshift, ne, eq
from operator import lshift, rshift from typing import Union, Set, cast, Any, Optional, Callable
import z3 import z3
from mythril.laser.smt.bool import Bool, And, Or from mythril.laser.smt.bool import Bool
from mythril.laser.smt.expression import Expression from mythril.laser.smt.expression import Expression
Annotations = Set[Any] Annotations = Set[Any]
@ -12,6 +13,15 @@ Annotations = Set[Any]
# fmt: off # fmt: off
def _padded_operation(a: z3.BitVec, b: z3.BitVec, operator):
if a.size() == b.size():
return operator(a, b)
if a.size() < b.size():
a, b = b, a
b = z3.Concat(z3.BitVecVal(0, a.size() - b.size()), b)
return operator(a, b)
class BitVec(Expression[z3.BitVecRef]): class BitVec(Expression[z3.BitVecRef]):
"""A bit vector symbol.""" """A bit vector symbol."""
@ -203,10 +213,9 @@ class BitVec(Expression[z3.BitVecRef]):
union = self.annotations.union(other.annotations) union = self.annotations.union(other.annotations)
# Some of the BitVecs can be 512 bit due to sha3() # Some of the BitVecs can be 512 bit due to sha3()
if self.raw.size() != other.raw.size(): eq_check = _padded_operation(self.raw, other.raw, eq)
return Bool(z3.BoolVal(False), annotations=union)
# MYPY: fix complaints due to z3 overriding __eq__ # MYPY: fix complaints due to z3 overriding __eq__
return Bool(cast(z3.BoolRef, self.raw == other.raw), annotations=union) return Bool(cast(z3.BoolRef, eq_check), annotations=union)
# MYPY: fix complains about overriding __ne__ # MYPY: fix complains about overriding __ne__
def __ne__(self, other: Union[int, "BitVec"]) -> Bool: # type: ignore def __ne__(self, other: Union[int, "BitVec"]) -> Bool: # type: ignore
@ -224,10 +233,9 @@ class BitVec(Expression[z3.BitVecRef]):
union = self.annotations.union(other.annotations) union = self.annotations.union(other.annotations)
# Some of the BitVecs can be 512 bit due to sha3() # Some of the BitVecs can be 512 bit due to sha3()
if self.raw.size() != other.raw.size(): neq_check = _padded_operation(self.raw, other.raw, ne)
return Bool(z3.BoolVal(True), annotations=union)
# MYPY: fix complaints due to z3 overriding __eq__ # MYPY: fix complaints due to z3 overriding __eq__
return Bool(cast(z3.BoolRef, self.raw != other.raw), annotations=union) return Bool(cast(z3.BoolRef, neq_check), annotations=union)
def _handle_shift(self, other: Union[int, "BitVec"], operator: Callable) -> "BitVec": def _handle_shift(self, other: Union[int, "BitVec"], operator: Callable) -> "BitVec":
""" """
@ -269,276 +277,5 @@ class BitVec(Expression[z3.BitVecRef]):
return self.raw.__hash__() return self.raw.__hash__()
def _comparison_helper(
a: BitVec, b: BitVec, operation: Callable, default_value: bool, inputs_equal: bool
) -> Bool:
annotations = a.annotations.union(b.annotations)
if isinstance(a, BitVecFunc):
if not a.symbolic and not b.symbolic:
return Bool(operation(a.raw, b.raw), annotations=annotations)
if (
not isinstance(b, BitVecFunc)
or not a.func_name
or not a.input_
or not a.func_name == b.func_name
):
return Bool(z3.BoolVal(default_value), annotations=annotations)
return And(
Bool(operation(a.raw, b.raw), annotations=annotations),
a.input_ == b.input_ if inputs_equal else a.input_ != b.input_,
)
return Bool(operation(a.raw, b.raw), annotations)
def _arithmetic_helper(a: BitVec, b: BitVec, operation: Callable) -> BitVec:
raw = operation(a.raw, b.raw)
union = a.annotations.union(b.annotations)
if isinstance(a, BitVecFunc) and isinstance(b, BitVecFunc):
return BitVecFunc(raw=raw, func_name=None, input_=None, annotations=union)
elif isinstance(a, BitVecFunc):
return BitVecFunc(
raw=raw, func_name=a.func_name, input_=a.input_, annotations=union
)
elif isinstance(b, BitVecFunc):
return BitVecFunc(
raw=raw, func_name=b.func_name, input_=b.input_, annotations=union
)
return BitVec(raw, annotations=union)
def LShR(a: BitVec, b: BitVec):
return _arithmetic_helper(a, b, z3.LShR)
def If(a: Union[Bool, bool], b: Union[BitVec, int], c: Union[BitVec, int]) -> BitVec:
"""Create an if-then-else expression.
:param a:
:param b:
:param c:
:return:
"""
# TODO: Handle BitVecFunc
if not isinstance(a, Bool):
a = Bool(z3.BoolVal(a))
if not isinstance(b, BitVec):
b = BitVec(z3.BitVecVal(b, 256))
if not isinstance(c, BitVec):
c = BitVec(z3.BitVecVal(c, 256))
union = a.annotations.union(b.annotations).union(c.annotations)
return BitVec(z3.If(a.raw, b.raw, c.raw), union)
def UGT(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned greater than expression.
:param a:
:param b:
:return:
"""
return _comparison_helper(a, b, z3.UGT, default_value=False, inputs_equal=False)
def UGE(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned greater or equals expression.
:param a:
:param b:
:return:
"""
return Or(UGT(a, b), a == b)
def ULT(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned less than expression.
:param a:
:param b:
:return:
"""
return _comparison_helper(a, b, z3.ULT, default_value=False, inputs_equal=False)
def ULE(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned less than expression.
:param a:
:param b:
:return:
"""
return Or(ULT(a, b), a == b)
@overload
def Concat(*args: List[BitVec]) -> BitVec: ...
@overload
def Concat(*args: BitVec) -> BitVec: ...
def Concat(*args: Union[BitVec, List[BitVec]]) -> BitVec:
"""Create a concatenation expression.
:param args:
:return:
"""
# The following statement is used if a list is provided as an argument to concat
if len(args) == 1 and isinstance(args[0], list):
bvs = args[0] # type: List[BitVec]
else:
bvs = cast(List[BitVec], args)
nraw = z3.Concat([a.raw for a in bvs])
annotations = set() # type: Annotations
bitvecfunc = False
for bv in bvs:
annotations = annotations.union(bv.annotations)
if isinstance(bv, BitVecFunc):
bitvecfunc = True
if bitvecfunc:
# Added this not so good and misleading NOTATION to help with this
str_hash = ",".join(["hashed({})".format(hash(bv)) for bv in bvs])
input_string = "MisleadingNotationConcat({})".format(str_hash)
return BitVecFunc(
raw=nraw, func_name="Hybrid", input_=BitVec(z3.BitVec(input_string, 256), annotations=annotations)
)
return BitVec(nraw, annotations)
def Extract(high: int, low: int, bv: BitVec) -> BitVec:
"""Create an extract expression.
:param high:
:param low:
:param bv:
:return:
"""
raw = z3.Extract(high, low, bv.raw)
if isinstance(bv, BitVecFunc):
input_string = "MisleadingNotationExtract({}, {}, hashed({}))".format(high, low, hash(bv))
# Is there a better value to set func_name and input to in this case?
return BitVecFunc(
raw=raw, func_name="Hybrid", input_=BitVec(z3.BitVec(input_string, 256), annotations=bv.annotations)
)
return BitVec(raw, annotations=bv.annotations)
def URem(a: BitVec, b: BitVec) -> BitVec:
"""Create an unsigned remainder expression.
:param a:
:param b:
:return:
"""
return _arithmetic_helper(a, b, z3.URem)
def SRem(a: BitVec, b: BitVec) -> BitVec:
"""Create a signed remainder expression.
:param a:
:param b:
:return:
"""
return _arithmetic_helper(a, b, z3.SRem)
def UDiv(a: BitVec, b: BitVec) -> BitVec:
"""Create an unsigned division expression.
:param a:
:param b:
:return:
"""
return _arithmetic_helper(a, b, z3.UDiv)
def Sum(*args: BitVec) -> BitVec:
"""Create sum expression.
:return:
"""
raw = z3.Sum([a.raw for a in args])
annotations = set() # type: Annotations
bitvecfuncs = []
for bv in args:
annotations = annotations.union(bv.annotations)
if isinstance(bv, BitVecFunc):
bitvecfuncs.append(bv)
if len(bitvecfuncs) >= 2:
return BitVecFunc(raw=raw, func_name="Hybrid", input_=None, annotations=annotations)
elif len(bitvecfuncs) == 1:
return BitVecFunc(
raw=raw,
func_name=bitvecfuncs[0].func_name,
input_=bitvecfuncs[0].input_,
annotations=annotations,
)
return BitVec(raw, annotations)
def BVAddNoOverflow(a: Union[BitVec, int], b: Union[BitVec, int], signed: bool) -> Bool:
"""Creates predicate that verifies that the addition doesn't overflow.
:param a:
:param b:
:param signed:
:return:
"""
if not isinstance(a, BitVec):
a = BitVec(z3.BitVecVal(a, 256))
if not isinstance(b, BitVec):
b = BitVec(z3.BitVecVal(b, 256))
return Bool(z3.BVAddNoOverflow(a.raw, b.raw, signed))
def BVMulNoOverflow(a: Union[BitVec, int], b: Union[BitVec, int], signed: bool) -> Bool:
"""Creates predicate that verifies that the multiplication doesn't
overflow.
:param a:
:param b:
:param signed:
:return:
"""
if not isinstance(a, BitVec):
a = BitVec(z3.BitVecVal(a, 256))
if not isinstance(b, BitVec):
b = BitVec(z3.BitVecVal(b, 256))
return Bool(z3.BVMulNoOverflow(a.raw, b.raw, signed))
def BVSubNoUnderflow(
a: Union[BitVec, int], b: Union[BitVec, int], signed: bool
) -> Bool:
"""Creates predicate that verifies that the subtraction doesn't overflow.
:param a:
:param b:
:param signed:
:return:
"""
if not isinstance(a, BitVec):
a = BitVec(z3.BitVecVal(a, 256))
if not isinstance(b, BitVec):
b = BitVec(z3.BitVecVal(b, 256))
return Bool(z3.BVSubNoUnderflow(a.raw, b.raw, signed))
# TODO: Fix circular import issues # TODO: Fix circular import issues
from mythril.laser.smt.bitvecfunc import BitVecFunc from mythril.laser.smt.bitvecfunc import BitVecFunc

@ -0,0 +1,291 @@
from typing import Union, overload, List, Set, cast, Any, Optional, Callable
from operator import lshift, rshift, ne, eq
import z3
from mythril.laser.smt.bool import Bool, And, Or
from mythril.laser.smt.bitvec import BitVec
from mythril.laser.smt.bitvecfunc import BitVecFunc
from mythril.laser.smt.bitvecfunc import _arithmetic_helper as _func_arithmetic_helper
from mythril.laser.smt.bitvecfunc import _comparison_helper as _func_comparison_helper
Annotations = Set[Any]
def _comparison_helper(
a: BitVec, b: BitVec, operation: Callable, default_value: bool, inputs_equal: bool
) -> Bool:
annotations = a.annotations.union(b.annotations)
if isinstance(a, BitVecFunc):
return _func_comparison_helper(a, b, operation, default_value, inputs_equal)
return Bool(operation(a.raw, b.raw), annotations)
def _arithmetic_helper(a: BitVec, b: BitVec, operation: Callable) -> BitVec:
raw = operation(a.raw, b.raw)
union = a.annotations.union(b.annotations)
if isinstance(a, BitVecFunc):
return _func_arithmetic_helper(a, b, operation)
elif isinstance(b, BitVecFunc):
return _func_arithmetic_helper(b, a, operation)
return BitVec(raw, annotations=union)
def LShR(a: BitVec, b: BitVec):
return _arithmetic_helper(a, b, z3.LShR)
def If(a: Union[Bool, bool], b: Union[BitVec, int], c: Union[BitVec, int]) -> BitVec:
"""Create an if-then-else expression.
:param a:
:param b:
:param c:
:return:
"""
# TODO: Handle BitVecFunc
if not isinstance(a, Bool):
a = Bool(z3.BoolVal(a))
if not isinstance(b, BitVec):
b = BitVec(z3.BitVecVal(b, 256))
if not isinstance(c, BitVec):
c = BitVec(z3.BitVecVal(c, 256))
union = a.annotations.union(b.annotations).union(c.annotations)
bvf = [] # type: List[BitVecFunc]
if isinstance(a, BitVecFunc):
bvf += [a]
if isinstance(b, BitVecFunc):
bvf += [b]
if isinstance(c, BitVecFunc):
bvf += [c]
if bvf:
raw = z3.If(a.raw, b.raw, c.raw)
nested_functions = [nf for func in bvf for nf in func.nested_functions] + bvf
return BitVecFunc(raw, func_name="Hybrid", nested_functions=nested_functions)
return BitVec(z3.If(a.raw, b.raw, c.raw), union)
def UGT(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned greater than expression.
:param a:
:param b:
:return:
"""
return _comparison_helper(a, b, z3.UGT, default_value=False, inputs_equal=False)
def UGE(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned greater or equals expression.
:param a:
:param b:
:return:
"""
return Or(UGT(a, b), a == b)
def ULT(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned less than expression.
:param a:
:param b:
:return:
"""
return _comparison_helper(a, b, z3.ULT, default_value=False, inputs_equal=False)
def ULE(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned less than expression.
:param a:
:param b:
:return:
"""
return Or(ULT(a, b), a == b)
@overload
def Concat(*args: List[BitVec]) -> BitVec:
...
@overload
def Concat(*args: BitVec) -> BitVec:
...
def Concat(*args: Union[BitVec, List[BitVec]]) -> BitVec:
"""Create a concatenation expression.
:param args:
:return:
"""
# The following statement is used if a list is provided as an argument to concat
if len(args) == 1 and isinstance(args[0], list):
bvs = args[0] # type: List[BitVec]
else:
bvs = cast(List[BitVec], args)
nraw = z3.Concat([a.raw for a in bvs])
annotations = set() # type: Annotations
nested_functions = [] # type: List[BitVecFunc]
for bv in bvs:
annotations = annotations.union(bv.annotations)
if isinstance(bv, BitVecFunc):
nested_functions += bv.nested_functions
nested_functions += [bv]
if nested_functions:
return BitVecFunc(
raw=nraw,
func_name="Hybrid",
input_=BitVec(z3.BitVec("", 256), annotations=annotations),
nested_functions=nested_functions,
)
return BitVec(nraw, annotations)
def Extract(high: int, low: int, bv: BitVec) -> BitVec:
"""Create an extract expression.
:param high:
:param low:
:param bv:
:return:
"""
raw = z3.Extract(high, low, bv.raw)
if isinstance(bv, BitVecFunc):
input_string = ""
# Is there a better value to set func_name and input to in this case?
return BitVecFunc(
raw=raw,
func_name="Hybrid",
input_=BitVec(z3.BitVec(input_string, 256), annotations=bv.annotations),
nested_functions=bv.nested_functions + [bv],
)
return BitVec(raw, annotations=bv.annotations)
def URem(a: BitVec, b: BitVec) -> BitVec:
"""Create an unsigned remainder expression.
:param a:
:param b:
:return:
"""
return _arithmetic_helper(a, b, z3.URem)
def SRem(a: BitVec, b: BitVec) -> BitVec:
"""Create a signed remainder expression.
:param a:
:param b:
:return:
"""
return _arithmetic_helper(a, b, z3.SRem)
def UDiv(a: BitVec, b: BitVec) -> BitVec:
"""Create an unsigned division expression.
:param a:
:param b:
:return:
"""
return _arithmetic_helper(a, b, z3.UDiv)
def Sum(*args: BitVec) -> BitVec:
"""Create sum expression.
:return:
"""
raw = z3.Sum([a.raw for a in args])
annotations = set() # type: Annotations
bitvecfuncs = []
for bv in args:
annotations = annotations.union(bv.annotations)
if isinstance(bv, BitVecFunc):
bitvecfuncs.append(bv)
nested_functions = [
nf for func in bitvecfuncs for nf in func.nested_functions
] + bitvecfuncs
if len(bitvecfuncs) >= 2:
return BitVecFunc(
raw=raw,
func_name="Hybrid",
input_=None,
annotations=annotations,
nested_functions=nested_functions,
)
elif len(bitvecfuncs) == 1:
return BitVecFunc(
raw=raw,
func_name=bitvecfuncs[0].func_name,
input_=bitvecfuncs[0].input_,
annotations=annotations,
nested_functions=nested_functions,
)
return BitVec(raw, annotations)
def BVAddNoOverflow(a: Union[BitVec, int], b: Union[BitVec, int], signed: bool) -> Bool:
"""Creates predicate that verifies that the addition doesn't overflow.
:param a:
:param b:
:param signed:
:return:
"""
if not isinstance(a, BitVec):
a = BitVec(z3.BitVecVal(a, 256))
if not isinstance(b, BitVec):
b = BitVec(z3.BitVecVal(b, 256))
return Bool(z3.BVAddNoOverflow(a.raw, b.raw, signed))
def BVMulNoOverflow(a: Union[BitVec, int], b: Union[BitVec, int], signed: bool) -> Bool:
"""Creates predicate that verifies that the multiplication doesn't
overflow.
:param a:
:param b:
:param signed:
:return:
"""
if not isinstance(a, BitVec):
a = BitVec(z3.BitVecVal(a, 256))
if not isinstance(b, BitVec):
b = BitVec(z3.BitVecVal(b, 256))
return Bool(z3.BVMulNoOverflow(a.raw, b.raw, signed))
def BVSubNoUnderflow(
a: Union[BitVec, int], b: Union[BitVec, int], signed: bool
) -> Bool:
"""Creates predicate that verifies that the subtraction doesn't overflow.
:param a:
:param b:
:param signed:
:return:
"""
if not isinstance(a, BitVec):
a = BitVec(z3.BitVecVal(a, 256))
if not isinstance(b, BitVec):
b = BitVec(z3.BitVecVal(b, 256))
return Bool(z3.BVSubNoUnderflow(a.raw, b.raw, signed))

@ -1,11 +1,10 @@
from typing import Optional, Union, cast, Callable import operator
from itertools import product
from typing import Optional, Union, cast, Callable, List
import z3 import z3
from mythril.laser.smt.bitvec import BitVec, Bool, And, Annotations from mythril.laser.smt.bitvec import BitVec, Annotations
from mythril.laser.smt.bool import Or from mythril.laser.smt.bool import Or, Bool, And
import operator
def _arithmetic_helper( def _arithmetic_helper(
@ -26,18 +25,19 @@ def _arithmetic_helper(
union = a.annotations.union(b.annotations) union = a.annotations.union(b.annotations)
if isinstance(b, BitVecFunc): if isinstance(b, BitVecFunc):
# TODO: Find better value to set input and name to in this case?
input_string = "MisleadingNotationop(invhash({}) {} invhash({})".format(
hash(a), operation, hash(b)
)
return BitVecFunc( return BitVecFunc(
raw=raw, raw=raw,
func_name="Hybrid", func_name="Hybrid",
input_=BitVec(z3.BitVec(input_string, 256), annotations=union), input_=BitVec(z3.BitVec("", 256), annotations=union),
nested_functions=a.nested_functions + b.nested_functions + [a, b],
) )
return BitVecFunc( return BitVecFunc(
raw=raw, func_name=a.func_name, input_=a.input_, annotations=union raw=raw,
func_name=a.func_name,
input_=a.input_,
annotations=union,
nested_functions=a.nested_functions + [a],
) )
@ -62,18 +62,55 @@ def _comparison_helper(
union = a.annotations.union(b.annotations) union = a.annotations.union(b.annotations)
if not a.symbolic and not b.symbolic: if not a.symbolic and not b.symbolic:
if operation == z3.UGT:
operation = operator.gt
if operation == z3.ULT:
operation = operator.lt
return Bool(z3.BoolVal(operation(a.value, b.value)), annotations=union) return Bool(z3.BoolVal(operation(a.value, b.value)), annotations=union)
if ( if (
not isinstance(b, BitVecFunc) not isinstance(b, BitVecFunc)
or not a.func_name or not a.func_name
or not a.input_ or not a.input_
or not a.func_name == b.func_name or not a.func_name == b.func_name
or str(operation) not in ("<built-in function eq>", "<built-in function ne>")
): ):
return Bool(z3.BoolVal(default_value), annotations=union) return Bool(z3.BoolVal(default_value), annotations=union)
condition = True
for a_nest, b_nest in product(a.nested_functions, b.nested_functions):
if a_nest.func_name != b_nest.func_name:
continue
if a_nest.func_name == "Hybrid":
continue
# a.input (eq/neq) b.input ==> a == b
if inputs_equal:
condition = z3.And(
condition,
z3.Or(
z3.Not((a_nest.input_ == b_nest.input_).raw),
(a_nest.raw == b_nest.raw),
),
z3.Or(
z3.Not((a_nest.raw == b_nest.raw)),
(a_nest.input_ == b_nest.input_).raw,
),
)
else:
condition = z3.And(
condition,
z3.Or(
z3.Not((a_nest.input_ != b_nest.input_).raw),
(a_nest.raw == b_nest.raw),
),
z3.Or(
z3.Not((a_nest.raw == b_nest.raw)),
(a_nest.input_ != b_nest.input_).raw,
),
)
return And( return And(
Bool(cast(z3.BoolRef, operation(a.raw, b.raw)), annotations=union), Bool(cast(z3.BoolRef, operation(a.raw, b.raw)), annotations=union),
Bool(condition) if b.nested_functions else Bool(True),
a.input_ == b.input_ if inputs_equal else a.input_ != b.input_, a.input_ == b.input_ if inputs_equal else a.input_ != b.input_,
) )
@ -87,6 +124,7 @@ class BitVecFunc(BitVec):
func_name: Optional[str], func_name: Optional[str],
input_: "BitVec" = None, input_: "BitVec" = None,
annotations: Optional[Annotations] = None, annotations: Optional[Annotations] = None,
nested_functions: Optional[List["BitVecFunc"]] = None,
): ):
""" """
@ -98,6 +136,10 @@ class BitVecFunc(BitVec):
self.func_name = func_name self.func_name = func_name
self.input_ = input_ self.input_ = input_
self.nested_functions = nested_functions or []
self.nested_functions = list(dict.fromkeys(self.nested_functions))
if isinstance(input_, BitVecFunc):
self.nested_functions.extend(input_.nested_functions)
super().__init__(raw, annotations) super().__init__(raw, annotations)
def __add__(self, other: Union[int, "BitVec"]) -> "BitVecFunc": def __add__(self, other: Union[int, "BitVec"]) -> "BitVecFunc":

@ -10,6 +10,7 @@ from mythril.support.source_support import Source
from mythril.support.loader import DynLoader from mythril.support.loader import DynLoader
from mythril.analysis.symbolic import SymExecWrapper from mythril.analysis.symbolic import SymExecWrapper
from mythril.analysis.callgraph import generate_graph from mythril.analysis.callgraph import generate_graph
from mythril.analysis.analysis_args import analysis_args
from mythril.analysis.traceexplore import get_serializable_statespace from mythril.analysis.traceexplore import get_serializable_statespace
from mythril.analysis.security import fire_lasers, retrieve_callback_issues from mythril.analysis.security import fire_lasers, retrieve_callback_issues
from mythril.analysis.report import Report, Issue from mythril.analysis.report import Report, Issue
@ -39,6 +40,9 @@ class MythrilAnalyzer:
create_timeout: Optional[int] = None, create_timeout: Optional[int] = None,
enable_iprof: bool = False, enable_iprof: bool = False,
disable_dependency_pruning: bool = False, disable_dependency_pruning: bool = False,
solver_timeout: Optional[int] = None,
enable_coverage_strategy: bool = False,
custom_modules_directory: str = "",
): ):
""" """
@ -59,6 +63,11 @@ class MythrilAnalyzer:
self.create_timeout = create_timeout self.create_timeout = create_timeout
self.enable_iprof = enable_iprof self.enable_iprof = enable_iprof
self.disable_dependency_pruning = disable_dependency_pruning self.disable_dependency_pruning = disable_dependency_pruning
self.enable_coverage_strategy = enable_coverage_strategy
self.custom_modules_directory = custom_modules_directory
analysis_args.set_loop_bound(loop_bound)
analysis_args.set_solver_timeout(solver_timeout)
def dump_statespace(self, contract: EVMContract = None) -> str: def dump_statespace(self, contract: EVMContract = None) -> str:
""" """
@ -81,6 +90,8 @@ class MythrilAnalyzer:
enable_iprof=self.enable_iprof, enable_iprof=self.enable_iprof,
disable_dependency_pruning=self.disable_dependency_pruning, disable_dependency_pruning=self.disable_dependency_pruning,
run_analysis_modules=False, run_analysis_modules=False,
enable_coverage_strategy=self.enable_coverage_strategy,
custom_modules_directory=self.custom_modules_directory,
) )
return get_serializable_statespace(sym) return get_serializable_statespace(sym)
@ -116,6 +127,8 @@ class MythrilAnalyzer:
enable_iprof=self.enable_iprof, enable_iprof=self.enable_iprof,
disable_dependency_pruning=self.disable_dependency_pruning, disable_dependency_pruning=self.disable_dependency_pruning,
run_analysis_modules=False, run_analysis_modules=False,
enable_coverage_strategy=self.enable_coverage_strategy,
custom_modules_directory=self.custom_modules_directory,
) )
return generate_graph(sym, physics=enable_physics, phrackify=phrackify) return generate_graph(sym, physics=enable_physics, phrackify=phrackify)
@ -153,18 +166,23 @@ class MythrilAnalyzer:
compulsory_statespace=False, compulsory_statespace=False,
enable_iprof=self.enable_iprof, enable_iprof=self.enable_iprof,
disable_dependency_pruning=self.disable_dependency_pruning, disable_dependency_pruning=self.disable_dependency_pruning,
enable_coverage_strategy=self.enable_coverage_strategy,
custom_modules_directory=self.custom_modules_directory,
) )
issues = fire_lasers(sym, modules, self.custom_modules_directory)
issues = fire_lasers(sym, modules)
except KeyboardInterrupt: except KeyboardInterrupt:
log.critical("Keyboard Interrupt") log.critical("Keyboard Interrupt")
issues = retrieve_callback_issues(modules) issues = retrieve_callback_issues(
modules, self.custom_modules_directory
)
except Exception: except Exception:
log.critical( log.critical(
"Exception occurred, aborting analysis. Please report this issue to the Mythril GitHub page.\n" "Exception occurred, aborting analysis. Please report this issue to the Mythril GitHub page.\n"
+ traceback.format_exc() + traceback.format_exc()
) )
issues = retrieve_callback_issues(modules) issues = retrieve_callback_issues(
modules, self.custom_modules_directory
)
exceptions.append(traceback.format_exc()) exceptions.append(traceback.format_exc())
for issue in issues: for issue in issues:
issue.add_code_info(contract) issue.add_code_info(contract)

@ -1,10 +1,10 @@
import logging import logging
import re import re
import solc import solc
import solcx
import os import os
from ethereum import utils from ethereum import utils
from solc.exceptions import SolcError
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
from mythril.ethereum import util from mythril.ethereum import util
from mythril.ethereum.interface.rpc.client import EthJsonRpc from mythril.ethereum.interface.rpc.client import EthJsonRpc
@ -30,11 +30,11 @@ class MythrilDisassembler:
self, self,
eth: Optional[EthJsonRpc] = None, eth: Optional[EthJsonRpc] = None,
solc_version: str = None, solc_version: str = None,
solc_args: str = None, solc_settings_json: str = None,
enable_online_lookup: bool = False, enable_online_lookup: bool = False,
) -> None: ) -> None:
self.solc_binary = self._init_solc_binary(solc_version) self.solc_binary = self._init_solc_binary(solc_version)
self.solc_args = solc_args self.solc_settings_json = solc_settings_json
self.eth = eth self.eth = eth
self.enable_online_lookup = enable_online_lookup self.enable_online_lookup = enable_online_lookup
self.sigs = signatures.SignatureDB(enable_online_lookup=enable_online_lookup) self.sigs = signatures.SignatureDB(enable_online_lookup=enable_online_lookup)
@ -63,15 +63,24 @@ class MythrilDisassembler:
solc_binary = os.environ.get("SOLC") or "solc" solc_binary = os.environ.get("SOLC") or "solc"
else: else:
solc_binary = util.solc_exists(version) solc_binary = util.solc_exists(version)
if solc_binary: if solc_binary and solc_binary != util.solc_exists(
"default_ubuntu_version"
):
log.info("Given version is already installed") log.info("Given version is already installed")
else: else:
try: try:
solc.install_solc("v" + version) if version.startswith("0.4"):
solc.install_solc("v" + version)
else:
solcx.install_solc("v" + version)
solc_binary = util.solc_exists(version) solc_binary = util.solc_exists(version)
if not solc_binary: if not solc_binary:
raise SolcError() raise solc.exceptions.SolcError()
except SolcError: except solc.exceptions.SolcError:
raise CriticalError(
"There was an error when trying to install the specified solc version"
)
except solcx.exceptions.SolcError:
raise CriticalError( raise CriticalError(
"There was an error when trying to install the specified solc version" "There was an error when trying to install the specified solc version"
) )
@ -163,13 +172,15 @@ class MythrilDisassembler:
try: try:
# import signatures from solidity source # import signatures from solidity source
self.sigs.import_solidity_file( self.sigs.import_solidity_file(
file, solc_binary=self.solc_binary, solc_args=self.solc_args file,
solc_binary=self.solc_binary,
solc_settings_json=self.solc_settings_json,
) )
if contract_name is not None: if contract_name is not None:
contract = SolidityContract( contract = SolidityContract(
input_file=file, input_file=file,
name=contract_name, name=contract_name,
solc_args=self.solc_args, solc_settings_json=self.solc_settings_json,
solc_binary=self.solc_binary, solc_binary=self.solc_binary,
) )
self.contracts.append(contract) self.contracts.append(contract)
@ -177,7 +188,7 @@ class MythrilDisassembler:
else: else:
for contract in get_contracts_from_file( for contract in get_contracts_from_file(
input_file=file, input_file=file,
solc_args=self.solc_args, solc_settings_json=self.solc_settings_json,
solc_binary=self.solc_binary, solc_binary=self.solc_binary,
): ):
self.contracts.append(contract) self.contracts.append(contract)
@ -186,7 +197,27 @@ class MythrilDisassembler:
except FileNotFoundError: except FileNotFoundError:
raise CriticalError("Input file not found: " + file) raise CriticalError("Input file not found: " + file)
except CompilerError as e: except CompilerError as e:
raise CriticalError(e) error_msg = str(e)
# Check if error is related to solidity version mismatch
if (
"Error: Source file requires different compiler version"
in error_msg
):
# Grab relevant line "pragma solidity <solv>...", excluding any comments
solv_pragma_line = error_msg.split("\n")[-3].split("//")[0]
# Grab solidity version from relevant line
solv_match = re.findall(r"[0-9]+\.[0-9]+\.[0-9]+", solv_pragma_line)
error_suggestion = (
"<version_number>" if len(solv_match) != 1 else solv_match[0]
)
error_msg = (
error_msg
+ '\nSolidityVersionMismatch: Try adding the option "--solv '
+ error_suggestion
+ '"\n'
)
raise CriticalError(error_msg)
except NoContractFoundError: except NoContractFoundError:
log.error( log.error(
"The file " + file + " does not contain a compilable contract." "The file " + file + " does not contain a compilable contract."

@ -0,0 +1,111 @@
import sys
import os
import time
from mythx_models.exceptions import MythXAPIError
from typing import List, Dict, Any
from mythril.analysis.report import Issue, Report
from mythril.solidity.soliditycontract import SolidityContract
from pythx import Client
import logging
log = logging.getLogger(__name__)
TRIAL_ETH_ADDRESS = "0x0000000000000000000000000000000000000000"
TRIAL_PASSWORD = "trial"
def analyze(contracts: List[SolidityContract], analysis_mode: str = "quick") -> Report:
"""
Analyze contracts via the MythX API.
:param contracts: List of solidity contracts to analyze
:param analysis_mode: The mode to submit the analysis request with. "quick" or "full" (default: "quick")
:return: Report with analyzed contracts
"""
assert analysis_mode in ("quick", "full"), "analysis_mode must be 'quick' or 'full'"
c = Client(
eth_address=os.environ.get("MYTHX_ETH_ADDRESS", TRIAL_ETH_ADDRESS),
password=os.environ.get("MYTHX_PASSWORD", TRIAL_PASSWORD),
)
if c.eth_address == TRIAL_ETH_ADDRESS:
print(
"You are currently running MythX in Trial mode. This mode reports only a partial analysis of your smart contracts, limited to three vulnerabilities. To get a more complete analysis, sign up for a free account at https://mythx.io."
)
issues = [] # type: List[Issue]
# TODO: Analyze multiple contracts asynchronously.
for contract in contracts:
source_codes = {}
source_list = []
sources = {} # type: Dict[str, Any]
main_source = None
try:
main_source = contract.input_file
for solidity_file in contract.solidity_files:
source_codes[solidity_file.filename] = solidity_file.data
for filename in contract.solc_json["sources"].keys():
sources[filename] = {}
if source_codes[filename]:
sources[filename]["source"] = source_codes[filename]
sources[filename]["ast"] = contract.solc_json["sources"][filename][
"ast"
]
source_list.append(filename)
source_list.sort(
key=lambda fname: contract.solc_json["sources"][fname]["id"]
)
except AttributeError:
# No solidity file
pass
assert contract.creation_code, "Creation bytecode must exist."
try:
resp = c.analyze(
contract_name=contract.name,
analysis_mode=analysis_mode,
bytecode=contract.creation_code or None,
deployed_bytecode=contract.code or None,
sources=sources or None,
main_source=main_source,
source_list=source_list or None,
)
except MythXAPIError as e:
log.critical(e)
while not c.analysis_ready(resp.uuid):
log.info(c.status(resp.uuid).analysis)
time.sleep(5)
for issue in c.report(resp.uuid):
issue = Issue(
contract=contract.name,
function_name=None,
address=issue.locations[0].source_map.components[0].offset
if issue.locations
else -1,
swc_id=issue.swc_id[4:] or "None", # remove 'SWC-' prefix
title=issue.swc_title,
bytecode=contract.creation_code,
severity=issue.severity.capitalize(),
description_head=issue.description_short,
description_tail=issue.description_long,
)
issue.add_code_info(contract)
issues.append(issue)
report = Report(contracts=contracts)
for issue in issues:
report.append_issue(issue)
return report

@ -44,23 +44,28 @@ class SourceCodeInfo:
self.solc_mapping = mapping self.solc_mapping = mapping
def get_contracts_from_file(input_file, solc_args=None, solc_binary="solc"): def get_contracts_from_file(input_file, solc_settings_json=None, solc_binary="solc"):
""" """
:param input_file: :param input_file:
:param solc_args: :param solc_settings_json:
:param solc_binary: :param solc_binary:
""" """
data = get_solc_json(input_file, solc_args=solc_args, solc_binary=solc_binary) data = get_solc_json(
input_file, solc_settings_json=solc_settings_json, solc_binary=solc_binary
)
try: try:
for key, contract in data["contracts"].items(): for contract_name in data["contracts"][input_file].keys():
filename, name = key.split(":") if len(
if filename == input_file and len(contract["bin-runtime"]): data["contracts"][input_file][contract_name]["evm"]["deployedBytecode"][
"object"
]
):
yield SolidityContract( yield SolidityContract(
input_file=input_file, input_file=input_file,
name=name, name=contract_name,
solc_args=solc_args, solc_settings_json=solc_settings_json,
solc_binary=solc_binary, solc_binary=solc_binary,
) )
except KeyError: except KeyError:
@ -70,16 +75,22 @@ def get_contracts_from_file(input_file, solc_args=None, solc_binary="solc"):
class SolidityContract(EVMContract): class SolidityContract(EVMContract):
"""Representation of a Solidity contract.""" """Representation of a Solidity contract."""
def __init__(self, input_file, name=None, solc_args=None, solc_binary="solc"): def __init__(
data = get_solc_json(input_file, solc_args=solc_args, solc_binary=solc_binary) self, input_file, name=None, solc_settings_json=None, solc_binary="solc"
):
data = get_solc_json(
input_file, solc_settings_json=solc_settings_json, solc_binary=solc_binary
)
self.solidity_files = [] self.solidity_files = []
self.solc_json = data
self.input_file = input_file
for filename in data["sourceList"]: for filename, contract in data["sources"].items():
with open(filename, "r", encoding="utf-8") as file: with open(filename, "r", encoding="utf-8") as file:
code = file.read() code = file.read()
full_contract_src_maps = self.get_full_contract_src_maps( full_contract_src_maps = self.get_full_contract_src_maps(
data["sources"][filename]["AST"] contract["ast"]
) )
self.solidity_files.append( self.solidity_files.append(
SolidityFile(filename, code, full_contract_src_maps) SolidityFile(filename, code, full_contract_src_maps)
@ -91,32 +102,28 @@ class SolidityContract(EVMContract):
srcmap_constructor = [] srcmap_constructor = []
srcmap = [] srcmap = []
if name: if name:
for key, contract in sorted(data["contracts"].items()): contract = data["contracts"][input_file][name]
filename, _name = key.split(":") if len(contract["evm"]["deployedBytecode"]["object"]):
code = contract["evm"]["deployedBytecode"]["object"]
if ( creation_code = contract["evm"]["bytecode"]["object"]
filename == input_file srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";")
and name == _name srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split(";")
and len(contract["bin-runtime"]) has_contract = True
):
code = contract["bin-runtime"]
creation_code = contract["bin"]
srcmap = contract["srcmap-runtime"].split(";")
srcmap_constructor = contract["srcmap"].split(";")
has_contract = True
break
# If no contract name is specified, get the last bytecode entry for the input file # If no contract name is specified, get the last bytecode entry for the input file
else: else:
for key, contract in sorted(data["contracts"].items()): for contract_name, contract in sorted(
filename, name = key.split(":") data["contracts"][input_file].items()
):
if filename == input_file and len(contract["bin-runtime"]): if len(contract["evm"]["deployedBytecode"]["object"]):
code = contract["bin-runtime"] name = contract_name
creation_code = contract["bin"] code = contract["evm"]["deployedBytecode"]["object"]
srcmap = contract["srcmap-runtime"].split(";") creation_code = contract["evm"]["bytecode"]["object"]
srcmap_constructor = contract["srcmap"].split(";") srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";")
srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split(
";"
)
has_contract = True has_contract = True
if not has_contract: if not has_contract:
@ -139,8 +146,8 @@ class SolidityContract(EVMContract):
:return: The source maps :return: The source maps
""" """
source_maps = set() source_maps = set()
for child in ast["children"]: for child in ast["nodes"]:
if "contractKind" in child["attributes"]: if child.get("contractKind"):
source_maps.add(child["src"]) source_maps.add(child["src"])
return source_maps return source_maps

@ -3,6 +3,11 @@ and dependencies."""
from mythril.disassembler.disassembly import Disassembly from mythril.disassembler.disassembly import Disassembly
import logging import logging
import re import re
import functools
from mythril.ethereum.interface.rpc.client import EthJsonRpc
from typing import Optional
LRU_CACHE_SIZE = 4096
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -10,7 +15,9 @@ log = logging.getLogger(__name__)
class DynLoader: class DynLoader:
"""The dynamic loader class.""" """The dynamic loader class."""
def __init__(self, eth, contract_loading=True, storage_loading=True): def __init__(
self, eth: Optional[EthJsonRpc], contract_loading=True, storage_loading=True
):
""" """
:param eth: :param eth:
@ -18,11 +25,11 @@ class DynLoader:
:param storage_loading: :param storage_loading:
""" """
self.eth = eth self.eth = eth
self.storage_cache = {}
self.contract_loading = contract_loading self.contract_loading = contract_loading
self.storage_loading = storage_loading self.storage_loading = storage_loading
def read_storage(self, contract_address: str, index: int): @functools.lru_cache(LRU_CACHE_SIZE)
def read_storage(self, contract_address: str, index: int) -> str:
""" """
:param contract_address: :param contract_address:
@ -30,43 +37,28 @@ class DynLoader:
:return: :return:
""" """
if not self.storage_loading: if not self.storage_loading:
raise Exception( raise ValueError(
"Cannot load from the storage when the storage_loading flag is false" "Cannot load from the storage when the storage_loading flag is false"
) )
if not self.eth:
raise ValueError("Cannot load from the storage when eth is None")
try: return self.eth.eth_getStorageAt(
contract_ref = self.storage_cache[contract_address] contract_address, position=index, block="latest"
data = contract_ref[index] )
except KeyError:
self.storage_cache[contract_address] = {}
data = self.eth.eth_getStorageAt(
contract_address, position=index, block="latest"
)
self.storage_cache[contract_address][index] = data
except IndexError:
data = self.eth.eth_getStorageAt(
contract_address, position=index, block="latest"
)
self.storage_cache[contract_address][index] = data
return data
def dynld(self, dependency_address): @functools.lru_cache(LRU_CACHE_SIZE)
def dynld(self, dependency_address: str) -> Optional[Disassembly]:
""" """
:param dependency_address: :param dependency_address:
:return: :return:
""" """
if not self.contract_loading: if not self.contract_loading:
raise ValueError("Cannot load contract when contract_loading flag is false") raise ValueError("Cannot load contract when contract_loading flag is false")
if not self.eth:
raise ValueError("Cannot load from the storage when eth is None")
log.debug("Dynld at contract " + dependency_address) log.debug("Dynld at contract %s", dependency_address)
# Ensure that dependency_address is the correct length, with 0s prepended as needed. # Ensure that dependency_address is the correct length, with 0s prepended as needed.
dependency_address = ( dependency_address = (
@ -81,7 +73,7 @@ class DynLoader:
else: else:
return None return None
log.debug("Dependency address: " + dependency_address) log.debug("Dependency address: %s", dependency_address)
code = self.eth.eth_getCode(dependency_address) code = self.eth.eth_getCode(dependency_address)

@ -1,5 +1,6 @@
"""The Mythril function signature database.""" """The Mythril function signature database."""
import functools import functools
import json
import logging import logging
import multiprocessing import multiprocessing
import os import os
@ -9,6 +10,7 @@ from collections import defaultdict
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
from typing import List, Set, DefaultDict, Dict from typing import List, Set, DefaultDict, Dict
from mythril.ethereum.util import get_solc_json
from mythril.exceptions import CompilerError from mythril.exceptions import CompilerError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -231,53 +233,20 @@ class SignatureDB(object, metaclass=Singleton):
return [] return []
def import_solidity_file( def import_solidity_file(
self, file_path: str, solc_binary: str = "solc", solc_args: str = None self, file_path: str, solc_binary: str = "solc", solc_settings_json: str = None
): ):
"""Import Function Signatures from solidity source files. """Import Function Signatures from solidity source files.
:param solc_binary: :param solc_binary:
:param solc_args: :param solc_settings_json:
:param file_path: solidity source code file path :param file_path: solidity source code file path
:return: :return:
""" """
cmd = [solc_binary, "--hashes", file_path] solc_json = get_solc_json(file_path, solc_binary, solc_settings_json)
if solc_args:
cmd.extend(solc_args.split())
try: for contract in solc_json["contracts"][file_path].values():
p = Popen(cmd, stdout=PIPE, stderr=PIPE) for name, hash in contract["evm"]["methodIdentifiers"].items():
stdout, stderr = p.communicate() self.add("0x" + hash, name)
ret = p.returncode
if ret != 0:
raise CompilerError(
"Solc has experienced a fatal error (code {}).\n\n{}".format(
ret, stderr.decode("utf-8")
)
)
except FileNotFoundError:
raise CompilerError(
(
"Compiler not found. Make sure that solc is installed and in PATH, "
"or the SOLC environment variable is set."
)
)
stdout = stdout.decode("unicode_escape").split("\n")
for line in stdout:
# the ':' need not be checked but just to be sure
if all(map(lambda x: x in line, ["(", ")", ":"])):
solc_bytes = "0x" + line.split(":")[0]
solc_text = line.split(":")[1].strip()
self.solidity_sigs[solc_bytes].append(solc_text)
log.debug(
"Signatures: found %d signatures after parsing" % len(self.solidity_sigs)
)
# update DB with what we've found
for byte_sig, text_sigs in self.solidity_sigs.items():
for text_sig in text_sigs:
self.add(byte_sig, text_sig)
@staticmethod @staticmethod
def lookup_online(byte_sig: str, timeout: int, proxies=None) -> List[str]: def lookup_online(byte_sig: str, timeout: int, proxies=None) -> List[str]:

@ -1,7 +1,7 @@
coloredlogs>=10.0 coloredlogs>=10.0
configparser>=3.5.0 configparser>=3.5.0
coverage coverage
py_ecc==1.4.2 py_ecc==1.6.0
eth_abi==1.3.0 eth_abi==1.3.0
eth-account>=0.1.0a2,<=0.3.0 eth-account>=0.1.0a2,<=0.3.0
ethereum>=2.3.2 ethereum>=2.3.2
@ -12,19 +12,21 @@ eth-keys>=0.2.0b3,<0.3.0
eth-rlp>=0.1.0 eth-rlp>=0.1.0
eth-tester==0.1.0b32 eth-tester==0.1.0b32
eth-typing>=2.0.0 eth-typing>=2.0.0
eth-utils>=1.0.1
jinja2>=2.9 jinja2>=2.9
mock mock
persistent>=4.2.0 persistent>=4.2.0
plyvel plyvel
py-flags py-flags
py-solc-x==0.6.0
py-solc py-solc
pytest>=3.6.0 pytest>=3.6.0
pytest-cov pytest-cov
pytest_mock pytest_mock
requests requests>=2.22.0
rlp>=1.0.1 rlp>=1.0.1
semantic_version==2.8.1
transaction>=2.2.1 transaction>=2.2.1
z3-solver>=4.8.5.0 z3-solver>=4.8.5.0
pysha3 pysha3
matplotlib matplotlib
pythx

@ -25,14 +25,15 @@ REQUIRES_PYTHON = ">=3.5.0"
# What packages are required for this module to be executed? # What packages are required for this module to be executed?
REQUIRED = [ REQUIRED = [
"coloredlogs>=10.0", "coloredlogs>=10.0",
"py_ecc==1.4.2", "py_ecc==1.6.0",
"ethereum>=2.3.2", "ethereum>=2.3.2",
"z3-solver>=4.8.5.0", "z3-solver>=4.8.5.0",
"requests", "requests>=2.22.0",
"py-solc", "py-solc",
"py-solc-x==0.6.0",
"semantic_version==2.8.1",
"plyvel", "plyvel",
"eth_abi==1.3.0", "eth_abi==1.3.0",
"eth-utils>=1.0.1",
"eth-account>=0.1.0a2,<=0.3.0", "eth-account>=0.1.0a2,<=0.3.0",
"eth-hash>=0.1.0", "eth-hash>=0.1.0",
"eth-keyfile>=0.5.1", "eth-keyfile>=0.5.1",
@ -50,6 +51,7 @@ REQUIRED = [
"persistent>=4.2.0", "persistent>=4.2.0",
"ethereum-input-decoder>=0.2.2", "ethereum-input-decoder>=0.2.2",
"matplotlib", "matplotlib",
"pythx",
] ]
TESTS_REQUIRE = ["mypy", "pytest>=3.6.0", "pytest_mock", "pytest-cov"] TESTS_REQUIRE = ["mypy", "pytest>=3.6.0", "pytest_mock", "pytest-cov"]

File diff suppressed because one or more lines are too long

@ -0,0 +1,72 @@
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.machine_state import MachineState
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.instructions import Instruction
from mythril.laser.ethereum.transaction.transaction_models import MessageCallTransaction
from mythril.laser.ethereum.state.calldata import ConcreteCalldata
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.smt import symbol_factory
# contract A {
# uint256 public val = 10;
# }
contract_init_code = "6080604052600a600055348015601457600080fd5b506084806100236000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80633c6bb43614602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea265627a7a72315820d3cfe7a909450a953cbd7e47d8c17477f2609baa5208d043e965efec69d1ed9864736f6c634300050b0032"
contract_runtime_code = "6080604052348015600f57600080fd5b506004361060285760003560e01c80633c6bb43614602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea265627a7a72315820d3cfe7a909450a953cbd7e47d8c17477f2609baa5208d043e965efec69d1ed9864736f6c634300050b0032"
last_state = None
created_contract_account = None
def execute_create():
global last_state
global created_contract_account
if not last_state and not created_contract_account:
code_raw = []
for i in range(len(contract_init_code) // 2):
code_raw.append(int(contract_init_code[2 * i : 2 * (i + 1)], 16))
calldata = ConcreteCalldata(0, code_raw)
world_state = WorldState()
account = world_state.create_account(balance=1000000, address=101)
account.code = Disassembly("60a760006000f000")
environment = Environment(account, None, calldata, None, None, None)
og_state = GlobalState(
world_state, environment, None, MachineState(gas_limit=8000000)
)
og_state.transaction_stack.append(
(MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None)
)
laser = LaserEVM()
states = [og_state]
last_state = og_state
for state in states:
new_states, op_code = laser.execute_state(state)
last_state = state
if op_code == "STOP":
break
states.extend(new_states)
created_contract_address = last_state.mstate.stack[-1].value
created_contract_account = last_state.world_state.accounts[
created_contract_address
]
return last_state, created_contract_account
def test_create_has_code():
last_state, created_contract_account = execute_create()
assert created_contract_account.code.bytecode == contract_runtime_code
def test_create_has_storage():
last_state, created_contract_account = execute_create()
storage = created_contract_account.storage
# From contract, val = 10.
assert storage[symbol_factory.BitVecVal(0, 256)] == symbol_factory.BitVecVal(
10, 256
)

@ -0,0 +1,48 @@
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.machine_state import MachineState
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.instructions import Instruction
from mythril.laser.ethereum.transaction.transaction_models import MessageCallTransaction
from mythril.support.support_utils import get_code_hash
from mythril.laser.smt import symbol_factory
# Arrange
world_state = WorldState()
account = world_state.create_account(balance=10, address=101)
account.code = Disassembly("60606040")
world_state.create_account(balance=10, address=1000)
environment = Environment(account, None, None, None, None, None)
og_state = GlobalState(world_state, environment, None, MachineState(gas_limit=8000000))
og_state.transaction_stack.append(
(MessageCallTransaction(world_state=WorldState(), gas_limit=8000000), None)
)
instruction = Instruction("extcodehash", dynamic_loader=None)
def test_extcodehash_no_account():
# If account does not exist, return 0
og_state.mstate.stack = [symbol_factory.BitVecVal(1, 256)]
new_state = instruction.evaluate(og_state)[0]
assert new_state.mstate.stack[-1] == 0
def test_extcodehash_no_code():
# If account code does not exist, return hash of empty set.
og_state.mstate.stack = [symbol_factory.BitVecVal(1000, 256)]
new_state = instruction.evaluate(og_state)[0]
assert hex(new_state.mstate.stack[-1].value) == get_code_hash("")
def test_extcodehash_return_hash():
# If account code exists, return hash of the code.
og_state.mstate.stack = [symbol_factory.BitVecVal(101, 256)]
new_state = instruction.evaluate(og_state)[0]
assert hex(new_state.mstate.stack[-1].value) == get_code_hash("60606040")

@ -0,0 +1,124 @@
import pytest
from mock import patch
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.smt import symbol_factory
from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.machine_state import MachineState
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.instructions import Instruction
from mythril.laser.ethereum.transaction.transaction_models import MessageCallTransaction
from mythril.laser.ethereum.call import SymbolicCalldata
from mythril.laser.ethereum.transaction import TransactionStartSignal
from mythril.laser.ethereum.evm_exceptions import WriteProtection
def get_global_state():
active_account = Account("0x0", code=Disassembly("60606040"))
environment = Environment(
active_account, None, SymbolicCalldata("2"), None, None, None
)
world_state = WorldState()
world_state.put_account(active_account)
state = GlobalState(world_state, environment, None, MachineState(gas_limit=8000000))
state.transaction_stack.append(
(MessageCallTransaction(world_state=world_state, gas_limit=8000000), None)
)
return state
@patch(
"mythril.laser.ethereum.instructions.get_call_parameters",
return_value=(
"0",
Account(code=Disassembly(code="0x00"), address="0x19"),
0,
0,
0,
0,
0,
),
)
def test_staticcall(f1):
# Arrange
state = get_global_state()
state.mstate.stack = [10, 10, 10, 10, 10, 10, 10, 10, 0]
instruction = Instruction("staticcall", dynamic_loader=None)
# Act and Assert
with pytest.raises(TransactionStartSignal) as ts:
instruction.evaluate(state)
assert ts.value.transaction.static
assert ts.value.transaction.initial_global_state().environment.static
test_data = (
"suicide",
"create",
"create2",
"log0",
"log1",
"log2",
"log3",
"log4",
"sstore",
)
@pytest.mark.parametrize("input", test_data)
def test_staticness(input):
# Arrange
state = get_global_state()
state.environment.static = True
state.mstate.stack = []
instruction = Instruction(input, dynamic_loader=None)
# Act and Assert
with pytest.raises(WriteProtection):
instruction.evaluate(state)
test_data_call = ((0, True), (100, False))
@pytest.mark.parametrize("input, success", test_data_call)
@patch("mythril.laser.ethereum.instructions.get_call_parameters")
def test_staticness_call_concrete(f1, input, success):
# Arrange
state = get_global_state()
state.environment.static = True
state.mstate.stack = []
code = Disassembly(code="616263")
f1.return_value = ("0", Account(code=code, address="0x19"), 0, input, 0, 0, 0)
instruction = Instruction("call", dynamic_loader=None)
# Act and Assert
if success:
with pytest.raises(TransactionStartSignal) as ts:
instruction.evaluate(state)
assert ts.value.transaction.static
else:
with pytest.raises(WriteProtection):
instruction.evaluate(state)
@patch("mythril.laser.ethereum.instructions.get_call_parameters")
def test_staticness_call_symbolic(f1):
# Arrange
state = get_global_state()
state.environment.static = True
state.mstate.stack = []
call_value = symbol_factory.BitVecSym("x", 256)
code = Disassembly(code="616263")
f1.return_value = ("0", Account(code=code, address="0x19"), 0, call_value, 0, 0, 0)
instruction = Instruction("call", dynamic_loader=None)
# Act and Assert
with pytest.raises(TransactionStartSignal) as ts:
instruction.evaluate(state)
assert ts.value.transaction.static
assert ts.value.global_state.mstate.constraints[-1] == (call_value == 0)

@ -0,0 +1,27 @@
from mock import patch
from eth_utils import decode_hex
from mythril.laser.ethereum.natives import ec_add
from py_ecc.optimized_bn128 import FQ
VECTOR_A = decode_hex(
"0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000020"
"0000000000000000000000000000000000000000000000000000000000000020"
"03"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"
)
def test_ec_add_sanity():
assert ec_add(VECTOR_A) == []
@patch("mythril.laser.ethereum.natives.validate_point", return_value=1)
@patch("mythril.laser.ethereum.natives.bn128.add", return_value=1)
@patch("mythril.laser.ethereum.natives.bn128.normalize")
def test_ec_add(f1, f2, f3):
FQ.fielf_modulus = 128
a = FQ(val=1)
f1.return_value = (a, a)
assert ec_add(VECTOR_A) == ([0] * 31 + [1]) * 2

@ -0,0 +1,35 @@
from mock import patch
from mythril.laser.ethereum.natives import ec_pair
from py_ecc.optimized_bn128 import FQ
def test_ec_pair_192_check():
vec_c = [0] * 100
assert ec_pair(vec_c) == []
@patch("mythril.laser.ethereum.natives.validate_point", return_value=1)
@patch("mythril.laser.ethereum.natives.bn128.is_on_curve", return_value=True)
@patch("mythril.laser.ethereum.natives.bn128.pairing", return_value=1)
@patch("mythril.laser.ethereum.natives.bn128.normalize")
def test_ec_pair(f1, f2, f3, f4):
FQ.fielf_modulus = 100
a = FQ(val=1)
f1.return_value = (a, a)
vec_c = [0] * 192
assert ec_pair(vec_c) == [0] * 31 + [1]
@patch("mythril.laser.ethereum.natives.validate_point", return_value=False)
def test_ec_pair_point_validation_failure(f1):
vec_c = [0] * 192
assert ec_pair(vec_c) == []
@patch("mythril.laser.ethereum.natives.validate_point", return_value=1)
def test_ec_pair_field_exceed_mod(f1):
FQ.fielf_modulus = 100
a = FQ(val=1)
f1.return_value = (a, a)
vec_c = [10] * 192
assert ec_pair(vec_c) == []

@ -0,0 +1,27 @@
from mock import patch
from eth_utils import decode_hex
from mythril.laser.ethereum.natives import ec_mul
from py_ecc.optimized_bn128 import FQ
VECTOR_A = decode_hex(
"0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000020"
"0000000000000000000000000000000000000000000000000000000000000020"
"03"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"
)
@patch("mythril.laser.ethereum.natives.validate_point", return_value=1)
@patch("mythril.laser.ethereum.natives.bn128.multiply", return_value=1)
@patch("mythril.laser.ethereum.natives.bn128.normalize")
def test_ec_mul(f1, f2, f3):
FQ.fielf_modulus = 128
a = FQ(val=1)
f1.return_value = (a, a)
assert ec_mul(VECTOR_A) == ([0] * 31 + [1]) * 2
def test_ec_mul_validation_failure():
assert ec_mul(VECTOR_A) == []

@ -0,0 +1,61 @@
import pytest
from eth_utils import decode_hex
from mythril.laser.ethereum.natives import mod_exp
from ethereum.utils import big_endian_to_int
EIP198_VECTOR_A = decode_hex(
"0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000020"
"0000000000000000000000000000000000000000000000000000000000000020"
"03"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"
)
EIP198_VECTOR_B = decode_hex(
"0000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000020"
"0000000000000000000000000000000000000000000000000000000000000020"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e"
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"
)
EIP198_VECTOR_C = decode_hex(
"0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000002"
"0000000000000000000000000000000000000000000000000000000000000020"
"03"
"ffff"
"8000000000000000000000000000000000000000000000000000000000000000"
"07"
)
EIP198_VECTOR_D = decode_hex(
"0000000000000000000000000000000000000000000000000000000000000001"
"0000000000000000000000000000000000000000000000000000000000000002"
"0000000000000000000000000000000000000000000000000000000000000020"
"03"
"ffff"
"80"
)
@pytest.mark.parametrize(
"data,expected",
(
(EIP198_VECTOR_A, 1),
(EIP198_VECTOR_B, 0),
(
EIP198_VECTOR_C,
26689440342447178617115869845918039756797228267049433585260346420242739014315,
),
(
EIP198_VECTOR_D,
26689440342447178617115869845918039756797228267049433585260346420242739014315,
),
),
)
def test_modexp_result(data, expected):
actual = mod_exp(data)
assert big_endian_to_int(actual) == expected

@ -47,7 +47,7 @@
} }
}, },
"0xcd1722f3947def4cf144679da39c4c32bdc35681" : { "0xcd1722f3947def4cf144679da39c4c32bdc35681" : {
"balance" : "0x17", "balance" : "0x0186a0",
"code" : "0x6000355415600957005b60203560003555", "code" : "0x6000355415600957005b60203560003555",
"nonce" : "0x00", "nonce" : "0x00",
"storage" : { "storage" : {
@ -55,4 +55,4 @@
} }
} }
} }
} }

@ -54,7 +54,7 @@
} }
}, },
"0xcd1722f3947def4cf144679da39c4c32bdc35681" : { "0xcd1722f3947def4cf144679da39c4c32bdc35681" : {
"balance" : "0x17", "balance" : "0x0186a0",
"code" : "0x6000355415600957005b60203560003555", "code" : "0x6000355415600957005b60203560003555",
"nonce" : "0x00", "nonce" : "0x00",
"storage" : { "storage" : {
@ -62,4 +62,4 @@
} }
} }
} }
} }

@ -47,7 +47,7 @@
} }
}, },
"0xcd1722f3947def4cf144679da39c4c32bdc35681" : { "0xcd1722f3947def4cf144679da39c4c32bdc35681" : {
"balance" : "0x17", "balance" : "0x0186a0",
"code" : "0x6000355415600957005b60203560003555", "code" : "0x6000355415600957005b60203560003555",
"nonce" : "0x00", "nonce" : "0x00",
"storage" : { "storage" : {
@ -55,4 +55,4 @@
} }
} }
} }
} }

@ -1,4 +1,4 @@
from mythril.laser.smt import Solver, symbol_factory, bitvec from mythril.laser.smt import Solver, symbol_factory, UGT, UGE, ULT, ULE
import z3 import z3
import pytest import pytest
@ -42,10 +42,10 @@ def test_bitvecfunc_arithmetic(operation, expected):
(operator.le, z3.sat), (operator.le, z3.sat),
(operator.gt, z3.unsat), (operator.gt, z3.unsat),
(operator.ge, z3.sat), (operator.ge, z3.sat),
(bitvec.UGT, z3.unsat), (UGT, z3.unsat),
(bitvec.UGE, z3.sat), (UGE, z3.sat),
(bitvec.ULT, z3.unsat), (ULT, z3.unsat),
(bitvec.ULE, z3.sat), (ULE, z3.sat),
], ],
) )
def test_bitvecfunc_bitvecfunc_comparison(operation, expected): def test_bitvecfunc_bitvecfunc_comparison(operation, expected):
@ -80,3 +80,158 @@ def test_bitvecfunc_bitvecfuncval_comparison():
# Assert # Assert
assert s.check() == z3.sat assert s.check() == z3.sat
assert s.model().eval(input2.raw) == 1337 assert s.model().eval(input2.raw) == 1337
def test_bitvecfunc_nested_comparison():
# arrange
s = Solver()
input1 = symbol_factory.BitVecSym("input1", 256)
input2 = symbol_factory.BitVecSym("input2", 256)
bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1)
bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1)
bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2)
bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3)
# Act
s.add(input1 == input2)
s.add(bvf2 == bvf4)
# Assert
assert s.check() == z3.sat
def test_bitvecfunc_unequal_nested_comparison():
# arrange
s = Solver()
input1 = symbol_factory.BitVecSym("input1", 256)
input2 = symbol_factory.BitVecSym("input2", 256)
bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1)
bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1)
bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2)
bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3)
# Act
s.add(input1 != input2)
s.add(bvf2 == bvf4)
# Assert
assert s.check() == z3.unsat
def test_bitvecfunc_ext_nested_comparison():
# arrange
s = Solver()
input1 = symbol_factory.BitVecSym("input1", 256)
input2 = symbol_factory.BitVecSym("input2", 256)
input3 = symbol_factory.BitVecSym("input3", 256)
input4 = symbol_factory.BitVecSym("input4", 256)
bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1)
bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1 + input3)
bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2)
bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3 + input4)
# Act
s.add(input1 == input2)
s.add(input3 == input4)
s.add(bvf2 == bvf4)
# Assert
assert s.check() == z3.sat
def test_bitvecfunc_ext_unequal_nested_comparison():
# Arrange
s = Solver()
input1 = symbol_factory.BitVecSym("input1", 256)
input2 = symbol_factory.BitVecSym("input2", 256)
input3 = symbol_factory.BitVecSym("input3", 256)
input4 = symbol_factory.BitVecSym("input4", 256)
bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1)
bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1 + input3)
bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2)
bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3 + input4)
# Act
s.add(input1 == input2)
s.add(input3 != input4)
s.add(bvf2 == bvf4)
# Assert
assert s.check() == z3.unsat
def test_bitvecfunc_ext_unequal_nested_comparison_f():
# Arrange
s = Solver()
input1 = symbol_factory.BitVecSym("input1", 256)
input2 = symbol_factory.BitVecSym("input2", 256)
input3 = symbol_factory.BitVecSym("input3", 256)
input4 = symbol_factory.BitVecSym("input4", 256)
bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1)
bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1 + input3)
bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2)
bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3 + input4)
# Act
s.add(input1 != input2)
s.add(input3 == input4)
s.add(bvf2 == bvf4)
# Assert
assert s.check() == z3.unsat
def test_bitvecfunc_find_input():
# Arrange
s = Solver()
input1 = symbol_factory.BitVecSym("input1", 256)
input2 = symbol_factory.BitVecSym("input2", 256)
bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1)
bvf2 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2)
# Act
s.add(input1 == symbol_factory.BitVecVal(1, 256))
s.add(bvf1 == bvf2)
# Assert
assert s.check() == z3.sat
assert s.model()[input2.raw] == 1
def test_bitvecfunc_nested_find_input():
# Arrange
s = Solver()
input1 = symbol_factory.BitVecSym("input1", 256)
input2 = symbol_factory.BitVecSym("input2", 256)
bvf1 = symbol_factory.BitVecFuncSym("bvf1", "sha3", 256, input_=input1)
bvf2 = symbol_factory.BitVecFuncSym("bvf2", "sha3", 256, input_=bvf1)
bvf3 = symbol_factory.BitVecFuncSym("bvf3", "sha3", 256, input_=input2)
bvf4 = symbol_factory.BitVecFuncSym("bvf4", "sha3", 256, input_=bvf3)
# Act
s.add(input1 == symbol_factory.BitVecVal(123, 256))
s.add(bvf2 == bvf4)
# Assert
assert s.check() == z3.sat
assert s.model()[input2.raw] == 123

@ -44,7 +44,7 @@ def test_sym_exec():
contract, contract,
address=(util.get_indexed_address(0)), address=(util.get_indexed_address(0)),
strategy="dfs", strategy="dfs",
execution_timeout=10, execution_timeout=25,
) )
issues = fire_lasers(sym) issues = fire_lasers(sym)

@ -76,6 +76,8 @@ class NativeTests(BaseTestCase):
@staticmethod @staticmethod
def runTest(): def runTest():
"""""" """"""
# The solidity version (0.5.3 at the moment) should be kept in sync with
# pragma in ./tests/native_tests.sol
disassembly = SolidityContract( disassembly = SolidityContract(
"./tests/native_tests.sol", "./tests/native_tests.sol",
solc_binary=MythrilDisassembler._init_solc_binary("0.5.3"), solc_binary=MythrilDisassembler._init_solc_binary("0.5.3"),

@ -1,4 +1,4 @@
pragma solidity 0.5.0; pragma solidity ^0.5.0;
contract Caller { contract Caller {

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py35,py36 envlist = py36
[testenv] [testenv]
deps = deps =

Loading…
Cancel
Save