[](https://discord.com/invite/consensys)
[](https://pypi.python.org/pypi/mythril)
[](https://mythril-classic.readthedocs.io/en/develop/)
[](https://dl.circleci.com/status-badge/redirect/gh/Consensys/mythril/tree/develop)
[](https://sonarcloud.io/dashboard?id=mythril)
[](https://pepy.tech/project/mythril)
[](https://cloud.docker.com/u/mythril/repository/docker/mythril/myth)
Mythril is a symbolic-execution-based security analysis tool for EVM bytecode. It detects security vulnerabilities in smart contracts built for Ethereum and other EVM-compatible blockchains.
Whether you want to contribute, need support, or want to learn what we have cooking for the future, you can checkout diligence-mythx channel in [ConsenSys Discord server](https://discord.gg/consensys).
## Installation and setup
Get it with [Docker](https://www.docker.com):
```bash
$ docker pull mythril/myth
```
Install from Pypi (Python 3.7-3.10):
```bash
$ pip3 install mythril
```
Use it via pre-commit hook (replace `$GIT_TAG` with real tag):
```YAML
- repo: https://github.com/Consensys/mythril
rev: $GIT_TAG
hooks:
- id: mythril
```
Additionally, set `args: [disassemble]` or `args: [read-storage]` to use a different command than `analyze`.
See the [docs](https://mythril-classic.readthedocs.io/en/master/installation.html) for more detailed instructions.
## Usage
Run:
```
$ myth analyze
```
Or:
```
$ myth analyze -a
```
Specify the maximum number of transactions to explore with `-t `. You can also set a timeout with `--execution-timeout `.
Here is an example of running Mythril on the file `killbilly.sol` which is in the `solidity_examples` directory for `3` transactions:
```
> myth a killbilly.sol -t 3
==== Unprotected Selfdestruct ====
SWC ID: 106
Severity: High
Contract: KillBilly
Function name: commencekilling()
PC address: 354
Estimated Gas Usage: 974 - 1399
Any sender can cause the contract to self-destruct.
Any sender can trigger execution of the SELFDESTRUCT instruction to destroy this contract account and withdraw its balance to an arbitrary address. Review the transaction trace generated for this issue and make sure that appropriate security controls are in place to prevent unrestricted access.
--------------------
In file: killbilly.sol:22
selfdestruct(msg.sender)
--------------------
Initial State:
Account: [CREATOR], balance: 0x2, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x1001, nonce:0, storage:{}
Transaction Sequence:
Caller: [CREATOR], calldata: , decoded_data: , value: 0x0
Caller: [ATTACKER], function: killerize(address), txdata: 0x9fa299cc000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef, decoded_data: ('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',), 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/develop/).
For support or general discussions please checkout [diligence-mythx channel](https://discord.com/channels/697535391594446898/712829485350649886) in [ConsenSys Discord server](https://discord.gg/consensys)..
## Building the Documentation
Mythril's documentation is contained in the `docs` folder and is published to [Read the Docs](https://mythril-classic.readthedocs.io/en/develop/). It is based on Sphinx and can be built using the Makefile contained in the subdirectory:
```
cd docs
make html
```
This will create a `build` output directory containing the HTML output. Alternatively, PDF documentation can be built with `make latexpdf`. The available output format options can be seen with `make help`.
## Vulnerability Remediation
Visit the [Smart Contract Vulnerability Classification Registry](https://swcregistry.io/) to find detailed information and remediation guidance for the vulnerabilities reported.
================================================
FILE: all_tests.sh
================================================
#!/bin/bash
set -euo pipefail
echo -n "Checking Python version... "
python -c 'import sys
print(sys.version)
assert sys.version_info[0:2] >= (3,5), \
"""Please make sure you are using Python 3.5 or later.
You ran with {}""".format(sys.version)' || exit $?
rm -rf ./tests/testdata/outputs_current/
mkdir -p ./tests/testdata/outputs_current/
rm -rf ./tests/testdata/outputs_current_laser_result/
mkdir -p ./tests/testdata/outputs_current_laser_result/
mkdir -p /tmp/test-reports
pytest --junitxml=/tmp/test-reports/junit.xml
================================================
FILE: coverage_report.sh
================================================
#!/bin/sh
python --version
echo "Please make sure you are using python 3.6.x"
rm -rf ./tests/testdata/outputs_current/
mkdir -p ./tests/testdata/outputs_current/
rm -rf ./tests/testdata/outputs_current_laser_result/
mkdir -p ./tests/testdata/outputs_current_laser_result/
rm -rf coverage_html_report
py.test \
--cov=mythril \
--cov-config=tox.ini \
--cov-report=html:coverage_reports/coverage_html_report \
--cov-report=xml:coverage_reports/coverage_xml_report.xml
================================================
FILE: docker/docker-entrypoint.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
# Install extra solc versions if SOLC is set
if [[ ${SOLC:-} != "" ]]; then
read -ra solc_versions <<<"${SOLC:?}"
svm install "${solc_versions[@]}"
fi
# Always sync versions, as the should be at least one solc version installed
# in the base image, and we may be running as root rather than the mythril user.
sync-svm-solc-versions-with-solcx
# By default we run myth with options from arguments we received. But if the
# first argument is a valid program, we execute that instead so that people can
# run other commands without overriding the entrypoint (e.g. bash).
if command -v "${1:-}" >/dev/null; then
exec -- "$@"
fi
exec -- myth "$@"
================================================
FILE: docker/sync-svm-solc-versions-with-solcx.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
# Let solcx know about the solc versions installed by svm.
# We do this by symlinking svm's solc binaries into solcx's solc dir.
[[ -e ~/.svm ]] || exit 0
mkdir -p ~/.solcx
readarray -t svm_solc_bins <<<"$(find ~/.svm -type f -name 'solc-*')"
[[ ${svm_solc_bins[0]} != "" ]] || exit 0
for svm_solc in "${svm_solc_bins[@]}"; do
name=$(basename "${svm_solc:?}")
version="${name#"solc-"}" # strip solc- prefix
solcx_solc=~/.solcx/"solc-v${version:?}"
if [[ ! -e $solcx_solc ]]; then
ln -s "${svm_solc:?}" "${solcx_solc:?}"
fi
done
================================================
FILE: docker-bake.hcl
================================================
variable "REGISTRY" {
default = "docker.io"
}
variable "VERSION" {
default = "dev"
}
variable "PYTHON_VERSION" {
default = "3.10"
}
variable "INSTALLED_SOLC_VERSIONS" {
default = "0.8.19"
}
function "myth-tags" {
params = [NAME]
result = formatlist("${REGISTRY}/${NAME}:%s", split(",", VERSION))
}
group "default" {
targets = ["myth", "myth-smoke-test"]
}
target "_myth-base" {
target = "myth"
args = {
PYTHON_VERSION = PYTHON_VERSION
INSTALLED_SOLC_VERSIONS = INSTALLED_SOLC_VERSIONS
}
platforms = [
"linux/amd64",
"linux/arm64"
]
}
target "myth" {
inherits = ["_myth-base"]
tags = myth-tags("mythril/myth")
}
target "myth-dev" {
inherits = ["_myth-base"]
tags = myth-tags("mythril/myth-dev")
}
target "myth-smoke-test" {
inherits = ["_myth-base"]
target = "myth-smoke-test"
output = ["build/docker/smoke-test"]
}
================================================
FILE: docker_build_and_deploy.sh
================================================
#!/bin/bash
set -eo pipefail
NAME=$1
if [[ ! $NAME =~ ^mythril/myth(-dev)?$ ]]; then
echo "Error: unknown image name: $NAME" >&2
exit 1
fi
if [ -n "$CIRCLE_TAG" ]; then
GIT_VERSION=${CIRCLE_TAG#?}
else
GIT_VERSION=${CIRCLE_SHA1}
fi
export DOCKER_BUILDKIT=1
docker buildx create --use
# Build and test all versions of the image. (The result will stay in the cache,
# so the next build should be almost instant.)
docker buildx bake myth-smoke-test
if [ -z "$DOCKERHUB_USERNAME" ]; then
echo "Finishing without pushing to dockerhub"
exit 0
fi
echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
# strip mythril/ from NAME, e.g. myth or myth-dev
BAKE_TARGET="${NAME#mythril/}"
VERSION="${GIT_VERSION:?},latest" docker buildx bake --push "${BAKE_TARGET:?}"
================================================
FILE: docs/Makefile
================================================
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
================================================
FILE: docs/make.bat
================================================
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd
================================================
FILE: docs/source/about.rst
================================================
What is Mythril?
========================
Mythril is a security analysis tool for Ethereum smart contracts. It was `introduced at HITBSecConf 2018 `_.
Mythril detects a range of security issues, including integer underflows, owner-overwrite-to-Ether-withdrawal, and others. Note that Mythril is targeted at finding common vulnerabilities, and is not able to discover issues in the business logic of an application. Furthermore, Mythril and symbolic executors are generally unsound, as they are often unable to explore all possible states of a program.
================================================
FILE: docs/source/analysis-modules.rst
================================================
Analysis Modules
================
Mythril's detection capabilities are written in modules in the `/analysis/module/modules `_ directory.
.. toctree::
:maxdepth: 2
module-list.rst
create-module.rst
================================================
FILE: docs/source/conf.py
================================================
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import os
import sys
sys.path.insert(0, os.path.abspath("../../"))
# -- Project information -----------------------------------------------------
project = "Mythril"
copyright = "2019, ConsenSys Diligence"
author = "ConsenSys Dilligence"
# The short X.Y version
version = ""
# The full version, including alpha/beta/rc tags
from mythril.__version__ import __version__ as VERSION
release = VERSION
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.coverage",
"sphinx.ext.mathjax",
"sphinx.ext.viewcode",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = "Mythrildoc"
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(
master_doc,
"Mythril.tex",
"Mythril Documentation",
"ConsenSys Dilligence",
"manual",
)
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, "mythril", "Mythril Documentation", [author], 1)]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"Mythril",
"Mythril Documentation",
author,
"Mythril",
"One line description of project.",
"Miscellaneous",
)
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------
================================================
FILE: docs/source/create-module.rst
================================================
Creating a Module
=================
Create a module in the :code:`analysis/modules` directory, and create an instance of a class that inherits :code:`DetectionModule` named :code:`detector`. Take a look at the `suicide module `_ as an example.
================================================
FILE: docs/source/index.rst
================================================
Welcome to Mythril's documentation!
===========================================
.. toctree::
:maxdepth: 1
:caption: Table of Contents:
about
installation
tutorial
security-analysis
analysis-modules
mythril
Indices and Tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
================================================
FILE: docs/source/installation.rst
================================================
Installation and Setup
======================
Mythril can be setup using different methods.
**************
PyPI on Mac OS
**************
.. code-block:: bash
brew update
brew upgrade
brew tap ethereum/ethereum
brew install solidity
pip3 install mythril
**************
PyPI on Ubuntu
**************
.. code-block:: bash
# Update
sudo apt update
# Install solc
sudo apt install software-properties-common
sudo add-apt-repository ppa:ethereum/ethereum
sudo apt install solc
# Install libssl-dev, python3-dev, and python3-pip
sudo apt install libssl-dev python3-dev python3-pip
# Install mythril
pip3 install mythril
myth version
******
Docker
******
All Mythril releases, starting from v0.18.3, are published to DockerHub as Docker images under the :code:`mythril/myth` name.
After installing `Docker CE `_:
.. code-block:: bash
# Pull the latest release of mythril/myth
$ docker pull mythril/myth
Use :code:`docker run mythril/myth` the same way you would use the :code:`myth` command
.. code-block:: bash
docker run mythril/myth --help
docker run mythril/myth disassemble -c "0x6060"
To pass a file from your host machine to the dockerized Mythril, you must mount its containing folder to the container properly. For :code:`contract.sol` in the current working directory, do:
.. code-block:: bash
docker run -v $(pwd):/tmp mythril/myth analyze /tmp/contract.sol
================================================
FILE: docs/source/module-list.rst
================================================
Modules
=======
***********************************
Delegate Call To Untrusted Contract
***********************************
The `delegatecall module `_ detects `SWC-112 (DELEGATECALL to Untrusted Callee) `_.
***********************************
Dependence on Predictable Variables
***********************************
The `predictable variables module `_ detects `SWC-120 (Weak Randomness) `_ and `SWC-116 (Timestamp Dependence) `_.
***********
Ether Thief
***********
The `Ether Thief module `_ detects `SWC-105 (Unprotected Ether Withdrawal) `_.
**********
Exceptions
**********
The `exceptions module `_ detects `SWC-110 (Assert Violation) `_.
**************
External Calls
**************
The `external calls module `_ warns about `SWC-107 (Reentrancy) `_ by detecting calls to external contracts.
*******
Integer
*******
The `integer module `_ detects `SWC-101 (Integer Overflow and Underflow) `_.
**************
Multiple Sends
**************
The `multiple sends module `_ detects `SWC-113 (Denial of Service with Failed Call) `_ by checking for multiple calls or sends in a single transaction.
*******
Suicide
*******
The `suicide module `_ detects `SWC-106 (Unprotected SELFDESTRUCT) `_.
****************************
State Change External Calls
****************************
The `state change external calls module `_ detects `SWC-107 (Reentrancy) `_ by detecting state change after calls to an external contract.
****************
Unchecked Retval
****************
The `unchecked retval module `_ detects `SWC-104 (Unchecked Call Return Value) `_.
************************
User Supplied assertion
************************
The `user supplied assertion module `_ detects `SWC-110 (Assert Violation) `_ for user-supplied assertions. User supplied assertions should be log messages of the form: :code:`emit AssertionFailed(string)`.
************************
Arbitrary Storage Write
************************
The `arbitrary storage write module `_ detects `SWC-124 (Write to Arbitrary Storage Location) `_.
****************
Arbitrary Jump
****************
The `arbitrary jump module `_ detects `SWC-127 (Arbitrary Jump with Function Type Variable) `_.
****************************
Transaction Order Dependence
****************************
The `transaction order dependence module `_ detects `SWC-114 (Transaction Order Dependence) `_.
****************************
Requirement Violation
****************************
The `Requirement Violation module `_ detects `SWC-123 (Requirement Violation) `_.
****************************
Unexpected Ether balance
****************************
The `Unexpected Ether balance module `_ detects `SWC-132 (Unexpected Ether balance) `_.
================================================
FILE: docs/source/modules.rst
================================================
mythril
=======
.. toctree::
:maxdepth: 4
mythril
================================================
FILE: docs/source/mythril.analysis.module.modules.rst
================================================
mythril.analysis.module.modules package
=======================================
Submodules
----------
mythril.analysis.module.modules.arbitrary\_jump module
------------------------------------------------------
.. automodule:: mythril.analysis.module.modules.arbitrary_jump
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.arbitrary\_write module
-------------------------------------------------------
.. automodule:: mythril.analysis.module.modules.arbitrary_write
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.delegatecall module
---------------------------------------------------
.. automodule:: mythril.analysis.module.modules.delegatecall
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.dependence\_on\_origin module
-------------------------------------------------------------
.. automodule:: mythril.analysis.module.modules.dependence_on_origin
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.dependence\_on\_predictable\_vars module
------------------------------------------------------------------------
.. automodule:: mythril.analysis.module.modules.dependence_on_predictable_vars
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.ether\_thief module
---------------------------------------------------
.. automodule:: mythril.analysis.module.modules.ether_thief
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.exceptions module
-------------------------------------------------
.. automodule:: mythril.analysis.module.modules.exceptions
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.external\_calls module
------------------------------------------------------
.. automodule:: mythril.analysis.module.modules.external_calls
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.integer module
----------------------------------------------
.. automodule:: mythril.analysis.module.modules.integer
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.multiple\_sends module
------------------------------------------------------
.. automodule:: mythril.analysis.module.modules.multiple_sends
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.state\_change\_external\_calls module
---------------------------------------------------------------------
.. automodule:: mythril.analysis.module.modules.state_change_external_calls
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.suicide module
----------------------------------------------
.. automodule:: mythril.analysis.module.modules.suicide
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.unchecked\_retval module
--------------------------------------------------------
.. automodule:: mythril.analysis.module.modules.unchecked_retval
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.modules.user\_assertions module
-------------------------------------------------------
.. automodule:: mythril.analysis.module.modules.user_assertions
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.analysis.module.modules
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.analysis.module.rst
================================================
mythril.analysis.module package
===============================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.analysis.module.modules
Submodules
----------
mythril.analysis.module.base module
-----------------------------------
.. automodule:: mythril.analysis.module.base
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.loader module
-------------------------------------
.. automodule:: mythril.analysis.module.loader
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.module\_helpers module
----------------------------------------------
.. automodule:: mythril.analysis.module.module_helpers
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.module.util module
-----------------------------------
.. automodule:: mythril.analysis.module.util
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.analysis.module
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.analysis.rst
================================================
mythril.analysis package
========================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.analysis.module
Submodules
----------
mythril.analysis.analysis\_args module
--------------------------------------
.. automodule:: mythril.analysis.analysis_args
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.call\_helpers module
-------------------------------------
.. automodule:: mythril.analysis.call_helpers
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.callgraph module
---------------------------------
.. automodule:: mythril.analysis.callgraph
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.issue\_annotation module
-----------------------------------------
.. automodule:: mythril.analysis.issue_annotation
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.ops module
---------------------------
.. automodule:: mythril.analysis.ops
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.potential\_issues module
-----------------------------------------
.. automodule:: mythril.analysis.potential_issues
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.report module
------------------------------
.. automodule:: mythril.analysis.report
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.security module
--------------------------------
.. automodule:: mythril.analysis.security
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.solver module
------------------------------
.. automodule:: mythril.analysis.solver
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.swc\_data module
---------------------------------
.. automodule:: mythril.analysis.swc_data
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.symbolic module
--------------------------------
.. automodule:: mythril.analysis.symbolic
:members:
:undoc-members:
:show-inheritance:
mythril.analysis.traceexplore module
------------------------------------
.. automodule:: mythril.analysis.traceexplore
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.analysis
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.concolic.rst
================================================
mythril.concolic package
========================
Submodules
----------
mythril.concolic.concolic\_execution module
-------------------------------------------
.. automodule:: mythril.concolic.concolic_execution
:members:
:undoc-members:
:show-inheritance:
mythril.concolic.concrete\_data module
--------------------------------------
.. automodule:: mythril.concolic.concrete_data
:members:
:undoc-members:
:show-inheritance:
mythril.concolic.find\_trace module
-----------------------------------
.. automodule:: mythril.concolic.find_trace
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.concolic
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.disassembler.rst
================================================
mythril.disassembler package
============================
Submodules
----------
mythril.disassembler.asm module
-------------------------------
.. automodule:: mythril.disassembler.asm
:members:
:undoc-members:
:show-inheritance:
mythril.disassembler.disassembly module
---------------------------------------
.. automodule:: mythril.disassembler.disassembly
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.disassembler
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.ethereum.interface.rpc.rst
================================================
mythril.ethereum.interface.rpc package
======================================
Submodules
----------
mythril.ethereum.interface.rpc.base\_client module
--------------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.base_client
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.rpc.client module
--------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.client
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.rpc.constants module
-----------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.constants
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.rpc.exceptions module
------------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.exceptions
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.interface.rpc.utils module
-------------------------------------------
.. automodule:: mythril.ethereum.interface.rpc.utils
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.ethereum.interface.rpc
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.ethereum.interface.rst
================================================
mythril.ethereum.interface package
==================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.ethereum.interface.rpc
Module contents
---------------
.. automodule:: mythril.ethereum.interface
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.ethereum.rst
================================================
mythril.ethereum package
========================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.ethereum.interface
Submodules
----------
mythril.ethereum.evmcontract module
-----------------------------------
.. automodule:: mythril.ethereum.evmcontract
:members:
:undoc-members:
:show-inheritance:
mythril.ethereum.util module
----------------------------
.. automodule:: mythril.ethereum.util
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.ethereum
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.interfaces.rst
================================================
mythril.interfaces package
==========================
Submodules
----------
mythril.interfaces.cli module
-----------------------------
.. automodule:: mythril.interfaces.cli
:members:
:undoc-members:
:show-inheritance:
mythril.interfaces.epic module
------------------------------
.. automodule:: mythril.interfaces.epic
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.interfaces
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.ethereum.function_managers.rst
================================================
mythril.laser.ethereum.function\_managers package
=================================================
Submodules
----------
mythril.laser.ethereum.function\_managers.exponent\_function\_manager module
----------------------------------------------------------------------------
.. automodule:: mythril.laser.ethereum.function_managers.exponent_function_manager
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.function\_managers.keccak\_function\_manager module
--------------------------------------------------------------------------
.. automodule:: mythril.laser.ethereum.function_managers.keccak_function_manager
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum.function_managers
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.ethereum.rst
================================================
mythril.laser.ethereum package
==============================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.laser.ethereum.function_managers
mythril.laser.ethereum.state
mythril.laser.ethereum.strategy
mythril.laser.ethereum.transaction
Submodules
----------
mythril.laser.ethereum.call module
----------------------------------
.. automodule:: mythril.laser.ethereum.call
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.cfg module
---------------------------------
.. automodule:: mythril.laser.ethereum.cfg
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.evm\_exceptions module
---------------------------------------------
.. automodule:: mythril.laser.ethereum.evm_exceptions
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.instruction\_data module
-----------------------------------------------
.. automodule:: mythril.laser.ethereum.instruction_data
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.instructions module
------------------------------------------
.. automodule:: mythril.laser.ethereum.instructions
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.natives module
-------------------------------------
.. automodule:: mythril.laser.ethereum.natives
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.svm module
---------------------------------
.. automodule:: mythril.laser.ethereum.svm
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.time\_handler module
-------------------------------------------
.. automodule:: mythril.laser.ethereum.time_handler
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.util module
----------------------------------
.. automodule:: mythril.laser.ethereum.util
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.ethereum.state.rst
================================================
mythril.laser.ethereum.state package
====================================
Submodules
----------
mythril.laser.ethereum.state.account module
-------------------------------------------
.. automodule:: mythril.laser.ethereum.state.account
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.annotation module
----------------------------------------------
.. automodule:: mythril.laser.ethereum.state.annotation
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.calldata module
--------------------------------------------
.. automodule:: mythril.laser.ethereum.state.calldata
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.constraints module
-----------------------------------------------
.. automodule:: mythril.laser.ethereum.state.constraints
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.environment module
-----------------------------------------------
.. automodule:: mythril.laser.ethereum.state.environment
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.global\_state module
-------------------------------------------------
.. automodule:: mythril.laser.ethereum.state.global_state
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.machine\_state module
--------------------------------------------------
.. automodule:: mythril.laser.ethereum.state.machine_state
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.memory module
------------------------------------------
.. automodule:: mythril.laser.ethereum.state.memory
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.return\_data module
------------------------------------------------
.. automodule:: mythril.laser.ethereum.state.return_data
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.state.world\_state module
------------------------------------------------
.. automodule:: mythril.laser.ethereum.state.world_state
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum.state
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.ethereum.strategy.extensions.rst
================================================
mythril.laser.ethereum.strategy.extensions package
==================================================
Submodules
----------
mythril.laser.ethereum.strategy.extensions.bounded\_loops module
----------------------------------------------------------------
.. automodule:: mythril.laser.ethereum.strategy.extensions.bounded_loops
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum.strategy.extensions
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.ethereum.strategy.rst
================================================
mythril.laser.ethereum.strategy package
=======================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.laser.ethereum.strategy.extensions
Submodules
----------
mythril.laser.ethereum.strategy.basic module
--------------------------------------------
.. automodule:: mythril.laser.ethereum.strategy.basic
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.strategy.beam module
-------------------------------------------
.. automodule:: mythril.laser.ethereum.strategy.beam
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.strategy.concolic module
-----------------------------------------------
.. automodule:: mythril.laser.ethereum.strategy.concolic
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum.strategy
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.ethereum.transaction.rst
================================================
mythril.laser.ethereum.transaction package
==========================================
Submodules
----------
mythril.laser.ethereum.transaction.concolic module
--------------------------------------------------
.. automodule:: mythril.laser.ethereum.transaction.concolic
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.transaction.symbolic module
--------------------------------------------------
.. automodule:: mythril.laser.ethereum.transaction.symbolic
:members:
:undoc-members:
:show-inheritance:
mythril.laser.ethereum.transaction.transaction\_models module
-------------------------------------------------------------
.. automodule:: mythril.laser.ethereum.transaction.transaction_models
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.ethereum.transaction
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.plugin.plugins.coverage.rst
================================================
mythril.laser.plugin.plugins.coverage package
=============================================
Submodules
----------
mythril.laser.plugin.plugins.coverage.coverage\_plugin module
-------------------------------------------------------------
.. automodule:: mythril.laser.plugin.plugins.coverage.coverage_plugin
:members:
:undoc-members:
:show-inheritance:
mythril.laser.plugin.plugins.coverage.coverage\_strategy module
---------------------------------------------------------------
.. automodule:: mythril.laser.plugin.plugins.coverage.coverage_strategy
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.plugin.plugins.coverage
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.plugin.plugins.rst
================================================
mythril.laser.plugin.plugins package
====================================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.laser.plugin.plugins.coverage
mythril.laser.plugin.plugins.summary_backup
Submodules
----------
mythril.laser.plugin.plugins.benchmark module
---------------------------------------------
.. automodule:: mythril.laser.plugin.plugins.benchmark
:members:
:undoc-members:
:show-inheritance:
mythril.laser.plugin.plugins.call\_depth\_limiter module
--------------------------------------------------------
.. automodule:: mythril.laser.plugin.plugins.call_depth_limiter
:members:
:undoc-members:
:show-inheritance:
mythril.laser.plugin.plugins.dependency\_pruner module
------------------------------------------------------
.. automodule:: mythril.laser.plugin.plugins.dependency_pruner
:members:
:undoc-members:
:show-inheritance:
mythril.laser.plugin.plugins.instruction\_profiler module
---------------------------------------------------------
.. automodule:: mythril.laser.plugin.plugins.instruction_profiler
:members:
:undoc-members:
:show-inheritance:
mythril.laser.plugin.plugins.mutation\_pruner module
----------------------------------------------------
.. automodule:: mythril.laser.plugin.plugins.mutation_pruner
:members:
:undoc-members:
:show-inheritance:
mythril.laser.plugin.plugins.plugin\_annotations module
-------------------------------------------------------
.. automodule:: mythril.laser.plugin.plugins.plugin_annotations
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.plugin.plugins
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.plugin.plugins.summary_backup.rst
================================================
mythril.laser.plugin.plugins.summary\_backup package
====================================================
Module contents
---------------
.. automodule:: mythril.laser.plugin.plugins.summary_backup
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.plugin.rst
================================================
mythril.laser.plugin package
============================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.laser.plugin.plugins
Submodules
----------
mythril.laser.plugin.builder module
-----------------------------------
.. automodule:: mythril.laser.plugin.builder
:members:
:undoc-members:
:show-inheritance:
mythril.laser.plugin.interface module
-------------------------------------
.. automodule:: mythril.laser.plugin.interface
:members:
:undoc-members:
:show-inheritance:
mythril.laser.plugin.loader module
----------------------------------
.. automodule:: mythril.laser.plugin.loader
:members:
:undoc-members:
:show-inheritance:
mythril.laser.plugin.signals module
-----------------------------------
.. automodule:: mythril.laser.plugin.signals
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.plugin
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.rst
================================================
mythril.laser package
=====================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.laser.ethereum
mythril.laser.plugin
mythril.laser.smt
Submodules
----------
mythril.laser.execution\_info module
------------------------------------
.. automodule:: mythril.laser.execution_info
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.smt.rst
================================================
mythril.laser.smt package
=========================
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.laser.smt.solver
Submodules
----------
mythril.laser.smt.array module
------------------------------
.. automodule:: mythril.laser.smt.array
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.bitvec module
-------------------------------
.. automodule:: mythril.laser.smt.bitvec
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.bitvec\_helper module
---------------------------------------
.. automodule:: mythril.laser.smt.bitvec_helper
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.bool module
-----------------------------
.. automodule:: mythril.laser.smt.bool
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.expression module
-----------------------------------
.. automodule:: mythril.laser.smt.expression
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.function module
---------------------------------
.. automodule:: mythril.laser.smt.function
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.model module
------------------------------
.. automodule:: mythril.laser.smt.model
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.smt
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.laser.smt.solver.rst
================================================
mythril.laser.smt.solver package
================================
Submodules
----------
mythril.laser.smt.solver.independence\_solver module
----------------------------------------------------
.. automodule:: mythril.laser.smt.solver.independence_solver
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.solver.solver module
--------------------------------------
.. automodule:: mythril.laser.smt.solver.solver
:members:
:undoc-members:
:show-inheritance:
mythril.laser.smt.solver.solver\_statistics module
--------------------------------------------------
.. automodule:: mythril.laser.smt.solver.solver_statistics
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.laser.smt.solver
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.mythril.rst
================================================
mythril.mythril package
=======================
Submodules
----------
mythril.mythril.mythril\_analyzer module
----------------------------------------
.. automodule:: mythril.mythril.mythril_analyzer
:members:
:undoc-members:
:show-inheritance:
mythril.mythril.mythril\_config module
--------------------------------------
.. automodule:: mythril.mythril.mythril_config
:members:
:undoc-members:
:show-inheritance:
mythril.mythril.mythril\_disassembler module
--------------------------------------------
.. automodule:: mythril.mythril.mythril_disassembler
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.mythril
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.plugin.rst
================================================
mythril.plugin package
======================
Submodules
----------
mythril.plugin.discovery module
-------------------------------
.. automodule:: mythril.plugin.discovery
:members:
:undoc-members:
:show-inheritance:
mythril.plugin.interface module
-------------------------------
.. automodule:: mythril.plugin.interface
:members:
:undoc-members:
:show-inheritance:
mythril.plugin.loader module
----------------------------
.. automodule:: mythril.plugin.loader
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.plugin
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.rst
================================================
mythril package
===============
Subpackages
-----------
.. toctree::
:maxdepth: 4
mythril.analysis
mythril.concolic
mythril.disassembler
mythril.ethereum
mythril.interfaces
mythril.laser
mythril.mythril
mythril.plugin
mythril.solidity
mythril.support
Submodules
----------
mythril.exceptions module
-------------------------
.. automodule:: mythril.exceptions
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.solidity.rst
================================================
mythril.solidity package
========================
Submodules
----------
mythril.solidity.soliditycontract module
----------------------------------------
.. automodule:: mythril.solidity.soliditycontract
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.solidity
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/mythril.support.rst
================================================
mythril.support package
=======================
Submodules
----------
mythril.support.loader module
-----------------------------
.. automodule:: mythril.support.loader
:members:
:undoc-members:
:show-inheritance:
mythril.support.lock module
---------------------------
.. automodule:: mythril.support.lock
:members:
:undoc-members:
:show-inheritance:
mythril.support.model module
----------------------------
.. automodule:: mythril.support.model
:members:
:undoc-members:
:show-inheritance:
mythril.support.opcodes module
------------------------------
.. automodule:: mythril.support.opcodes
:members:
:undoc-members:
:show-inheritance:
mythril.support.signatures module
---------------------------------
.. automodule:: mythril.support.signatures
:members:
:undoc-members:
:show-inheritance:
mythril.support.source\_support module
--------------------------------------
.. automodule:: mythril.support.source_support
:members:
:undoc-members:
:show-inheritance:
mythril.support.start\_time module
----------------------------------
.. automodule:: mythril.support.start_time
:members:
:undoc-members:
:show-inheritance:
mythril.support.support\_args module
------------------------------------
.. automodule:: mythril.support.support_args
:members:
:undoc-members:
:show-inheritance:
mythril.support.support\_utils module
-------------------------------------
.. automodule:: mythril.support.support_utils
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: mythril.support
:members:
:undoc-members:
:show-inheritance:
================================================
FILE: docs/source/security-analysis.rst
================================================
Security Analysis
=================
Running :code:`myth analyze` with one of the input options described below will run the analysis modules in the `/analysis/modules `_ directory.
***********************
Analyzing Solidity Code
***********************
In order to work with Solidity source code files, the `solc command line compiler `_ needs to be installed and in PATH. You can then provide the source file(s) as positional arguments.
.. code-block:: bash
$ myth analyze ether_send.sol
==== Unprotected Ether Withdrawal ====
SWC ID: 105
Severity: High
Contract: Crowdfunding
Function name: withdrawfunds()
PC address: 730
Estimated Gas Usage: 1132 - 1743
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 analyze OmiseGo.sol:OMGToken
Specifying Solc Versions
########################
You can specify a version of the solidity compiler to be used with :code:`--solv `. Please be aware that this uses `py-solc `_ and will only work on Linux and macOS. It will check the version of solc in your path, and if this is not what is specified, it will download binaries on Linux or try to compile from source on macOS.
Output Formats
##############
By default, analysis results are printed to the terminal in text format. You can change the output format with the :code:`-o` argument:
.. code-block:: bash
myth analyze underflow.sol -o jsonv2
Available formats are :code:`text`, :code:`markdown`, :code:`json`, and :code:`jsonv2`. For integration with other tools, :code:`jsonv2` is generally preferred over :code:`json` because it is consistent with other `MythX `_ tools.
****************************
Analyzing On-Chain Contracts
****************************
When analyzing contracts on the blockchain, Mythril will by default attempt to query INFURA. You can use the built-in INFURA support or manually configure the RPC settings with the :code:`--rpc` argument.
+-------------------------------------------------+-------------------------------------------------+
| :code:`--rpc ganache` | Connect to local Ganache |
+-------------------------------------------------+-------------------------------------------------+
| :code:`--rpc infura-[netname] --infura-id ` | Connect to mainnet, rinkeby, kovan, or ropsten. |
+-------------------------------------------------+-------------------------------------------------+
| :code:`--rpc host:port` | Connect to custom rpc |
+-------------------------------------------------+-------------------------------------------------+
| :code:`--rpctls ` | RPC connection over TLS (default: False) |
+-------------------------------------------------+-------------------------------------------------+
To specify a contract address, use :code:`-a `
Analyze mainnet contract via INFURA:
.. code-block:: bash
myth analyze -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd --infura-id
You can also use the environment variable `INFURA_ID` instead of the cmd line argument or set it in ~/.mythril/config.ini.
.. code-block:: bash
myth -v4 analyze -a 0xEbFD99838cb0c132016B9E117563CB41f2B02264 --infura-id
******************
Speed vs. Coverage
******************
The execution timeout can be specified with the :code:`--execution-timeout ` argument. When the timeout is reached, mythril will stop analysis and print out all currently found issues.
The maximum recursion depth for the symbolic execution engine can be controlled with the :code:`--max-depth` argument. The default value is 22. Lowering this value will decrease the number of explored states and analysis time, while increasing this number will increase the number of explored states and increase analysis time. For some contracts, it helps to fine tune this number to get the best analysis results.
-
================================================
FILE: docs/source/tutorial.rst
================================================
Tutorial
======================
******************************************
Introduction
******************************************
Mythril is a popular security analysis tool for smart contracts. It is an open-source tool that can analyze Ethereum smart contracts and report potential security vulnerabilities in them. By analyzing the bytecode of a smart contract, Mythril can identify and report on possible security vulnerabilities, such as reentrancy attacks, integer overflows, and other common smart contract vulnerabilities.
This tutorial explains how to use Mythril to analyze simple Solidity contracts for security vulnerabilities. A simple contract is one that does not have any imports.
******************************************
Executing Mythril on Simple Contracts
******************************************
To start, we consider this simple contract, ``Exceptions``, which has a number of functions, including ``assert1()``, ``assert2()``, and ``assert3()``, that contain Solidity ``assert()`` statements. We will use Mythril to analyze this contract and report any potential vulnerabilities.
.. code-block:: solidity
contract Exceptions {
uint256[8] myarray;
uint counter = 0;
function assert1() public pure {
uint256 i = 1;
assert(i == 0);
}
function counter_increase() public {
counter+=1;
}
function assert5(uint input_x) public view{
require(counter>2);
assert(input_x > 10);
}
function assert2() public pure {
uint256 i = 1;
assert(i > 0);
}
function assert3(uint256 input) public pure {
assert(input != 23);
}
function require_is_fine(uint256 input) public pure {
require(input != 23);
}
function this_is_fine(uint256 input) public pure {
if (input > 0) {
uint256 i = 1/input;
}
}
function this_is_find_2(uint256 index) public view {
if (index < 8) {
uint256 i = myarray[index];
}
}
}
The sample contract has several functions, some of which contain vulnerabilities. For instance, the ``assert1()`` function contains an assertion violation. To analyze the contract using Mythril, the following command can be used:
.. code-block:: bash
$ myth analyze
The output will show the vulnerabilities in the contract. In the case of the "Exceptions" contract, Mythril detected two instances of assertion violations.
.. code-block:: none
==== Exception State ====
SWC ID: 110
Severity: Medium
Contract: Exceptions
Function name: assert1()
PC address: 708
Estimated Gas Usage: 207 - 492
An assertion violation was triggered.
It is possible to trigger an assertion violation. Note that Solidity assert() statements should only be used to check invariants. Review the transaction trace generated for this issue and either make sure your program logic is correct, or use require() instead of assert() if your goal is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers (for instance, via passed arguments) and callees (for instance, via return values).
--------------------
In file: solidity_examples/exceptions.sol:7
assert(i == 0)
--------------------
Initial State:
Account: [CREATOR], balance: 0x2, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}
Transaction Sequence:
Caller: [CREATOR], calldata: , value: 0x0
Caller: [ATTACKER], function: assert1(), txdata: 0xb34c3610, value: 0x0
==== Exception State ====
SWC ID: 110
Severity: Medium
Contract: Exceptions
Function name: assert3(uint256)
PC address: 708
Estimated Gas Usage: 482 - 767
An assertion violation was triggered.
It is possible to trigger an assertion violation. Note that Solidity assert() statements should only be used to check invariants. Review the transaction trace generated for this issue and either make sure your program logic is correct, or use require() instead of assert() if your goal is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers (for instance, via passed arguments) and callees (for instance, via return values).
--------------------
In file: solidity_examples/exceptions.sol:20
assert(input != 23)
--------------------
Initial State:
Account: [CREATOR], balance: 0x40207f9b0, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}
Transaction Sequence:
Caller: [CREATOR], calldata: , value: 0x0
Caller: [SOMEGUY], function: assert3(uint256), txdata: 0x546455b50000000000000000000000000000000000000000000000000000000000000017, value: 0x0
One of the functions, ``assert5(uint256)``, should also have an assertion failure, but it is not detected because Mythril's default configuration is to run three transactions.
To detect this vulnerability, the transaction count can be increased to four using the ``-t`` option, as shown below:
.. code-block:: bash
$ myth analyze -t 4
This gives the following execution output:
.. code-block:: none
==== Exception State ====
SWC ID: 110
Severity: Medium
Contract: Exceptions
Function name: assert1()
PC address: 731
Estimated Gas Usage: 207 - 492
An assertion violation was triggered.
It is possible to trigger an assertion violation. Note that Solidity assert() statements should only be used to check invariants. Review the transaction trace generated for this issue and either make sure your program logic is correct, or use require() instead of assert() if your goal is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers (for instance, via passed arguments) and callees (for instance, via return values).
--------------------
In file: solidity_examples/exceptions.sol:7
assert(i == 0)
--------------------
Initial State:
Account: [CREATOR], balance: 0x2, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}
Transaction Sequence:
Caller: [CREATOR], calldata: , value: 0x0
Caller: [ATTACKER], function: assert1(), txdata: 0xb34c3610, value: 0x0
==== Exception State ====
SWC ID: 110
Severity: Medium
Contract: Exceptions
Function name: assert3(uint256)
PC address: 731
Estimated Gas Usage: 504 - 789
An assertion violation was triggered.
It is possible to trigger an assertion violation. Note that Solidity assert() statements should only be used to check invariants. Review the transaction trace generated for this issue and either make sure your program logic is correct, or use require() instead of assert() if your goal is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers (for instance, via passed arguments) and callees (for instance, via return values).
--------------------
In file: solidity_examples/exceptions.sol:22
assert(input != 23)
--------------------
Initial State:
Account: [CREATOR], balance: 0x3, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x0, nonce:0, storage:{}
Transaction Sequence:
Caller: [CREATOR], calldata: , value: 0x0
Caller: [ATTACKER], function: assert3(uint256), txdata: 0x546455b50000000000000000000000000000000000000000000000000000000000000017, value: 0x0
==== Exception State ====
SWC ID: 110
Severity: Medium
Contract: Exceptions
Function name: assert5(uint256)
PC address: 731
Estimated Gas Usage: 1302 - 1587
An assertion violation was triggered.
It is possible to trigger an assertion violation. Note that Solidity assert() statements should only be used to check invariants. Review the transaction trace generated for this issue and either make sure your program logic is correct, or use require() instead of assert() if your goal is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers (for instance, via passed arguments) and callees (for instance, via return values).
--------------------
In file: solidity_examples/exceptions.sol:14
assert(input_x > 10)
--------------------
Initial State:
Account: [CREATOR], balance: 0x20000000, nonce:0, storage:{}
Account: [ATTACKER], balance: 0x1000000, nonce:0, storage:{}
Transaction Sequence:
Caller: [CREATOR], calldata: , value: 0x0
Caller: [ATTACKER], function: counter_increase(), txdata: 0xe47b0253, value: 0x0
Caller: [CREATOR], function: counter_increase(), txdata: 0xe47b0253, value: 0x0
Caller: [CREATOR], function: counter_increase(), txdata: 0xe47b0253, value: 0x0
Caller: [ATTACKER], function: assert5(uint256), txdata: 0x1d5d53dd0000000000000000000000000000000000000000000000000000000000000003, value: 0x0
For the violation in the 4th transaction, the input value should be less than 10. The transaction data generated by Mythril for the
4th transaction is ``0x1d5d53dd0000000000000000000000000000000000000000000000000000000000000003``, the first 4 bytes ``1d5d53dd``
correspond to the function signature hence the input generated by Mythril is ``0000000000000000000000000000000000000000000000000000000000000003``
in hex, which is 3. For automated resolution of the input try using a different output format such as JSON.
.. code-block:: bash
$ myth analyze -o json
This leads to the following output:
.. code-block:: json
{
"error": null,
"issues": [{
"address": 731,
"code": "assert(i == 0)",
"contract": "Exceptions",
"description": "An assertion violation was triggered.\nIt is possible to trigger an assertion violation. Note that Solidity assert() statements should only be used to check invariants. Review the transaction trace generated for this issue and either make sure your program logic is correct, or use require() instead of assert() if your goal is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers (for instance, via passed arguments) and callees (for instance, via return values).",
"filename": "solidity_examples/exceptions.sol",
"function": "assert1()",
"lineno": 7,
"max_gas_used": 492,
"min_gas_used": 207,
"severity": "Medium",
"sourceMap": ":::i",
"swc-id": "110",
"title": "Exception State",
"tx_sequence": {
"initialState": {
"accounts": {
"0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe": {
"balance": "0x2",
"code": "",
"nonce": 0,
"storage": "{}"
},
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef": {
"balance": "0x0",
"code": "",
"nonce": 0,
"storage": "{}"
}
}
},
"steps": [{
"address": "",
"calldata": "",
"input": "0x6080604052600060085534801561001557600080fd5b506103f7806100256000396000f3fe608060405234801561001057600080fd5b50600436106100885760003560e01c8063b34c36101161005b578063b34c3610146100fd578063b630d70614610107578063e47b025314610123578063f44f13d81461012d57610088565b806301d4277c1461008d5780631d5d53dd146100a9578063546455b5146100c557806378375f14146100e1575b600080fd5b6100a760048036038101906100a29190610251565b610137565b005b6100c360048036038101906100be9190610251565b61015e565b005b6100df60048036038101906100da9190610251565b610181565b005b6100fb60048036038101906100f69190610251565b610196565b005b6101056101a7565b005b610121600480360381019061011c9190610251565b6101c1565b005b61012b6101e0565b005b6101356101fc565b005b600881101561015b5760008082600881106101555761015461027e565b5b01549050505b50565b60026008541161016d57600080fd5b600a811161017e5761017d6102ad565b5b50565b6017811415610193576101926102ad565b5b50565b60178114156101a457600080fd5b50565b600060019050600081146101be576101bd6102ad565b5b50565b60008111156101dd5760008160016101d9919061033a565b9050505b50565b6001600860008282546101f3919061036b565b92505081905550565b60006001905060008111610213576102126102ad565b5b50565b600080fd5b6000819050919050565b61022e8161021b565b811461023957600080fd5b50565b60008135905061024b81610225565b92915050565b60006020828403121561026757610266610216565b5b60006102758482850161023c565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006103458261021b565b91506103508361021b565b9250826103605761035f6102dc565b5b828204905092915050565b60006103768261021b565b91506103818361021b565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156103b6576103b561030b565b5b82820190509291505056fea2646970667358221220b474c01fa60d997027e1ceb779bcb2b34b6752282e0ea3a038a08b889fe0163f64736f6c634300080c0033",
"name": "unknown",
"origin": "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe",
"value": "0x0"
}, {
"address": "0x901d12ebe1b195e5aa8748e62bd7734ae19b51f",
"calldata": "0xb34c3610",
"input": "0xb34c3610",
"name": "assert1()",
"origin": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"resolved_input": null,
"value": "0x0"
}]
}
}, {
"address": 731,
"code": "assert(input != 23)",
"contract": "Exceptions",
"description": "An assertion violation was triggered.\nIt is possible to trigger an assertion violation. Note that Solidity assert() statements should only be used to check invariants. Review the transaction trace generated for this issue and either make sure your program logic is correct, or use require() instead of assert() if your goal is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers (for instance, via passed arguments) and callees (for instance, via return values).",
"filename": "solidity_examples/exceptions.sol",
"function": "assert3(uint256)",
"lineno": 22,
"max_gas_used": 789,
"min_gas_used": 504,
"severity": "Medium",
"sourceMap": ":::i",
"swc-id": "110",
"title": "Exception State",
"tx_sequence": {
"initialState": {
"accounts": {
"0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe": {
"balance": "0x3",
"code": "",
"nonce": 0,
"storage": "{}"
},
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef": {
"balance": "0x0",
"code": "",
"nonce": 0,
"storage": "{}"
}
}
},
"steps": [{
"address": "",
"calldata": "",
"input": "0x6080604052600060085534801561001557600080fd5b506103f7806100256000396000f3fe608060405234801561001057600080fd5b50600436106100885760003560e01c8063b34c36101161005b578063b34c3610146100fd578063b630d70614610107578063e47b025314610123578063f44f13d81461012d57610088565b806301d4277c1461008d5780631d5d53dd146100a9578063546455b5146100c557806378375f14146100e1575b600080fd5b6100a760048036038101906100a29190610251565b610137565b005b6100c360048036038101906100be9190610251565b61015e565b005b6100df60048036038101906100da9190610251565b610181565b005b6100fb60048036038101906100f69190610251565b610196565b005b6101056101a7565b005b610121600480360381019061011c9190610251565b6101c1565b005b61012b6101e0565b005b6101356101fc565b005b600881101561015b5760008082600881106101555761015461027e565b5b01549050505b50565b60026008541161016d57600080fd5b600a811161017e5761017d6102ad565b5b50565b6017811415610193576101926102ad565b5b50565b60178114156101a457600080fd5b50565b600060019050600081146101be576101bd6102ad565b5b50565b60008111156101dd5760008160016101d9919061033a565b9050505b50565b6001600860008282546101f3919061036b565b92505081905550565b60006001905060008111610213576102126102ad565b5b50565b600080fd5b6000819050919050565b61022e8161021b565b811461023957600080fd5b50565b60008135905061024b81610225565b92915050565b60006020828403121561026757610266610216565b5b60006102758482850161023c565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006103458261021b565b91506103508361021b565b9250826103605761035f6102dc565b5b828204905092915050565b60006103768261021b565b91506103818361021b565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156103b6576103b561030b565b5b82820190509291505056fea2646970667358221220b474c01fa60d997027e1ceb779bcb2b34b6752282e0ea3a038a08b889fe0163f64736f6c634300080c0033",
"name": "unknown",
"origin": "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe",
"value": "0x0"
}, {
"address": "0x901d12ebe1b195e5aa8748e62bd7734ae19b51f",
"calldata": "0x546455b50000000000000000000000000000000000000000000000000000000000000017",
"input": "0x546455b50000000000000000000000000000000000000000000000000000000000000017",
"name": "assert3(uint256)",
"origin": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"resolved_input": [23],
"value": "0x0"
}]
}
}, {
"address": 731,
"code": "assert(input_x > 10)",
"contract": "Exceptions",
"description": "An assertion violation was triggered.\nIt is possible to trigger an assertion violation. Note that Solidity assert() statements should only be used to check invariants. Review the transaction trace generated for this issue and either make sure your program logic is correct, or use require() instead of assert() if your goal is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers (for instance, via passed arguments) and callees (for instance, via return values).",
"filename": "solidity_examples/exceptions.sol",
"function": "assert5(uint256)",
"lineno": 14,
"max_gas_used": 1587,
"min_gas_used": 1302,
"severity": "Medium",
"sourceMap": ":::i",
"swc-id": "110",
"title": "Exception State",
"tx_sequence": {
"initialState": {
"accounts": {
"0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe": {
"balance": "0x0",
"code": "",
"nonce": 0,
"storage": "{}"
},
"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef": {
"balance": "0x0",
"code": "",
"nonce": 0,
"storage": "{}"
}
}
},
"steps": [{
"address": "",
"calldata": "",
"input": "0x6080604052600060085534801561001557600080fd5b506103f7806100256000396000f3fe608060405234801561001057600080fd5b50600436106100885760003560e01c8063b34c36101161005b578063b34c3610146100fd578063b630d70614610107578063e47b025314610123578063f44f13d81461012d57610088565b806301d4277c1461008d5780631d5d53dd146100a9578063546455b5146100c557806378375f14146100e1575b600080fd5b6100a760048036038101906100a29190610251565b610137565b005b6100c360048036038101906100be9190610251565b61015e565b005b6100df60048036038101906100da9190610251565b610181565b005b6100fb60048036038101906100f69190610251565b610196565b005b6101056101a7565b005b610121600480360381019061011c9190610251565b6101c1565b005b61012b6101e0565b005b6101356101fc565b005b600881101561015b5760008082600881106101555761015461027e565b5b01549050505b50565b60026008541161016d57600080fd5b600a811161017e5761017d6102ad565b5b50565b6017811415610193576101926102ad565b5b50565b60178114156101a457600080fd5b50565b600060019050600081146101be576101bd6102ad565b5b50565b60008111156101dd5760008160016101d9919061033a565b9050505b50565b6001600860008282546101f3919061036b565b92505081905550565b60006001905060008111610213576102126102ad565b5b50565b600080fd5b6000819050919050565b61022e8161021b565b811461023957600080fd5b50565b60008135905061024b81610225565b92915050565b60006020828403121561026757610266610216565b5b60006102758482850161023c565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052600160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006103458261021b565b91506103508361021b565b9250826103605761035f6102dc565b5b828204905092915050565b60006103768261021b565b91506103818361021b565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156103b6576103b561030b565b5b82820190509291505056fea2646970667358221220b474c01fa60d997027e1ceb779bcb2b34b6752282e0ea3a038a08b889fe0163f64736f6c634300080c0033",
"name": "unknown",
"origin": "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe",
"value": "0x0"
}, {
"address": "0x901d12ebe1b195e5aa8748e62bd7734ae19b51f",
"calldata": "0xe47b0253",
"input": "0xe47b0253",
"name": "counter_increase()",
"origin": "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe",
"resolved_input": null,
"value": "0x0"
}, {
"address": "0x901d12ebe1b195e5aa8748e62bd7734ae19b51f",
"calldata": "0xe47b0253",
"input": "0xe47b0253",
"name": "counter_increase()",
"origin": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"resolved_input": null,
"value": "0x0"
}, {
"address": "0x901d12ebe1b195e5aa8748e62bd7734ae19b51f",
"calldata": "0xe47b0253",
"input": "0xe47b0253",
"name": "counter_increase()",
"origin": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"resolved_input": null,
"value": "0x0"
}, {
"address": "0x901d12ebe1b195e5aa8748e62bd7734ae19b51f",
"calldata": "0x1d5d53dd0000000000000000000000000000000000000000000000000000000000000003",
"input": "0x1d5d53dd0000000000000000000000000000000000000000000000000000000000000003",
"name": "assert5(uint256)",
"origin": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"resolved_input": [3],
"value": "0x0"
}]
}
}],
"success": true
}
We can observe that the "resolved_input" field for the final transaction resolves to ``[3]``. Although this resolution
fails in some circumstances where output generated by Mythril is although executable on the bytecode, it cannot be decoded due
to not being a valid ABI.
There are interesting options such as ``--execution-timeout `` and ``--solver-timeout ``
which can be increased for better results. The default execution-timeout and solver-timeout are 86400 seconds and
25000 milliseconds respectively.
********************************************************
Executing Mythril on Contracts with Imports
********************************************************
When using Mythril to analyze a Solidity contract, you may encounter issues related to import statements. Solidity does not have access to the import locations, which can result in errors when compiling the program. In order to provide import information to Solidity, you can use the remappings option in Mythril.
Consider the following Solidity contract, which imports the PRC20 contract from the ``@openzeppelin/contracts/token/PRC20/PRC20.sol`` file:
.. code-block:: solidity
import "@openzeppelin/contracts/token/PRC20/PRC20.sol";
contract Nothing is PRC20{
string x_0 = "";
bytes3 x_1 = "A";
bytes5 x_2 = "E";
bytes5 x_3 = "";
bytes3 x_4 = "I";
bytes3 x_5 = "U";
bytes3 x_6 = "O";
bytes3 x_7 = "0";
bytes3 x_8 = "U";
bytes3 x_9 = "U";
function stringCompare(string memory a, string memory b) internal pure returns (bool) {
if(bytes(a).length != bytes(b).length) {
return false;
} else {
return keccak256(bytes(a)) == keccak256(bytes(b));
}
}
function nothing(string memory g_0, bytes3 g_5, bytes3 g_6, bytes3 g_7, bytes3 g_8, bytes3 g_9, bytes3 g_10, bytes3 g_11) public view returns (bool){
if (!stringCompare(g_0, x_0)) return false;
if (g_5 != x_5) return false;
if (g_6 != x_6) return false;
if (g_7 != x_7) return false;
if (g_8 != x_8) return false;
if (g_9 != x_9) return false;
if (g_10 != x_9) return false;
if (g_11 != x_9) return false;
return true;
}
}
When this contract is directly executed by using the following command:
.. code-block:: bash
$ myth analyze
We encounter the following error:
.. code-block:: none
mythril.interfaces.cli [ERROR]: Solc experienced a fatal error.
ParserError: Source "@openzeppelin/contracts/token/PRC20/PRC20.sol" not found: File not found. Searched the following locations: "".
--> :1:1:
|
1 | import "@openzeppelin/contracts/token/PRC20/PRC20.sol";
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This error occurs because Solidity cannot locate the ``PRC20.sol`` file.
To solve this issue, you need to provide remapping information to Mythril, which will relay it to the Solidity compiler.
Remapping involves mapping an import statement to the path that contains the corresponding file.
In this example, we can map the import statement ``@openzeppelin/contracts/token/PRC20/`` to the path that contains ``PRC20.sol``. Let's assume that the file is located at ``node_modules/PRC20/PRC20.sol``. We can provide the remapping information to Mythril in a JSON file like this:
.. code-block:: json
{
"remappings": [ "@openzeppelin/contracts/token/PRC20/=node_modules/PRC20/"]
}
This JSON file maps the prefix ``@openzeppelin/contracts/token/PRC20/`` to the path ``node_modules/PRC20/`` in the file system.
When you run Mythril, you can use the ``--solc-json`` option to provide the remapping file:
.. code-block:: bash
$ myth analyze {file_path} --solc-json {json_file_path}
With this command, Mythril will be able to locate the ``PRC20.sol`` file, and the analysis should proceed without errors.
For more information on remappings, you can refer to the `Solidity documentation `_.
********************************************************
Executing Mythril by Restricting Transaction Sequences
********************************************************
Mythril is a security analysis tool that can be used to search certain transaction sequences.
The `--transaction-sequences` argument can be used to direct the search.
You should provide a list of transactions that are sequenced in the same order that they will be executed in the contract.
For example, suppose you want to find vulnerabilities in a contract that executes three transactions, where the first transaction is constrained with ``func_hash1`` and ``func_hash2``,
the second transaction is constrained with ``func_hash2`` and ``func_hash3``, and the final transaction is unconstrained on any function. You would provide ``--transaction-sequences [[func_hash1,func_hash2], [func_hash2,func_hash3],[]]`` as an argument to Mythril.
You can use ``-1`` as a proxy for the hash of the `fallback()` function and ``-2`` as a proxy for the hash of the ``receive()`` function.
Here is an example contract that demonstrates how to use Mythril with ``--transaction-sequences``.
Consider the following contract:
.. code-block:: solidity
pragma solidity ^0.5.0;
contract Rubixi {
//Declare variables for storage critical to contract
uint private balance = 0;
uint private collectedFees = 0;
uint private feePercent = 10;
uint private pyramidMultiplier = 300;
uint private payoutOrder = 0;
address payable private creator;
modifier onlyowner {
if (msg.sender == creator) _;
}
struct Participant {
address payable etherAddress;
uint payout;
}
//Fallback function
function() external payable {
init();
}
//Sets creator
function dynamicPyramid() public {
creator = msg.sender;
}
Participant[] private participants;
//Fee functions for creator
function collectAllFees() public onlyowner {
require(collectedFees > 0);
creator.transfer(collectedFees);
collectedFees = 0;
}
function collectFeesInEther(uint _amt) public onlyowner {
_amt *= 1 ether;
if (_amt > collectedFees) collectAllFees();
require(collectedFees > 0);
creator.transfer(_amt);
collectedFees -= _amt;
}
function collectPercentOfFees(uint _pcent) public onlyowner {
require(collectedFees > 0 && _pcent <= 100);
uint feesToCollect = collectedFees / 100 * _pcent;
creator.transfer(feesToCollect);
collectedFees -= feesToCollect;
}
//Functions for changing variables related to the contract
function changeOwner(address payable _owner) public onlyowner {
creator = _owner;
}
function changeMultiplier(uint _mult) public onlyowner {
require(_mult <= 300 && _mult >= 120);
pyramidMultiplier = _mult;
}
function changeFeePercentage(uint _fee) public onlyowner {
require(_fee <= 10);
feePercent = _fee;
}
//Functions to provide information to end-user using JSON interface or other interfaces
function currentMultiplier() public view returns (uint multiplier, string memory info) {
multiplier = pyramidMultiplier;
info = "This multiplier applies to you as soon as transaction is received, may be lowered to hasten payouts or increased if payouts are fast enough. Due to no float or decimals, multiplier is x100 for a fractional multiplier e.g. 250 is actually a 2.5x multiplier. Capped at 3x max and 1.2x min.";
}
function currentFeePercentage() public view returns (uint fee, string memory info) {
fee = feePercent;
info = "Shown in % form. Fee is halved(50%) for amounts equal or greater than 50 ethers. (Fee may change, but is capped to a maximum of 10%)";
}
function currentPyramidBalanceApproximately() public view returns (uint pyramidBalance, string memory info) {
pyramidBalance = balance / 1 ether;
info = "All balance values are measured in Ethers, note that due to no decimal placing, these values show up as integers only, within the contract itself you will get the exact decimal value you are supposed to";
}
function nextPayoutWhenPyramidBalanceTotalsApproximately() public view returns (uint balancePayout) {
balancePayout = participants[payoutOrder].payout / 1 ether;
}
function feesSeperateFromBalanceApproximately() public view returns (uint fees) {
fees = collectedFees / 1 ether;
}
function totalParticipants() public view returns (uint count) {
count = participants.length;
}
function numberOfParticipantsWaitingForPayout() public view returns (uint count) {
count = participants.length - payoutOrder;
}
function participantDetails(uint orderInPyramid) public view returns (address addr, uint payout) {
if (orderInPyramid <= participants.length) {
addr = participants[orderInPyramid].etherAddress;
payout = participants[orderInPyramid].payout / 1 ether;
}
}
//init function run on fallback
function init() private {
//Ensures only tx with value of 1 ether or greater are processed and added to pyramid
if (msg.value < 1 ether) {
collectedFees += msg.value;
return;
}
uint _fee = feePercent;
// 50% fee rebate on any ether value of 50 or greater
if (msg.value >= 50 ether) _fee /= 2;
addPayout(_fee);
}
//Function called for valid tx to the contract
function addPayout(uint _fee) private {
//Adds new address to participant array
participants.push(Participant(msg.sender, (msg.value * pyramidMultiplier) / 100));
// These statements ensure a quicker payout system to
// later pyramid entrants, so the pyramid has a longer lifespan
if (participants.length == 10) pyramidMultiplier = 200;
else if (participants.length == 25) pyramidMultiplier = 150;
// collect fees and update contract balance
balance += (msg.value * (100 - _fee)) / 100;
collectedFees += (msg.value * _fee) / 100;
//Pays earlier participiants if balance sufficient
while (balance > participants[payoutOrder].payout) {
uint payoutToSend = participants[payoutOrder].payout;
participants[payoutOrder].etherAddress.transfer(payoutToSend);
balance -= participants[payoutOrder].payout;
payoutOrder += 1;
}
}
}
Since this contract has ``16`` functions, it is infeasible to execute uninteresting functions such as ``feesSeperateFromBalanceApproximately()``.
To successfully explore useful transaction sequences we can use Mythril's ``--transaction-sequences`` argument.
.. code-block:: bash
$ myth analyze rubixi.sol -t 3 --transaction-sequences [["0x89b8ae9b"],[-1],["0x686f2c90","0xb4022950","0x4229616d"]]
The first transaction is constrained to the function ``dynamicPyramid()``, the second one to the ``fallback()`` function, and finally, the third transaction is constrained to``collectAllFees()``, ``collectFeesInEther(uint256)`` and ``collectPercentOfFees(uint256)``.
Make sure to use ``-t 3`` argument, since the length of the transaction sequence should match with the transaction count argument.
================================================
FILE: mypy-stubs/z3/__init__.pyi
================================================
from typing import (
Any,
Iterable,
Iterator,
List,
Optional,
Sequence,
Tuple,
TypeVar,
Union,
overload,
)
from .z3types import Ast, ContextObj
class Context: ...
class Z3PPObject: ...
class AstRef(Z3PPObject):
@overload
def __init__(self, ast: Ast, ctx: Context) -> None:
self.ast: Ast = ...
self.ctx: Context = ...
@overload
def __init__(self, ast: Ast) -> None:
self.ast: Ast = ...
self.ctx: Context = ...
def ctx_ref(self) -> ContextObj: ...
def as_ast(self) -> Ast: ...
def children(self) -> List[AstRef]: ...
def eq(self, other: AstRef) -> bool: ...
# TODO: Cannot add __eq__ currently: mypy complains conflict with
# object.__eq__ signature
# def __eq__(self, other: object) -> ArithRef: ...
class SortRef(AstRef): ...
class FuncDeclRef(AstRef):
def arity(self) -> int: ...
def name(self) -> str: ...
def __call__(self, *args: ExprRef) -> ExprRef: ...
class ExprRef(AstRef):
def sort(self) -> SortRef: ...
def decl(self) -> FuncDeclRef: ...
class BoolSortRef(SortRef): ...
class ArraySortRef(SortRef): ...
class BoolRef(ExprRef): ...
def is_true(a: BoolRef) -> bool: ...
def is_false(a: BoolRef) -> bool: ...
def is_int_value(a: AstRef) -> bool: ...
def substitute(a: AstRef, *m: Tuple[AstRef, AstRef]) -> AstRef: ...
def simplify(a: AstRef, *args: Any, **kwargs: Any) -> AstRef: ...
class ArithSortRef(SortRef): ...
class ArithRef(ExprRef):
def __neg__(self) -> ExprRef: ...
def __le__(self, other: ArithRef) -> BoolRef: ...
def __lt__(self, other: ArithRef) -> BoolRef: ...
def __ge__(self, other: ArithRef) -> BoolRef: ...
def __gt__(self, other: ArithRef) -> BoolRef: ...
def __add__(self, other: ArithRef) -> ArithRef: ...
def __sub__(self, other: ArithRef) -> ArithRef: ...
def __mul__(self, other: ArithRef) -> ArithRef: ...
def __div__(self, other: ArithRef) -> ArithRef: ...
def __truediv__(self, other: ArithRef) -> ArithRef: ...
def __mod__(self, other: ArithRef) -> ArithRef: ...
class BitVecSortRef(SortRef): ...
class BitVecRef(ExprRef):
def size(self) -> int: ...
def __add__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __radd__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __mul__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __rmul__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __sub__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __rsub__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __or__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __ror__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __and__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __rand__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __xor__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __rxor__(self, other: Union[BitVecRef, int]) -> BitVecRef: ...
def __pos__(self) -> BitVecRef: ...
def __neg__(self) -> BitVecRef: ...
def __invert__(self) -> BitVecRef: ...
def __div__(self, other: BitVecRef) -> BitVecRef: ...
def __rdiv__(self, other: BitVecRef) -> BitVecRef: ...
def __truediv__(self, other: BitVecRef) -> BitVecRef: ...
def __rtruediv__(self, other: BitVecRef) -> BitVecRef: ...
def __mod__(self, other: BitVecRef) -> BitVecRef: ...
def __rmod__(self, other: BitVecRef) -> BitVecRef: ...
def __le__(self, other: BitVecRef) -> BoolRef: ...
def __lt__(self, other: BitVecRef) -> BoolRef: ...
def __ge__(self, other: BitVecRef) -> BoolRef: ...
def __gt__(self, other: BitVecRef) -> BoolRef: ...
def __rshift__(self, other: BitVecRef) -> BitVecRef: ...
def __lshift__(self, other: BitVecRef) -> BitVecRef: ...
def __rrshift__(self, other: BitVecRef) -> BitVecRef: ...
def __rlshift__(self, other: BitVecRef) -> BitVecRef: ...
class BitVecNumRef(BitVecRef):
def as_long(self) -> int: ...
def as_signed_long(self) -> int: ...
def as_string(self) -> str: ...
class IntNumRef(ArithRef):
def as_long(self) -> int: ...
def as_string(self) -> str: ...
class SeqSortRef(ExprRef): ...
class SeqRef(ExprRef): ...
class ReSortRef(ExprRef): ...
class ReRef(ExprRef): ...
class ArrayRef(ExprRef): ...
class CheckSatResult: ...
class ModelRef(Z3PPObject):
def __getitem__(self, k: FuncDeclRef) -> IntNumRef: ...
def decls(self) -> Iterable[FuncDeclRef]: ...
def __iter__(self) -> Iterator[FuncDeclRef]: ...
class FuncEntry:
def num_args(self) -> int: ...
def arg_value(self, idx: int) -> ExprRef: ...
def value(self) -> ExprRef: ...
class FuncInterp(Z3PPObject):
def else_value(self) -> ExprRef: ...
def num_entries(self) -> int: ...
def arity(self) -> int: ...
def entry(self, idx: int) -> FuncEntry: ...
class Goal(Z3PPObject): ...
class Solver(Z3PPObject):
ctx: Context
def __init__(self, ctx: Optional[Context] = None) -> None: ...
def to_smt2(self) -> str: ...
def check(self) -> CheckSatResult: ...
def push(self) -> None: ...
def pop(self, num: Optional[int] = 1) -> None: ...
def model(self) -> ModelRef: ...
def set(self, *args: Any, **kwargs: Any) -> None: ...
@overload
def add(self, *args: Union[BoolRef, Goal]) -> None: ...
@overload
def add(self, args: Sequence[Union[BoolRef, Goal]]) -> None: ...
def reset(self) -> None: ...
class Optimize(Z3PPObject):
ctx: Context
def __init__(self, ctx: Optional[Context] = None) -> None: ...
def check(self) -> CheckSatResult: ...
def push(self) -> None: ...
def pop(self) -> None: ...
def model(self) -> ModelRef: ...
def set(self, *args: Any, **kwargs: Any) -> None: ...
@overload
def add(self, *args: Union[BoolRef, Goal]) -> None: ...
@overload
def add(self, args: Sequence[Union[BoolRef, Goal]]) -> None: ...
def minimize(self, element: ExprRef) -> None: ...
def maximize(self, element: ExprRef) -> None: ...
sat: CheckSatResult = ...
unsat: CheckSatResult = ...
@overload
def Int(name: str) -> ArithRef: ...
@overload
def Int(name: str, ctx: Context) -> ArithRef: ...
@overload
def Bool(name: str) -> BoolRef: ...
@overload
def Bool(name: str, ctx: Context) -> BoolRef: ...
@overload
def parse_smt2_string(s: str) -> ExprRef: ...
@overload
def parse_smt2_string(s: str, ctx: Context) -> ExprRef: ...
def Array(name: str, domain: SortRef, range: SortRef) -> ArrayRef: ...
def K(domain: SortRef, v: Union[ExprRef, int, bool, str]) -> ArrayRef: ...
# Can't give more precise types here since func signature is
# a vararg list of ExprRef optionally followed by a Context
def Or(*args: Any) -> BoolRef: ...
def And(*args: Any) -> BoolRef: ...
def Not(p: BoolRef, ctx: Optional[Context] = None) -> BoolRef: ...
def Implies(a: BoolRef, b: BoolRef, ctx: Context) -> BoolRef: ...
T = TypeVar("T", bound=ExprRef)
def If(a: BoolRef, b: T, c: T, ctx: Optional[Context] = None) -> T: ...
def ULE(a: T, b: T) -> BoolRef: ...
def ULT(a: T, b: T) -> BoolRef: ...
def UGE(a: T, b: T) -> BoolRef: ...
def UGT(a: T, b: T) -> BoolRef: ...
def UDiv(a: T, b: T) -> T: ...
def URem(a: T, b: T) -> T: ...
def SRem(a: T, b: T) -> T: ...
def LShR(a: T, b: T) -> T: ...
def RotateLeft(a: T, b: T) -> T: ...
def RotateRight(a: T, b: T) -> T: ...
def SignExt(n: int, a: BitVecRef) -> BitVecRef: ...
def ZeroExt(n: int, a: BitVecRef) -> BitVecRef: ...
@overload
def Concat(args: List[Union[SeqRef, str]]) -> SeqRef: ...
@overload
def Concat(*args: Union[SeqRef, str]) -> SeqRef: ...
@overload
def Concat(args: List[ReRef]) -> ReRef: ...
@overload
def Concat(*args: ReRef) -> ReRef: ...
@overload
def Concat(args: List[BitVecRef]) -> BitVecRef: ...
@overload
def Concat(*args: BitVecRef) -> BitVecRef: ...
@overload
def Extract(
high: Union[SeqRef], lo: Union[int, ArithRef], a: Union[int, ArithRef]
) -> SeqRef: ...
@overload
def Extract(
high: Union[int, ArithRef], lo: Union[int, ArithRef], a: BitVecRef
) -> BitVecRef: ...
@overload
def Sum(arg: BitVecRef, *args: Union[BitVecRef, int]) -> BitVecRef: ...
@overload
def Sum(arg: Union[List[BitVecRef], int]) -> BitVecRef: ...
@overload
def Sum(arg: ArithRef, *args: Union[ArithRef, int]) -> ArithRef: ...
# Can't include this overload as it overlaps with the second overload.
# @overload
# def Sum(arg: Union[List[ArithRef], int]) -> ArithRef: ...
def Function(name: str, *sig: SortRef) -> FuncDeclRef: ...
def IntVal(val: int, ctx: Optional[Context] = None) -> IntNumRef: ...
def BoolVal(val: bool, ctx: Optional[Context] = None) -> BoolRef: ...
def BitVecVal(
val: int, bv: Union[int, BitVecSortRef], ctx: Optional[Context] = None
) -> BitVecRef: ...
def BitVec(
val: str, bv: Union[int, BitVecSortRef], ctx: Optional[Context] = None
) -> BitVecRef: ...
def IntSort(ctx: Optional[Context] = None) -> ArithSortRef: ...
def BoolSort(ctx: Optional[Context] = None) -> BoolSortRef: ...
def ArraySort(domain: SortRef, range: SortRef) -> ArraySortRef: ...
def BitVecSort(domain: int, ctx: Optional[Context] = None) -> BoolSortRef: ...
def ForAll(vs: List[ExprRef], expr: ExprRef) -> ExprRef: ...
def Select(arr: ExprRef, ind: ExprRef) -> ExprRef: ...
def Update(arr: ArrayRef, ind: ExprRef, newVal: ExprRef) -> ArrayRef: ...
def Store(arr: ArrayRef, ind: ExprRef, newVal: ExprRef) -> ArrayRef: ...
def BVAddNoOverflow(a: BitVecRef, b: BitVecRef, signed: bool) -> BoolRef: ...
def BVAddNoUnderflow(a: BitVecRef, b: BitVecRef) -> BoolRef: ...
def BVSubNoOverflow(a: BitVecRef, b: BitVecRef) -> BoolRef: ...
def BVSubNoUnderflow(a: BitVecRef, b: BitVecRef, signed: bool) -> BoolRef: ...
def BVSDivNoOverflow(a: BitVecRef, b: BitVecRef) -> BoolRef: ...
def BVSNegNoOverflow(a: BitVecRef) -> BoolRef: ...
def BVMulNoOverflow(a: BitVecRef, b: BitVecRef, signed: bool) -> BoolRef: ...
def BVMulNoUnderflow(a: BitVecRef, b: BitVecRef) -> BoolRef: ...
================================================
FILE: mypy-stubs/z3/z3core.pyi
================================================
from .z3types import Ast, ContextObj
def Z3_mk_eq(ctx: ContextObj, a: Ast, b: Ast) -> Ast: ...
def Z3_mk_div(ctx: ContextObj, a: Ast, b: Ast) -> Ast: ...
================================================
FILE: mypy-stubs/z3/z3types.pyi
================================================
from typing import Any
class Z3Exception(Exception):
def __init__(self, a: Any) -> None:
self.value = a
...
class ContextObj: ...
class Ast: ...
================================================
FILE: myth
================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""mythril.py: Bug hunting on the Ethereum blockchain
http://www.github.com/ConsenSys/mythril
"""
from sys import exit
import mythril.interfaces.cli
if __name__ == "__main__":
mythril.interfaces.cli.main()
exit()
================================================
FILE: mythril/__init__.py
================================================
# We use RsT document formatting in docstring. For example :param to mark parameters.
# See PEP 287
__docformat__ = "restructuredtext"
import logging
from mythril.plugin.loader import MythrilPluginLoader
# Accept mythril.VERSION to get mythril's current version number
from .__version__ import __version__ as VERSION
log = logging.getLogger(__name__)
================================================
FILE: mythril/__main__.py
================================================
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import mythril.interfaces.cli
if __name__ == "__main__":
mythril.interfaces.cli.main()
================================================
FILE: mythril/__version__.py
================================================
"""This file contains the current Mythril version.
This file is suitable for sourcing inside POSIX shell, e.g. bash as well
as for importing into Python.
"""
__version__ = "v0.24.8"
================================================
FILE: mythril/analysis/__init__.py
================================================
================================================
FILE: mythril/analysis/analysis_args.py
================================================
================================================
FILE: mythril/analysis/call_helpers.py
================================================
"""This module provides helper functions for the analysis modules to deal with
call functionality."""
from typing import Union
from mythril.analysis.ops import Call, VarType, get_variable
from mythril.laser.ethereum.natives import PRECOMPILE_COUNT
from mythril.laser.ethereum.state.global_state import GlobalState
def get_call_from_state(state: GlobalState) -> Union[Call, None]:
"""
:param state:
:return:
"""
instruction = state.get_current_instruction()
op = instruction["opcode"]
stack = state.mstate.stack
if op in ("CALL", "CALLCODE"):
gas, to, value, meminstart, meminsz, _, _ = (
get_variable(stack[-1]),
get_variable(stack[-2]),
get_variable(stack[-3]),
get_variable(stack[-4]),
get_variable(stack[-5]),
get_variable(stack[-6]),
get_variable(stack[-7]),
)
if to.type == VarType.CONCRETE and 0 < to.val <= PRECOMPILE_COUNT:
return None
if meminstart.type == VarType.CONCRETE and meminsz.type == VarType.CONCRETE:
return Call(
state.node,
state,
None,
op,
to,
gas,
value,
state.mstate.memory[meminstart.val : meminsz.val * 4],
)
else:
return Call(state.node, state, None, op, to, gas, value)
else:
gas, to, meminstart, meminsz, _, _ = (
get_variable(stack[-1]),
get_variable(stack[-2]),
get_variable(stack[-3]),
get_variable(stack[-4]),
get_variable(stack[-5]),
get_variable(stack[-6]),
)
return Call(state.node, state, None, op, to, gas)
================================================
FILE: mythril/analysis/callgraph.py
================================================
"""This module contains the configuration and functions to create call
graphs."""
import re
from typing import Dict, List
from jinja2 import Environment, PackageLoader, select_autoescape
from z3 import Z3Exception
from mythril.laser.ethereum.svm import NodeFlags
from mythril.laser.smt import simplify
default_opts = {
"autoResize": True,
"height": "100%",
"width": "100%",
"manipulation": False,
"layout": {
"improvedLayout": True,
"hierarchical": {
"enabled": True,
"levelSeparation": 450,
"nodeSpacing": 200,
"treeSpacing": 100,
"blockShifting": True,
"edgeMinimization": True,
"parentCentralization": False,
"direction": "LR",
"sortMethod": "directed",
},
},
"nodes": {
"color": "#000000",
"borderWidth": 1,
"borderWidthSelected": 2,
"chosen": True,
"shape": "box",
"font": {"align": "left", "color": "#FFFFFF"},
},
"edges": {
"font": {
"color": "#FFFFFF",
"face": "arial",
"background": "none",
"strokeWidth": 0,
"strokeColor": "#ffffff",
"align": "horizontal",
"multi": False,
"vadjust": 0,
}
},
"physics": {"enabled": False},
}
phrack_opts = {
"nodes": {
"color": "#000000",
"borderWidth": 1,
"borderWidthSelected": 1,
"shapeProperties": {"borderDashes": False, "borderRadius": 0},
"chosen": True,
"shape": "box",
"font": {"face": "courier new", "align": "left", "color": "#000000"},
},
"edges": {
"font": {
"color": "#000000",
"face": "courier new",
"background": "none",
"strokeWidth": 0,
"strokeColor": "#ffffff",
"align": "horizontal",
"multi": False,
"vadjust": 0,
}
},
}
default_colors = [
{
"border": "#26996f",
"background": "#2f7e5b",
"highlight": {"border": "#26996f", "background": "#28a16f"},
},
{
"border": "#9e42b3",
"background": "#842899",
"highlight": {"border": "#9e42b3", "background": "#933da6"},
},
{
"border": "#b82323",
"background": "#991d1d",
"highlight": {"border": "#b82323", "background": "#a61f1f"},
},
{
"border": "#4753bf",
"background": "#3b46a1",
"highlight": {"border": "#4753bf", "background": "#424db3"},
},
{
"border": "#26996f",
"background": "#2f7e5b",
"highlight": {"border": "#26996f", "background": "#28a16f"},
},
{
"border": "#9e42b3",
"background": "#842899",
"highlight": {"border": "#9e42b3", "background": "#933da6"},
},
{
"border": "#b82323",
"background": "#991d1d",
"highlight": {"border": "#b82323", "background": "#a61f1f"},
},
{
"border": "#4753bf",
"background": "#3b46a1",
"highlight": {"border": "#4753bf", "background": "#424db3"},
},
]
phrack_color = {
"border": "#000000",
"background": "#ffffff",
"highlight": {"border": "#000000", "background": "#ffffff"},
}
def extract_nodes(statespace) -> List[Dict]:
"""
Extract nodes from the given statespace and create a list of node dictionaries
with visual attributes for graph representation.
:param statespace: The statespace object containing nodes and states information.
:return: A list of dictionaries representing each node with its attributes.
"""
nodes = []
color_map = {}
for node_key, node in statespace.nodes.items():
instructions = [state.get_current_instruction() for state in node.states]
code_split = []
for instruction in instructions:
address = instruction["address"]
opcode = instruction["opcode"]
if opcode.startswith("PUSH"):
code_line = f"{address} {opcode} {instruction.get('argument', '')}"
elif (
opcode.startswith("JUMPDEST")
and NodeFlags.FUNC_ENTRY in node.flags
and address == node.start_addr
):
code_line = node.function_name
else:
code_line = f"{address} {opcode}"
code_line = re.sub(r"([0-9a-f]{8})[0-9a-f]+", r"\1(...)", code_line)
code_split.append(code_line)
truncated_code = (
"\n".join(code_split)
if len(code_split) < 7
else "\n".join(code_split[:6]) + "\n(click to expand +)"
)
contract_name = node.get_cfg_dict()["contract_name"]
if contract_name not in color_map:
color = default_colors[len(color_map) % len(default_colors)]
color_map[contract_name] = color
nodes.append(
{
"id": str(node_key),
"color": color_map.get(contract_name, default_colors[0]),
"size": 150,
"fullLabel": "\n".join(code_split),
"label": truncated_code,
"truncLabel": truncated_code,
"isExpanded": False,
}
)
return nodes
def extract_edges(statespace):
"""
:param statespace:
:return:
"""
edges = []
for edge in statespace.edges:
if edge.condition is None:
label = ""
else:
try:
label = str(simplify(edge.condition)).replace("\n", "")
except Z3Exception:
label = str(edge.condition).replace("\n", "")
label = re.sub(
r"([^_])([\d]{2}\d+)", lambda m: m.group(1) + hex(int(m.group(2))), label
)
edges.append(
{
"from": str(edge.as_dict["from"]),
"to": str(edge.as_dict["to"]),
"arrows": "to",
"label": label,
"smooth": {"type": "cubicBezier"},
}
)
return edges
def generate_graph(
statespace,
title="Mythril / Ethereum LASER Symbolic VM",
physics=False,
phrackify=False,
):
"""
:param statespace:
:param title:
:param physics:
:param phrackify:
:return:
"""
env = Environment(
loader=PackageLoader("mythril.analysis"),
autoescape=select_autoescape(["html", "xml"]),
)
template = env.get_template("callgraph.html")
graph_opts = default_opts
graph_opts["physics"]["enabled"] = physics
return template.render(
title=title,
nodes=extract_nodes(statespace),
edges=extract_edges(statespace),
phrackify=phrackify,
opts=graph_opts,
)
================================================
FILE: mythril/analysis/issue_annotation.py
================================================
from copy import deepcopy
from typing import List
from mythril.analysis.report import Issue
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.smt import SMTBool as Bool
class IssueAnnotation(StateAnnotation):
def __init__(self, conditions: List[Bool], issue: Issue, detector):
"""
Issue Annotation to propagate issues
- conditions: A list of independent conditions [a, b, c, ...]
Each of these have to be independently be satisfied
- issue: The issue of the annotation
- detector: The detection module
"""
self.conditions = conditions
self.issue = issue
self.detector = detector
def persist_to_world_state(self) -> bool:
return True
@property
def persist_over_calls(self) -> bool:
return True
def __copy__(self):
return IssueAnnotation(
conditions=deepcopy(self.conditions),
issue=self.issue,
detector=self.detector,
)
def check_merge_annotation(self, annotation: "IssueAnnotation") -> bool:
if self.conditions == annotation.conditions:
return False
if self.issue.address != annotation.issue.address:
return False
if type(self.detector) != type(annotation.detector):
return False
return True
def merge_annotation(self, annotation: "IssueAnnotation") -> "IssueAnnotation":
return self
================================================
FILE: mythril/analysis/module/__init__.py
================================================
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.module.loader import ModuleLoader
from mythril.analysis.module.util import (
get_detection_module_hooks,
reset_callback_modules,
)
================================================
FILE: mythril/analysis/module/base.py
================================================
"""Mythril Detection Modules
This module includes an definition of the DetectionModule interface.
DetectionModules implement different analysis rules to find weaknesses and vulnerabilities.
"""
import logging
from abc import ABC, abstractmethod
from enum import Enum
from typing import List, Optional, Set, Tuple
from mythril.analysis.report import Issue
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.support.support_args import args
from mythril.support.support_utils import get_code_hash
# Get logger instance
log = logging.getLogger(__name__)
class EntryPoint(Enum):
"""EntryPoint Enum
This enum is used to signify the entry_point of detection modules.
See also the class documentation of DetectionModule
"""
POST = 1
CALLBACK = 2
class DetectionModule(ABC):
"""The base detection module.
All custom-built detection modules must inherit from this class.
There are several class properties that expose information about the detection modules
:param name: The name of the detection module
:param swc_id: The SWC ID associated with the weakness that the module detects
:param description: A description of the detection module, and what it detects
:param entry_point: Mythril can run callback style detection modules, or modules that search the statespace.
[IMPORTANT] POST entry points severely slow down the analysis, try to always use callback style modules
:param pre_hooks: A list of instructions to hook the laser vm for (pre execution of the instruction)
:param post_hooks: A list of instructions to hook the laser vm for (post execution of the instruction)
"""
name = "Detection Module Name / Title"
swc_id = "SWC-000"
description = "Detection module description"
entry_point: EntryPoint = EntryPoint.CALLBACK
pre_hooks: List[str] = []
post_hooks: List[str] = []
def __init__(self) -> None:
self.issues: List[Issue] = []
self.cache: Set[Tuple[int, str]] = set()
self.auto_cache = True
def reset_module(self):
"""Resets the storage of this module"""
self.issues = []
def update_cache(self, issues=None):
"""
Updates cache with param issues, updates against self.issues, if the param is None
:param issues: The issues used to update the cache
"""
issues = issues or self.issues
for issue in issues:
self.cache.add((issue.address, issue.bytecode_hash))
def execute(self, target: GlobalState) -> Optional[List[Issue]]:
"""The entry point for execution, which is being called by Mythril.
:param target: The target of the analysis, either a global state (callback) or the entire statespace (post)
:return: List of encountered issues
"""
log.debug("Entering analysis module: {}".format(self.__class__.__name__))
if (
target.get_current_instruction()["address"],
get_code_hash(target.environment.code.bytecode),
) in self.cache and self.auto_cache:
log.debug(
f"Issue in cache for the analysis module: {self.__class__.__name__}, address: {target.get_current_instruction()['address']}"
)
return []
result = self._execute(target)
log.debug("Exiting analysis module: {}".format(self.__class__.__name__))
if result and not args.use_issue_annotations:
if self.auto_cache:
self.update_cache(result)
self.issues += result
return result
@abstractmethod
def _execute(self, target) -> Optional[List[Issue]]:
"""Module main method (override this)
:param target: The target of the analysis, either a global state (callback) or the entire statespace (post)
:return: List of encountered issues
"""
pass
def __repr__(self) -> str:
return (
"<"
"DetectionModule "
"name={0.name} "
"swc_id={0.swc_id} "
"pre_hooks={0.pre_hooks} "
"post_hooks={0.post_hooks} "
"description={0.description}"
">"
).format(self)
================================================
FILE: mythril/analysis/module/loader.py
================================================
from typing import List, Optional
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.module.modules.arbitrary_jump import ArbitraryJump
from mythril.analysis.module.modules.arbitrary_write import ArbitraryStorage
from mythril.analysis.module.modules.delegatecall import ArbitraryDelegateCall
from mythril.analysis.module.modules.dependence_on_origin import TxOrigin
from mythril.analysis.module.modules.dependence_on_predictable_vars import (
PredictableVariables,
)
from mythril.analysis.module.modules.ether_thief import EtherThief
from mythril.analysis.module.modules.exceptions import Exceptions
from mythril.analysis.module.modules.external_calls import ExternalCalls
from mythril.analysis.module.modules.integer import IntegerArithmetics
from mythril.analysis.module.modules.multiple_sends import MultipleSends
from mythril.analysis.module.modules.requirements_violation import RequirementsViolation
from mythril.analysis.module.modules.state_change_external_calls import (
StateChangeAfterCall,
)
from mythril.analysis.module.modules.suicide import AccidentallyKillable
from mythril.analysis.module.modules.transaction_order_dependence import (
TransactionOrderDependence,
)
from mythril.analysis.module.modules.unchecked_retval import UncheckedRetval
from mythril.analysis.module.modules.unexpected_ether import UnexpectedEther
from mythril.analysis.module.modules.user_assertions import UserAssertions
from mythril.exceptions import DetectorNotFoundError
from mythril.support.support_args import args
from mythril.support.support_utils import Singleton
class ModuleLoader(object, metaclass=Singleton):
"""ModuleLoader
The module loader class implements a singleton loader for detection modules.
By default it will load the detection modules in the mythril package.
Additional detection modules can be loaded using the register_module function call implemented by the ModuleLoader
"""
def __init__(self):
self._modules = []
self._register_mythril_modules()
def register_module(self, detection_module: DetectionModule):
"""Registers a detection module with the module loader"""
if not isinstance(detection_module, DetectionModule):
raise ValueError("The passed variable is not a valid detection module")
self._modules.append(detection_module)
def get_detection_modules(
self,
entry_point: Optional[EntryPoint] = None,
white_list: Optional[List[str]] = None,
) -> List[DetectionModule]:
"""Gets registered detection modules
:param entry_point: If specified: only return detection modules with this entry point
:param white_list: If specified: only return whitelisted detection modules
:return: The selected detection modules
"""
result = self._modules[:]
if white_list:
# Sanity check
available_names = [type(module).__name__ for module in result]
for name in white_list:
if name not in available_names:
raise DetectorNotFoundError(
"Invalid detection module: {}".format(name)
)
result = [
module for module in result if type(module).__name__ in white_list
]
if args.use_integer_module is False:
result = [
module
for module in result
if type(module).__name__ != "IntegerArithmetics"
]
if entry_point:
result = [module for module in result if module.entry_point == entry_point]
return result
def _register_mythril_modules(self):
self._modules.extend(
[
AccidentallyKillable(),
ArbitraryJump(),
ArbitraryStorage(),
ArbitraryDelegateCall(),
EtherThief(),
Exceptions(),
ExternalCalls(),
IntegerArithmetics(),
MultipleSends(),
PredictableVariables(),
RequirementsViolation(),
StateChangeAfterCall(),
TransactionOrderDependence(),
TxOrigin(),
UncheckedRetval(),
UnexpectedEther(),
UserAssertions(),
]
)
================================================
FILE: mythril/analysis/module/module_helpers.py
================================================
import traceback
def is_prehook() -> bool:
"""Check if we are in prehook. One of Bernhard's trademark hacks!
Let's leave it to this for now, unless we need to check prehook for
a lot more modules.
"""
assert ("pre_hook" in traceback.format_stack()[-5]) or (
"post_hook" in traceback.format_stack()[-5]
)
return "pre_hook" in traceback.format_stack()[-5]
================================================
FILE: mythril/analysis/module/modules/__init__.py
================================================
================================================
FILE: mythril/analysis/module/modules/arbitrary_jump.py
================================================
"""This module contains the detection code for Arbitrary jumps."""
import logging
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule, EntryPoint, Issue
from mythril.analysis.solver import UnsatError, get_transaction_sequence
from mythril.analysis.swc_data import ARBITRARY_JUMP
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import And, BitVec, symbol_factory
from mythril.support.model import get_model
log = logging.getLogger(__name__)
DESCRIPTION = """
Search for jumps to arbitrary locations in the bytecode
"""
def is_unique_jumpdest(jump_dest: BitVec, state: GlobalState) -> bool:
"""
Handles cases where jump_dest evaluates to a single concrete value
"""
try:
model = get_model(state.world_state.constraints)
except UnsatError:
return True
concrete_jump_dest = model.eval(jump_dest.raw, model_completion=True)
try:
model = get_model(
state.world_state.constraints
+ [symbol_factory.BitVecVal(concrete_jump_dest.as_long(), 256) != jump_dest]
)
except UnsatError:
return True
return False
class ArbitraryJump(DetectionModule):
"""This module searches for JUMPs to a user-specified location."""
name = "Caller can redirect execution to arbitrary bytecode locations"
swc_id = ARBITRARY_JUMP
description = DESCRIPTION
entry_point = EntryPoint.CALLBACK
pre_hooks = ["JUMP", "JUMPI"]
def reset_module(self):
"""
Resets the module by clearing everything
:return:
"""
super().reset_module()
def _execute(self, state: GlobalState) -> None:
"""
:param state:
:return:
"""
return self._analyze_state(state)
def _analyze_state(self, state):
"""
:param state:
:return:
"""
jump_dest = state.mstate.stack[-1]
if jump_dest.symbolic is False:
return []
if is_unique_jumpdest(jump_dest, state) is True:
return []
try:
transaction_sequence = get_transaction_sequence(
state, state.world_state.constraints
)
except UnsatError:
return []
log.info("Detected arbitrary jump dest")
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=state.get_current_instruction()["address"],
swc_id=ARBITRARY_JUMP,
title="Jump to an arbitrary instruction",
severity="High",
bytecode=state.environment.code.bytecode,
description_head="The caller can redirect execution to arbitrary bytecode locations.",
description_tail="It is possible to redirect the control flow to arbitrary locations in the code. "
"This may allow an attacker to bypass security controls or manipulate the business logic of the "
"smart contract. Avoid using low-level-operations and assembly to prevent this issue.",
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
transaction_sequence=transaction_sequence,
)
state.annotate(
IssueAnnotation(
conditions=[And(*state.world_state.constraints)],
issue=issue,
detector=self,
)
)
return [issue]
detector = ArbitraryJump()
================================================
FILE: mythril/analysis/module/modules/arbitrary_write.py
================================================
"""This module contains the detection code for arbitrary storage write."""
import logging
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.potential_issues import (
PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import WRITE_TO_ARBITRARY_STORAGE
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import symbol_factory
log = logging.getLogger(__name__)
DESCRIPTION = """
Search for any writes to an arbitrary storage slot
"""
class ArbitraryStorage(DetectionModule):
"""This module searches for a feasible write to an arbitrary storage slot."""
name = "Caller can write to arbitrary storage locations"
swc_id = WRITE_TO_ARBITRARY_STORAGE
description = DESCRIPTION
entry_point = EntryPoint.CALLBACK
pre_hooks = ["SSTORE"]
def reset_module(self):
"""
Resets the module by clearing everything
:return:
"""
super().reset_module()
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):
"""
:param state:
:return:
"""
write_slot = state.mstate.stack[-1]
constraints = state.world_state.constraints + [
write_slot == symbol_factory.BitVecVal(324345425435, 256)
]
potential_issue = PotentialIssue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=state.get_current_instruction()["address"],
swc_id=WRITE_TO_ARBITRARY_STORAGE,
title="Write to an arbitrary storage location",
severity="High",
bytecode=state.environment.code.bytecode,
description_head="The caller can write to arbitrary storage locations.",
description_tail="It is possible to write to arbitrary storage locations. By modifying the values of "
"storage variables, attackers may bypass security controls or manipulate the business logic of "
"the smart contract.",
detector=self,
constraints=constraints,
)
return [potential_issue]
detector = ArbitraryStorage()
================================================
FILE: mythril/analysis/module/modules/delegatecall.py
================================================
"""This module contains the detection code for insecure delegate call usage."""
import logging
from typing import List
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.potential_issues import (
PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import DELEGATECALL_TO_UNTRUSTED_CONTRACT
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.transaction.symbolic import ACTORS
from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction,
)
from mythril.laser.smt import UGT, symbol_factory
log = logging.getLogger(__name__)
class ArbitraryDelegateCall(DetectionModule):
"""This module detects delegatecall to a user-supplied address."""
name = "Delegatecall to a user-specified address"
swc_id = DELEGATECALL_TO_UNTRUSTED_CONTRACT
description = "Check for invocations of delegatecall to a user-supplied address."
entry_point = EntryPoint.CALLBACK
pre_hooks = ["DELEGATECALL"]
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) -> List[PotentialIssue]:
"""
:param state: the current state
:return: returns the issues for that corresponding state
"""
gas = state.mstate.stack[-1]
to = state.mstate.stack[-2]
constraints = [
to == ACTORS.attacker,
UGT(gas, symbol_factory.BitVecVal(2300, 256)),
state.new_bitvec(
"retval_{}".format(state.get_current_instruction()["address"]), 256
)
== 1,
]
for tx in state.world_state.transaction_sequence:
if not isinstance(tx, ContractCreationTransaction):
constraints.append(tx.caller == ACTORS.attacker)
try:
address = state.get_current_instruction()["address"]
logging.debug(
"[DELEGATECALL] Detected potential 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.This could allow an attacker to "
"execute arbitrary code in the context of this contract account and manipulate the state of the "
"contract account or execute actions on its behalf."
)
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 to user-supplied address",
severity="High",
description_head=description_head,
description_tail=description_tail,
constraints=constraints,
detector=self,
)
]
except UnsatError:
return []
detector = ArbitraryDelegateCall()
================================================
FILE: mythril/analysis/module/modules/dependence_on_origin.py
================================================
"""This module contains the detection code for predictable variable
dependence."""
import logging
from copy import copy
from typing import List
from mythril.analysis import solver
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import TX_ORIGIN_USAGE
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import And
log = logging.getLogger(__name__)
class TxOriginAnnotation:
"""Symbol annotation added to a variable that is initialized with a call to the ORIGIN instruction."""
def __init__(self) -> None:
pass
class TxOrigin(DetectionModule):
"""This module detects whether control flow decisions are made based on the transaction origin."""
name = "Control flow depends on tx.origin"
swc_id = TX_ORIGIN_USAGE
description = "Check whether control flow decisions are influenced by tx.origin"
entry_point = EntryPoint.CALLBACK
pre_hooks = ["JUMPI"]
post_hooks = ["ORIGIN"]
def _execute(self, state: GlobalState) -> List[Issue]:
"""
:param state:
:return:
"""
return self._analyze_state(state)
def _analyze_state(self, state: GlobalState) -> List[Issue]:
"""
:param state:
:return:
"""
issues = []
if state.get_current_instruction()["opcode"] == "JUMPI":
# We're in JUMPI prehook
for annotation in state.mstate.stack[-2].annotations:
if isinstance(annotation, TxOriginAnnotation):
constraints = copy(state.world_state.constraints)
try:
transaction_sequence = solver.get_transaction_sequence(
state, constraints
)
except UnsatError:
continue
description = (
"The tx.origin environment variable has been found to influence a control flow decision. "
"Note that using tx.origin as a security control might cause a situation where a user "
"inadvertently authorizes a smart contract to perform an action on their behalf. It is "
"recommended to use msg.sender instead."
)
severity = "Low"
"""
Note: We report the location of the JUMPI instruction. Usually this maps to an if or
require statement.
"""
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=state.get_current_instruction()["address"],
swc_id=TX_ORIGIN_USAGE,
bytecode=state.environment.code.bytecode,
title="Dependence on tx.origin",
severity=severity,
description_head="Use of tx.origin as a part of authorization control.",
description_tail=description,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
transaction_sequence=transaction_sequence,
)
state.annotate(
IssueAnnotation(
conditions=[And(*constraints)], issue=issue, detector=self
)
)
issues.append(issue)
else:
# In ORIGIN posthook
state.mstate.stack[-1].annotate(TxOriginAnnotation())
return issues
detector = TxOrigin()
================================================
FILE: mythril/analysis/module/modules/dependence_on_predictable_vars.py
================================================
"""This module contains the detection code for predictable variable
dependence."""
import logging
from typing import List, cast
from mythril.analysis import solver
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.module.module_helpers import is_prehook
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import TIMESTAMP_DEPENDENCE, WEAK_RANDOMNESS
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.smt import ULT, And, symbol_factory
log = logging.getLogger(__name__)
predictable_ops = ["COINBASE", "GASLIMIT", "TIMESTAMP", "NUMBER"]
class PredictableValueAnnotation:
"""Symbol annotation used if a variable is initialized from a predictable environment variable."""
def __init__(self, operation: str) -> None:
self.operation = operation
class OldBlockNumberUsedAnnotation(StateAnnotation):
"""Symbol annotation used if a variable is initialized from a predictable environment variable."""
def __init__(self) -> None:
pass
class PredictableVariables(DetectionModule):
"""This module detects whether control flow decisions are made using predictable
parameters."""
name = "Control flow depends on a predictable environment variable"
swc_id = "{} {}".format(TIMESTAMP_DEPENDENCE, WEAK_RANDOMNESS)
description = (
"Check whether control flow decisions are influenced by block.coinbase,"
"block.gaslimit, block.timestamp or block.number."
)
entry_point = EntryPoint.CALLBACK
pre_hooks = ["JUMPI", "BLOCKHASH"]
post_hooks = ["BLOCKHASH"] + predictable_ops
def _execute(self, state: GlobalState) -> List[Issue]:
"""
:param state:
:return:
"""
return self._analyze_state(state)
def _analyze_state(self, state: GlobalState) -> List[Issue]:
"""
:param state:
:return:
"""
issues = []
if is_prehook():
opcode = state.get_current_instruction()["opcode"]
if opcode == "JUMPI":
# Look for predictable state variables in jump condition
for annotation in state.mstate.stack[-2].annotations:
if isinstance(annotation, PredictableValueAnnotation):
constraints = state.world_state.constraints
try:
transaction_sequence = solver.get_transaction_sequence(
state, constraints
)
except UnsatError:
continue
description = (
annotation.operation
+ " is used to determine a control flow decision. "
)
description += (
"Note that the values of variables like coinbase, gaslimit, block number and timestamp are "
"predictable and can be manipulated by a malicious miner. Also keep in mind that "
"attackers know hashes of earlier blocks. Don't use any of those environment variables "
"as sources of randomness and be aware that use of these variables introduces "
"a certain level of trust into miners."
)
"""
Usually report low severity except in cases where the hash of a previous block is used to
determine control flow.
"""
severity = "Low"
swc_id = (
TIMESTAMP_DEPENDENCE
if "timestamp" in annotation.operation
else WEAK_RANDOMNESS
)
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=state.get_current_instruction()["address"],
swc_id=swc_id,
bytecode=state.environment.code.bytecode,
title="Dependence on predictable environment variable",
severity=severity,
description_head="A control flow decision is made based on {}.".format(
annotation.operation
),
description_tail=description,
gas_used=(
state.mstate.min_gas_used,
state.mstate.max_gas_used,
),
transaction_sequence=transaction_sequence,
)
state.annotate(
IssueAnnotation(
conditions=[And(*constraints)],
issue=issue,
detector=self,
)
)
issues.append(issue)
elif opcode == "BLOCKHASH":
param = state.mstate.stack[-1]
constraint = [
ULT(param, state.environment.block_number),
ULT(
state.environment.block_number,
symbol_factory.BitVecVal(2**255, 256),
),
]
# Why the second constraint? Because without it Z3 returns a solution where param overflows.
try:
solver.get_model(
state.world_state.constraints + constraint # type: ignore
)
state.annotate(OldBlockNumberUsedAnnotation())
except UnsatError:
pass
else:
# we're in post hook
opcode = state.environment.code.instruction_list[state.mstate.pc - 1][
"opcode"
]
if opcode == "BLOCKHASH":
# if we're in the post hook of a BLOCKHASH op, check if an old block number was used to create it.
annotations = cast(
List[OldBlockNumberUsedAnnotation],
list(state.get_annotations(OldBlockNumberUsedAnnotation)),
)
if len(annotations):
# We can append any block constraint here
state.mstate.stack[-1].annotate(
PredictableValueAnnotation("The block hash of a previous block")
)
else:
# Always create an annotation when COINBASE, GASLIMIT, TIMESTAMP or NUMBER is executed.
state.mstate.stack[-1].annotate(
PredictableValueAnnotation(
"The block.{} environment variable".format(opcode.lower())
)
)
return issues
detector = PredictableVariables()
================================================
FILE: mythril/analysis/module/modules/ether_thief.py
================================================
"""This module contains the detection code for unauthorized ether
withdrawal."""
import logging
from copy import copy
from mythril.analysis import solver
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.potential_issues import (
PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import UNPROTECTED_ETHER_WITHDRAWAL
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.transaction.symbolic import ACTORS
from mythril.laser.smt import UGT
log = logging.getLogger(__name__)
DESCRIPTION = """
Search for cases where Ether can be withdrawn to a user-specified address.
An issue is reported if there is a valid end state where the attacker has successfully
increased their Ether balance.
"""
class EtherThief(DetectionModule):
"""This module search for cases where Ether can be withdrawn to a user-
specified address."""
name = "Any sender can withdraw ETH from the contract account"
swc_id = UNPROTECTED_ETHER_WITHDRAWAL
description = DESCRIPTION
entry_point = EntryPoint.CALLBACK
post_hooks = ["CALL", "STATICCALL"]
def reset_module(self):
"""
Resets the module by clearing everything
:return:
"""
super().reset_module()
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):
"""
:param state:
:return:
"""
state = copy(state)
instruction = state.get_current_instruction()
constraints = copy(state.world_state.constraints)
constraints += [
UGT(
state.world_state.balances[ACTORS.attacker],
state.world_state.starting_balances[ACTORS.attacker],
),
state.environment.sender == ACTORS.attacker,
state.current_transaction.caller == state.current_transaction.origin,
]
try:
# Pre-solve so we only add potential issues if the attacker's balance is increased.
solver.get_model(constraints)
potential_issue = PotentialIssue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=instruction["address"]
- 1, # In post hook we use offset of previous instruction
swc_id=UNPROTECTED_ETHER_WITHDRAWAL,
title="Unprotected Ether Withdrawal",
severity="High",
bytecode=state.environment.code.bytecode,
description_head="Any sender can withdraw Ether from the contract account.",
description_tail="Arbitrary senders other than the contract creator can profitably extract Ether "
"from the contract account. Verify the business logic carefully and make sure that appropriate "
"security controls are in place to prevent unexpected loss of funds.",
detector=self,
constraints=constraints,
)
return [potential_issue]
except UnsatError:
return []
detector = EtherThief()
================================================
FILE: mythril/analysis/module/modules/exceptions.py
================================================
"""This module contains the detection code for reachable exceptions."""
import logging
from typing import List, Optional, cast
from mythril.analysis import solver
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import ASSERT_VIOLATION
from mythril.exceptions import UnsatError
from mythril.laser.ethereum import util
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import And
from mythril.support.support_utils import get_code_hash
log = logging.getLogger(__name__)
# The function signature of Panic(uint256)
PANIC_SIGNATURE = [78, 72, 123, 113]
class LastJumpAnnotation(StateAnnotation):
"""State Annotation used if an overflow is both possible and used in the annotated path"""
def __init__(self, last_jump: Optional[int] = None) -> None:
self.last_jump: int = last_jump
def __copy__(self):
new_annotation = LastJumpAnnotation(self.last_jump)
return new_annotation
class Exceptions(DetectionModule):
""""""
name = "Assertion violation"
swc_id = ASSERT_VIOLATION
description = "Checks whether any exception states are reachable."
entry_point = EntryPoint.CALLBACK
pre_hooks = ["INVALID", "JUMP", "REVERT"]
def __init__(self):
super().__init__()
self.auto_cache = False
def _execute(self, state: GlobalState) -> List[Issue]:
"""
:param state:
:return:
"""
issues = self._analyze_state(state)
for issue in issues:
self.cache.add((issue.source_location, issue.bytecode_hash))
return issues
def _analyze_state(self, state) -> List[Issue]:
"""
:param state:
:return:
"""
opcode = state.get_current_instruction()["opcode"]
address = state.get_current_instruction()["address"]
annotations = cast(
List[LastJumpAnnotation],
[a for a in state.get_annotations(LastJumpAnnotation)],
)
if len(annotations) == 0:
state.annotate(LastJumpAnnotation())
annotations = cast(
List[LastJumpAnnotation],
[a for a in state.get_annotations(LastJumpAnnotation)],
)
if opcode == "JUMP":
annotations[0].last_jump = address
return []
if opcode == "REVERT" and not is_assertion_failure(state):
return []
cache_address = annotations[0].last_jump
if (
cache_address,
get_code_hash(state.environment.code.bytecode),
) in self.cache:
return []
log.debug(
"ASSERT_FAIL/REVERT in function " + state.environment.active_function_name
)
try:
description_tail = (
"It is possible to trigger an assertion violation. Note that Solidity assert() statements should "
"only be used to check invariants. Review the transaction trace generated for this issue and "
"either make sure your program logic is correct, or use require() instead of assert() if your goal "
"is to constrain user inputs or enforce preconditions. Remember to validate inputs from both callers "
"(for instance, via passed arguments) and callees (for instance, via return values)."
)
transaction_sequence = solver.get_transaction_sequence(
state, state.world_state.constraints
)
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=address,
swc_id=ASSERT_VIOLATION,
title="Exception State",
severity="Medium",
description_head="An assertion violation was triggered.",
description_tail=description_tail,
bytecode=state.environment.code.bytecode,
transaction_sequence=transaction_sequence,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
source_location=cache_address,
)
state.annotate(
IssueAnnotation(
conditions=[And(*state.world_state.constraints)],
issue=issue,
detector=self,
)
)
return [issue]
except UnsatError:
log.debug("no model found")
return []
def is_assertion_failure(global_state):
state = global_state.mstate
offset, length = state.stack[-1], state.stack[-2]
try:
return_data = state.memory[
util.get_concrete_int(offset) : util.get_concrete_int(offset + length)
]
except TypeError:
return False
return return_data[:4] == PANIC_SIGNATURE and return_data[-1] == 1
detector = Exceptions()
================================================
FILE: mythril/analysis/module/modules/external_calls.py
================================================
"""This module contains the detection code for potentially insecure low-level
calls."""
import logging
from copy import copy
from mythril.analysis import solver
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.potential_issues import (
PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import REENTRANCY
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.natives import PRECOMPILE_COUNT
from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.transaction.symbolic import ACTORS
from mythril.laser.smt import UGT, BitVec, Or, symbol_factory
log = logging.getLogger(__name__)
DESCRIPTION = """
Search for external calls with unrestricted gas to a user-specified address.
"""
def _is_precompile_call(global_state: GlobalState):
to: BitVec = global_state.mstate.stack[-2]
constraints: Constraints = copy(global_state.world_state.constraints)
constraints += [
Or(
to < symbol_factory.BitVecVal(1, 256),
to > symbol_factory.BitVecVal(PRECOMPILE_COUNT, 256),
)
]
try:
solver.get_model(constraints)
return False
except UnsatError:
return True
class ExternalCalls(DetectionModule):
"""This module searches for low level calls (e.g. call.value()) that
forward all gas to the callee."""
name = "External call to another contract"
swc_id = REENTRANCY
description = DESCRIPTION
entry_point = EntryPoint.CALLBACK
pre_hooks = ["CALL"]
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:
"""
if state.environment.active_function_name == "constructor":
return []
gas = state.mstate.stack[-1]
to = state.mstate.stack[-2]
address = state.get_current_instruction()["address"]
try:
constraints = Constraints(
[UGT(gas, symbol_factory.BitVecVal(2300, 256)), to == ACTORS.attacker]
)
solver.get_transaction_sequence(
state, constraints + state.world_state.constraints
)
description_head = "A call to a user-supplied address is executed."
description_tail = (
"An external message call to an address specified by the caller is executed. Note that "
"the callee account might contain arbitrary code and could re-enter any function "
"within this contract. Reentering the contract in an intermediate state may lead to "
"unexpected behaviour. Make sure that no state modifications "
"are executed after this call and/or reentrancy guards are in place."
)
issue = PotentialIssue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=address,
swc_id=REENTRANCY,
title="External Call To User-Supplied Address",
bytecode=state.environment.code.bytecode,
severity="Low",
description_head=description_head,
description_tail=description_tail,
constraints=constraints,
detector=self,
)
except UnsatError:
log.debug("[EXTERNAL_CALLS] No model found.")
return []
return [issue]
detector = ExternalCalls()
================================================
FILE: mythril/analysis/module/modules/integer.py
================================================
"""This module contains the detection code for integer overflows and
underflows."""
import logging
from copy import copy
from math import ceil, log2
from typing import List, Set, cast
from mythril.analysis import solver
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import INTEGER_OVERFLOW_AND_UNDERFLOW
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.smt import (
And,
BitVec,
BVAddNoOverflow,
BVMulNoOverflow,
BVSubNoUnderflow,
Expression,
If,
Not,
symbol_factory,
)
from mythril.laser.smt import (
SMTBool as Bool,
)
log = logging.getLogger(__name__)
class OverUnderflowAnnotation:
"""Symbol Annotation used if a BitVector can overflow"""
def __init__(
self, overflowing_state: GlobalState, operator: str, constraint: Bool
) -> None:
self.overflowing_state = overflowing_state
self.operator = operator
self.constraint = constraint
def __deepcopy__(self, memodict={}):
new_annotation = copy(self)
return new_annotation
class OverUnderflowStateAnnotation(StateAnnotation):
"""State Annotation used if an overflow is both possible and used in the annotated path"""
def __init__(self) -> None:
self.overflowing_state_annotations: Set[OverUnderflowAnnotation] = set()
def __copy__(self):
new_annotation = OverUnderflowStateAnnotation()
new_annotation.overflowing_state_annotations = copy(
self.overflowing_state_annotations
)
return new_annotation
class IntegerArithmetics(DetectionModule):
"""This module searches for integer over- and underflows."""
name = "Integer overflow or underflow"
swc_id = INTEGER_OVERFLOW_AND_UNDERFLOW
description = (
"For every SUB instruction, check if there's a possible state "
"where op1 > op0. For every ADD, MUL instruction, check if "
"there's a possible state where op1 + op0 > 2^32 - 1"
)
entry_point = EntryPoint.CALLBACK
pre_hooks = [
"ADD",
"MUL",
"EXP",
"SUB",
"SSTORE",
"JUMPI",
"STOP",
"RETURN",
"CALL",
]
def __init__(self) -> None:
"""
Cache satisfiability of overflow constraints
"""
super().__init__()
self._ostates_satisfiable: Set[GlobalState] = set()
self._ostates_unsatisfiable: Set[GlobalState] = set()
def reset_module(self):
"""
Resets the module
:return:
"""
super().reset_module()
self._ostates_satisfiable = set()
self._ostates_unsatisfiable = set()
def _execute(self, state: GlobalState) -> List[Issue]:
"""Executes analysis module for integer underflow and integer overflow.
:param state: Statespace to analyse
:return: Found issues
"""
opcode = state.get_current_instruction()["opcode"]
funcs = {
"ADD": [self._handle_add],
"SUB": [self._handle_sub],
"MUL": [self._handle_mul],
"SSTORE": [self._handle_sstore],
"JUMPI": [self._handle_jumpi],
"CALL": [self._handle_call],
"RETURN": [self._handle_return, self._handle_transaction_end],
"STOP": [self._handle_transaction_end],
"EXP": [self._handle_exp],
}
results = []
for func in funcs[opcode]:
result = func(state)
if result and len(result) > 0:
results += result
return results
def _get_args(self, state):
stack = state.mstate.stack
op0, op1 = (
self._make_bitvec_if_not(stack, -1),
self._make_bitvec_if_not(stack, -2),
)
return op0, op1
def _handle_add(self, state):
op0, op1 = self._get_args(state)
c = Not(BVAddNoOverflow(op0, op1, False))
annotation = OverUnderflowAnnotation(state, "addition", c)
op0.annotate(annotation)
def _handle_mul(self, state):
op0, op1 = self._get_args(state)
c = Not(BVMulNoOverflow(op0, op1, False))
annotation = OverUnderflowAnnotation(state, "multiplication", c)
op0.annotate(annotation)
def _handle_sub(self, state):
op0, op1 = self._get_args(state)
c = Not(BVSubNoUnderflow(op0, op1, False))
annotation = OverUnderflowAnnotation(state, "subtraction", c)
op0.annotate(annotation)
def _handle_exp(self, state):
op0, op1 = self._get_args(state)
if (op1.symbolic is False and op1.value == 0) or (
op0.symbolic is False and op0.value < 2
):
return
if op0.symbolic and op1.symbolic:
constraint = And(
op1 > symbol_factory.BitVecVal(256, 256),
op0 > symbol_factory.BitVecVal(1, 256),
)
elif op0.symbolic:
constraint = op0 >= symbol_factory.BitVecVal(
2 ** ceil(256 / op1.value), 256
)
else:
constraint = op1 >= symbol_factory.BitVecVal(
ceil(256 / log2(op0.value)), 256
)
annotation = OverUnderflowAnnotation(state, "exponentiation", constraint)
op0.annotate(annotation)
@staticmethod
def _make_bitvec_if_not(stack, index):
value = stack[index]
if isinstance(value, BitVec):
return value
if isinstance(value, Bool):
return If(value, 1, 0)
stack[index] = symbol_factory.BitVecVal(value, 256)
return stack[index]
@staticmethod
def _get_title(_type):
return "Integer {}".format(_type)
@staticmethod
def _handle_sstore(state: GlobalState) -> None:
stack = state.mstate.stack
value = stack[-2]
if not isinstance(value, Expression):
return
state_annotation = _get_overflowunderflow_state_annotation(state)
for annotation in value.annotations:
if isinstance(annotation, OverUnderflowAnnotation):
state_annotation.overflowing_state_annotations.add(annotation)
@staticmethod
def _handle_jumpi(state):
stack = state.mstate.stack
value = stack[-2]
state_annotation = _get_overflowunderflow_state_annotation(state)
for annotation in value.annotations:
if isinstance(annotation, OverUnderflowAnnotation):
state_annotation.overflowing_state_annotations.add(annotation)
@staticmethod
def _handle_call(state):
stack = state.mstate.stack
value = stack[-3]
state_annotation = _get_overflowunderflow_state_annotation(state)
for annotation in value.annotations:
if isinstance(annotation, OverUnderflowAnnotation):
state_annotation.overflowing_state_annotations.add(annotation)
@staticmethod
def _handle_return(state: GlobalState) -> None:
"""
Adds all the annotations into the state which correspond to the
locations in the memory returned by RETURN opcode.
:param state: The Global State
"""
stack = state.mstate.stack
offset, length = stack[-1], stack[-2]
state_annotation = _get_overflowunderflow_state_annotation(state)
for element in state.mstate.memory[offset : offset + length]:
if not isinstance(element, Expression):
continue
for annotation in element.annotations:
if isinstance(annotation, OverUnderflowAnnotation):
state_annotation.overflowing_state_annotations.add(annotation)
def _handle_transaction_end(self, state: GlobalState) -> List[Issue]:
state_annotation = _get_overflowunderflow_state_annotation(state)
issues = []
for annotation in state_annotation.overflowing_state_annotations:
ostate = annotation.overflowing_state
if ostate in self._ostates_unsatisfiable:
continue
if ostate not in self._ostates_satisfiable:
try:
constraints = ostate.world_state.constraints + [
annotation.constraint
]
solver.get_model(constraints)
self._ostates_satisfiable.add(ostate)
except:
self._ostates_unsatisfiable.add(ostate)
continue
log.debug(
"Checking overflow in {} at transaction end address {}, ostate address {}".format(
state.get_current_instruction()["opcode"],
state.get_current_instruction()["address"],
ostate.get_current_instruction()["address"],
)
)
try:
constraints = state.world_state.constraints + [annotation.constraint]
transaction_sequence = solver.get_transaction_sequence(
state, constraints
)
except UnsatError:
continue
description_head = "The arithmetic operator can {}.".format(
"underflow" if annotation.operator == "subtraction" else "overflow"
)
description_tail = "It is possible to cause an integer overflow or underflow in the arithmetic operation. "
"Prevent this by constraining inputs using the require() statement or use the OpenZeppelin "
"SafeMath library for integer arithmetic operations. "
"Refer to the transaction trace generated for this issue to reproduce the issue."
issue = Issue(
contract=ostate.environment.active_account.contract_name,
function_name=ostate.environment.active_function_name,
address=ostate.get_current_instruction()["address"],
swc_id=INTEGER_OVERFLOW_AND_UNDERFLOW,
bytecode=ostate.environment.code.bytecode,
title="Integer Arithmetic Bugs",
severity="High",
description_head=description_head,
description_tail=description_tail,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
transaction_sequence=transaction_sequence,
)
state.annotate(
IssueAnnotation(
issue=issue, detector=self, conditions=[And(*constraints)]
)
)
issues.append(issue)
return issues
detector = IntegerArithmetics()
def _get_address_from_state(state):
return state.get_current_instruction()["address"]
def _get_overflowunderflow_state_annotation(
state: GlobalState,
) -> OverUnderflowStateAnnotation:
state_annotations = cast(
List[OverUnderflowStateAnnotation],
list(state.get_annotations(OverUnderflowStateAnnotation)),
)
if len(state_annotations) == 0:
state_annotation = OverUnderflowStateAnnotation()
state.annotate(state_annotation)
return state_annotation
else:
return state_annotations[0]
================================================
FILE: mythril/analysis/module/modules/multiple_sends.py
================================================
"""This module contains the detection code to find multiple sends occurring in
a single transaction."""
import logging
from copy import copy
from typing import List, cast
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.report import Issue
from mythril.analysis.solver import UnsatError, get_transaction_sequence
from mythril.analysis.swc_data import MULTIPLE_SENDS
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import And
log = logging.getLogger(__name__)
class MultipleSendsAnnotation(StateAnnotation):
def __init__(self) -> None:
self.call_offsets: List[int] = []
def __copy__(self):
result = MultipleSendsAnnotation()
result.call_offsets = copy(self.call_offsets)
return result
class MultipleSends(DetectionModule):
"""This module checks for multiple sends in a single transaction."""
name = "Multiple external calls in the same transaction"
swc_id = MULTIPLE_SENDS
description = "Check for multiple sends in a single transaction"
entry_point = EntryPoint.CALLBACK
pre_hooks = ["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE", "RETURN", "STOP"]
def _execute(self, state: GlobalState) -> None:
return self._analyze_state(state)
def _analyze_state(self, state: GlobalState):
"""
:param state: the current state
:return: returns the issues for that corresponding state
"""
instruction = state.get_current_instruction()
annotations = cast(
List[MultipleSendsAnnotation],
list(state.get_annotations(MultipleSendsAnnotation)),
)
if len(annotations) == 0:
state.annotate(MultipleSendsAnnotation())
annotations = cast(
List[MultipleSendsAnnotation],
list(state.get_annotations(MultipleSendsAnnotation)),
)
call_offsets = annotations[0].call_offsets
if instruction["opcode"] in ["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"]:
call_offsets.append(state.get_current_instruction()["address"])
else: # RETURN or STOP
for offset in call_offsets[1:]:
try:
transaction_sequence = get_transaction_sequence(
state, state.world_state.constraints
)
except UnsatError:
continue
description_tail = (
"This call is executed following another call within the same transaction. It is possible "
"that the call never gets executed if a prior call fails permanently. This might be caused "
"intentionally by a malicious callee. If possible, refactor the code such that each transaction "
"only executes one external call or "
"make sure that all callees can be trusted (i.e. they’re part of your own codebase)."
)
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=offset,
swc_id=MULTIPLE_SENDS,
bytecode=state.environment.code.bytecode,
title="Multiple Calls in a Single Transaction",
severity="Low",
description_head="Multiple calls are executed in the same transaction.",
description_tail=description_tail,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
transaction_sequence=transaction_sequence,
)
state.annotate(
IssueAnnotation(
conditions=[And(*state.world_state.constraints)],
issue=issue,
detector=self,
)
)
return [issue]
return []
detector = MultipleSends()
================================================
FILE: mythril/analysis/module/modules/requirements_violation.py
================================================
"""This module contains the detection code for requirement violations in a call"""
import logging
from typing import List
from mythril.analysis import solver
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import REQUIREMENT_VIOLATION
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import And
log = logging.getLogger(__name__)
class RequirementsViolation(DetectionModule):
"""This module detects transaction order dependence"""
name = "Requirement Violation"
swc_id = REQUIREMENT_VIOLATION
description = "Checks whether any requirements violate in a call."
entrypoint = "callback"
pre_hooks = ["REVERT"]
plugin_default_enabled = True
def _execute(self, state: GlobalState) -> List[Issue]:
"""
:param state:
:return:
"""
issues = self._analyze_state(state)
for issue in issues:
self.cache.add((issue.source_location, issue.bytecode_hash))
return issues
def _analyze_state(self, state) -> List[Issue]:
"""
:param state:
:return:
"""
if len(state.transaction_stack) < 2:
return []
try:
address = state.get_current_instruction()["address"]
description_head = "A requirement was violated in a nested call and the call was reverted as a result."
description_tail = "Make sure valid inputs are provided to the nested call (for instance, via passed arguments)."
transaction_sequence = solver.get_transaction_sequence(
state, state.world_state.constraints
)
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=address,
swc_id=REQUIREMENT_VIOLATION,
title="requirement violation",
severity="Medium",
description_head=description_head,
description_tail=description_tail,
bytecode=state.environment.code.bytecode,
transaction_sequence=transaction_sequence,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
state.annotate(
IssueAnnotation(
conditions=[And(*state.world_state.constraints)],
issue=issue,
detector=self,
)
)
return [issue]
except UnsatError:
log.debug("no model found")
return []
detector = RequirementsViolation()
================================================
FILE: mythril/analysis/module/modules/state_change_external_calls.py
================================================
import logging
from copy import copy
from typing import List, Optional, cast
from mythril.analysis import solver
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.potential_issues import (
PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import REENTRANCY
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import UGT, BitVec, Or, symbol_factory
log = logging.getLogger(__name__)
DESCRIPTION = """
Check whether the account state is accesses after the execution of an external call
"""
CALL_LIST = ["CALL", "DELEGATECALL", "CALLCODE"]
STATE_READ_WRITE_LIST = ["SSTORE", "SLOAD", "CREATE", "CREATE2"]
class StateChangeCallsAnnotation(StateAnnotation):
def __init__(self, call_state: GlobalState, user_defined_address: bool) -> None:
self.call_state = call_state
self.state_change_states: List[GlobalState] = []
self.user_defined_address = user_defined_address
def __copy__(self):
new_annotation = StateChangeCallsAnnotation(
self.call_state, self.user_defined_address
)
new_annotation.state_change_states = self.state_change_states[:]
return new_annotation
def get_issue(
self, global_state: GlobalState, detector: DetectionModule
) -> Optional[PotentialIssue]:
if not self.state_change_states:
return None
constraints = Constraints()
gas = self.call_state.mstate.stack[-1]
to = self.call_state.mstate.stack[-2]
constraints += [
UGT(gas, symbol_factory.BitVecVal(2300, 256)),
Or(
to > symbol_factory.BitVecVal(16, 256),
to == symbol_factory.BitVecVal(0, 256),
),
]
if self.user_defined_address:
constraints += [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF]
try:
solver.get_transaction_sequence(
global_state, constraints + global_state.world_state.constraints
)
except UnsatError:
return None
severity = "Medium" if self.user_defined_address else "Low"
address = global_state.get_current_instruction()["address"]
logging.debug(
"[EXTERNAL_CALLS] Detected state changes at addresses: {}".format(address)
)
read_or_write = "Write to"
if global_state.get_current_instruction()["opcode"] == "SLOAD":
read_or_write = "Read of"
address_type = "user defined" if self.user_defined_address else "fixed"
description_head = "{} persistent state following external call".format(
read_or_write
)
description_tail = (
"The contract account state is accessed after an external call to a {} address. "
"To prevent reentrancy issues, consider accessing the state only before the call, especially if the callee is untrusted. "
"Alternatively, a reentrancy lock can be used to prevent "
"untrusted callees from re-entering the contract in an intermediate state.".format(
address_type
)
)
return PotentialIssue(
contract=global_state.environment.active_account.contract_name,
function_name=global_state.environment.active_function_name,
address=address,
title="State access after external call",
severity=severity,
description_head=description_head,
description_tail=description_tail,
swc_id=REENTRANCY,
bytecode=global_state.environment.code.bytecode,
constraints=constraints,
detector=detector,
)
class StateChangeAfterCall(DetectionModule):
"""This module searches for state change after low level calls (e.g. call.value()) that
forward gas to the callee."""
name = "State change after an external call"
swc_id = REENTRANCY
description = DESCRIPTION
entry_point = EntryPoint.CALLBACK
pre_hooks = CALL_LIST + STATE_READ_WRITE_LIST
def _execute(self, state: GlobalState) -> None:
issues = self._analyze_state(state)
annotation = get_potential_issues_annotation(state)
annotation.potential_issues.extend(issues)
@staticmethod
def _add_external_call(global_state: GlobalState) -> None:
gas = global_state.mstate.stack[-1]
to = global_state.mstate.stack[-2]
try:
constraints = copy(global_state.world_state.constraints)
solver.get_model(
constraints
+ [
UGT(gas, symbol_factory.BitVecVal(2300, 256)),
Or(
to > symbol_factory.BitVecVal(16, 256),
to == symbol_factory.BitVecVal(0, 256),
),
]
)
# Check whether we can also set the callee address
try:
constraints += [to == 0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF]
solver.get_model(constraints)
global_state.annotate(StateChangeCallsAnnotation(global_state, True))
except UnsatError:
global_state.annotate(StateChangeCallsAnnotation(global_state, False))
except UnsatError:
pass
def _analyze_state(self, global_state: GlobalState) -> List[PotentialIssue]:
if global_state.environment.active_function_name == "constructor":
return []
annotations = cast(
List[StateChangeCallsAnnotation],
list(global_state.get_annotations(StateChangeCallsAnnotation)),
)
op_code = global_state.get_current_instruction()["opcode"]
if len(annotations) == 0 and op_code in STATE_READ_WRITE_LIST:
return []
if op_code in STATE_READ_WRITE_LIST:
for annotation in annotations:
annotation.state_change_states.append(global_state)
# Record state changes following from a transfer of ether
if op_code in CALL_LIST:
value: BitVec = global_state.mstate.stack[-3]
if StateChangeAfterCall._balance_change(value, global_state):
for annotation in annotations:
annotation.state_change_states.append(global_state)
# Record external calls
if op_code in CALL_LIST:
StateChangeAfterCall._add_external_call(global_state)
# Check for vulnerabilities
vulnerabilities = []
for annotation in annotations:
if not annotation.state_change_states:
continue
issue = annotation.get_issue(global_state, self)
if issue:
vulnerabilities.append(issue)
return vulnerabilities
@staticmethod
def _balance_change(value: BitVec, global_state: GlobalState) -> bool:
if not value.symbolic:
assert value.value is not None
return value.value > 0
else:
constraints = copy(global_state.world_state.constraints)
try:
solver.get_model(
constraints + [value > symbol_factory.BitVecVal(0, 256)]
)
return True
except UnsatError:
return False
detector = StateChangeAfterCall()
================================================
FILE: mythril/analysis/module/modules/suicide.py
================================================
import logging
from mythril.analysis import solver
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import UNPROTECTED_SELFDESTRUCT
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.transaction.symbolic import ACTORS
from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction,
)
from mythril.laser.smt.bool import And
log = logging.getLogger(__name__)
DESCRIPTION = """
Check if the contact can be 'accidentally' killed by anyone.
For kill-able contracts, also check whether it is possible to direct the contract balance to the attacker.
"""
class AccidentallyKillable(DetectionModule):
"""This module checks if the contact can be 'accidentally' killed by
anyone."""
name = "Contract can be accidentally killed by anyone"
swc_id = UNPROTECTED_SELFDESTRUCT
description = DESCRIPTION
entry_point = EntryPoint.CALLBACK
pre_hooks = ["SELFDESTRUCT"]
def __init__(self):
super().__init__()
self._cache_address = {}
def reset_module(self):
"""
Resets the module
:return:
"""
super().reset_module()
def _execute(self, state: GlobalState) -> None:
"""
:param state:
:return:
"""
return self._analyze_state(state)
def _analyze_state(self, state):
log.info("Suicide module: Analyzing suicide instruction")
instruction = state.get_current_instruction()
to = state.mstate.stack[-1]
log.debug("SELFDESTRUCT in function %s", state.environment.active_function_name)
description_head = "Any sender can cause the contract to self-destruct."
attacker_constraints = []
for tx in state.world_state.transaction_sequence:
if not isinstance(tx, ContractCreationTransaction):
attacker_constraints.append(
And(tx.caller == ACTORS.attacker, tx.caller == tx.origin)
)
try:
try:
constraints = (
state.world_state.constraints
+ [to == ACTORS.attacker]
+ attacker_constraints
)
transaction_sequence = solver.get_transaction_sequence(
state, constraints
)
description_tail = (
"Any sender can trigger execution of the SELFDESTRUCT instruction to destroy this "
"contract account and withdraw its balance to an arbitrary address. Review the transaction trace "
"generated for this issue and make sure that appropriate security controls are in place to prevent "
"unrestricted access."
)
except UnsatError:
constraints = state.world_state.constraints + attacker_constraints
transaction_sequence = solver.get_transaction_sequence(
state, constraints
)
description_tail = (
"Any sender can trigger execution of the SELFDESTRUCT instruction to destroy this "
"contract account. Review the transaction trace generated for this issue and make sure that "
"appropriate security controls are in place to prevent unrestricted access."
)
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=instruction["address"],
swc_id=UNPROTECTED_SELFDESTRUCT,
bytecode=state.environment.code.bytecode,
title="Unprotected Selfdestruct",
severity="High",
description_head=description_head,
description_tail=description_tail,
transaction_sequence=transaction_sequence,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
state.annotate(
IssueAnnotation(
conditions=[And(*constraints)], issue=issue, detector=self
)
)
return [issue]
except UnsatError:
log.debug("No model found")
return []
detector = AccidentallyKillable()
================================================
FILE: mythril/analysis/module/modules/transaction_order_dependence.py
================================================
"""This module contains the detection code for transaction order dependence."""
import logging
from mythril.analysis import solver
from mythril.analysis.module.base import DetectionModule
from mythril.analysis.potential_issues import (
PotentialIssue,
get_potential_issues_annotation,
)
from mythril.analysis.swc_data import TX_ORDER_DEPENDENCE
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.transaction.symbolic import ACTORS
from mythril.laser.smt import Or
from mythril.laser.smt import SMTBool as Bool
log = logging.getLogger(__name__)
DESCRIPTION = """
Search for calls whose value depends on balance or storage.
"""
class BalanceAnnotation:
def __init__(self, caller):
self.caller = caller
class StorageAnnotation:
def __init__(self, caller):
self.caller = caller
class TransactionOrderDependence(DetectionModule):
"""This module detects transaction order dependence."""
name = "Transaction Order Dependence"
swc_id = TX_ORDER_DEPENDENCE
description = DESCRIPTION
entrypoint = "callback"
pre_hooks = ["CALL"]
post_hooks = ["BALANCE", "SLOAD"]
plugin_default_enabled = True
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)
@staticmethod
def annotate_balance_storage_vals(state, opcode):
val = state.mstate.stack[-1]
if opcode == "BALANCE":
annotation = BalanceAnnotation
else:
annotation = StorageAnnotation
if len(val.get_annotations(annotation)) == 0:
state.mstate.stack[-1].annotate(annotation(state.environment.sender))
return []
def _analyze_state(self, state: GlobalState):
"""
:param state:
:return:
"""
opcode = state.get_current_instruction()["opcode"]
if opcode != "CALL":
opcode = state.environment.code.instruction_list[state.mstate.pc - 1][
"opcode"
]
if opcode in ("BALANCE", "SLOAD"):
self.annotate_balance_storage_vals(state, opcode)
return []
value = state.mstate.stack[-3]
if (
len(value.get_annotations(StorageAnnotation))
+ len(value.get_annotations(BalanceAnnotation))
== 0
):
return []
callers = []
storage_annotations = value.get_annotations(StorageAnnotation)
if len(storage_annotations) == 1:
callers.append(storage_annotations[0].caller)
balance_annotations = value.get_annotations(BalanceAnnotation)
if len(balance_annotations) == 1:
callers.append(balance_annotations[0].caller)
address = state.get_current_instruction()["address"]
call_constraint = Bool(False)
for caller in callers:
call_constraint = Or(call_constraint, ACTORS.attacker == caller)
try:
constraints = state.world_state.constraints + [call_constraint]
solver.get_transaction_sequence(state, constraints)
description_head = (
"The value of the call is dependent on balance or storage write"
)
description_tail = (
"This can lead to race conditions. An attacker may be able to run a transaction after our transaction "
"which can change the value of the call"
)
issue = PotentialIssue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=address,
swc_id=TX_ORDER_DEPENDENCE,
title="Transaction Order Dependence",
bytecode=state.environment.code.bytecode,
severity="Medium",
description_head=description_head,
description_tail=description_tail,
constraints=constraints,
detector=self,
)
except UnsatError:
log.debug("[Transaction Order Dependence] No model found.")
return []
return [issue]
detector = TransactionOrderDependence()
================================================
FILE: mythril/analysis/module/modules/unchecked_retval.py
================================================
"""This module contains detection code to find occurrences of calls whose
return value remains unchecked."""
import logging
from copy import copy
from typing import List, cast
from typing_extensions import TypedDict
from mythril.analysis import solver
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.report import Issue
from mythril.analysis.swc_data import UNCHECKED_RET_VAL
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.smt import And
from mythril.laser.smt.bitvec import BitVec
log = logging.getLogger(__name__)
class RetVal(TypedDict):
address: int
retval: BitVec
class UncheckedRetvalAnnotation(StateAnnotation):
def __init__(self) -> None:
self.retvals: List[RetVal] = []
def __copy__(self):
result = UncheckedRetvalAnnotation()
result.retvals = copy(self.retvals)
return result
class UncheckedRetval(DetectionModule):
"""A detection module to test whether CALL return value is checked."""
name = "Return value of an external call is not checked"
swc_id = UNCHECKED_RET_VAL
description = (
"Test whether CALL return value is checked. "
"For direct calls, the Solidity compiler auto-generates this check. E.g.:\n"
" Alice c = Alice(address);\n"
" c.ping(42);\n"
"Here the CALL will be followed by IZSERO(retval), if retval = ZERO then state is reverted. "
"For low-level-calls this check is omitted. E.g.:\n"
' c.call.value(0)(bytes4(sha3("ping(uint256)")),1);'
)
entry_point = EntryPoint.CALLBACK
pre_hooks = ["STOP", "RETURN"]
post_hooks = ["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"]
def _execute(self, state: GlobalState) -> List[Issue]:
"""
:param state:
:return:
"""
return self._analyze_state(state)
def _analyze_state(self, state: GlobalState) -> list:
instruction = state.get_current_instruction()
annotations = cast(
List[UncheckedRetvalAnnotation],
[a for a in state.get_annotations(UncheckedRetvalAnnotation)],
)
if len(annotations) == 0:
state.annotate(UncheckedRetvalAnnotation())
annotations = cast(
List[UncheckedRetvalAnnotation],
[a for a in state.get_annotations(UncheckedRetvalAnnotation)],
)
retvals = annotations[0].retvals
if instruction["opcode"] in ("STOP", "RETURN"):
issues = []
for retval in retvals:
try:
"""
To check whether retval is unconstrained we are checking it against retval = 0 and retval = 1
"""
solver.get_transaction_sequence(
state, state.world_state.constraints + [retval["retval"] == 1]
)
transaction_sequence = solver.get_transaction_sequence(
state, state.world_state.constraints + [retval["retval"] == 0]
)
except UnsatError:
continue
description_tail = (
"External calls return a boolean value. If the callee halts with an exception, 'false' is "
"returned and execution continues in the caller. "
"The caller should check whether an exception happened and react accordingly to avoid unexpected behavior. "
"For example it is often desirable to wrap external calls in require() so the transaction is reverted if the call fails."
)
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=retval["address"],
bytecode=state.environment.code.bytecode,
title="Unchecked return value from external call.",
swc_id=UNCHECKED_RET_VAL,
severity="Medium",
description_head="The return value of a message call is not checked.",
description_tail=description_tail,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
transaction_sequence=transaction_sequence,
)
conditions = [
And(*(state.world_state.constraints + [retval["retval"] == 1])),
And(*(state.world_state.constraints + [retval["retval"] == 0])),
]
state.annotate(
IssueAnnotation(conditions=conditions, issue=issue, detector=self)
)
issues.append(issue)
return issues
else:
log.debug("End of call, extracting retval")
if state.environment.code.instruction_list[state.mstate.pc - 1][
"opcode"
] not in ["CALL", "DELEGATECALL", "STATICCALL", "CALLCODE"]:
# Return is pointless with OOG. The pc does not get updated in such cases
return []
return_value = state.mstate.stack[-1]
retvals.append(
{"address": state.instruction["address"] - 1, "retval": return_value}
)
return []
detector = UncheckedRetval()
================================================
FILE: mythril/analysis/module/modules/unexpected_ether.py
================================================
"""This module contains the detection code for unexpected ether balance."""
import logging
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule
from mythril.analysis.module.module_helpers import is_prehook
from mythril.analysis.report import Issue
from mythril.analysis.solver import UnsatError, get_transaction_sequence
from mythril.analysis.swc_data import UNEXPECTED_ETHER_BALANCE
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import And, BitVec
log = logging.getLogger(__name__)
DESCRIPTION = """
Check for strict equality checks with contract balance
"""
class EtherBalanceCheckAnnotation(StateAnnotation):
"""State Annotation used if an overflow is both possible and used in the annotated path"""
def __init__(self, balance: BitVec) -> None:
self.balance = balance
class BalanceConditionAnnotation:
""""""
def __init__(self, address=None) -> None:
self.address = address
class UnexpectedEther(DetectionModule):
"""This module checks for strict equality checks with contract balance."""
author = "MythX Team"
plugin_license = "All rights reserved - ConsenSys"
plugin_type = "Detection Module"
plugin_description = DESCRIPTION
plugin_default_enabled = True
name = "Unexpected Ether Balance"
swc_id = "132"
description = DESCRIPTION
pre_hooks = ["INVALID", "EQ", "RETURN", "STOP"]
post_hooks = ["BALANCE"]
def _execute(self, state: GlobalState) -> None:
"""
:param state:
:return:
"""
return self._analyze_state(state)
def _analyze_state(self, state):
"""
:param state:
:return:
"""
node = state.node
instruction = state.get_current_instruction()
if is_prehook() is False:
balance = state.mstate.stack[-1]
annotations = state.get_annotations(EtherBalanceCheckAnnotation)
for annotation in annotations:
if annotation.balance == balance:
return []
state.annotate(EtherBalanceCheckAnnotation(balance))
return []
if instruction["opcode"] == "EQ":
op1, op2 = state.mstate.stack[-2:]
annotations = list(state.get_annotations(EtherBalanceCheckAnnotation))
op = None
for annotation in annotations:
if hash(annotation.balance) != hash(op1) and hash(
annotation.balance
) != hash(op2):
continue
if hash(annotation.balance) == hash(op1):
op = op1
else:
op = op2
break
if op is None:
return []
op.annotate(
BalanceConditionAnnotation(
address=state.get_current_instruction()["address"]
)
)
log.debug(
"Equality check for contract balance in function " + node.function_name
)
return []
for constraint in state.world_state.constraints:
for annotation in constraint.get_annotations(BalanceConditionAnnotation):
if annotation.address in self.cache:
continue
try:
transaction_sequence = get_transaction_sequence(
state, state.world_state.constraints
)
except UnsatError:
continue
log.debug("Detected a strict equality check")
title = "Strict Ether balance check"
description_head = "Use of strict ether balance checking"
description_tail = "Ether can be forcefully sent to this contract, This may make the contract unusable."
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=annotation.address,
title=title,
bytecode=state.environment.code.bytecode,
swc_id=UNEXPECTED_ETHER_BALANCE,
severity="Low",
description_head=description_head,
description_tail=description_tail,
transaction_sequence=transaction_sequence,
)
state.annotate(
IssueAnnotation(
conditions=[And(*state.world_state.constraints)],
issue=issue,
detector=self,
)
)
return [issue]
return []
detector = UnexpectedEther()
================================================
FILE: mythril/analysis/module/modules/user_assertions.py
================================================
"""This module contains the detection code for potentially insecure low-level
calls."""
import logging
import eth_abi
from mythril.analysis import solver
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.potential_issues import Issue
from mythril.analysis.swc_data import ASSERT_VIOLATION
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import And, Extract
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
)
mstore_pattern = "0xcafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe"
class UserAssertions(DetectionModule):
"""This module searches for user supplied exceptions: emit AssertionFailed("Error")."""
name = "A user-defined assertion has been triggered"
swc_id = ASSERT_VIOLATION
description = DESCRIPTION
entry_point = EntryPoint.CALLBACK
pre_hooks = ["LOG1", "MSTORE"]
def _execute(self, state: GlobalState) -> None:
"""
:param state:
:return:
"""
return self._analyze_state(state)
def _analyze_state(self, state: GlobalState):
"""
:param state:
:return:
"""
opcode = state.get_current_instruction()["opcode"]
message = None
if opcode == "MSTORE":
value = state.mstate.stack[-2]
if value.symbolic:
return []
if mstore_pattern not in hex(value.value)[:126]:
return []
message = "Failed property id {}".format(Extract(15, 0, value).value)
else:
topic, size, mem_start = state.mstate.stack[-3:]
if topic.symbolic or topic.value != assertion_failed_hash:
return []
if not mem_start.symbolic and not size.symbolic:
try:
message = eth_abi.decode_single(
"string",
bytes(
state.mstate.memory[
mem_start.value + 32 : mem_start.value + size.value
]
),
)
except:
pass
try:
transaction_sequence = solver.get_transaction_sequence(
state, state.world_state.constraints
)
if message:
description_tail = (
"A user-provided assertion failed with the message '{}'".format(
message
)
)
else:
description_tail = "A user-provided assertion failed."
log.debug("MythX assertion emitted: {}".format(description_tail))
address = state.get_current_instruction()["address"]
issue = Issue(
contract=state.environment.active_account.contract_name,
function_name=state.environment.active_function_name,
address=address,
swc_id=ASSERT_VIOLATION,
title="Exception State",
severity="Medium",
description_head="A user-provided assertion failed.",
description_tail=description_tail,
bytecode=state.environment.code.bytecode,
transaction_sequence=transaction_sequence,
gas_used=(state.mstate.min_gas_used, state.mstate.max_gas_used),
)
state.annotate(
IssueAnnotation(
detector=self,
issue=issue,
conditions=[And(*state.world_state.constraints)],
)
)
return [issue]
except UnsatError:
log.debug("no model found")
return []
detector = UserAssertions()
================================================
FILE: mythril/analysis/module/util.py
================================================
import logging
from collections import defaultdict
from typing import Callable, Dict, List, Mapping, Optional
from mythril.analysis.module.base import DetectionModule, EntryPoint
from mythril.analysis.module.loader import ModuleLoader
from mythril.support.opcodes import OPCODES
log = logging.getLogger(__name__)
OP_CODE_LIST = OPCODES.keys()
def get_detection_module_hooks(
modules: List[DetectionModule], hook_type="pre"
) -> Dict[str, List[Callable]]:
"""Gets a dictionary with the hooks for the passed detection modules
:param modules: The modules for which to retrieve hooks
:param hook_type: The type of hooks to retrieve (default: "pre")
:return: Dictionary with discovered hooks
"""
hook_dict: Mapping[str, List[Callable]] = defaultdict(list)
for module in modules:
hooks = module.pre_hooks if hook_type == "pre" else module.post_hooks
for op_code in map(lambda x: x.upper(), hooks):
# A hook can be either OP_CODE or START*
# When an entry like the second is encountered we hook all opcodes that start with START
if op_code in OP_CODE_LIST:
hook_dict[op_code].append(module.execute)
elif op_code.endswith("*"):
to_register = filter(lambda x: x.startswith(op_code[:-1]), OP_CODE_LIST)
for actual_hook in to_register:
hook_dict[actual_hook].append(module.execute)
else:
log.error(
"Encountered invalid hook opcode %s in module %s",
op_code,
module.name,
)
return dict(hook_dict)
def reset_callback_modules(module_names: Optional[List[str]] = None):
"""Clean the issue records of every callback-based module."""
modules = ModuleLoader().get_detection_modules(EntryPoint.CALLBACK, module_names)
for module in modules:
module.reset_module()
================================================
FILE: mythril/analysis/ops.py
================================================
"""This module contains various helper methods for dealing with EVM
operations."""
from enum import Enum
from mythril.laser.ethereum import util
from mythril.laser.smt import simplify
class VarType(Enum):
"""An enum denoting whether a value is symbolic or concrete."""
SYMBOLIC = 1
CONCRETE = 2
class Variable:
"""The representation of a variable with value and type."""
def __init__(self, val, _type):
"""
:param val:
:param _type:
"""
self.val = val
self.type = _type
def __str__(self):
"""
:return:
"""
return str(self.val)
def get_variable(i):
"""
:param i:
:return:
"""
try:
return Variable(util.get_concrete_int(i), VarType.CONCRETE)
except TypeError:
return Variable(simplify(i), VarType.SYMBOLIC)
class Op:
"""The base type for operations referencing current node and state."""
def __init__(self, node, state, state_index):
"""
:param node:
:param state:
:param state_index:
"""
self.node = node
self.state = state
self.state_index = state_index
class Call(Op):
"""The representation of a CALL operation."""
def __init__(
self,
node,
state,
state_index,
_type,
to,
gas,
value=Variable(0, VarType.CONCRETE),
data=None,
):
"""
:param node:
:param state:
:param state_index:
:param _type:
:param to:
:param gas:
:param value:
:param data:
"""
super().__init__(node, state, state_index)
self.to = to
self.gas = gas
self.type = _type
self.value = value
self.data = data
================================================
FILE: mythril/analysis/potential_issues.py
================================================
from mythril.analysis.issue_annotation import IssueAnnotation
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
from mythril.laser.smt import And
from mythril.support.support_args import args
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 = []
@property
def search_importance(self):
return 10 * len(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)
unsat_potential_issues = []
for potential_issue in annotation.potential_issues:
try:
transaction_sequence = get_transaction_sequence(
state, state.world_state.constraints + potential_issue.constraints
)
except UnsatError:
unsat_potential_issues.append(potential_issue)
continue
issue = 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,
)
state.annotate(
IssueAnnotation(
detector=potential_issue.detector,
issue=issue,
conditions=[
And(*(state.world_state.constraints + potential_issue.constraints))
],
)
)
if args.use_issue_annotations is False:
potential_issue.detector.issues.append(issue)
potential_issue.detector.update_cache([issue])
annotation.potential_issues = unsat_potential_issues
================================================
FILE: mythril/analysis/report.py
================================================
"""This module provides classes that make up an issue report."""
import json
import logging
import operator
import re
try:
from eth_abi import decode
except ImportError:
from eth_abi import decode_abi as decode
import hashlib
from time import time
from typing import Any, Dict, Iterable, List, Optional
from jinja2 import Environment, PackageLoader
from mythril.analysis.swc_data import SWC_TO_TITLE
from mythril.laser.execution_info import ExecutionInfo
from mythril.solidity.soliditycontract import SolidityContract
from mythril.support.signatures import SignatureDB
from mythril.support.source_support import Source
from mythril.support.start_time import StartTime
from mythril.support.support_utils import get_code_hash
log = logging.getLogger(__name__)
class Issue:
"""Representation of an issue and its location."""
def __init__(
self,
contract: str,
function_name: str,
address: int,
swc_id: str,
title: str,
bytecode: str,
gas_used=(None, None),
severity=None,
description_head="",
description_tail="",
transaction_sequence=None,
source_location=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 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 transaction_sequence: The transaction sequence
"""
self.title = title
self.contract = contract
self.function = function_name
self.address = address
self.description_head = description_head
self.description_tail = description_tail
self.description = "%s\n%s" % (description_head, description_tail)
self.severity = severity
self.swc_id = swc_id
self.min_gas_used, self.max_gas_used = gas_used
self.filename = None
self.code = None
self.lineno = None
self.source_mapping = None
self.discovery_time = time() - StartTime().global_start_time
self.bytecode_hash = get_code_hash(bytecode)
self.transaction_sequence = transaction_sequence
self.source_location = source_location
@property
def transaction_sequence_users(self):
"""Returns the transaction sequence without pre-generated block data"""
return self.transaction_sequence
@property
def transaction_sequence_jsonv2(self):
"""Returns the transaction sequence as a json string with pre-generated block data"""
return (
self.add_block_data(self.transaction_sequence)
if self.transaction_sequence
else None
)
@staticmethod
def add_block_data(transaction_sequence: Dict):
"""Adds sane block data to a transaction_sequence"""
for step in transaction_sequence["steps"]:
step["gasLimit"] = "0x7d000"
step["gasPrice"] = "0x773594000"
step["blockCoinbase"] = "0xcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcb"
step["blockDifficulty"] = "0xa7d7343662e26"
step["blockGasLimit"] = "0x7d0000"
step["blockNumber"] = "0x66e393"
step["blockTime"] = "0x5bfa4639"
return transaction_sequence
@property
def as_dict(self):
"""
:return:
"""
issue = {
"title": self.title,
"swc-id": self.swc_id,
"contract": self.contract,
"description": self.description,
"function": self.function,
"severity": self.severity,
"address": self.address,
"tx_sequence": self.transaction_sequence,
"min_gas_used": self.min_gas_used,
"max_gas_used": self.max_gas_used,
"sourceMap": self.source_mapping,
}
if self.filename and self.lineno:
issue["filename"] = self.filename
issue["lineno"] = self.lineno
if self.code:
issue["code"] = self.code
return issue
def _set_internal_compiler_error(self):
"""
Adds the false positive to description and changes severity to low
"""
self.severity = "Low"
self.description_tail += (
" This issue is reported for internal compiler generated code."
)
self.description = "%s\n%s" % (self.description_head, self.description_tail)
self.code = ""
def add_code_info(self, contract):
"""
:param contract:
"""
if self.address and isinstance(contract, SolidityContract):
is_constructor = False
if self.function == "constructor":
is_constructor = True
if self.source_location:
codeinfo = contract.get_source_info(
self.source_location, constructor=is_constructor
)
else:
codeinfo = contract.get_source_info(
self.address, constructor=is_constructor
)
if codeinfo is None:
self.source_mapping = self.address
self.filename = "Internal File"
return
self.filename = codeinfo.filename
self.code = codeinfo.code
self.lineno = codeinfo.lineno
if self.lineno is None:
self._set_internal_compiler_error()
self.source_mapping = codeinfo.solc_mapping
else:
self.source_mapping = self.address
@staticmethod
def decode_bytes(val):
if isinstance(val, bytes):
return val.decode()
elif isinstance(val, list) or isinstance(val, tuple):
return [Issue.decode_bytes(x) for x in val]
else:
return val
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)
# TODO: Check other mythx tools for dependency before supporting multiple possible function names
if len(sig) > 0:
step["name"] = sig[0]
step["resolved_input"] = Issue.resolve_input(
step["calldata"], sig[0]
)
if step["resolved_input"] is not None:
step["resolved_input"] = list(step["resolved_input"])
for i, val in enumerate(step["resolved_input"]):
step["resolved_input"][i] = Issue.decode_bytes(val)
step["resolved_input"] = tuple(step["resolved_input"])
else:
step["name"] = "unknown"
except ValueError:
step["name"] = "unknown"
@staticmethod
def resolve_input(data, function_name):
"""
Adds decoded calldate to the tx sequence.
"""
data = data[10:]
# Eliminates the first and last brackets
# Since signature such as func((bytes32,bytes32,uint8)[],(address[],uint32)) are valid
type_info = function_name[function_name.find("(") + 1 : -1]
type_info = re.split(r",\s*(?![^()]*\))", type_info)
if len(data) % 64 > 0:
data += "0" * (64 - len(data) % 64)
try:
decoded_output = decode(type_info, bytes.fromhex(data))
decoded_output = tuple(
convert_bytes(item) if isinstance(item, (bytes, Iterable)) else item
for item in decoded_output
)
return decoded_output
except Exception:
return None
def convert_bytes(item):
"""
Converts bytes to a serializable format. Handles nested iterables.
"""
if isinstance(item, bytes):
return item.hex()
elif isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
# Recursively apply convert_bytes to each item in the iterable
return type(item)(convert_bytes(subitem) for subitem in item)
else:
return item
class Report:
"""A report containing the content of multiple issues."""
environment = Environment(
loader=PackageLoader("mythril.analysis"), trim_blocks=True
)
def __init__(
self,
contracts=None,
exceptions=None,
execution_info: Optional[List[ExecutionInfo]] = None,
):
"""
:param contracts:
:param exceptions:
"""
self.issues: Dict[bytes, Issue] = {}
self.solc_version = ""
self.meta: Dict[str, Any] = {}
self.source = Source()
self.source.get_source_from_contracts_list(contracts)
self.exceptions = exceptions or []
self.execution_info = execution_info or []
def sorted_issues(self):
"""
:return:
"""
issue_list = [issue.as_dict for key, issue in self.issues.items()]
return sorted(issue_list, key=operator.itemgetter("address", "title"))
def append_issue(self, issue):
"""
:param issue:
"""
m = hashlib.md5()
m.update(
(issue.contract + issue.function + str(issue.address) + issue.title).encode(
"utf-8"
)
)
issue.resolve_function_names()
self.issues[m.digest()] = issue
def as_text(self):
"""
:return:
"""
name = self._file_name()
template = Report.environment.get_template("report_as_text.jinja2")
return template.render(filename=name, issues=self.sorted_issues())
def as_json(self):
"""
:return:
"""
result = {"success": True, "error": None, "issues": self.sorted_issues()}
return json.dumps(result, sort_keys=True)
def _get_exception_data(self) -> dict:
if not self.exceptions:
return {}
logs: List[Dict] = []
for exception in self.exceptions:
logs += [{"level": "error", "hidden": True, "msg": exception}]
return {"logs": logs}
def as_swc_standard_format(self):
"""Format defined for integration and correlation.
:return:
"""
# Setup issues
_issues = []
for _, issue in self.issues.items():
idx = self.source.get_source_index(issue.bytecode_hash)
try:
title = SWC_TO_TITLE[issue.swc_id]
except KeyError:
title = "Unspecified Security Issue"
extra = {"discoveryTime": int(issue.discovery_time * 10**9)}
if issue.transaction_sequence_jsonv2:
extra["testCases"] = [issue.transaction_sequence_jsonv2]
_issues.append(
{
"swcID": "SWC-" + issue.swc_id,
"swcTitle": title,
"description": {
"head": issue.description_head,
"tail": issue.description_tail,
},
"severity": issue.severity,
"locations": [{"sourceMap": "%d:1:%d" % (issue.address, idx)}],
"extra": extra,
}
)
# Setup meta
meta_data = self.meta
# Add logs to meta
meta_data.update(self._get_exception_data())
# Add execution info to meta
analysis_duration = int(
round((time() - StartTime().global_start_time) * (10**9))
)
meta_data["mythril_execution_info"] = {"analysis_duration": analysis_duration}
for execution_info in self.execution_info:
meta_data["mythril_execution_info"].update(execution_info.as_dict())
result = [
{
"issues": _issues,
"sourceType": self.source.source_type,
"sourceFormat": self.source.source_format,
"sourceList": self.source.source_list,
"meta": meta_data,
}
]
return json.dumps(result, sort_keys=True)
def as_markdown(self):
"""
:return:
"""
filename = self._file_name()
template = Report.environment.get_template("report_as_markdown.jinja2")
return template.render(filename=filename, issues=self.sorted_issues())
def _file_name(self):
"""
:return:
"""
if len(self.issues.values()) > 0:
return list(self.issues.values())[0].filename
================================================
FILE: mythril/analysis/security.py
================================================
"""This module contains functionality for hooking in detection modules and
executing them."""
import logging
from typing import List, Optional
from mythril.analysis.module import ModuleLoader, reset_callback_modules
from mythril.analysis.module.base import EntryPoint
from mythril.analysis.report import Issue
log = logging.getLogger(__name__)
def retrieve_callback_issues(white_list: Optional[List[str]] = None) -> List[Issue]:
"""Get the issues discovered by callback type detection modules"""
issues: List[Issue] = []
for module in ModuleLoader().get_detection_modules(
entry_point=EntryPoint.CALLBACK, white_list=white_list
):
log.debug("Retrieving results for " + module.name)
issues += module.issues
reset_callback_modules(module_names=white_list)
return issues
def fire_lasers(statespace, white_list: Optional[List[str]] = None) -> List[Issue]:
"""Fire lasers at analysed statespace object
:param statespace: Symbolic statespace to analyze
:param white_list: Optionally whitelist modules to use for the analysis
:return: Discovered issues
"""
log.info("Starting analysis")
issues: List[Issue] = []
for module in ModuleLoader().get_detection_modules(
entry_point=EntryPoint.POST, white_list=white_list
):
log.info("Executing " + module.name)
issues += module.execute(statespace)
issues += retrieve_callback_issues(white_list)
return issues
================================================
FILE: mythril/analysis/solver.py
================================================
"""This module contains analysis module helpers to solve path constraints."""
import logging
from typing import Any, Dict, List, Tuple, Union
import z3
from z3 import FuncInterp
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.function_managers import (
keccak_function_manager,
)
from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.transaction import BaseTransaction
from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction,
)
from mythril.laser.smt import UGE, symbol_factory
from mythril.support.model import get_model
log = logging.getLogger(__name__)
z3.set_option(
max_args=10000000, max_lines=1000000, max_depth=10000000, max_visited=1000000
)
def pretty_print_model(model):
"""Pretty prints a z3 model
:param model:
:return:
"""
ret = ""
for d in model.decls():
if isinstance(model[d], FuncInterp):
condition = model[d].as_list()
ret += "%s: %s\n" % (d.name(), condition)
continue
try:
condition = "0x%x" % model[d].as_long()
except:
condition = str(z3.simplify(model[d]))
ret += "%s: %s\n" % (d.name(), condition)
return ret
def get_transaction_sequence(
global_state: GlobalState, constraints: Constraints
) -> Dict[str, Any]:
"""Generate concrete transaction sequence.
Note: This function only considers the constraints in constraint argument,
which in some cases is expected to differ from global_state's constraints
:param global_state: GlobalState to generate transaction sequence for
:param constraints: list of constraints used to generate transaction sequence
"""
transaction_sequence = global_state.world_state.transaction_sequence
concrete_transactions = []
tx_constraints, minimize = _set_minimisation_constraints(
transaction_sequence, constraints.copy(), [], 5000, global_state.world_state
)
try:
model = get_model(tx_constraints, minimize=minimize)
except UnsatError:
raise UnsatError
if isinstance(transaction_sequence[0], ContractCreationTransaction):
initial_world_state = transaction_sequence[0].prev_world_state
else:
initial_world_state = transaction_sequence[0].world_state
initial_accounts = initial_world_state.accounts
for transaction in transaction_sequence:
concrete_transaction = _get_concrete_transaction(model, transaction)
concrete_transactions.append(concrete_transaction)
min_price_dict: Dict[str, int] = {}
for address in initial_accounts.keys():
min_price_dict[address] = model.eval(
initial_world_state.starting_balances[
symbol_factory.BitVecVal(address, 256)
].raw,
model_completion=True,
).as_long()
concrete_initial_state = _get_concrete_state(initial_accounts, min_price_dict)
if isinstance(transaction_sequence[0], ContractCreationTransaction):
code = transaction_sequence[0].code
_replace_with_actual_sha(concrete_transactions, model, code)
else:
_replace_with_actual_sha(concrete_transactions, model)
_add_calldata_placeholder(concrete_transactions, transaction_sequence)
steps = {"initialState": concrete_initial_state, "steps": concrete_transactions}
return steps
def _add_calldata_placeholder(
concrete_transactions: List[Dict[str, str]],
transaction_sequence: List[BaseTransaction],
):
"""
Adds a calldata placeholder into the concrete transactions
:param concrete_transactions:
:param transaction_sequence:
:return:
"""
for tx in concrete_transactions:
tx["calldata"] = tx["input"]
if not isinstance(transaction_sequence[0], ContractCreationTransaction):
return
if isinstance(transaction_sequence[0].code.bytecode, tuple):
code_len = len(transaction_sequence[0].code.bytecode) * 2
else:
code_len = len(transaction_sequence[0].code.bytecode)
concrete_transactions[0]["calldata"] = concrete_transactions[0]["input"][
code_len + 2 :
]
def _replace_with_actual_sha(
concrete_transactions: List[Dict[str, str]], model: z3.Model, code=None
):
concrete_hashes = keccak_function_manager.get_concrete_hash_data(model)
for tx in concrete_transactions:
if keccak_function_manager.hash_matcher not in tx["input"]:
continue
if code is not None and code.bytecode in tx["input"]:
s_index = len(code.bytecode) + 2
else:
s_index = 10
for i in range(s_index, len(tx["input"])):
data_slice = tx["input"][i : i + 64]
if (
keccak_function_manager.hash_matcher not in data_slice
or len(data_slice) != 64
):
continue
find_input = symbol_factory.BitVecVal(int(data_slice, 16), 256)
input_ = None
for size in concrete_hashes:
_, inverse = keccak_function_manager.store_function[size]
if find_input.value not in concrete_hashes[size]:
continue
input_ = symbol_factory.BitVecVal(
model.eval(inverse(find_input).raw).as_long(), size
)
if input_ is None:
continue
keccak = keccak_function_manager.find_concrete_keccak(input_)
hex_keccak = hex(keccak.value)[2:]
if len(hex_keccak) != 64:
hex_keccak = "0" * (64 - len(hex_keccak)) + hex_keccak
tx["input"] = tx["input"][:s_index] + tx["input"][s_index:].replace(
tx["input"][i : 64 + i], hex_keccak
)
def _get_concrete_state(
initial_accounts: Dict, min_price_dict: Dict[str, int]
) -> Dict[str, Dict]:
"""Gets a concrete state"""
accounts = {}
for address, account in initial_accounts.items():
# Skip empty default account
data: Dict[str, Union[int, str]] = {}
data["nonce"] = account.nonce
data["code"] = account.serialised_code()
data["storage"] = str(account.storage)
data["balance"] = hex(min_price_dict.get(address, 0))
accounts[hex(address)] = data
return {"accounts": accounts}
def _get_concrete_transaction(model: z3.Model, transaction: BaseTransaction):
"""Gets a concrete transaction from a transaction and z3 model"""
# Get concrete values from transaction
address = hex(transaction.callee_account.address.value)
value = model.eval(transaction.call_value.raw, model_completion=True).as_long()
caller = "0x" + (
"%x" % model.eval(transaction.caller.raw, model_completion=True).as_long()
).zfill(40)
input_ = ""
if isinstance(transaction, ContractCreationTransaction):
address = ""
input_ += transaction.code.bytecode
input_ += "".join(
[
hex(b)[2:] if len(hex(b)) % 2 == 0 else "0" + hex(b)[2:]
for b in transaction.call_data.concrete(model)
]
)
# Create concrete transaction dict
concrete_transaction: Dict[str, str] = dict()
concrete_transaction["input"] = "0x" + input_
concrete_transaction["value"] = "0x%x" % value
# Fixme: base origin assignment on origin symbol
concrete_transaction["origin"] = caller
concrete_transaction["address"] = "%s" % address
return concrete_transaction
def _set_minimisation_constraints(
transaction_sequence, constraints, minimize, max_size, world_state
) -> Tuple[Constraints, tuple]:
"""Set constraints that minimise key transaction values
Constraints generated:
- Upper bound on calldata size
- Minimisation of call value's and calldata sizes
:param transaction_sequence: Transaction for which the constraints should be applied
:param constraints: The constraints array which should contain any added constraints
:param minimize: The minimisation array which should contain any variables that should be minimised
:param max_size: The max size of the calldata array
:return: updated constraints, minimize
"""
for transaction in transaction_sequence:
# Set upper bound on calldata size
max_calldata_size = symbol_factory.BitVecVal(max_size, 256)
constraints.append(UGE(max_calldata_size, transaction.call_data.calldatasize))
# Minimize
minimize.append(transaction.call_data.calldatasize)
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)
================================================
FILE: mythril/analysis/swc_data.py
================================================
"""This module maps SWC IDs to their registry equivalents."""
DEFAULT_FUNCTION_VISIBILITY = "100"
INTEGER_OVERFLOW_AND_UNDERFLOW = "101"
OUTDATED_COMPILER_VERSION = "102"
FLOATING_PRAGMA = "103"
UNCHECKED_RET_VAL = "104"
UNPROTECTED_ETHER_WITHDRAWAL = "105"
UNPROTECTED_SELFDESTRUCT = "106"
REENTRANCY = "107"
DEFAULT_STATE_VARIABLE_VISIBILITY = "108"
UNINITIALIZED_STORAGE_POINTER = "109"
ASSERT_VIOLATION = "110"
DEPRECATED_FUNCTIONS_USAGE = "111"
DELEGATECALL_TO_UNTRUSTED_CONTRACT = "112"
MULTIPLE_SENDS = "113"
TX_ORDER_DEPENDENCE = "114"
TX_ORIGIN_USAGE = "115"
TIMESTAMP_DEPENDENCE = "116"
SIGNATURE_MALLEABILITY = "117"
INCORRECT_CONSTRUCTOR_NAME = "118"
SHADOWING_STATE_VARIABLES = "119"
WEAK_RANDOMNESS = "120"
SIGNATURE_REPLAY = "121"
IMPROPER_VERIFICATION_BASED_ON_MSG_SENDER = "122"
REQUIREMENT_VIOLATION = "123"
WRITE_TO_ARBITRARY_STORAGE = "124"
INCORRECT_INHERITANCE_ORDER = "125"
ARBITRARY_JUMP = "127"
DOS_WITH_BLOCK_GAS_LIMIT = "128"
TYPOGRAPHICAL_ERROR = "129"
UNEXPECTED_ETHER_BALANCE = "132"
EFFECT_FREE_CODE = "135"
SWC_TO_TITLE = {
"100": "Function Default Visibility",
"101": "Integer Overflow and Underflow",
"102": "Outdated Compiler Version",
"103": "Floating Pragma",
"104": "Unchecked Call Return Value",
"105": "Unprotected Ether Withdrawal",
"106": "Unprotected SELFDESTRUCT Instruction",
"107": "Reentrancy",
"108": "State Variable Default Visibility",
"109": "Uninitialized Storage Pointer",
"110": "Assert Violation",
"111": "Use of Deprecated Solidity Functions",
"112": "Delegatecall to Untrusted Callee",
"113": "DoS with Failed Call",
"114": "Transaction Order Dependence",
"115": "Authorization through tx.origin",
"116": "Timestamp Dependence",
"117": "Signature Malleability",
"118": "Incorrect Constructor Name",
"119": "Shadowing State Variables",
"120": "Weak Sources of Randomness from Chain Attributes",
"121": "Missing Protection against Signature Replay Attacks",
"122": "Lack of Proper Signature Verification",
"123": "Requirement Violation",
"124": "Write to Arbitrary Storage Location",
"125": "Incorrect Inheritance Order",
"127": "Arbitrary Jump with Function Type Variable",
"128": "DoS With Block Gas Limit",
"129": "Typographical Error",
"132": "Unexpected Ether Balance",
"135": "Effect Free Code",
}
================================================
FILE: mythril/analysis/symbolic.py
================================================
"""This module contains a wrapper around LASER for extended analysis
purposes."""
from typing import List, Optional, Type, Union
from mythril.analysis.module import EntryPoint, ModuleLoader, get_detection_module_hooks
from mythril.laser.ethereum import svm
from mythril.laser.ethereum.natives import PRECOMPILE_COUNT
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.strategy.basic import (
BasicSearchStrategy,
BreadthFirstSearchStrategy,
DepthFirstSearchStrategy,
ReturnRandomNaivelyStrategy,
ReturnWeightedRandomStrategy,
)
from mythril.laser.ethereum.strategy.beam import BeamSearch
from mythril.laser.ethereum.strategy.constraint_strategy import DelayConstraintStrategy
from mythril.laser.ethereum.strategy.extensions.bounded_loops import (
BoundedLoopsStrategy,
)
from mythril.laser.ethereum.transaction.symbolic import ACTORS
from mythril.laser.ethereum.tx_prioritiser import RfTxPrioritiser
from mythril.laser.execution_info import ExecutionInfo
from mythril.laser.plugin.loader import LaserPluginLoader
from mythril.laser.plugin.plugins import (
CallDepthLimitBuilder,
CoverageMetricsPluginBuilder,
CoveragePluginBuilder,
DependencyPrunerBuilder,
InstructionProfilerBuilder,
MutationPrunerBuilder,
StateMergePluginBuilder,
SymbolicSummaryPluginBuilder,
)
from mythril.laser.smt import BitVec, symbol_factory
from mythril.solidity.soliditycontract import EVMContract, SolidityContract
from mythril.support.support_args import args
from .ops import Call, VarType, get_variable
class SymExecWrapper:
"""Wrapper class for the LASER Symbolic virtual machine.
Symbolically executes the code and does a bit of pre-analysis for
convenience.
"""
def __init__(
self,
contract,
address: Union[int, str, BitVec],
strategy: str,
dynloader=None,
max_depth: int = 22,
execution_timeout: Optional[int] = None,
loop_bound: int = 3,
create_timeout: Optional[int] = None,
transaction_count: int = 2,
modules: Optional[List[str]] = None,
compulsory_statespace: bool = True,
disable_dependency_pruning: bool = False,
run_analysis_modules: bool = True,
custom_modules_directory: str = "",
):
"""
:param contract: Contract to symbolically execute
:param address: Address of the contract to symbolically execute
:param strategy: Execution strategy to use (bfs, dfs, etc)
:param dynloader: Dynamic Loader
:param max_depth: Max analysis depth
:param execution_timeout: Timeout for the entire analysis
:param create_timeout: Timeout for the creation transaction
:param transaction_count: Number of transactions to symbolically execute
:param modules: Analysis modules to run during analysis
:param compulsory_statespace: Boolean indicating whether or not the statespace should be saved
:param iprof: Instruction Profiler
:param disable_dependency_pruning: Boolean indicating whether dependency pruning should be disabled
:param run_analysis_modules: Boolean indicating whether analysis modules should be executed
:param enable_coverage_strategy: Boolean indicating whether the coverage strategy should be enabled
:param custom_modules_directory: The directory to read custom analysis modules from
"""
if isinstance(address, str):
address = symbol_factory.BitVecVal(int(address, 16), 256)
if isinstance(address, int):
address = symbol_factory.BitVecVal(address, 256)
beam_width = None
if strategy == "dfs":
s_strategy: Type[BasicSearchStrategy] = DepthFirstSearchStrategy
elif strategy == "bfs":
s_strategy = BreadthFirstSearchStrategy
elif strategy == "naive-random":
s_strategy = ReturnRandomNaivelyStrategy
elif strategy == "weighted-random":
s_strategy = ReturnWeightedRandomStrategy
elif "beam-search: " in strategy:
beam_width = int(strategy.split("beam-search: ")[1])
s_strategy = BeamSearch
elif "pending" in strategy:
s_strategy = DelayConstraintStrategy
else:
raise ValueError("Invalid strategy argument supplied")
if args.incremental_txs is False:
tx_strategy = RfTxPrioritiser(contract)
else:
tx_strategy = None
creator_account = Account(
hex(ACTORS.creator.value), "", dynamic_loader=None, contract_name=None
)
attacker_account = Account(
hex(ACTORS.attacker.value), "", dynamic_loader=None, contract_name=None
)
requires_statespace = (
compulsory_statespace
or len(ModuleLoader().get_detection_modules(EntryPoint.POST, modules)) > 0
)
if not contract.creation_code:
self.accounts = {hex(ACTORS.attacker.value): attacker_account}
else:
self.accounts = {
hex(ACTORS.creator.value): creator_account,
hex(ACTORS.attacker.value): attacker_account,
}
self.laser = svm.LaserEVM(
dynamic_loader=dynloader,
max_depth=max_depth,
execution_timeout=execution_timeout,
strategy=s_strategy,
create_timeout=create_timeout,
transaction_count=transaction_count,
requires_statespace=requires_statespace,
beam_width=beam_width,
tx_strategy=tx_strategy,
)
if loop_bound is not None:
self.laser.extend_strategy(
BoundedLoopsStrategy, loop_bound=loop_bound, beam_width=beam_width
)
plugin_loader = LaserPluginLoader()
plugin_loader.load(CoverageMetricsPluginBuilder())
if args.enable_state_merge:
plugin_loader.load(StateMergePluginBuilder())
if not args.disable_coverage_strategy:
plugin_loader.load(CoveragePluginBuilder())
if not args.disable_mutation_pruner:
plugin_loader.load(MutationPrunerBuilder())
if not args.disable_iprof:
plugin_loader.load(InstructionProfilerBuilder())
if args.enable_summaries:
plugin_loader.load(SymbolicSummaryPluginBuilder())
plugin_loader.load(CallDepthLimitBuilder())
plugin_loader.add_args(
"call-depth-limit", call_depth_limit=args.call_depth_limit
)
if not disable_dependency_pruning:
plugin_loader.load(DependencyPrunerBuilder())
plugin_loader.instrument_virtual_machine(self.laser, None)
world_state = WorldState()
for account in self.accounts.values():
world_state.put_account(account)
if run_analysis_modules:
analysis_modules = ModuleLoader().get_detection_modules(
EntryPoint.CALLBACK, modules
)
self.laser.register_hooks(
hook_type="pre",
hook_dict=get_detection_module_hooks(analysis_modules, hook_type="pre"),
)
self.laser.register_hooks(
hook_type="post",
hook_dict=get_detection_module_hooks(
analysis_modules, hook_type="post"
),
)
if isinstance(contract, SolidityContract) and create_timeout != 0:
self.laser.sym_exec(
creation_code=contract.creation_code,
contract_name=contract.name,
world_state=world_state,
)
elif isinstance(contract, EVMContract) and contract.creation_code:
self.laser.sym_exec(
creation_code=contract.creation_code,
contract_name=contract.name,
world_state=world_state,
)
else:
account = Account(
address,
contract.disassembly,
dynamic_loader=dynloader,
contract_name=contract.name,
balances=world_state.balances,
concrete_storage=(
True if (dynloader is not None and dynloader.active) else False
),
) # concrete_storage can get overridden by global args
if dynloader is not None:
if isinstance(address, int):
try:
_balance = dynloader.read_balance(
"{0:#0{1}x}".format(address, 42)
)
account.set_balance(_balance)
except:
# Initial balance will be a symbolic variable
pass
elif isinstance(address, str):
try:
_balance = dynloader.read_balance(address)
account.set_balance(_balance)
except:
# Initial balance will be a symbolic variable
pass
elif isinstance(address, BitVec):
try:
_balance = dynloader.read_balance(
"{0:#0{1}x}".format(address.value, 42)
)
account.set_balance(_balance)
except:
# Initial balance will be a symbolic variable
pass
world_state.put_account(account)
self.laser.sym_exec(world_state=world_state, target_address=address.value)
if not requires_statespace:
return
self.nodes = self.laser.nodes
self.edges = self.laser.edges
# Parse calls to make them easily accessible
self.calls: List[Call] = []
for key in self.nodes:
state_index = 0
for state in self.nodes[key].states:
instruction = state.get_current_instruction()
op = instruction["opcode"]
if op in ("CALL", "CALLCODE", "DELEGATECALL", "STATICCALL"):
stack = state.mstate.stack
if op in ("CALL", "CALLCODE"):
gas, to, value, meminstart, meminsz, _, _ = (
get_variable(stack[-1]),
get_variable(stack[-2]),
get_variable(stack[-3]),
get_variable(stack[-4]),
get_variable(stack[-5]),
get_variable(stack[-6]),
get_variable(stack[-7]),
)
if (
to.type == VarType.CONCRETE
and 0 < to.val <= PRECOMPILE_COUNT
):
# ignore prebuilts
continue
if (
meminstart.type == VarType.CONCRETE
and meminsz.type == VarType.CONCRETE
):
self.calls.append(
Call(
self.nodes[key],
state,
state_index,
op,
to,
gas,
value,
state.mstate.memory[
meminstart.val : meminsz.val + meminstart.val
],
)
)
else:
self.calls.append(
Call(
self.nodes[key],
state,
state_index,
op,
to,
gas,
value,
)
)
else:
gas, to, meminstart, meminsz, _, _ = (
get_variable(stack[-1]),
get_variable(stack[-2]),
get_variable(stack[-3]),
get_variable(stack[-4]),
get_variable(stack[-5]),
get_variable(stack[-6]),
)
self.calls.append(
Call(self.nodes[key], state, state_index, op, to, gas)
)
state_index += 1
@property
def execution_info(self) -> List[ExecutionInfo]:
return self.laser.execution_info
================================================
FILE: mythril/analysis/templates/callgraph.html
================================================
Call Graph
{% if not phrackify %}
{% else %}
{% endif %}
{{ title }}
================================================
FILE: mythril/analysis/templates/report_as_markdown.jinja2
================================================
# Analysis results for {{ filename }}
{% if issues %}
{% for issue in issues %}
## {{ issue.title }}
- SWC ID: {{ issue['swc-id'] }}
- Severity: {{ issue.severity }}
- Contract: {{ issue.contract | default("Unknown") }}
{% if issue.function %}
- Function name: `{{ issue.function }}`
{% endif %}
- 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 }}
{% endif %}
### Description
{{ issue.description.rstrip() }}
{% if issue.filename and issue.lineno %}
In file: {{ issue.filename }}:{{ issue.lineno }}
{% elif issue.filename %}
In file: {{ issue.filename }}
{% endif %}
{% if issue.code %}
### Code
```
{{ issue.code }}
```
{% endif %}
{% 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
{% for step in issue.tx_sequence.steps %}
{% if step == issue.tx_sequence.steps[0] and step.input != "0x" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}
Caller: [CREATOR], calldata: {{ step.calldata }}, {% if step.resolved_input is not none %}decoded_data: {{ step.resolved_input }}, {% endif %}value: {{ step.value }}
{% else %}
Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, function: {{ step.name }}, txdata: {{ step.input }}, {% if step.resolved_input is not none %}decoded_data: {{ step.resolved_input }}, {% endif %}value: {{ step.value }}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% else %}
The analysis was completed successfully. No issues were detected.
{% endif %}
================================================
FILE: mythril/analysis/templates/report_as_text.jinja2
================================================
{% if issues %}
{% for issue in issues %}
==== {{ issue.title }} ====
SWC ID: {{ issue['swc-id'] }}
Severity: {{ issue.severity }}
Contract: {{ issue.contract | default("Unknown") }}
{% if issue.function %}
Function name: {{ issue.function }}
{% endif %}
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 }}
{% endif %}
{{ issue.description }}
--------------------
{% if issue.filename and issue.lineno %}
In file: {{ issue.filename }}:{{ issue.lineno }}
{% endif %}
{% if issue.code %}
{{ issue.code }}
--------------------
{% endif %}
{% 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:
{% for step in issue.tx_sequence.steps %}
{% if step == issue.tx_sequence.steps[0] and step.address == "" and step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}
Caller: [CREATOR], calldata: {{ step.calldata }}, {% if step.resolved_input is not none %}decoded_data: {{ step.resolved_input }}, {% endif %}value: {{ step.value }}
{% else %}
Caller: {% if step.origin == "0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe" %}[CREATOR]{% elif step.origin == "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" %}[ATTACKER]{% else %}[SOMEGUY]{% endif %}, function: {{ step.name }}, txdata: {{ step.input }}, {% if step.resolved_input is not none %}decoded_data: {{ step.resolved_input }}, {% endif %}value: {{ step.value }}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% else %}
The analysis was completed successfully. No issues were detected.
{% endif %}
================================================
FILE: mythril/analysis/traceexplore.py
================================================
"""This module provides a function to convert a state space into a set of state
nodes and transition edges."""
import re
from z3 import Z3Exception
from mythril.laser.ethereum.svm import NodeFlags
from mythril.laser.smt import simplify
colors = [
{
"border": "#26996f",
"background": "#2f7e5b",
"highlight": {"border": "#fff", "background": "#28a16f"},
},
{
"border": "#9e42b3",
"background": "#842899",
"highlight": {"border": "#fff", "background": "#933da6"},
},
{
"border": "#b82323",
"background": "#991d1d",
"highlight": {"border": "#fff", "background": "#a61f1f"},
},
{
"border": "#4753bf",
"background": "#3b46a1",
"highlight": {"border": "#fff", "background": "#424db3"},
},
{
"border": "#26996f",
"background": "#2f7e5b",
"highlight": {"border": "#fff", "background": "#28a16f"},
},
{
"border": "#9e42b3",
"background": "#842899",
"highlight": {"border": "#fff", "background": "#933da6"},
},
{
"border": "#b82323",
"background": "#991d1d",
"highlight": {"border": "#fff", "background": "#a61f1f"},
},
{
"border": "#4753bf",
"background": "#3b46a1",
"highlight": {"border": "#fff", "background": "#424db3"},
},
]
def get_serializable_statespace(statespace):
"""
:param statespace:
:return:
"""
nodes = []
edges = []
color_map = {}
i = 0
for k in statespace.accounts:
color_map[statespace.accounts[k].contract_name] = colors[i]
i += 1
for node_key in statespace.nodes:
node = statespace.nodes[node_key]
code = node.get_cfg_dict()["code"]
code = re.sub("([0-9a-f]{8})[0-9a-f]+", lambda m: m.group(1) + "(...)", code)
if NodeFlags.FUNC_ENTRY in node.flags:
code = re.sub("JUMPDEST", node.function_name, code)
code_split = code.split("\\n")
truncated_code = (
code
if (len(code_split) < 7)
else "\\n".join(code_split[:6]) + "\\n(click to expand +)"
)
try:
color = color_map[node.get_cfg_dict()["contract_name"]]
except KeyError:
color = colors[i]
i += 1
color_map[node.get_cfg_dict()["contract_name"]] = color
def get_state_accounts(node_state):
"""
:param node_state:
:return:
"""
state_accounts = []
for key in node_state.accounts:
account = node_state.accounts[key].as_dict
account.pop("code", None)
account["balance"] = str(account["balance"])
storage = {}
for storage_key in account["storage"].printable_storage:
storage[str(storage_key)] = str(account["storage"][storage_key])
state_accounts.append({"address": key, "storage": storage})
return state_accounts
states = [
{"machine": x.mstate.as_dict, "accounts": get_state_accounts(x)}
for x in node.states
]
for state in states:
state["machine"]["stack"] = [str(s) for s in state["machine"]["stack"]]
state["machine"]["memory"] = [
str(m)
for m in state["machine"]["memory"][: len(state["machine"]["memory"])]
]
truncated_code = truncated_code.replace("\\n", "\n")
code = code.replace("\\n", "\n")
s_node = {
"id": str(node_key),
"func": str(node.function_name),
"label": truncated_code,
"code": code,
"truncated": truncated_code,
"states": states,
"color": color,
"instructions": code.split("\n"),
}
nodes.append(s_node)
for edge in statespace.edges:
if edge.condition is None:
label = ""
else:
try:
label = str(simplify(edge.condition)).replace("\n", "")
except Z3Exception:
label = str(edge.condition).replace("\n", "")
label = re.sub(
"([^_])([\d]{2}\d+)", lambda m: m.group(1) + hex(int(m.group(2))), label
)
s_edge = {
"from": str(edge.as_dict["from"]),
"to": str(edge.as_dict["to"]),
"arrows": "to",
"label": label,
"smooth": {"type": "cubicBezier"},
}
edges.append(s_edge)
return {"edges": edges, "nodes": nodes}
================================================
FILE: mythril/concolic/__init__.py
================================================
from mythril.concolic.concolic_execution import concolic_execution
from mythril.concolic.find_trace import concrete_execution
================================================
FILE: mythril/concolic/concolic_execution.py
================================================
from copy import deepcopy
from datetime import datetime
from typing import Any, Dict, List
from mythril.concolic.concrete_data import ConcreteData
from mythril.concolic.find_trace import concrete_execution
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.strategy.concolic import ConcolicStrategy
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.ethereum.time_handler import time_handler
from mythril.laser.ethereum.transaction.symbolic import execute_transaction
from mythril.laser.ethereum.transaction.transaction_models import tx_id_manager
from mythril.laser.smt import symbol_factory
from mythril.support.support_args import args
def flip_branches(
init_state: WorldState,
concrete_data: ConcreteData,
jump_addresses: List[str],
trace: List,
) -> List[Dict[str, Dict[str, Any]]]:
"""
Flips branches and prints the input required for branch flip
:param concrete_data: Concrete data
:param jump_addresses: Jump addresses to flip
:param trace: trace to follow
"""
tx_id_manager.restart_counter()
output_list = []
laser_evm = LaserEVM(
execution_timeout=600, use_reachability_check=False, transaction_count=10
)
laser_evm.open_states = [deepcopy(init_state)]
laser_evm.strategy = ConcolicStrategy(
work_list=laser_evm.work_list,
max_depth=100,
trace=trace,
flip_branch_addresses=jump_addresses,
)
time_handler.start_execution(laser_evm.execution_timeout)
laser_evm.time = datetime.now()
for transaction in concrete_data["steps"]:
execute_transaction(
laser_evm,
callee_address=transaction["address"],
caller_address=symbol_factory.BitVecVal(
int(transaction["origin"], 16), 256
),
data=transaction["input"][2:],
)
if laser_evm.strategy.results:
for addr in jump_addresses:
output_list.append(laser_evm.strategy.results[addr])
return output_list
def concolic_execution(
concrete_data: ConcreteData, jump_addresses: List, solver_timeout=100000
) -> List[Dict[str, Dict[str, Any]]]:
"""
Executes codes and prints input required to cover the branch flips
:param input_file: Input file
:param jump_addresses: Jump addresses to flip
:param solver_timeout: Solver timeout
"""
init_state, trace = concrete_execution(concrete_data)
args.solver_timeout = solver_timeout
output_list = flip_branches(
init_state=init_state,
concrete_data=concrete_data,
jump_addresses=jump_addresses,
trace=trace,
)
return output_list
================================================
FILE: mythril/concolic/concrete_data.py
================================================
from typing import Dict, List
from typing_extensions import TypedDict
class AccountData(TypedDict):
balance: str
code: str
nonce: int
storage: dict
class InitialState(TypedDict):
accounts: Dict[str, AccountData]
class TransactionData(TypedDict):
address: str
blockCoinbase: str
blockDifficulty: str
blockGasLimit: str
blockNumber: str
blockTime: str
calldata: str
gasLimit: str
gasPrice: str
input: str
name: str
origin: str
value: str
class ConcreteData(TypedDict):
initialState: InitialState
steps: List[TransactionData]
================================================
FILE: mythril/concolic/find_trace.py
================================================
import binascii
from copy import deepcopy
from datetime import datetime
from typing import List, Tuple
from mythril.concolic.concrete_data import ConcreteData
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.ethereum.time_handler import time_handler
from mythril.laser.ethereum.transaction.concolic import execute_transaction
from mythril.laser.ethereum.transaction.transaction_models import tx_id_manager
from mythril.laser.plugin.loader import LaserPluginLoader
from mythril.laser.plugin.plugins import TraceFinderBuilder
from mythril.laser.smt import symbol_factory
from mythril.support.support_args import args
def setup_concrete_initial_state(concrete_data: ConcreteData) -> WorldState:
"""
Sets up concrete initial state
:param concrete_data: Concrete data
:return: initialised world state
"""
world_state = WorldState()
for address, details in concrete_data["initialState"]["accounts"].items():
account = Account(address, concrete_storage=True)
account.code = Disassembly(details["code"][2:])
account.nonce = details["nonce"]
if isinstance(details["storage"], str):
details["storage"] = eval(details["storage"]) # type: ignore
for key, value in details["storage"].items():
key_bitvec = symbol_factory.BitVecVal(int(key, 16), 256)
account.storage[key_bitvec] = symbol_factory.BitVecVal(int(value, 16), 256)
world_state.put_account(account)
account.set_balance(int(details["balance"], 16))
return world_state
def concrete_execution(concrete_data: ConcreteData) -> Tuple[WorldState, List]:
"""
Executes code concretely to find the path to be followed by concolic executor
:param concrete_data: Concrete data
:return: path trace
"""
args.pruning_factor = 1
tx_id_manager.restart_counter()
init_state = setup_concrete_initial_state(concrete_data)
laser_evm = LaserEVM(execution_timeout=1000)
laser_evm.open_states = [deepcopy(init_state)]
plugin_loader = LaserPluginLoader()
plugin_loader.load(TraceFinderBuilder())
time_handler.start_execution(laser_evm.execution_timeout)
laser_evm.time = datetime.now()
plugin_loader.instrument_virtual_machine(laser_evm, None)
for transaction in concrete_data["steps"]:
execute_transaction(
laser_evm,
callee_address=transaction["address"],
caller_address=symbol_factory.BitVecVal(
int(transaction["origin"], 16), 256
),
origin_address=symbol_factory.BitVecVal(
int(transaction["origin"], 16), 256
),
gas_limit=int(transaction.get("gasLimit", "0x9999999999999999999999"), 16),
data=binascii.a2b_hex(transaction["input"][2:]),
gas_price=int(transaction.get("gasPrice", "0x773594000"), 16),
value=int(transaction["value"], 16),
track_gas=False,
)
tx_id_manager.restart_counter()
return init_state, plugin_loader.plugin_list["MythX Trace Finder"].tx_trace # type: ignore
================================================
FILE: mythril/config.ini
================================================
[defaults]
dynamic_loading = infura
infura_id =
================================================
FILE: mythril/disassembler/__init__.py
================================================
================================================
FILE: mythril/disassembler/asm.py
================================================
"""This module contains various helper classes and functions to deal with EVM
code disassembly."""
import re
try:
from collections.abc import Generator
except ImportError:
from collections import Generator
from functools import lru_cache
from mythril.ethereum import util
from mythril.support.opcodes import ADDRESS, ADDRESS_OPCODE_MAPPING, OPCODES
regex_PUSH = re.compile(r"^PUSH(\d*)$")
class EvmInstruction:
"""Model to hold the information of the disassembly."""
def __init__(self, address, op_code, argument=None):
self.address = address
self.op_code = op_code
self.argument = argument
def to_dict(self) -> dict:
"""
:return:
"""
result = {"address": self.address, "opcode": self.op_code}
if self.argument:
result["argument"] = self.argument
return result
def instruction_list_to_easm(instruction_list: list) -> str:
"""Convert a list of instructions into an easm op code string.
:param instruction_list:
:return:
"""
result = ""
for instruction in instruction_list:
result += "{} {}".format(instruction["address"], instruction["opcode"])
if "argument" in instruction:
result += " " + instruction["argument"]
result += "\n"
return result
def get_opcode_from_name(operation_name: str) -> int:
"""Get an op code based on its name.
:param operation_name:
:return:
"""
if operation_name in OPCODES:
return OPCODES[operation_name][ADDRESS]
raise RuntimeError("Unknown opcode")
def find_op_code_sequence(pattern: list, instruction_list: list) -> Generator:
"""Returns all indices in instruction_list that point to instruction
sequences following a pattern.
:param pattern: The pattern to look for, e.g. [["PUSH1", "PUSH2"], ["EQ"]] where ["PUSH1", "EQ"] satisfies pattern
:param instruction_list: List of instructions to look in
:return: Indices to the instruction sequences
"""
for i in range(0, len(instruction_list) - len(pattern) + 1):
if is_sequence_match(pattern, instruction_list, i):
yield i
def is_sequence_match(pattern: list, instruction_list: list, index: int) -> bool:
"""Checks if the instructions starting at index follow a pattern.
:param pattern: List of lists describing a pattern, e.g. [["PUSH1", "PUSH2"], ["EQ"]] where ["PUSH1", "EQ"] satisfies pattern
:param instruction_list: List of instructions
:param index: Index to check for
:return: Pattern matched
"""
for index, pattern_slot in enumerate(pattern, start=index):
try:
if not instruction_list[index]["opcode"] in pattern_slot:
return False
except IndexError:
return False
return True
lru_cache(maxsize=2**10)
def disassemble(bytecode) -> list:
"""Disassembles evm bytecode and returns a list of instructions.
:param bytecode:
:return:
"""
instruction_list = []
address = 0
length = len(bytecode)
if isinstance(bytecode, str):
bytecode = util.safe_decode(bytecode)
length = len(bytecode)
part_code = bytecode[-43:]
else:
try:
part_code = bytes(bytecode[-43:])
except TypeError:
part_code = ""
try:
if "bzzr" in str(part_code):
# ignore swarm hash
length -= 43
except ValueError:
pass
while address < length:
try:
op_code = ADDRESS_OPCODE_MAPPING[bytecode[address]]
except KeyError:
instruction_list.append(EvmInstruction(address, "INVALID"))
address += 1
continue
current_instruction = EvmInstruction(address, op_code)
match = re.search(regex_PUSH, op_code)
if match:
argument_bytes = bytecode[address + 1 : address + 1 + int(match.group(1))]
if isinstance(argument_bytes, bytes):
current_instruction.argument = "0x" + argument_bytes.hex()
else:
current_instruction.argument = argument_bytes
address += int(match.group(1))
instruction_list.append(current_instruction)
address += 1
# We use a to_dict() here for compatibility reasons
return [element.to_dict() for element in instruction_list]
================================================
FILE: mythril/disassembler/disassembly.py
================================================
"""This module contains the class used to represent disassembly code."""
from typing import Dict, List, Tuple
from mythril.disassembler import asm
from mythril.ethereum import util
from mythril.support.signatures import SignatureDB
class Disassembly(object):
"""Disassembly class.
Stores bytecode, and its disassembly.
Additionally it will gather the following information on the existing functions in the disassembled code:
- function hashes
- function name to entry point mapping
- function entry point to function name mapping
"""
def __init__(self, code: str) -> None:
"""
:param code:
"""
self.bytecode = code
if isinstance(code, str):
self.instruction_list = asm.disassemble(util.safe_decode(code))
else:
self.instruction_list = asm.disassemble(code)
self.func_hashes: List[str] = []
self.function_name_to_address: Dict[str, int] = {}
self.address_to_function_name: Dict[int, str] = {}
self.assign_bytecode(bytecode=code)
def assign_bytecode(self, bytecode):
self.bytecode = bytecode
# open from default locations
# control if you want to have online signature hash lookups
signatures = SignatureDB()
self.instruction_list = asm.disassemble(bytecode)
# Need to take from PUSH1 to PUSH4 because solc seems to remove excess 0s at the beginning for optimizing
jump_table_indices = asm.find_op_code_sequence(
[("PUSH1", "PUSH2", "PUSH3", "PUSH4"), ("EQ",)], self.instruction_list
)
for index in jump_table_indices:
function_hash, jump_target, function_name = get_function_info(
index, self.instruction_list, signatures
)
self.func_hashes.append(function_hash)
if jump_target is not None and function_name is not None:
self.function_name_to_address[function_name] = jump_target
self.address_to_function_name[jump_target] = function_name
def get_easm(self):
"""
:return:
"""
return asm.instruction_list_to_easm(self.instruction_list)
def get_function_info(
index: int, instruction_list: list, signature_database: SignatureDB
) -> Tuple[str, int, str]:
"""Finds the function information for a call table entry Solidity uses the
first 4 bytes of the calldata to indicate which function the message call
should execute The generated code that directs execution to the correct
function looks like this:
- PUSH function_hash
- EQ
- PUSH entry_point
- JUMPI
This function takes an index that points to the first instruction, and from that finds out the function hash,
function entry and the function name.
:param index: Start of the entry pattern
:param instruction_list: Instruction list for the contract that is being analyzed
:param signature_database: Database used to map function hashes to their respective function names
:return: function hash, function entry point, function name
"""
# Append with missing 0s at the beginning
if isinstance(instruction_list[index]["argument"], tuple):
try:
function_hash = "0x" + bytes(
instruction_list[index]["argument"]
).hex().rjust(8, "0")
except AttributeError:
raise ValueError(
"Mythril currently does not support symbolic function signatures"
)
else:
function_hash = "0x" + instruction_list[index]["argument"][2:].rjust(8, "0")
function_names = signature_database.get(function_hash)
if len(function_names) > 0:
function_name = " or ".join(set(function_names))
else:
function_name = "_function_" + function_hash
try:
offset = instruction_list[index + 2]["argument"]
if isinstance(offset, tuple):
offset = bytes(offset).hex()
entry_point = int(offset, 16)
except (KeyError, IndexError):
return function_hash, None, None
return function_hash, entry_point, function_name
================================================
FILE: mythril/ethereum/__init__.py
================================================
================================================
FILE: mythril/ethereum/evmcontract.py
================================================
"""This module contains the class representing EVM contracts, aka Smart
Contracts."""
import logging
import re
import persistent
from mythril.disassembler.disassembly import Disassembly
from mythril.support.support_utils import get_code_hash, sha3
log = logging.getLogger(__name__)
class EVMContract(persistent.Persistent):
"""This class represents an address with associated code (Smart
Contract)."""
def __init__(self, code="", creation_code="", name="Unknown"):
"""Create a new contract.
Workaround: We currently do not support compile-time linking.
Dynamic contract addresses of the format __[contract-name]_____________ are replaced with a generic address
Apply this for creation_code & code
:param code:
:param creation_code:
:param name:
"""
creation_code = re.sub(r"(_{2}.{38})", "aa" * 20, creation_code)
code = re.sub(r"(_{2}.{38})", "aa" * 20, code)
self.creation_code = creation_code
self.name = name
self.code = code
self.disassembly = Disassembly(code)
self.creation_disassembly = Disassembly(creation_code)
@property
def bytecode_hash(self):
"""
:return: runtime bytecode hash
"""
return get_code_hash(self.code)
@property
def creation_bytecode_hash(self):
"""
:return: Creation bytecode hash
"""
return get_code_hash(self.creation_code)
def as_dict(self):
"""
:return:
"""
return {
"name": self.name,
"code": self.code,
"creation_code": self.creation_code,
"disassembly": self.disassembly,
}
def get_easm(self):
"""
:return:
"""
return self.disassembly.get_easm()
def get_creation_easm(self):
"""
:return:
"""
return self.creation_disassembly.get_easm()
def matches_expression(self, expression):
"""
:param expression:
:return:
"""
str_eval = ""
easm_code = None
tokens = re.split(r"\s+(and|or|not)\s+", expression, re.IGNORECASE)
for token in tokens:
if token in ("and", "or", "not"):
str_eval += " " + token + " "
continue
m = re.match(r"^code#([a-zA-Z0-9\s,\[\]]+)#", token)
if m:
if easm_code is None:
easm_code = self.get_easm()
code = m.group(1).replace(",", "\\n")
str_eval += '"' + code + '" in easm_code'
continue
m = re.match(r"^func#([a-zA-Z0-9\s_,(\\)\[\]]+)#$", token)
if m:
sign_hash = "0x" + sha3(m.group(1))[:4].hex()
str_eval += '"' + sign_hash + '" in self.disassembly.func_hashes'
return eval(str_eval.strip())
================================================
FILE: mythril/ethereum/interface/__init__.py
================================================
================================================
FILE: mythril/ethereum/interface/rpc/__init__.py
================================================
================================================
FILE: mythril/ethereum/interface/rpc/base_client.py
================================================
"""This module provides a basic RPC interface client.
This code is adapted from: https://github.com/ConsenSys/ethjsonrpc
"""
from abc import abstractmethod
from .constants import BLOCK_TAG_LATEST, BLOCK_TAGS
from .utils import hex_to_dec, validate_block
GETH_DEFAULT_RPC_PORT = 8545
ETH_DEFAULT_RPC_PORT = 8545
PARITY_DEFAULT_RPC_PORT = 8545
PYETHAPP_DEFAULT_RPC_PORT = 4000
MAX_RETRIES = 3
JSON_MEDIA_TYPE = "application/json"
class BaseClient(object):
"""The base RPC client class."""
@abstractmethod
def _call(self, method, params=None, _id=1):
"""TODO: documentation
:param method:
:param params:
:param _id:
:return:
"""
pass
def eth_coinbase(self):
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_coinbase
TESTED
"""
return self._call("eth_coinbase")
def eth_blockNumber(self):
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_blocknumber
TESTED
"""
return hex_to_dec(self._call("eth_blockNumber"))
def eth_getBalance(self, address=None, block=BLOCK_TAG_LATEST):
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getbalance
TESTED
"""
address = address or self.eth_coinbase()
block = validate_block(block)
return hex_to_dec(self._call("eth_getBalance", [address, block]))
def eth_getStorageAt(self, address=None, position=0, block=BLOCK_TAG_LATEST):
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getstorageat
TESTED
"""
block = validate_block(block)
return self._call("eth_getStorageAt", [address, hex(position), block])
def eth_getCode(self, address, default_block=BLOCK_TAG_LATEST):
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getcode
NEEDS TESTING
"""
if isinstance(default_block, str):
if default_block not in BLOCK_TAGS:
raise ValueError
return self._call("eth_getCode", [address, default_block])
def eth_getBlockByNumber(self, block=BLOCK_TAG_LATEST, tx_objects=True):
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber
TESTED
"""
block = validate_block(block)
return self._call("eth_getBlockByNumber", [block, tx_objects])
def eth_getTransactionReceipt(self, tx_hash):
"""TODO: documentation
https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt
TESTED
"""
return self._call("eth_getTransactionReceipt", [tx_hash])
================================================
FILE: mythril/ethereum/interface/rpc/client.py
================================================
"""This module contains a basic Ethereum RPC client.
This code is adapted from: https://github.com/ConsenSys/ethjsonrpc
"""
import json
import logging
import requests
from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError as RequestsConnectionError
from .base_client import BaseClient
from .exceptions import (
BadJsonError,
BadResponseError,
BadStatusCodeError,
ConnectionError,
)
log = logging.getLogger(__name__)
GETH_DEFAULT_RPC_PORT = 8545
ETH_DEFAULT_RPC_PORT = 8545
PARITY_DEFAULT_RPC_PORT = 8545
PYETHAPP_DEFAULT_RPC_PORT = 4000
MAX_RETRIES = 3
JSON_MEDIA_TYPE = "application/json"
class EthJsonRpc(BaseClient):
"""Ethereum JSON-RPC client class."""
def __init__(self, host="localhost", port=GETH_DEFAULT_RPC_PORT, tls=False):
"""
:param host:
:param port:
:param tls:
"""
self.host = host
self.port = port
self.tls = tls
self.session = requests.Session()
self.session.mount(self.host, HTTPAdapter(max_retries=MAX_RETRIES))
def _call(self, method, params=None, _id=1):
"""
:param method:
:param params:
:param _id:
:return:
"""
params = params or []
data = {"jsonrpc": "2.0", "method": method, "params": params, "id": _id}
scheme = "http"
if self.tls:
scheme += "s"
if self.host:
if self.port:
url = "{}://{}:{}".format(scheme, self.host, self.port)
else:
url = "{}://{}".format(scheme, self.host)
else:
url = "{}".format(scheme)
headers = {"Content-Type": JSON_MEDIA_TYPE}
log.debug("rpc send: %s" % json.dumps(data))
try:
r = self.session.post(url, headers=headers, data=json.dumps(data))
except RequestsConnectionError:
raise ConnectionError
if r.status_code / 100 != 2:
raise BadStatusCodeError(r.status_code)
try:
response = r.json()
log.debug("rpc response: %s" % response)
except ValueError:
raise BadJsonError(r.text)
try:
return response["result"]
except KeyError:
raise BadResponseError(response)
def close(self):
"""Close the RPC client's session."""
self.session.close()
================================================
FILE: mythril/ethereum/interface/rpc/constants.py
================================================
"""This file contains constants used used by the Ethereum JSON RPC
interface."""
BLOCK_TAG_EARLIEST = "earliest"
BLOCK_TAG_LATEST = "latest"
BLOCK_TAG_PENDING = "pending"
BLOCK_TAGS = (BLOCK_TAG_EARLIEST, BLOCK_TAG_LATEST, BLOCK_TAG_PENDING)
================================================
FILE: mythril/ethereum/interface/rpc/exceptions.py
================================================
"""This module contains exceptions regarding JSON-RPC communication."""
class EthJsonRpcError(Exception):
"""The JSON-RPC base exception type."""
pass
class ConnectionError(EthJsonRpcError):
"""An RPC exception denoting there was an error in connecting to the RPC
instance."""
pass
class BadStatusCodeError(EthJsonRpcError):
"""An RPC exception denoting a bad status code returned by the RPC
instance."""
pass
class BadJsonError(EthJsonRpcError):
"""An RPC exception denoting that the RPC instance returned a bad JSON
object."""
pass
class BadResponseError(EthJsonRpcError):
"""An RPC exception denoting that the RPC instance returned a bad
response."""
pass
================================================
FILE: mythril/ethereum/interface/rpc/utils.py
================================================
"""This module contains various utility functions regarding the RPC data format
and validation."""
from .constants import BLOCK_TAGS
def hex_to_dec(x):
"""Convert hex to decimal.
:param x:
:return:
"""
return int(x, 16)
def clean_hex(d):
"""Convert decimal to hex and remove the "L" suffix that is appended to
large numbers.
:param d:
:return:
"""
return hex(d).rstrip("L")
def validate_block(block):
"""
:param block:
:return:
"""
if isinstance(block, str):
if block not in BLOCK_TAGS:
raise ValueError("invalid block tag")
if isinstance(block, int):
block = hex(block)
return block
def wei_to_ether(wei):
"""Convert wei to ether.
:param wei:
:return:
"""
return 1.0 * wei / 10**18
def ether_to_wei(ether):
"""Convert ether to wei.
:param ether:
:return:
"""
return ether * 10**18
================================================
FILE: mythril/ethereum/util.py
================================================
"""This module contains various utility functions regarding unit conversion and
solc integration."""
import binascii
import json
import logging
import os
import platform
import re
import typing
from json.decoder import JSONDecodeError
from subprocess import PIPE, Popen
from typing import Tuple
import semantic_version as semver
import solcx
from pyparsing import Combine, Optional, Regex, Word
from requests.exceptions import ConnectionError
from mythril.exceptions import CompilerError
from mythril.support.support_args import args
log = logging.getLogger(__name__)
def safe_decode(hex_encoded_string):
"""
:param hex_encoded_string:
:return:
"""
if hex_encoded_string.startswith("0x"):
return bytes.fromhex(hex_encoded_string[2:])
else:
return bytes.fromhex(hex_encoded_string)
def get_solc_json(file, solc_binary="solc", solc_settings_json=None):
"""
:param file:
:param solc_binary:
:param solc_settings_json:
:return:
"""
if args.solc_args is None:
cmd = [solc_binary, "--standard-json", "--allow-paths", ".,/"]
else:
cmd = [solc_binary, "--standard-json"] + args.solc_args.split()
settings = {}
if solc_settings_json:
with open(solc_settings_json) as f:
settings = json.load(f)
if "optimizer" not in settings:
settings.update({"optimizer": {"enabled": False}})
settings.update(
{
"outputSelection": {
"*": {
"": ["ast"],
"*": [
"metadata",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
],
}
},
}
)
input_json = json.dumps(
{
"language": "Solidity",
"sources": {file: {"urls": [file]}},
"settings": settings,
}
)
try:
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate(bytes(input_json, "utf8"))
except FileNotFoundError:
raise CompilerError(
"Compiler not found. Make sure that solc is installed and in PATH, or set the SOLC environment variable."
)
out = stdout.decode("UTF-8")
try:
result = json.loads(out)
except JSONDecodeError as e:
log.error(f"Encountered a decode error.\n stdout:{out}\n stderr: {stderr}")
raise e
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 get_random_address():
"""
:return:
"""
return binascii.b2a_hex(os.urandom(20)).decode("UTF-8")
def get_indexed_address(index):
"""
:param index:
:return:
"""
return "0x" + (hex(index)[2:] * 40)
def solc_exists(version):
"""
:param version:
:return:
"""
if platform.system() == "Darwin":
solcx.import_installed_solc()
solcx.install_solc("v" + version)
solcx.set_solc_version("v" + version)
solc_binary = solcx.install.get_executable()
return solc_binary
def parse_pragma(solidity_code):
lt = Word("<")
gtr = Word(">")
eq = Word("=")
carrot = Word("^")
version = Regex(r"\s*[0-9]+\s*\.\s*[0-9]+\s*(\.\s*[0-9]+)?")
inequality = Optional(
eq | (Combine(gtr + Optional(eq)) | Combine(lt + Optional(eq)))
)
min_version = Optional(carrot | inequality) + version
max_version = Optional(inequality + version)
pragma = Word("pragma") + Word("solidity") + min_version + Optional(max_version)
result = pragma.parseString(solidity_code)
min_inequality = result[2] if result[2] in [">", "<", ">=", "<=", "="] else ""
min_carrot = result[2] if result[2] == "^" else ""
min_version = result[3] if min_carrot != "" or min_inequality != "" else result[2]
return {
"min_carrot": min_carrot,
"min_inequality": min_inequality,
"min_version": min_version,
"max_inequality": result[4] if len(result) > 4 else None,
"max_version": result[5] if len(result) > 5 else None,
}
try:
all_versions = solcx.get_installable_solc_versions()
except ConnectionError:
# No internet, trying to proceed with installed compilers
all_versions = solcx.get_installed_solc_versions()
VOID_START = re.compile("//|/\\*|\"|'")
QUOTE_END = re.compile("(? str:
"""Return program without Solidity comments and strings
:param str program: Solidity program with lines separated by \\n
:return: program with strings emptied and comments removed
:rtype: str
"""
result = ""
while True:
match_start_of_void = VOID_START.search(program)
if not match_start_of_void:
result += program
break
else:
result += program[: match_start_of_void.start()]
if match_start_of_void[0] == "//":
end = program.find("\n", match_start_of_void.end())
program = "" if end == -1 else program[end:]
elif match_start_of_void[0] == "/*":
end = program.find("*/", match_start_of_void.end())
result += " "
program = "" if end == -1 else program[end + 2 :]
else:
if match_start_of_void[0] == "'":
match_end_of_string = QUOTE_END.search(
program[match_start_of_void.end() :]
)
else:
match_end_of_string = DQUOTE_END.search(
program[match_start_of_void.end() :]
)
if not match_end_of_string: # unclosed string
break
program = program[
match_start_of_void.end() + match_end_of_string.end() :
]
return result
def extract_version_line(program: typing.Optional[str]) -> typing.Optional[str]:
if not program:
return None
# normalize line endings
if "\n" in program:
program = program.replace("\r", "")
else:
program = program.replace("\r", "\n")
# extract regular pragma
program_wo_comments_strings = remove_comments_strings(program)
for line in program_wo_comments_strings.split("\n"):
if "pragma solidity" in line:
return line.rstrip()
# extract pragma from comments
for line in program.split("\n"):
if "pragma solidity" in line:
return line.rstrip()
return None
def extract_version(program: typing.Optional[str]) -> typing.Optional[str]:
version_line = extract_version_line(program)
if not version_line:
return None
assert "pragma solidity" in version_line
if version_line[-1] == ";":
version_line = version_line[:-1]
version_line = version_line[version_line.find("pragma") :]
pragma_dict = parse_pragma(version_line)
min_inequality = pragma_dict.get("min_inequality", None)
max_inequality = pragma_dict.get("max_inequality", None)
min_version = pragma_dict.get("min_version", None)
if min_version is not None:
min_version = min_version.replace(" ", "").replace("\t", "")
max_version = pragma_dict.get("max_version", None)
if max_version is not None:
max_version = max_version.replace(" ", "").replace("\t", "")
version_spec = (
f"{min_inequality}{min_version},{max_inequality}{max_version}"
if max_version
else min_version
)
version_constraint = semver.SimpleSpec(version_spec)
for version in all_versions:
semver_version = semver.Version(str(version))
if semver_version in version_constraint:
if "0.5.17" in str(semver_version):
# Solidity 0.5.17 Does not compile in a lot of cases.
continue
return str(semver_version)
def extract_binary(file: str) -> Tuple[str, str]:
program = None
with open(file) as f:
program = f.read()
version = extract_version(program)
if version is None:
return os.environ.get("SOLC") or "solc", version
return solc_exists(version), version
================================================
FILE: mythril/exceptions.py
================================================
"""This module contains general exceptions used by Mythril."""
class MythrilBaseException(Exception):
"""The Mythril exception base type."""
pass
class CompilerError(MythrilBaseException):
"""A Mythril exception denoting an error during code compilation."""
pass
class UnsatError(MythrilBaseException):
"""A Mythril exception denoting the unsatisfiability of a series of
constraints."""
pass
class SolverTimeOutException(UnsatError):
"""A Mythril exception denoting the unsatisfiability of a series of
constraints."""
pass
class NoContractFoundError(MythrilBaseException):
"""A Mythril exception denoting that a given contract file was not
found."""
pass
class CriticalError(MythrilBaseException):
"""A Mythril exception denoting an unknown critical error has been
encountered."""
pass
class DetectorNotFoundError(MythrilBaseException):
"""A Mythril exception denoting attempted usage of a non-existant
detection module."""
pass
class IllegalArgumentError(ValueError):
"""The argument used does not exist"""
pass
================================================
FILE: mythril/interfaces/__init__.py
================================================
================================================
FILE: mythril/interfaces/cli.py
================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""mythril.py: Bug hunting on the Ethereum blockchain
http://www.github.com/ConsenSys/mythril
"""
import argparse
import json
import logging
import os
import sys
import traceback
from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
from ast import literal_eval
import coloredlogs
from mythril.__version__ import __version__ as VERSION
from mythril.analysis.module import ModuleLoader
from mythril.analysis.report import Report
from mythril.concolic import concolic_execution
from mythril.exceptions import (
CriticalError,
DetectorNotFoundError,
)
from mythril.laser.ethereum.transaction.symbolic import ACTORS
from mythril.mythril import MythrilAnalyzer, MythrilConfig, MythrilDisassembler
from mythril.plugin.loader import MythrilPluginLoader
# Initialise core Mythril Component
_ = MythrilPluginLoader()
ANALYZE_LIST = ("analyze", "a")
DISASSEMBLE_LIST = ("disassemble", "d")
FOUNDRY_LIST = ("foundry", "f")
CONCOLIC_LIST = ("concolic", "c")
SAFE_FUNCTIONS_COMMAND = "safe-functions"
READ_STORAGE_COMNAND = "read-storage"
FUNCTION_TO_HASH_COMMAND = "function-to-hash"
HASH_TO_ADDRESS_COMMAND = "hash-to-address"
LIST_DETECTORS_COMMAND = "list-detectors"
VERSION_COMMAND = "version"
HELP_COMMAND = "help"
log = logging.getLogger(__name__)
COMMAND_LIST = (
ANALYZE_LIST
+ DISASSEMBLE_LIST
+ FOUNDRY_LIST
+ CONCOLIC_LIST
+ (
READ_STORAGE_COMNAND,
SAFE_FUNCTIONS_COMMAND,
FUNCTION_TO_HASH_COMMAND,
HASH_TO_ADDRESS_COMMAND,
LIST_DETECTORS_COMMAND,
VERSION_COMMAND,
HELP_COMMAND,
)
)
def exit_with_error(format_, message):
"""
Exits with error
:param format_: The format of the message
:param message: message
"""
if format_ == "text" or format_ == "markdown":
log.error(message)
elif format_ == "json":
result = {"success": False, "error": str(message), "issues": []}
print(json.dumps(result))
else:
result = [
{
"issues": [],
"sourceType": "",
"sourceFormat": "",
"sourceList": [],
"meta": {"logs": [{"level": "error", "hidden": True, "msg": message}]},
}
]
print(json.dumps(result))
sys.exit()
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
:return: Parser which handles input
"""
parser = ArgumentParser(add_help=False)
parser.add_argument(
"-c",
"--code",
help='hex-encoded bytecode string ("6060604052...")',
metavar="BYTECODE",
)
parser.add_argument(
"-f",
"--codefile",
help="file containing hex-encoded bytecode string",
metavar="BYTECODEFILE",
type=argparse.FileType("r"),
)
return parser
def get_safe_functions_parser() -> ArgumentParser:
"""
Returns Parser which handles checking for safe functions
:return: Parser which handles checking for safe functions
"""
parser = ArgumentParser(add_help=False)
parser.add_argument(
"-c",
"--code",
help='hex-encoded bytecode string ("6060604052...")',
metavar="BYTECODE",
)
parser.add_argument(
"-f",
"--codefile",
help="file containing hex-encoded bytecode string",
metavar="BYTECODEFILE",
type=argparse.FileType("r"),
)
return parser
def get_output_parser() -> ArgumentParser:
"""
Get parser which handles output
:return: Parser which handles output
"""
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
"-o",
"--outform",
choices=["text", "markdown", "json", "jsonv2"],
default="text",
help="report output format",
metavar="",
)
return parser
def get_rpc_parser() -> ArgumentParser:
"""
Get parser which handles RPC flags
:return: Parser which handles rpc inputs
"""
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
"--rpc",
help="custom RPC settings",
metavar="HOST:PORT / ganache / infura-[network_name]",
default="infura-mainnet",
)
parser.add_argument(
"--rpctls", type=bool, default=False, help="RPC connection over TLS"
)
parser.add_argument("--infura-id", help="set infura id for onchain analysis")
return parser
def get_utilities_parser() -> ArgumentParser:
"""
Get parser which handles utilities flags
:return: Parser which handles utility flags
"""
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
"--solc-json",
help="Json for the optional 'settings' parameter of solc's standard-json input",
)
parser.add_argument(
"--solc-args",
help="""Provide solc args, example: --solc-args "--allow-paths --include-path /root_folder/node_modules --base-path /home/contracts" """,
type=str,
)
parser.add_argument(
"--solv",
help="specify solidity compiler version. If not present, will try to install it (Experimental)",
metavar="SOLV",
)
return parser
def create_concolic_parser(parser: ArgumentParser) -> ArgumentParser:
"""
Get parser which handles arguments for concolic branch flipping
"""
parser.add_argument(
"input",
help="The input jsonv2 file with concrete data",
)
parser.add_argument(
"--branches",
help="branch addresses to be flipped. usage: --branches 34,6f8,16a",
required=True,
metavar="BRANCH",
)
parser.add_argument(
"--solver-timeout",
type=int,
default=100000,
help="The maximum amount of time(in milli seconds) the solver spends for queries from analysis modules",
)
return parser
def main() -> None:
"""The main CLI interface entry point."""
rpc_parser = get_rpc_parser()
utilities_parser = get_utilities_parser()
runtime_input_parser = get_runtime_input_parser()
creation_input_parser = get_creation_input_parser()
output_parser = get_output_parser()
parser = argparse.ArgumentParser(
description="Security analysis of Ethereum smart contracts"
)
parser.add_argument("--epic", action="store_true", help=argparse.SUPPRESS)
parser.add_argument(
"-v", type=int, help="log level (0-5)", metavar="LOG_LEVEL", default=2
)
subparsers = parser.add_subparsers(dest="command", help="Commands")
safe_function_parser = subparsers.add_parser(
SAFE_FUNCTIONS_COMMAND,
help="Check functions which are completely safe using symbolic execution",
parents=[
rpc_parser,
utilities_parser,
creation_input_parser,
runtime_input_parser,
output_parser,
],
formatter_class=RawTextHelpFormatter,
)
analyzer_parser = subparsers.add_parser(
ANALYZE_LIST[0],
help="Triggers the analysis of the smart contract",
parents=[
rpc_parser,
utilities_parser,
creation_input_parser,
runtime_input_parser,
output_parser,
],
aliases=ANALYZE_LIST[1:],
formatter_class=RawTextHelpFormatter,
)
create_safe_functions_parser(safe_function_parser)
create_analyzer_parser(analyzer_parser)
disassemble_parser = subparsers.add_parser(
DISASSEMBLE_LIST[0],
help="Disassembles the smart contract",
aliases=DISASSEMBLE_LIST[1:],
parents=[
rpc_parser,
utilities_parser,
creation_input_parser,
runtime_input_parser,
],
formatter_class=RawTextHelpFormatter,
)
create_disassemble_parser(disassemble_parser)
concolic_parser = subparsers.add_parser(
CONCOLIC_LIST[0],
help="Runs concolic execution to flip the desired branches",
aliases=CONCOLIC_LIST[1:],
parents=[],
formatter_class=RawTextHelpFormatter,
)
create_concolic_parser(concolic_parser)
foundry_parser = subparsers.add_parser(
FOUNDRY_LIST[0],
help="Triggers the analysis of the smart contract",
parents=[
rpc_parser,
utilities_parser,
output_parser,
],
aliases=FOUNDRY_LIST[1:],
formatter_class=RawTextHelpFormatter,
)
_ = subparsers.add_parser(
LIST_DETECTORS_COMMAND,
parents=[output_parser],
help="Lists available detection modules",
)
read_storage_parser = subparsers.add_parser(
READ_STORAGE_COMNAND,
help="Retrieves storage slots from a given address through rpc",
parents=[rpc_parser],
)
contract_func_to_hash = subparsers.add_parser(
FUNCTION_TO_HASH_COMMAND, help="Returns the hash signature of the function"
)
contract_hash_to_addr = subparsers.add_parser(
HASH_TO_ADDRESS_COMMAND,
help="converts the hashes in the blockchain to ethereum address",
)
subparsers.add_parser(
VERSION_COMMAND, parents=[output_parser], help="Outputs the version"
)
create_read_storage_parser(read_storage_parser)
create_hash_to_addr_parser(contract_hash_to_addr)
create_func_to_hash_parser(contract_func_to_hash)
create_foundry_parser(foundry_parser)
subparsers.add_parser(HELP_COMMAND, add_help=False)
# Get config values
args = parser.parse_args()
parse_args_and_execute(parser=parser, args=args)
def create_disassemble_parser(parser: ArgumentParser):
"""
Modify parser to handle disassembly
:param parser:
:return:
"""
# 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_read_storage_parser(read_storage_parser: ArgumentParser):
"""
Modify parser to handle storage slots
:param read_storage_parser:
:return:
"""
read_storage_parser.add_argument(
"storage_slots",
help="read state variables from storage index",
metavar="INDEX,NUM_SLOTS,[array] / mapping,INDEX,[KEY1, KEY2...]",
)
read_storage_parser.add_argument(
"address", help="contract address", metavar="ADDRESS"
)
def create_func_to_hash_parser(parser: ArgumentParser):
"""
Modify parser to handle func_to_hash command
:param parser:
:return:
"""
parser.add_argument(
"func_name", help="calculate function signature hash", metavar="SIGNATURE"
)
def create_hash_to_addr_parser(hash_parser: ArgumentParser):
"""
Modify parser to handle hash_to_addr command
:param hash_parser:
:return:
"""
hash_parser.add_argument(
"hash", help="Find the address from hash", metavar="FUNCTION_NAME"
)
def add_graph_commands(parser: ArgumentParser):
commands = parser.add_argument_group("commands")
commands.add_argument("-g", "--graph", help="generate a control flow graph")
commands.add_argument(
"-j",
"--statespace-json",
help="dumps the statespace json",
metavar="OUTPUT_FILE",
)
def create_safe_functions_parser(parser: ArgumentParser):
"""
The duplication exists between safe-functions and analyze as some of them have different default values.
:param parser: Parser
"""
parser.add_argument(
"solidity_files",
nargs="*",
help="Inputs file name and contract name. \n"
"usage: file1.sol:OptionalContractName file2.sol file3.sol:OptionalContractName",
)
options = parser.add_argument_group("options")
add_analysis_args(options)
def add_analysis_args(options):
"""
Adds arguments for analysis
:param options: Analysis Options
"""
options.add_argument(
"-m",
"--modules",
help="Comma-separated list of security analysis modules",
metavar="MODULES",
)
options.add_argument(
"--max-depth",
type=int,
default=128,
help="Maximum recursion depth for symbolic execution",
)
options.add_argument(
"--call-depth-limit",
type=int,
default=3,
help="Maximum call depth limit for symbolic execution",
)
options.add_argument(
"--strategy",
choices=["dfs", "bfs", "naive-random", "weighted-random", "pending"],
default="bfs",
help="Symbolic execution strategy",
)
options.add_argument(
"--transaction-sequences",
type=str,
default=None,
help="The possible transaction sequences to be executed. "
"Like [[func_hash1, func_hash2], [func_hash2, func_hash3]] where the first transaction is constrained "
"with func_hash1 and func_hash2, and the second tx is constrained with func_hash2 and func_hash3. Use -1 as a proxy for fallback() function and -2 for receive() function.",
)
options.add_argument(
"--beam-search",
type=int,
default=None,
help="Beam search with with",
)
options.add_argument(
"-b",
"--loop-bound",
type=int,
default=3,
help="Bound loops at n iterations",
metavar="N",
)
options.add_argument(
"-t",
"--transaction-count",
type=int,
default=2,
help="Maximum number of transactions issued by laser",
)
options.add_argument(
"--execution-timeout",
type=int,
default=3600,
help="The amount of seconds to spend on symbolic execution",
)
options.add_argument(
"--solver-timeout",
type=int,
default=25000,
help="The maximum amount of time(in milli seconds) the solver spends for queries from analysis modules",
)
options.add_argument(
"--create-timeout",
type=int,
default=30,
help="The amount of seconds to spend on the initial contract creation",
)
options.add_argument(
"--parallel-solving",
action="store_true",
help="Enable solving z3 queries in parallel",
)
options.add_argument(
"--solver-log",
help="Path to the directory for solver log",
metavar="SOLVER_LOG",
)
options.add_argument(
"--no-onchain-data",
action="store_true",
help="Don't attempt to retrieve contract code, variables and balances from the blockchain",
)
options.add_argument(
"--pruning-factor",
type=float,
default=None,
help="Checks for reachability at the rate of (range 0-1.0). Where 1.0 would mean checking for every execution",
)
options.add_argument(
"--unconstrained-storage",
action="store_true",
help="Default storage value is symbolic, turns off the on-chain storage loading",
)
options.add_argument(
"--phrack", action="store_true", help="Phrack-style call graph"
)
options.add_argument(
"--enable-physics", action="store_true", help="Enable graph physics simulation"
)
options.add_argument(
"-q",
"--query-signature",
action="store_true",
help="Lookup function signatures through www.4byte.directory",
)
options.add_argument(
"--disable-iprof", action="store_true", help="Disable the instruction profiler"
)
options.add_argument(
"--disable-dependency-pruning",
action="store_true",
help="Deactivate dependency-based pruning",
)
options.add_argument(
"--disable-coverage-strategy",
action="store_true",
help="Disable coverage based search strategy",
)
options.add_argument(
"--disable-mutation-pruner",
action="store_true",
help="Disable mutation pruner",
)
options.add_argument(
"--enable-state-merging",
action="store_true",
help="Enable State Merging",
)
options.add_argument(
"--enable-summaries",
action="store_true",
help="Enable using symbolic summaries",
)
options.add_argument(
"--custom-modules-directory",
help="Designates a separate directory to search for custom analysis modules",
metavar="CUSTOM_MODULES_DIRECTORY",
)
options.add_argument(
"--attacker-address",
help="Designates a specific attacker address to use during analysis",
metavar="ATTACKER_ADDRESS",
)
options.add_argument(
"--creator-address",
help="Designates a specific creator address to use during analysis",
metavar="CREATOR_ADDRESS",
)
def create_analyzer_parser(analyzer_parser: ArgumentParser):
"""
Modify parser to handle analyze command
:param analyzer_parser:
:return:
"""
analyzer_parser.add_argument(
"solidity_files",
nargs="*",
help="Inputs file name and contract name. \n"
"usage: file1.sol:OptionalContractName file2.sol file3.sol:OptionalContractName",
)
add_graph_commands(analyzer_parser)
options = analyzer_parser.add_argument_group("options")
add_analysis_args(options)
def create_foundry_parser(foundry_parser: ArgumentParser):
add_graph_commands(foundry_parser)
options = foundry_parser.add_argument_group("options")
add_analysis_args(options)
def validate_args(args: Namespace):
"""
Validate cli args
:param args:
:return:
"""
if hasattr(args, "v"):
if 0 <= args.v < 6:
log_levels = [
logging.NOTSET,
logging.CRITICAL,
logging.ERROR,
logging.WARNING,
logging.INFO,
logging.DEBUG,
]
coloredlogs.install(
fmt="%(name)s [%(levelname)s]: %(message)s", level=log_levels[args.v]
)
else:
exit_with_error(
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 getattr(args, "transaction_sequences", None):
if getattr(args, "disable_dependency_pruning", False) is False:
log.warning(
"It is advised to disable dependency pruning (use the flag --disable-dependency-pruning) when specifying transaction sequences."
)
try:
args.transaction_sequences = literal_eval(str(args.transaction_sequences))
except ValueError:
exit_with_error(
args.outform,
"The transaction sequence is in incorrect format, It should be "
"[list of possible function hashes in 1st transaction, "
"list of possible func hashes in 2nd tx, ...] "
"If any list is empty then all possible functions are considered for that transaction."
"Use -1 as a proxy for fallback() and -2 for receive() function.",
)
if len(args.transaction_sequences) != args.transaction_count:
args.transaction_count = len(args.transaction_sequences)
def set_config(args: Namespace):
"""
Set config based on args
:param args:
:return: modified config
"""
config = MythrilConfig()
if getattr(args, "infura_id", None):
config.set_api_infura_id(args.infura_id)
if (args.command in ANALYZE_LIST and not args.no_onchain_data) and not (
args.rpc or args.i
):
config.set_api_from_config_path()
if getattr(args, "rpc", None):
# Establish RPC connection if necessary
config.set_api_rpc(rpc=args.rpc, rpctls=args.rpctls)
return config
def load_code(disassembler: MythrilDisassembler, args: Namespace):
"""
Loads code into disassembly and returns address
:param disassembler:
:param args:
:return: Address
"""
address = None
if getattr(args, "code", None):
# Load from bytecode
code = args.code[2:] if args.code.startswith("0x") else args.code
address, _ = disassembler.load_from_bytecode(code, args.bin_runtime)
elif getattr(args, "codefile", None):
bytecode = "".join([l.strip() for l in args.codefile if len(l.strip()) > 0])
bytecode = bytecode[2:] if bytecode.startswith("0x") else bytecode
address, _ = disassembler.load_from_bytecode(bytecode, args.bin_runtime)
elif getattr(args, "address", None):
# Get bytecode from a contract address
address, _ = disassembler.load_from_address(args.address)
elif getattr(args, "solidity_files", None):
# Compile Solidity source file(s)
if args.command in ANALYZE_LIST and args.graph and len(args.solidity_files) > 1:
exit_with_error(
args.outform,
"Cannot generate call graphs from multiple input files. Please do it one at a time.",
)
address, _ = disassembler.load_from_solidity(
args.solidity_files
) # list of files
elif args.command in FOUNDRY_LIST:
address, _ = disassembler.load_from_foundry()
else:
exit_with_error(
getattr(args, "outform", "text"),
"No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, -f BYTECODE_FILE or ",
)
return address
def print_function_report(myth_disassembler: MythrilDisassembler, report: Report):
"""
Prints the function report
:param report: Mythril's report
:return:
"""
contract_data = {}
for contract in myth_disassembler.contracts:
contract_data[contract.name] = list(
set(contract.disassembly.address_to_function_name.values())
)
for issue in report.issues.values():
if issue.function in contract_data[issue.contract]:
contract_data[issue.contract].remove(issue.function)
for contract, function_list in contract_data.items():
print(f"Contract {contract}: \n")
print(
f"""{len(function_list)} functions are deemed safe in this contract: {", ".join(function_list)}\n\n"""
)
def execute_command(
disassembler: MythrilDisassembler,
address: str,
parser: ArgumentParser,
args: Namespace,
):
"""
Execute command
:param disassembler:
:param address:
:param parser:
:param args:
:return:
"""
if getattr(args, "beam_search", None):
strategy = f"beam-search: {args.beam_search}"
else:
strategy = getattr(args, "strategy", "dfs")
if args.command == READ_STORAGE_COMNAND:
storage = disassembler.get_state_variable_from_storage(
address=address,
params=[a.strip() for a in args.storage_slots.strip().split(",")],
)
print(storage)
elif args.command in DISASSEMBLE_LIST:
if disassembler.contracts[0].code:
print("Runtime Disassembly: \n" + disassembler.contracts[0].get_easm())
if disassembler.contracts[0].creation_code:
print("Disassembly: \n" + disassembler.contracts[0].get_creation_easm())
elif args.command == SAFE_FUNCTIONS_COMMAND:
args.no_onchain_data = args.disable_dependency_pruning = (
args.unconstrained_storage
) = True
args.pruning_factor = 1
function_analyzer = MythrilAnalyzer(
strategy=strategy, disassembler=disassembler, address=address, cmd_args=args
)
try:
report = function_analyzer.fire_lasers(
modules=(
[m.strip() for m in args.modules.strip().split(",")]
if args.modules
else None
),
transaction_count=1,
)
print_function_report(disassembler, report)
except DetectorNotFoundError as e:
exit_with_error("text", format(e))
except CriticalError as e:
exit_with_error("text", "Analysis error encountered: " + format(e))
elif args.command in ANALYZE_LIST + FOUNDRY_LIST:
analyzer = MythrilAnalyzer(
strategy=strategy, disassembler=disassembler, address=address, cmd_args=args
)
if not disassembler.contracts:
exit_with_error(
args.outform, "input files do not contain any valid contracts"
)
if args.attacker_address:
try:
ACTORS["ATTACKER"] = args.attacker_address
except ValueError:
exit_with_error(args.outform, "Attacker address is invalid")
if args.creator_address:
try:
ACTORS["CREATOR"] = args.creator_address
except ValueError:
exit_with_error(args.outform, "Creator address is invalid")
if args.graph:
html = analyzer.graph_html(
contract=analyzer.contracts[0],
enable_physics=args.enable_physics,
phrackify=args.phrack,
transaction_count=args.transaction_count,
)
try:
with open(args.graph, "w") as f:
f.write(html)
except Exception as e:
exit_with_error(args.outform, "Error saving graph: " + str(e))
elif args.statespace_json:
if not analyzer.contracts:
exit_with_error(
args.outform, "input files do not contain any valid contracts"
)
statespace = analyzer.dump_statespace(contract=analyzer.contracts[0])
try:
with open(args.statespace_json, "w") as f:
json.dump(statespace, f)
except Exception as e:
exit_with_error(args.outform, "Error saving json: " + str(e))
else:
try:
report = analyzer.fire_lasers(
modules=(
[m.strip() for m in args.modules.strip().split(",")]
if args.modules
else None
),
transaction_count=args.transaction_count,
)
outputs = {
"json": report.as_json(),
"jsonv2": report.as_swc_standard_format(),
"text": report.as_text(),
"markdown": report.as_markdown(),
}
print(outputs[args.outform])
if len(report.issues) > 0:
exit(1)
else:
exit(0)
except DetectorNotFoundError as e:
exit_with_error(args.outform, format(e))
except CriticalError as e:
exit_with_error(
args.outform, "Analysis error encountered: " + format(e)
)
else:
parser.print_help()
def contract_hash_to_address(args: Namespace):
"""
prints the hash from function signature
:param args:
:return:
"""
print(MythrilDisassembler.hash_for_function_signature(args.func_name))
sys.exit()
def parse_args_and_execute(parser: ArgumentParser, args: Namespace) -> None:
"""
Parses the arguments
:param parser: The parser
:param args: The args
"""
if args.epic:
path = os.path.dirname(os.path.realpath(__file__))
sys.argv.remove("--epic")
os.system(" ".join(sys.argv) + " | python3 " + path + "/epic.py")
sys.exit()
if args.command not in COMMAND_LIST or args.command is None:
parser.print_help()
sys.exit()
if args.command == VERSION_COMMAND:
if args.outform == "json":
print(json.dumps({"version_str": VERSION}))
else:
print("Mythril version {}".format(VERSION))
sys.exit()
if args.command == LIST_DETECTORS_COMMAND:
modules = []
for module in ModuleLoader().get_detection_modules():
modules.append({"classname": type(module).__name__, "title": module.name})
if args.outform == "json":
print(json.dumps(modules))
else:
for module_data in modules:
print("{}: {}".format(module_data["classname"], module_data["title"]))
sys.exit()
if args.command == HELP_COMMAND:
parser.print_help()
sys.exit()
if args.command in CONCOLIC_LIST:
_ = MythrilConfig.init_mythril_dir()
with open(args.input) as f:
concrete_data = json.load(f)
output_list = concolic_execution(
concrete_data, args.branches.split(","), args.solver_timeout
)
json.dump(output_list, sys.stdout, indent=4)
sys.exit()
# Parse cmdline args
validate_args(args)
try:
if args.command == FUNCTION_TO_HASH_COMMAND:
contract_hash_to_address(args)
config = set_config(args)
solc_json = getattr(args, "solc_json", None)
solv = getattr(args, "solv", None)
solc_args = getattr(args, "solc_args", None)
disassembler = MythrilDisassembler(
eth=config.eth,
solc_version=solv,
solc_settings_json=solc_json,
solc_args=solc_args,
)
address = load_code(disassembler, args)
execute_command(
disassembler=disassembler, address=address, parser=parser, args=args
)
except CriticalError as ce:
exit_with_error(getattr(args, "outform", "text"), str(ce))
except Exception:
exit_with_error(getattr(args, "outform", "text"), traceback.format_exc())
if __name__ == "__main__":
main()
================================================
FILE: mythril/interfaces/epic.py
================================================
"""Don't ask."""
#!/usr/bin/env python
#
# "THE BEER-WARE LICENSE" (Revision 43~maze)
#
# wrote these files. As long as you retain this notice you
# can do whatever you want with this stuff. If we meet some day, and you think
# this stuff is worth it, you can buy me a beer in return.
# https://github.com/tehmaze/lolcat
import argparse
import atexit
import math
import os
import random
import re
import sys
import time
PY3 = sys.version_info >= (3,)
# Reset terminal colors at exit
def reset():
""""""
sys.stdout.write("\x1b[0m")
sys.stdout.flush()
atexit.register(reset)
STRIP_ANSI = re.compile(r"\x1b\[(\d+)(;\d+)?(;\d+)?[m|K]")
COLOR_ANSI = (
(0x00, 0x00, 0x00),
(0xCD, 0x00, 0x00),
(0x00, 0xCD, 0x00),
(0xCD, 0xCD, 0x00),
(0x00, 0x00, 0xEE),
(0xCD, 0x00, 0xCD),
(0x00, 0xCD, 0xCD),
(0xE5, 0xE5, 0xE5),
(0x7F, 0x7F, 0x7F),
(0xFF, 0x00, 0x00),
(0x00, 0xFF, 0x00),
(0xFF, 0xFF, 0x00),
(0x5C, 0x5C, 0xFF),
(0xFF, 0x00, 0xFF),
(0x00, 0xFF, 0xFF),
(0xFF, 0xFF, 0xFF),
)
class LolCat(object):
"""Cats lel."""
def __init__(self, mode=256, output=sys.stdout):
self.mode = mode
self.output = output
def _distance(self, rgb1, rgb2):
return sum(map(lambda c: (c[0] - c[1]) ** 2, zip(rgb1, rgb2)))
def ansi(self, rgb):
"""
:param rgb:
:return:
"""
r, g, b = rgb
if self.mode in (8, 16):
colors = COLOR_ANSI[: self.mode]
matches = [
(self._distance(c, map(int, rgb)), i) for i, c in enumerate(colors)
]
matches.sort()
color = matches[0][1]
return "3%d" % (color,)
else:
gray_possible = True
sep = 2.5
while gray_possible:
if r < sep or g < sep or b < sep:
gray = r < sep and g < sep and b < sep
gray_possible = False
sep += 42.5
if gray:
color = 232 + int(float(sum(rgb) / 33.0))
else:
color = sum(
[16]
+ [
int(6 * float(val) / 256) * mod
for val, mod in zip(rgb, [36, 6, 1])
]
)
return "38;5;%d" % (color,)
def wrap(self, *codes):
"""
:param codes:
:return:
"""
return "\x1b[%sm" % ("".join(codes),)
def rainbow(self, freq, i):
"""
:param freq:
:param i:
:return:
"""
r = math.sin(freq * i) * 127 + 128
g = math.sin(freq * i + 2 * math.pi / 3) * 127 + 128
b = math.sin(freq * i + 4 * math.pi / 3) * 127 + 128
return [r, g, b]
def cat(self, fd, options):
"""
:param fd:
:param options:
"""
if options.animate:
self.output.write("\x1b[?25l")
for line in fd:
options.os += 1
self.println(line, options)
if options.animate:
self.output.write("\x1b[?25h")
def println(self, s, options):
"""
:param s:
:param options:
"""
s = s.rstrip()
if options.force or self.output.isatty():
s = STRIP_ANSI.sub("", s)
if options.animate:
self.println_ani(s, options)
else:
self.println_plain(s, options)
self.output.write("\n")
self.output.flush()
def println_ani(self, s, options):
"""
:param s:
:param options:
:return:
"""
if not s:
return
for _ in range(1, options.duration):
self.output.write("\x1b[%dD" % (len(s),))
self.output.flush()
options.os += options.spread
self.println_plain(s, options)
time.sleep(1.0 / options.speed)
def println_plain(self, s, options):
"""
:param s:
:param options:
"""
for i, c in enumerate(s if PY3 else s.decode(options.charset_py2, "replace")):
rgb = self.rainbow(options.freq, options.os + i / options.spread)
self.output.write(
"".join(
[
self.wrap(self.ansi(rgb)),
c if PY3 else c.encode(options.charset_py2, "replace"),
]
)
)
def detect_mode(term_hint="xterm-256color"):
"""Poor-mans color mode detection."""
if "ANSICON" in os.environ:
return 16
elif os.environ.get("ConEmuANSI", "OFF") == "ON":
return 256
else:
term = os.environ.get("TERM", term_hint)
if term.endswith("-256color") or term in ("xterm", "screen"):
return 256
elif term.endswith("-color") or term in ("rxvt",):
return 16
else:
return 256 # optimistic default
def run():
"""Main entry point."""
parser = argparse.ArgumentParser(usage=r"%prog [] [file ...]")
parser.add_argument(
"-p", "--spread", type=float, default=3.0, help="Rainbow spread"
)
parser.add_argument(
"-F", "--freq", type=float, default=0.1, help="Rainbow frequency"
)
parser.add_argument("-S", "--seed", type=int, default=0, help="Rainbow seed")
parser.add_argument(
"-a",
"--animate",
action="store_true",
default=False,
help="Enable psychedelics",
)
parser.add_argument(
"-d", "--duration", type=int, default=12, help="Animation duration"
)
parser.add_argument(
"-s", "--speed", type=float, default=20.0, help="Animation speed"
)
parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force colour even when stdout is not a tty",
)
parser.add_argument(
"-3", action="store_const", dest="mode", const=8, help="Force 3 bit colour mode"
)
parser.add_argument(
"-4",
action="store_const",
dest="mode",
const=16,
help="Force 4 bit colour mode",
)
parser.add_argument(
"-8",
action="store_const",
dest="mode",
const=256,
help="Force 8 bit colour mode",
)
parser.add_argument(
"-c",
"--charset-py2",
default="utf-8",
help="Manually set a charset to convert from, for python 2.7",
)
options = parser.parse_args()
options.os = random.randint(0, 256) if options.seed == 0 else options.seed
options.mode = options.mode or detect_mode()
lolcat = LolCat(mode=options.mode)
args = ["-"]
for filename in args:
if filename == "-":
lolcat.cat(sys.stdin, options)
else:
try:
with open(filename, "r") as handle:
lolcat.cat(handle, options)
except IOError as error:
sys.stderr.write(str(error) + "\n")
if __name__ == "__main__":
sys.exit(run())
================================================
FILE: mythril/laser/__init__.py
================================================
================================================
FILE: mythril/laser/ethereum/__init__.py
================================================
================================================
FILE: mythril/laser/ethereum/call.py
================================================
"""This module contains the business logic used by Instruction in
instructions.py to get the necessary elements from the stack and determine the
parameters for the new global state."""
import logging
import re
from typing import List, Optional, Union, cast
from eth.constants import GAS_CALLSTIPEND
import mythril.laser.ethereum.util as util
from mythril.laser.ethereum import natives
from mythril.laser.ethereum.instruction_data import calculate_native_gas
from mythril.laser.ethereum.natives import PRECOMPILE_COUNT, PRECOMPILE_FUNCTIONS
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.calldata import (
BaseCalldata,
ConcreteCalldata,
SymbolicCalldata,
)
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.util import insert_ret_val
from mythril.laser.smt import BitVec, Expression, If, simplify, symbol_factory
from mythril.support.loader import DynLoader
"""
This module contains the business logic used by Instruction in instructions.py
to get the necessary elements from the stack and determine the parameters for the new global state.
"""
log = logging.getLogger(__name__)
SYMBOLIC_CALLDATA_SIZE = 320 # Used when copying symbolic calldata
def get_call_parameters(
global_state: GlobalState, dynamic_loader: DynLoader, with_value=False
):
"""Gets call parameters from global state Pops the values from the stack
and determines output parameters.
:param global_state: state to look in
:param dynamic_loader: dynamic loader to use
:param with_value: whether to pop the value argument from the stack
:return: callee_account, call_data, value, call_data_type, gas
"""
gas, to = global_state.mstate.pop(2)
value = global_state.mstate.pop() if with_value else 0
(
memory_input_offset,
memory_input_size,
memory_out_offset,
memory_out_size,
) = global_state.mstate.pop(4)
callee_address = get_callee_address(global_state, dynamic_loader, to)
callee_account = None
call_data = get_call_data(global_state, memory_input_offset, memory_input_size)
if isinstance(callee_address, BitVec) or (
isinstance(callee_address, str)
and (int(callee_address, 16) > PRECOMPILE_COUNT or int(callee_address, 16) == 0)
):
callee_account = get_callee_account(
global_state, callee_address, dynamic_loader
)
gas = gas + If(value > 0, symbol_factory.BitVecVal(GAS_CALLSTIPEND, gas.size()), 0)
return (
callee_address,
callee_account,
call_data,
value,
gas,
memory_out_offset,
memory_out_size,
)
def _get_padded_hex_address(address: int) -> str:
hex_address = hex(address)[2:]
return "0x{}{}".format("0" * (40 - len(hex_address)), hex_address)
def get_callee_address(
global_state: GlobalState,
dynamic_loader: DynLoader,
symbolic_to_address: Expression,
):
"""Gets the address of the callee.
:param global_state: state to look in
:param dynamic_loader: dynamic loader to use
:param symbolic_to_address: The (symbolic) callee address
:return: Address of the callee
"""
environment = global_state.environment
try:
callee_address = _get_padded_hex_address(
util.get_concrete_int(symbolic_to_address)
)
except TypeError:
log.debug("Symbolic call encountered")
match = re.search(r"Storage\[(\d+)\]", str(simplify(symbolic_to_address)))
if match is None or dynamic_loader is None:
return symbolic_to_address
index = int(match.group(1))
log.debug("Dynamic contract address at storage index {}".format(index))
# attempt to read the contract address from instance storage
try:
callee_address = dynamic_loader.read_storage(
"0x{:040X}".format(environment.active_account.address.value), index
)
# TODO: verify whether this happens or not
except:
return symbolic_to_address
# testrpc simply returns the address, geth response is more elaborate.
if not re.match(r"^0x[0-9a-f]{40}$", callee_address):
callee_address = "0x" + callee_address[26:]
return callee_address
def get_callee_account(
global_state: GlobalState,
callee_address: Union[str, BitVec],
dynamic_loader: DynLoader,
):
"""Gets the callees account from the global_state.
:param global_state: state to look in
:param callee_address: address of the callee
:param dynamic_loader: dynamic loader to use
:return: Account belonging to callee
"""
if isinstance(callee_address, BitVec):
if callee_address.symbolic:
return Account(callee_address, balances=global_state.world_state.balances)
else:
callee_address = hex(callee_address.value)[2:]
return global_state.world_state.accounts_exist_or_load(
callee_address, dynamic_loader
)
def get_call_data(
global_state: GlobalState,
memory_start: Union[int, BitVec],
memory_size: Union[int, BitVec],
):
"""Gets call_data from the global_state.
:param global_state: state to look in
:param memory_start: Start index
:param memory_size: Size
:return: Tuple containing: call_data array from memory or empty array if symbolic, type found
"""
state = global_state.mstate
transaction_id = "{}_internalcall".format(global_state.current_transaction.id)
memory_start = cast(
BitVec,
(
symbol_factory.BitVecVal(memory_start, 256)
if isinstance(memory_start, int)
else memory_start
),
)
memory_size = cast(
BitVec,
(
symbol_factory.BitVecVal(memory_size, 256)
if isinstance(memory_size, int)
else memory_size
),
)
if memory_size.symbolic:
memory_size = SYMBOLIC_CALLDATA_SIZE
try:
calldata_from_mem = state.memory[
util.get_concrete_int(memory_start) : util.get_concrete_int(
memory_start + memory_size
)
]
return ConcreteCalldata(transaction_id, calldata_from_mem)
except TypeError:
log.debug("Unsupported symbolic memory offset and size")
return SymbolicCalldata(transaction_id)
def native_call(
global_state: GlobalState,
callee_address: Union[str, BitVec],
call_data: BaseCalldata,
memory_out_offset: Union[int, Expression],
memory_out_size: Union[int, Expression],
) -> Optional[List[GlobalState]]:
if isinstance(callee_address, BitVec) or not (
0 < int(callee_address, 16) <= PRECOMPILE_COUNT
):
return None
# Disabled due to issues with how multi contract calls are handled with some older solc versions
"""
if hevm_cheat_code.is_cheat_address(callee_address):
log.info("HEVM cheat code address triggered")
handle_cheat_codes(
global_state, callee_address, call_data, memory_out_offset, memory_out_size
)
return [global_state]
"""
log.debug("Native contract called: " + callee_address)
try:
mem_out_start = util.get_concrete_int(memory_out_offset)
mem_out_sz = util.get_concrete_int(memory_out_size)
except TypeError:
insert_ret_val(global_state)
log.debug("CALL with symbolic start or offset not supported")
return [global_state]
call_address_int = int(callee_address, 16)
native_gas_min, native_gas_max = calculate_native_gas(
global_state.mstate.calculate_extension_size(mem_out_start, mem_out_sz),
PRECOMPILE_FUNCTIONS[call_address_int - 1].__name__,
)
global_state.mstate.min_gas_used += native_gas_min
global_state.mstate.max_gas_used += native_gas_max
global_state.mstate.mem_extend(mem_out_start, mem_out_sz)
try:
data = natives.native_contracts(call_address_int, call_data)
except natives.NativeContractException:
for i in range(mem_out_sz):
global_state.mstate.memory[mem_out_start + i] = global_state.new_bitvec(
PRECOMPILE_FUNCTIONS[call_address_int - 1].__name__
+ "("
+ str(call_data)
+ ")",
8,
)
insert_ret_val(global_state)
return [global_state]
for i in range(
min(len(data), mem_out_sz)
): # If more data is used then it's chopped off
global_state.mstate.memory[mem_out_start + i] = data[i]
insert_ret_val(global_state)
return [global_state]
================================================
FILE: mythril/laser/ethereum/cfg.py
================================================
"""This module."""
from enum import Enum
from typing import TYPE_CHECKING, Dict, List
from flags import Flags
from mythril.laser.ethereum.state.constraints import Constraints
if TYPE_CHECKING:
from mythril.laser.ethereum.state.global_state import GlobalState
class JumpType(Enum):
"""An enum to represent the types of possible JUMP scenarios."""
CONDITIONAL = 1
UNCONDITIONAL = 2
CALL = 3
RETURN = 4
Transaction = 5
class NodeFlags(Flags):
"""A collection of flags to denote the type a call graph node can have."""
def __or__(self, other) -> "NodeFlags":
return super().__or__(other)
FUNC_ENTRY = 1
CALL_RETURN = 2
class Node:
"""The representation of a call graph node."""
def __init__(
self,
contract_name: str,
start_addr=0,
constraints=None,
function_name="unknown",
) -> None:
"""
:param contract_name:
:param start_addr:
:param constraints:
"""
constraints = constraints if constraints else Constraints()
self.contract_name = contract_name
self.start_addr = start_addr
self.states: List[GlobalState] = []
self.constraints = constraints
self.function_name = function_name
self.flags = NodeFlags()
self.uid = hash(self)
def get_cfg_dict(self) -> Dict:
"""
Generate a configuration dictionary for the current state of the contract.
:return: A dictionary containing the contract's configuration details.
"""
code_lines = [
f"{instruction['address']} {instruction['opcode']}"
+ (
f" {instruction['argument']}"
if instruction["opcode"].startswith("PUSH")
and "argument" in instruction
else ""
)
for state in self.states
for instruction in [state.get_current_instruction()]
]
code = "\\n".join(code_lines)
return {
"contract_name": self.contract_name,
"start_addr": self.start_addr,
"function_name": self.function_name,
"code": code,
}
class Edge:
"""The representation of a call graph edge."""
def __init__(
self,
node_from: int,
node_to: int,
edge_type=JumpType.UNCONDITIONAL,
condition=None,
) -> None:
"""
:param node_from:
:param node_to:
:param edge_type:
:param condition:
"""
self.node_from = node_from
self.node_to = node_to
self.type = edge_type
self.condition = condition
def __str__(self) -> str:
"""
:return:
"""
return str(self.as_dict)
@property
def as_dict(self) -> Dict[str, int]:
"""
:return:
"""
return {"from": self.node_from, "to": self.node_to}
================================================
FILE: mythril/laser/ethereum/cheat_code.py
================================================
from typing import Union
from mythril.laser.ethereum.state.calldata import (
BaseCalldata,
)
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.util import insert_ret_val
from mythril.laser.smt import BitVec, Expression
class hevm_cheat_code:
# https://github.com/dapphub/ds-test/blob/cd98eff28324bfac652e63a239a60632a761790b/src/test.sol
address = 0x7109709ECFA91A80626FF3989D68F67F5B1DD12D
fail_payload = int(
"70ca10bb"
+ "0000000000000000000000007109709ecfa91a80626ff3989d68f67f5b1dd12d"
+ "6661696c65640000000000000000000000000000000000000000000000000000"
+ "0000000000000000000000000000000000000000000000000000000000000001",
16,
)
assume_sig = 0x4C63E562
@staticmethod
def is_cheat_address(address):
if int(address, 16) == int("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D", 16):
return True
if int(address, 16) == int("0x72c68108a82e82617b93d1be0d7975d762035015", 16):
return True
return False
def handle_cheat_codes(
global_state: GlobalState,
callee_address: Union[str, BitVec],
call_data: BaseCalldata,
memory_out_offset: Union[int, Expression],
memory_out_size: Union[int, Expression],
):
insert_ret_val(global_state)
pass
================================================
FILE: mythril/laser/ethereum/evm_exceptions.py
================================================
"""This module contains EVM exception types used by LASER."""
class VmException(Exception):
"""The base VM exception type."""
pass
class StackUnderflowException(IndexError, VmException):
"""A VM exception regarding stack underflows."""
pass
class StackOverflowException(VmException):
"""A VM exception regarding stack overflows."""
pass
class InvalidJumpDestination(VmException):
"""A VM exception regarding JUMPs to invalid destinations."""
pass
class InvalidInstruction(VmException):
"""A VM exception denoting an invalid op code has been encountered."""
pass
class OutOfGasException(VmException):
"""A VM exception denoting the current execution has run out of gas."""
pass
class WriteProtection(VmException):
"""A VM exception denoting that a write operation is executed on a write protected environment"""
pass
================================================
FILE: mythril/laser/ethereum/function_managers/__init__.py
================================================
from .exponent_function_manager import exponent_function_manager
from .keccak_function_manager import KeccakFunctionManager, keccak_function_manager
================================================
FILE: mythril/laser/ethereum/function_managers/exponent_function_manager.py
================================================
import logging
from typing import Tuple
from mythril.laser.smt import (
And,
BitVec,
Function,
URem,
symbol_factory,
)
from mythril.laser.smt import (
SMTBool as Bool,
)
log = logging.getLogger(__name__)
class ExponentFunctionManager:
"""
Uses an uninterpreted function for exponentiation with the following properties:
1) power(a, b) > 0
2) if a = 256 => forall i if b = i then power(a, b) = (256 ^ i) % (2^256)
Only these two properties are added as to handle indexing of boolean arrays.
Caution should be exercised when increasing the conditions since it severely affects
the solving time.
"""
def __init__(self):
power = Function("Power", [256, 256], 256)
NUMBER_256 = symbol_factory.BitVecVal(256, 256)
self.concrete_constraints = And(
*[
power(NUMBER_256, symbol_factory.BitVecVal(i, 256))
== symbol_factory.BitVecVal(256**i, 256)
for i in range(0, 32)
]
)
def create_condition(self, base: BitVec, exponent: BitVec) -> Tuple[BitVec, Bool]:
"""
Creates a condition for exponentiation
:param base: The base of exponentiation
:param exponent: The exponent of the exponentiation
:return: Tuple of condition and the exponentiation result
"""
power = Function("Power", [256, 256], 256)
exponentiation = power(base, exponent)
if exponent.symbolic is False and base.symbolic is False:
const_exponentiation = symbol_factory.BitVecVal(
pow(base.value, exponent.value, 2**256),
256,
annotations=base.annotations.union(exponent.annotations),
)
constraint = const_exponentiation == exponentiation
return const_exponentiation, constraint
constraint = exponentiation > 0
constraint = And(constraint, self.concrete_constraints)
if base.value == 256:
constraint = And(
constraint,
power(base, URem(exponent, symbol_factory.BitVecVal(32, 256)))
== power(base, exponent),
)
return exponentiation, constraint
exponent_function_manager = ExponentFunctionManager()
================================================
FILE: mythril/laser/ethereum/function_managers/keccak_function_manager.py
================================================
import logging
from typing import Dict, List, Optional, Tuple
from mythril.laser.smt import (
ULE,
ULT,
And,
BitVec,
Function,
Or,
URem,
symbol_factory,
)
from mythril.laser.smt import (
SMTBool as Bool,
)
from mythril.support.support_utils import sha3
TOTAL_PARTS = 10**40
PART = (2**256 - 1) // TOTAL_PARTS
INTERVAL_DIFFERENCE = 10**30
log = logging.getLogger(__name__)
class KeccakFunctionManager:
"""
A bunch of uninterpreted functions are considered like keccak256_160 ,...
where keccak256_160 means the input of keccak256() is 160 bit number.
the range of these functions are constrained to some mutually disjoint intervals
All the hashes modulo 64 are 0 as we need a spread among hashes for array type data structures
All the functions are kind of one to one due to constraint of the existence of inverse
for each encountered input.
For more info https://files.sri.inf.ethz.ch/website/papers/sp20-verx.pdf
"""
hash_matcher = "fffffff" # This is usually the prefix for the hash in the output
def __init__(self):
self.store_function: Dict[int, Tuple[Function, Function]] = {}
self.interval_hook_for_size: Dict[int, int] = {}
self._index_counter = TOTAL_PARTS - 34534
self.hash_result_store: Dict[int, List[BitVec]] = {}
self.quick_inverse: Dict[BitVec, BitVec] = {} # This is for VMTests
self.concrete_hashes: Dict[BitVec, BitVec] = {}
self.symbolic_inputs: Dict[int, List[BitVec]] = {}
def reset(self):
self.store_function = {}
self.interval_hook_for_size = {}
self.hash_result_store: Dict[int, List[BitVec]] = {}
self.quick_inverse = {}
self.concrete_hashes = {}
self.symbolic_inputs = {}
@staticmethod
def find_concrete_keccak(data: BitVec) -> BitVec:
"""
Calculates concrete keccak
:param data: input bitvecval
:return: concrete keccak output
"""
keccak = symbol_factory.BitVecVal(
int.from_bytes(
sha3(data.value.to_bytes(data.size() // 8, byteorder="big")), "big"
),
256,
)
return keccak
def get_function(self, length: int) -> Tuple[Function, Function]:
"""
Returns the keccak functions for the corresponding length
:param length: input size
:return: tuple of keccak and it's inverse
"""
try:
func, inverse = self.store_function[length]
except KeyError:
func = Function("keccak256_{}".format(length), [length], 256)
inverse = Function("keccak256_{}-1".format(length), [256], length)
self.store_function[length] = (func, inverse)
self.hash_result_store[length] = []
return func, inverse
@staticmethod
def get_empty_keccak_hash() -> BitVec:
"""
returns sha3("")
:return:
"""
val = 89477152217924674838424037953991966239322087453347756267410168184682657981552
return symbol_factory.BitVecVal(val, 256)
def create_keccak(self, data: BitVec) -> BitVec:
"""
Creates Keccak of the data
:param data: input
:return: Tuple of keccak and the condition it should satisfy
"""
length = data.size()
func, _ = self.get_function(length)
if data.symbolic is False:
concrete_hash = self.find_concrete_keccak(data)
self.concrete_hashes[data] = concrete_hash
return concrete_hash
if length not in self.symbolic_inputs:
self.symbolic_inputs[length] = []
self.symbolic_inputs[length].append(data)
self.hash_result_store[length].append(func(data))
return func(data)
def create_conditions(self) -> Bool:
condition = symbol_factory.Bool(True)
for inputs_list in self.symbolic_inputs.values():
for symbolic_input in inputs_list:
condition = And(
condition, self._create_condition(func_input=symbolic_input)
)
for concrete_input, concrete_hash in self.concrete_hashes.items():
func, inverse = self.get_function(concrete_input.size())
condition = And(
condition,
func(concrete_input) == concrete_hash,
inverse(func(concrete_input)) == concrete_input,
)
return condition
def get_concrete_hash_data(self, model) -> Dict[int, List[Optional[int]]]:
"""
returns concrete values of hashes in the self.hash_result_store
:param model: The z3 model to query for concrete values
:return: A dictionary with concrete hashes { : [, ]}
"""
concrete_hashes: Dict[int, List[Optional[int]]] = {}
for size in self.hash_result_store:
concrete_hashes[size] = []
for val in self.hash_result_store[size]:
eval_ = model.eval(val.raw)
try:
concrete_val = eval_.as_long()
concrete_hashes[size].append(concrete_val)
except AttributeError:
continue
return concrete_hashes
def _create_condition(self, func_input: BitVec) -> Bool:
"""
Creates the constraints for hash
:param func_input: input of the hash
:return: condition
"""
length = func_input.size()
func, inv = self.get_function(length)
try:
index = self.interval_hook_for_size[length]
except KeyError:
self.interval_hook_for_size[length] = self._index_counter
index = self._index_counter
self._index_counter -= INTERVAL_DIFFERENCE
lower_bound = index * PART
upper_bound = lower_bound + PART
cond = And(
inv(func(func_input)) == func_input,
ULE(symbol_factory.BitVecVal(lower_bound, 256), func(func_input)),
ULT(func(func_input), symbol_factory.BitVecVal(upper_bound, 256)),
URem(func(func_input), symbol_factory.BitVecVal(64, 256)) == 0,
)
concrete_cond = symbol_factory.Bool(False)
for key, keccak in self.concrete_hashes.items():
if key.size() == func_input.size():
hash_eq = And(func(func_input) == keccak, key == func_input)
concrete_cond = Or(concrete_cond, hash_eq)
return And(inv(func(func_input)) == func_input, Or(cond, concrete_cond))
keccak_function_manager = KeccakFunctionManager()
================================================
FILE: mythril/laser/ethereum/instruction_data.py
================================================
from typing import Tuple
from eth._utils.numeric import ceil32
from eth.constants import (
GAS_ECRECOVER,
GAS_IDENTITY,
GAS_IDENTITYWORD,
GAS_RIPEMD160,
GAS_RIPEMD160WORD,
GAS_SHA3,
GAS_SHA3WORD,
GAS_SHA256,
GAS_SHA256WORD,
)
from mythril.support.opcodes import GAS, OPCODES, STACK
def calculate_sha3_gas(length: int):
"""
:param length:
:return:
"""
gas_val = GAS_SHA3 + GAS_SHA3WORD * (ceil32(length) // 32)
return gas_val, gas_val
def calculate_native_gas(size: int, contract: str):
"""
:param size:
:param contract:
:return:
"""
gas_value = 0
word_num = ceil32(size) // 32
if contract == "ecrecover":
gas_value = GAS_ECRECOVER
elif contract == "sha256":
gas_value = GAS_SHA256 + word_num * GAS_SHA256WORD
elif contract == "ripemd160":
gas_value = GAS_RIPEMD160 + word_num * GAS_RIPEMD160WORD
elif contract == "identity":
gas_value = GAS_IDENTITY + word_num * GAS_IDENTITYWORD
else:
# TODO: Add gas for other precompiles, computation should be shifted to natives.py
# as some data isn't available here
pass
return gas_value, gas_value
def get_opcode_gas(opcode: str) -> Tuple[int, int]:
return OPCODES[opcode][GAS]
def get_required_stack_elements(opcode: str) -> int:
return OPCODES[opcode][STACK][0]
================================================
FILE: mythril/laser/ethereum/instructions.py
================================================
"""This module contains a representation class for EVM instructions and
transitions between them."""
import logging
from copy import copy, deepcopy
from typing import Callable, List, Tuple, Union, cast
import mythril.laser.ethereum.util as helper
from mythril.disassembler.disassembly import Disassembly
from mythril.exceptions import UnsatError
from mythril.laser.ethereum import util
from mythril.laser.ethereum.call import (
SYMBOLIC_CALLDATA_SIZE,
get_call_data,
get_call_parameters,
native_call,
)
from mythril.laser.ethereum.evm_exceptions import (
InvalidInstruction,
InvalidJumpDestination,
OutOfGasException,
StackUnderflowException,
VmException,
WriteProtection,
)
from mythril.laser.ethereum.function_managers import (
exponent_function_manager,
keccak_function_manager,
)
from mythril.laser.ethereum.instruction_data import calculate_sha3_gas, get_opcode_gas
from mythril.laser.ethereum.state.calldata import ConcreteCalldata, SymbolicCalldata
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.return_data import ReturnData
from mythril.laser.ethereum.transaction import (
ContractCreationTransaction,
MessageCallTransaction,
TransactionStartSignal,
tx_id_manager,
)
from mythril.laser.smt import (
UGE,
UGT,
ULT,
BitVec,
Concat,
Expression,
Extract,
If,
LShR,
Not,
SRem,
UDiv,
URem,
is_false,
simplify,
symbol_factory,
)
from mythril.laser.smt import (
SMTBool as Bool,
)
from mythril.support.loader import DynLoader
from mythril.support.model import get_model
from mythril.support.support_utils import get_code_hash
log = logging.getLogger(__name__)
TT256 = symbol_factory.BitVecVal(0, 256)
TT256M1 = symbol_factory.BitVecVal(2**256 - 1, 256)
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.world_state.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):
"""Decorator that handles global state copy and original return.
This decorator calls the decorated instruction mutator function on a
copy of the state that is passed to it. After the call, the
resulting new states' program counter is automatically incremented
if `increment_pc=True`.
"""
def __init__(
self, increment_pc=True, enable_gas=True, is_state_mutation_instruction=False
):
"""
:param increment_pc:
:param enable_gas:
:param is_state_mutation_instruction: The function mutates state
"""
self.increment_pc = increment_pc
self.enable_gas = enable_gas
self.is_state_mutation_instruction = is_state_mutation_instruction
@staticmethod
def call_on_state_copy(func: Callable, func_obj: "Instruction", state: GlobalState):
"""
:param func:
:param func_obj:
:param state:
:return:
"""
global_state_copy = copy(state)
return func(func_obj, global_state_copy)
def increment_states_pc(self, states: List[GlobalState]) -> List[GlobalState]:
"""
:param states:
:return:
"""
if self.increment_pc:
for state in states:
state.mstate.pc += 1
return states
@staticmethod
def check_gas_usage_limit(global_state: GlobalState):
"""
:param global_state:
:return:
"""
global_state.mstate.check_gas()
if isinstance(global_state.current_transaction.gas_limit, BitVec):
value = global_state.current_transaction.gas_limit.value
if value is None:
return
global_state.current_transaction.gas_limit = value
if (
global_state.mstate.min_gas_used
>= global_state.current_transaction.gas_limit
):
raise OutOfGasException()
def accumulate_gas(self, global_state: GlobalState):
"""
:param global_state:
:return:
"""
if not self.enable_gas:
return global_state
opcode = global_state.instruction["opcode"]
min_gas, max_gas = get_opcode_gas(opcode)
global_state.mstate.min_gas_used += min_gas
global_state.mstate.max_gas_used += max_gas
self.check_gas_usage_limit(global_state)
return global_state
def __call__(self, func: Callable) -> Callable:
def wrapper(
func_obj: "Instruction", global_state: GlobalState
) -> List[GlobalState]:
"""
:param func_obj:
:param global_state:
: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.accumulate_gas(state) for state in new_global_states
]
return self.increment_states_pc(new_global_states)
return wrapper
class Instruction:
"""Instruction class is used to mutate a state according to the current
instruction."""
def __init__(
self,
op_code: str,
dynamic_loader: DynLoader,
pre_hooks: List[Callable] = None,
post_hooks: List[Callable] = None,
) -> None:
"""
:param op_code:
:param dynamic_loader:
:param iprof:
"""
self.dynamic_loader = dynamic_loader
self.op_code = op_code.upper()
self.pre_hook = pre_hooks if pre_hooks else []
self.post_hook = post_hooks if post_hooks else []
def _execute_pre_hooks(self, global_state: GlobalState):
for hook in self.pre_hook:
hook(global_state)
def _execute_post_hooks(self, global_state: GlobalState):
for hook in self.post_hook:
hook(global_state)
def evaluate(self, global_state: GlobalState, post=False) -> List[GlobalState]:
"""Performs the mutation for this instruction.
:param global_state:
:param post:
:return:
"""
# Generalize some ops
log.debug("Evaluating %s at %i", self.op_code, global_state.mstate.pc)
op = self.op_code.lower()
if self.op_code.startswith("PUSH"):
op = "push"
elif self.op_code.startswith("DUP"):
op = "dup"
elif self.op_code.startswith("SWAP"):
op = "swap"
elif self.op_code.startswith("LOG"):
op = "log"
instruction_mutator = (
getattr(self, op + "_", None)
if not post
else getattr(self, op + "_" + "post", None)
)
if instruction_mutator is None:
raise NotImplementedError
self._execute_pre_hooks(global_state)
result = instruction_mutator(global_state)
self._execute_post_hooks(global_state)
return result
@StateTransition()
def jumpdest_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
return [global_state]
@StateTransition()
def push_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
push_instruction = global_state.get_current_instruction()
push_value = push_instruction.get("argument", 0)
try:
length_of_value = 2 * int(push_instruction["opcode"][4:])
except ValueError:
raise VmException("Invalid Push instruction")
if length_of_value == 0:
global_state.mstate.stack.append(symbol_factory.BitVecVal(0, 256))
elif isinstance(push_value, tuple):
if isinstance(push_value[0], int):
new_value = symbol_factory.BitVecVal(push_value[0], 8)
else:
new_value = push_value[0]
if len(push_value) > 1:
for val in push_value[1:]:
if isinstance(val, int):
new_value = Concat(new_value, symbol_factory.BitVecVal(val, 8))
else:
new_value = Concat(new_value, val)
pad_length = length_of_value // 2 - len(push_value)
if pad_length > 0:
new_value = Concat(new_value, symbol_factory.BitVecVal(0, pad_length))
if new_value.size() < 256:
new_value = Concat(
symbol_factory.BitVecVal(0, 256 - new_value.size()), new_value
)
global_state.mstate.stack.append(new_value)
else:
push_value += "0" * max(length_of_value - (len(push_value) - 2), 0)
global_state.mstate.stack.append(
symbol_factory.BitVecVal(int(push_value, 16), 256)
)
return [global_state]
@StateTransition()
def dup_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
value = int(global_state.get_current_instruction()["opcode"][3:], 10)
global_state.mstate.stack.append(global_state.mstate.stack[-value])
return [global_state]
@StateTransition()
def swap_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
depth = int(self.op_code[4:])
stack = global_state.mstate.stack
stack[-depth - 1], stack[-1] = stack[-1], stack[-depth - 1]
return [global_state]
@StateTransition()
def pop_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.pop()
return [global_state]
@StateTransition()
def and_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
stack = global_state.mstate.stack
op1, op2 = stack.pop(), stack.pop()
if isinstance(op1, Bool):
op1 = If(
op1, symbol_factory.BitVecVal(1, 256), symbol_factory.BitVecVal(0, 256)
)
if isinstance(op2, Bool):
op2 = If(
op2, symbol_factory.BitVecVal(1, 256), symbol_factory.BitVecVal(0, 256)
)
if not isinstance(op1, Expression):
op1 = symbol_factory.BitVecVal(op1, 256)
if not isinstance(op2, Expression):
op2 = symbol_factory.BitVecVal(op2, 256)
stack.append(op1 & op2)
return [global_state]
@StateTransition()
def or_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
stack = global_state.mstate.stack
op1, op2 = stack.pop(), stack.pop()
if isinstance(op1, Bool):
op1 = If(
op1, symbol_factory.BitVecVal(1, 256), symbol_factory.BitVecVal(0, 256)
)
if isinstance(op2, Bool):
op2 = If(
op2, symbol_factory.BitVecVal(1, 256), symbol_factory.BitVecVal(0, 256)
)
stack.append(op1 | op2)
return [global_state]
@StateTransition()
def xor_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
mstate = global_state.mstate
mstate.stack.append(mstate.stack.pop() ^ mstate.stack.pop())
return [global_state]
@StateTransition()
def not_(self, global_state: GlobalState):
"""
:param global_state:
:return:
"""
mstate = global_state.mstate
mstate.stack.append(TT256M1 - mstate.stack.pop())
return [global_state]
@StateTransition()
def byte_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
mstate = global_state.mstate
op0, op1 = mstate.stack.pop(), mstate.stack.pop()
if not isinstance(op1, Expression):
op1 = symbol_factory.BitVecVal(op1, 256)
try:
index = util.get_concrete_int(op0)
offset = (31 - index) * 8
if offset >= 0:
result: Union[int, Expression] = simplify(
Concat(
symbol_factory.BitVecVal(0, 248),
Extract(offset + 7, offset, op1),
)
)
else:
result = 0
except TypeError:
log.debug("BYTE: Unsupported symbolic byte offset")
result = global_state.new_bitvec(
str(simplify(op1)) + "[" + str(simplify(op0)) + "]", 256
)
mstate.stack.append(result)
return [global_state]
# Arithmetic
@StateTransition()
def add_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(
(
helper.pop_bitvec(global_state.mstate)
+ helper.pop_bitvec(global_state.mstate)
)
)
return [global_state]
@StateTransition()
def sub_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(
(
helper.pop_bitvec(global_state.mstate)
- helper.pop_bitvec(global_state.mstate)
)
)
return [global_state]
@StateTransition()
def mul_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(
(
helper.pop_bitvec(global_state.mstate)
* helper.pop_bitvec(global_state.mstate)
)
)
return [global_state]
@StateTransition()
def div_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
op0, op1 = (
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
)
if op1 == 0:
global_state.mstate.stack.append(symbol_factory.BitVecVal(0, 256))
else:
global_state.mstate.stack.append(UDiv(op0, op1))
return [global_state]
@StateTransition()
def sdiv_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
s0, s1 = (
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
)
if s1 == 0:
global_state.mstate.stack.append(symbol_factory.BitVecVal(0, 256))
else:
global_state.mstate.stack.append(s0 / s1)
return [global_state]
@StateTransition()
def mod_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
s0, s1 = (
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
)
global_state.mstate.stack.append(0 if s1 == 0 else URem(s0, s1))
return [global_state]
@StateTransition()
def shl_(self, global_state: GlobalState) -> List[GlobalState]:
shift, value = (
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
)
global_state.mstate.stack.append(value << shift)
return [global_state]
@StateTransition()
def shr_(self, global_state: GlobalState) -> List[GlobalState]:
shift, value = (
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
)
global_state.mstate.stack.append(LShR(value, shift))
return [global_state]
@StateTransition()
def sar_(self, global_state: GlobalState) -> List[GlobalState]:
shift, value = (
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
)
global_state.mstate.stack.append(value >> shift)
return [global_state]
@StateTransition()
def smod_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
s0, s1 = (
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
)
global_state.mstate.stack.append(0 if s1 == 0 else SRem(s0, s1))
return [global_state]
@StateTransition()
def addmod_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
s0, s1, s2 = (
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
)
global_state.mstate.stack.append(URem(URem(s0, s2) + URem(s1, s2), s2))
return [global_state]
@StateTransition()
def mulmod_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
s0, s1, s2 = (
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
util.pop_bitvec(global_state.mstate),
)
global_state.mstate.stack.append(URem(URem(s0, s2) * URem(s1, s2), s2))
return [global_state]
@StateTransition()
def exp_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
base, exponent = util.pop_bitvec(state), util.pop_bitvec(state)
exponentiation, constraint = exponent_function_manager.create_condition(
base, exponent
)
state.stack.append(exponentiation)
global_state.world_state.constraints.append(constraint)
return [global_state]
@StateTransition()
def signextend_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
mstate = global_state.mstate
s0, s1 = mstate.stack.pop(), mstate.stack.pop()
testbit = s0 * symbol_factory.BitVecVal(8, 256) + symbol_factory.BitVecVal(
7, 256
)
set_testbit = symbol_factory.BitVecVal(1, 256) << testbit
sign_bit_set = simplify(Not(s1 & set_testbit == 0))
mstate.stack.append(
simplify(
If(
s0 <= 31,
If(
sign_bit_set, s1 | (TT256 - set_testbit), s1 & (set_testbit - 1)
),
s1,
)
)
)
return [global_state]
# Comparisons
@StateTransition()
def lt_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
exp = ULT(util.pop_bitvec(state), util.pop_bitvec(state))
state.stack.append(exp)
return [global_state]
@StateTransition()
def gt_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
op1, op2 = util.pop_bitvec(state), util.pop_bitvec(state)
exp = UGT(op1, op2)
state.stack.append(exp)
return [global_state]
@StateTransition()
def slt_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
exp = util.pop_bitvec(state) < util.pop_bitvec(state)
state.stack.append(exp)
return [global_state]
@StateTransition()
def sgt_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
exp = util.pop_bitvec(state) > util.pop_bitvec(state)
state.stack.append(exp)
return [global_state]
@StateTransition()
def eq_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
op1 = state.stack.pop()
op2 = state.stack.pop()
if isinstance(op1, Bool):
op1 = If(
op1, symbol_factory.BitVecVal(1, 256), symbol_factory.BitVecVal(0, 256)
)
if isinstance(op2, Bool):
op2 = If(
op2, symbol_factory.BitVecVal(1, 256), symbol_factory.BitVecVal(0, 256)
)
exp = op1 == op2
state.stack.append(exp)
return [global_state]
@StateTransition()
def iszero_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
val = state.stack.pop()
exp = Not(val) if isinstance(val, Bool) else val == 0
exp = If(
exp, symbol_factory.BitVecVal(1, 256), symbol_factory.BitVecVal(0, 256)
)
state.stack.append(simplify(exp))
return [global_state]
# Call data
@StateTransition()
def callvalue_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
environment = global_state.environment
state.stack.append(environment.callvalue)
return [global_state]
@StateTransition()
def calldataload_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
environment = global_state.environment
op0 = state.stack.pop()
value = environment.calldata.get_word_at(op0)
state.stack.append(value)
return [global_state]
@StateTransition()
def calldatasize_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(global_state.environment.calldata.calldatasize)
return [global_state]
@staticmethod
def _calldata_copy_helper(global_state, mstate, mstart, dstart, size):
environment = global_state.environment
try:
mstart = util.get_concrete_int(mstart)
except TypeError:
log.debug("Unsupported symbolic memory offset in CALLDATACOPY")
return [global_state]
try:
dstart: Union[int, BitVec] = util.get_concrete_int(dstart)
except TypeError:
log.debug("Unsupported symbolic calldata offset in CALLDATACOPY")
dstart = simplify(dstart)
try:
size: Union[int, BitVec] = util.get_concrete_int(size)
except TypeError:
log.debug("Unsupported symbolic size in CALLDATACOPY")
size = SYMBOLIC_CALLDATA_SIZE # The excess size will get overwritten
size = cast(int, size)
if size > 0:
try:
mstate.mem_extend(mstart, size)
except TypeError as e:
log.debug("Memory allocation error: {}".format(e))
mstate.mem_extend(mstart, 1)
mstate.memory[mstart] = global_state.new_bitvec(
"calldata_"
+ str(environment.active_account.contract_name)
+ "["
+ str(dstart)
+ ": + "
+ str(size)
+ "]",
8,
)
return [global_state]
try:
i_data = dstart
new_memory = []
for i in range(size):
value = environment.calldata[i_data]
new_memory.append(value)
i_data = (
i_data + 1
if isinstance(i_data, int)
else simplify(cast(BitVec, i_data) + 1)
)
for i in range(len(new_memory)):
mstate.memory[i + mstart] = new_memory[i]
except IndexError:
log.debug("Exception copying calldata to memory")
mstate.memory[mstart] = global_state.new_bitvec(
"calldata_"
+ str(environment.active_account.contract_name)
+ "["
+ str(dstart)
+ ": + "
+ str(size)
+ "]",
8,
)
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
@StateTransition()
def address_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
environment = global_state.environment
state.stack.append(environment.address)
return [global_state]
@StateTransition()
def balance_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
address = state.stack.pop()
onchain_access = True
if address.symbolic is False:
try:
balance = global_state.world_state.accounts_exist_or_load(
address.value, self.dynamic_loader
).balance()
except ValueError:
onchain_access = False
else:
onchain_access = False
if onchain_access is False:
balance = symbol_factory.BitVecVal(0, 256)
for account in global_state.world_state.accounts.values():
balance = If(address == account.address, account.balance(), balance)
state.stack.append(balance)
return [global_state]
@StateTransition()
def origin_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
environment = global_state.environment
state.stack.append(environment.origin)
return [global_state]
@StateTransition()
def caller_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
environment = global_state.environment
state.stack.append(environment.sender)
return [global_state]
@StateTransition()
def chainid_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(global_state.environment.chainid)
return [global_state]
@StateTransition()
def selfbalance_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
balance = global_state.environment.active_account.balance()
global_state.mstate.stack.append(balance)
return [global_state]
@StateTransition()
def codesize_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
environment = global_state.environment
disassembly = environment.code
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.world_state.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]
@staticmethod
def _sha3_gas_helper(global_state, length):
min_gas, max_gas = calculate_sha3_gas(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)
def sha3_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
index, op1 = state.stack.pop(), state.stack.pop()
try:
length = util.get_concrete_int(op1)
except TypeError:
# Can't access symbolic memory offsets
length = 64
global_state.world_state.constraints.append(op1 == length)
Instruction._sha3_gas_helper(global_state, length)
state.mem_extend(index, length)
data_list = [
b if isinstance(b, BitVec) else symbol_factory.BitVecVal(b, 8)
for b in state.memory[index : index + length]
]
if len(data_list) > 1:
data = simplify(Concat(data_list))
elif len(data_list) == 1:
data = data_list[0]
else:
# TODO: handle finding x where func(x)==func("")
result = keccak_function_manager.get_empty_keccak_hash()
state.stack.append(result)
return [global_state]
result = keccak_function_manager.create_keccak(data)
state.stack.append(result)
return [global_state]
@StateTransition()
def gasprice_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(global_state.environment.gasprice)
return [global_state]
@StateTransition()
def basefee_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(global_state.environment.basefee)
return [global_state]
@StateTransition()
def codecopy_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
memory_offset, code_offset, size = (
global_state.mstate.stack.pop(),
global_state.mstate.stack.pop(),
global_state.mstate.stack.pop(),
)
code = global_state.environment.code.bytecode
if isinstance(code, tuple):
log.debug("unsupported symbolic code for CODECOPY")
return [global_state]
if code.startswith("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(
code=global_state.environment.code.bytecode,
memory_offset=memory_offset,
code_offset=code_offset,
size=size,
op="CODECOPY",
global_state=global_state,
)
@StateTransition()
def extcodesize_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
addr = state.stack.pop()
try:
addr = hex(helper.get_concrete_int(addr))
except TypeError:
log.debug("unsupported symbolic address for EXTCODESIZE")
state.stack.append(global_state.new_bitvec("extcodesize_" + str(addr), 256))
return [global_state]
try:
code = global_state.world_state.accounts_exist_or_load(
addr, self.dynamic_loader
).code.bytecode
except (ValueError, AttributeError) as e:
log.debug("error accessing contract storage due to: " + str(e))
state.stack.append(global_state.new_bitvec("extcodesize_" + str(addr), 256))
return [global_state]
state.stack.append(len(code) // 2)
return [global_state]
@staticmethod
def _code_copy_helper(
code: Union[str, Tuple],
memory_offset: Union[int, BitVec],
code_offset: Union[int, BitVec],
size: Union[int, BitVec],
op: str,
global_state: GlobalState,
) -> List[GlobalState]:
try:
concrete_memory_offset = helper.get_concrete_int(memory_offset)
except TypeError:
log.debug("Unsupported symbolic memory offset in {}".format(op))
return [global_state]
try:
concrete_size = helper.get_concrete_int(size)
global_state.mstate.mem_extend(concrete_memory_offset, concrete_size)
except TypeError:
# except both attribute error and Exception
global_state.mstate.mem_extend(concrete_memory_offset, 1)
global_state.mstate.memory[concrete_memory_offset] = (
global_state.new_bitvec(
"code({})".format(
global_state.environment.active_account.contract_name
),
8,
)
)
return [global_state]
try:
concrete_code_offset = helper.get_concrete_int(code_offset)
except TypeError:
log.debug("Unsupported symbolic code offset in {}".format(op))
global_state.mstate.mem_extend(concrete_memory_offset, concrete_size)
for i in range(concrete_size):
global_state.mstate.memory[concrete_memory_offset + i] = (
global_state.new_bitvec(
"code({})".format(
global_state.environment.active_account.contract_name
),
8,
)
)
return [global_state]
if isinstance(code, str) and code.startswith("0x"):
code = code[2:]
for i in range(concrete_size):
if isinstance(code, str):
if 2 * (concrete_code_offset + i + 1) > len(code):
break
global_state.mstate.memory[concrete_memory_offset + i] = int(
code[
2 * (concrete_code_offset + i) : 2
* (concrete_code_offset + i + 1)
],
16,
)
else:
if (concrete_code_offset + i + 1) > len(code):
break
global_state.mstate.memory[concrete_memory_offset + i] = code[
concrete_code_offset + i
]
return [global_state]
@StateTransition()
def extcodecopy_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
addr, memory_offset, code_offset, size = (
state.stack.pop(),
state.stack.pop(),
state.stack.pop(),
state.stack.pop(),
)
try:
addr = hex(helper.get_concrete_int(addr))
except TypeError:
log.debug("unsupported symbolic address for EXTCODECOPY")
return [global_state]
try:
code = global_state.world_state.accounts_exist_or_load(
addr, self.dynamic_loader
).code.bytecode
except (ValueError, AttributeError) as e:
log.debug("error accessing contract storage due to: " + str(e))
return [global_state]
return self._code_copy_helper(
code=code,
memory_offset=memory_offset,
code_offset=code_offset,
size=size,
op="EXTCODECOPY",
global_state=global_state,
)
@StateTransition()
def extcodehash_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return: List of global states possible, list of size 1 in this case
"""
world_state = global_state.world_state
stack = global_state.mstate.stack
address = Extract(159, 0, stack.pop())
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.bytecode
code_hash = symbol_factory.BitVecVal(int(get_code_hash(code), 16), 256)
stack.append(code_hash)
return [global_state]
@StateTransition()
def returndatacopy_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
memory_offset, return_offset, size = (
state.stack.pop(),
state.stack.pop(),
state.stack.pop(),
)
try:
concrete_memory_offset = helper.get_concrete_int(memory_offset)
except TypeError:
log.debug("Unsupported symbolic memory offset in RETURNDATACOPY")
return [global_state]
try:
concrete_return_offset = helper.get_concrete_int(return_offset)
except TypeError:
log.debug("Unsupported symbolic return offset in RETURNDATACOPY")
return [global_state]
try:
concrete_size = helper.get_concrete_int(size)
except TypeError:
log.debug("Unsupported symbolic max_length offset in RETURNDATACOPY")
return [global_state]
if global_state.last_return_data is None:
return [global_state]
global_state.mstate.mem_extend(concrete_memory_offset, concrete_size)
for i in range(concrete_size):
global_state.mstate.memory[concrete_memory_offset + i] = (
global_state.last_return_data[concrete_return_offset + i]
if concrete_return_offset + i < global_state.last_return_data.size
else 0
)
return [global_state]
@StateTransition()
def returndatasize_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
if global_state.last_return_data:
global_state.mstate.stack.append(global_state.last_return_data.size)
else:
global_state.mstate.stack.append(0)
return [global_state]
@StateTransition()
def blockhash_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
blocknumber = state.stack.pop()
state.stack.append(
global_state.new_bitvec("blockhash_block_" + str(blocknumber), 256)
)
return [global_state]
@StateTransition()
def coinbase_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(global_state.new_bitvec("coinbase", 256))
return [global_state]
@StateTransition()
def timestamp_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(global_state.new_bitvec("timestamp", 256))
return [global_state]
@StateTransition()
def number_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(global_state.environment.block_number)
return [global_state]
@StateTransition()
def difficulty_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(
global_state.new_bitvec("block_difficulty", 256)
)
return [global_state]
@StateTransition()
def gaslimit_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(global_state.mstate.gas_limit)
return [global_state]
# Memory operations
@StateTransition()
def mload_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
offset = state.stack.pop()
state.mem_extend(offset, 32)
data = state.memory.get_word_at(offset)
state.stack.append(data)
return [global_state]
@StateTransition()
def mstore_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
mstart, value = state.stack.pop(), state.stack.pop()
try:
state.mem_extend(mstart, 32)
except Exception:
log.debug("Error extending memory")
state.memory.write_word_at(mstart, value)
return [global_state]
@StateTransition()
def mstore8_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
offset, value = state.stack.pop(), state.stack.pop()
state.mem_extend(offset, 1)
try:
value_to_write: Union[int, BitVec] = util.get_concrete_int(value) % 256
except TypeError: # BitVec
value_to_write = Extract(7, 0, value)
state.memory[offset] = value_to_write
return [global_state]
@StateTransition()
def sload_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
index = state.stack.pop()
state.stack.append(global_state.environment.active_account.storage[index])
return [global_state]
@StateTransition(is_state_mutation_instruction=True)
def sstore_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
index, value = state.stack.pop(), state.stack.pop()
global_state.environment.active_account.storage[index] = value
return [global_state]
@StateTransition()
def tload_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
address = global_state.environment.active_account.address
index = state.stack.pop()
value = global_state.world_state.transient_storage.get(address, index)
state.stack.append(value)
return [global_state]
@StateTransition(is_state_mutation_instruction=True)
def tstore_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
address = global_state.environment.active_account.address
index, value = state.stack.pop(), state.stack.pop()
global_state.world_state.transient_storage.set(address, index, value)
return [global_state]
@StateTransition(increment_pc=False, enable_gas=False)
def jump_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
disassembly = global_state.environment.code
try:
jump_addr = util.get_concrete_int(state.stack.pop())
except TypeError:
raise InvalidJumpDestination("Invalid jump argument (symbolic address)")
except IndexError:
raise StackUnderflowException()
index = util.get_instruction_index(disassembly.instruction_list, jump_addr)
if index is None:
raise InvalidJumpDestination("JUMP to invalid address")
op_code = disassembly.instruction_list[index]["opcode"]
if op_code != "JUMPDEST":
raise InvalidJumpDestination(
"Skipping JUMP to invalid destination (not JUMPDEST): " + str(jump_addr)
)
new_state = copy(global_state)
# add JUMP gas cost
min_gas, max_gas = get_opcode_gas("JUMP")
new_state.mstate.min_gas_used += min_gas
new_state.mstate.max_gas_used += max_gas
# manually set PC to destination
new_state.mstate.pc = index
return [new_state]
@StateTransition(increment_pc=False, enable_gas=False)
def jumpi_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
state = global_state.mstate
disassembly = global_state.environment.code
min_gas, max_gas = get_opcode_gas("JUMPI")
states = []
op0, condition = state.stack.pop(), state.stack.pop()
try:
jump_addr = util.get_concrete_int(op0)
except TypeError:
log.debug("Skipping JUMPI to invalid destination.")
global_state.mstate.pc += 1
global_state.mstate.min_gas_used += min_gas
global_state.mstate.max_gas_used += max_gas
return [global_state]
# False case
negated = (
simplify(Not(condition)) if isinstance(condition, Bool) else condition == 0
)
negated.simplify()
# True case
condi = simplify(condition) if isinstance(condition, Bool) else condition != 0
condi.simplify()
negated_cond = (isinstance(negated, bool) and negated) or (
isinstance(negated, Bool) and not is_false(negated)
)
positive_cond = (isinstance(condi, bool) and condi) or (
isinstance(condi, Bool) and not is_false(condi)
)
if negated_cond:
# States have to be deep copied during a fork as summaries assume independence across states.
new_state = deepcopy(global_state)
# add JUMPI gas cost
new_state.mstate.min_gas_used += min_gas
new_state.mstate.max_gas_used += max_gas
# manually increment PC
new_state.mstate.depth += 1
new_state.mstate.pc += 1
new_state.world_state.constraints.append(negated)
states.append(new_state)
else:
log.debug("Pruned unreachable states.")
# Get jump destination
index = util.get_instruction_index(disassembly.instruction_list, jump_addr)
if index is None:
log.debug("Invalid jump destination: " + str(jump_addr))
return states
instr = disassembly.instruction_list[index]
if instr["opcode"] == "JUMPDEST":
if positive_cond:
new_state = deepcopy(global_state)
# add JUMPI gas cost
new_state.mstate.min_gas_used += min_gas
new_state.mstate.max_gas_used += max_gas
# manually set PC to destination
new_state.mstate.pc = index
new_state.mstate.depth += 1
new_state.world_state.constraints.append(condi)
states.append(new_state)
else:
log.debug("Pruned unreachable states.")
return states
@StateTransition()
def pc_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
index = global_state.mstate.pc
program_counter = global_state.environment.code.instruction_list[index][
"address"
]
global_state.mstate.stack.append(symbol_factory.BitVecVal(program_counter, 256))
return [global_state]
@StateTransition()
def msize_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
global_state.mstate.stack.append(global_state.mstate.memory_size)
return [global_state]
@StateTransition()
def gas_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
# TODO: Push a Constrained variable which lies between min gas and max gas
global_state.mstate.stack.append(global_state.new_bitvec("gas", 256))
return [global_state]
@StateTransition(is_state_mutation_instruction=True)
def log_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
# TODO: implement me
state = global_state.mstate
depth = int(self.op_code[3:])
state.stack.pop(), state.stack.pop()
_ = [state.stack.pop() for _ in range(depth)]
# Not supported
return [global_state]
def _create_transaction_helper(
self, global_state, call_value, mem_offset, mem_size, create2_salt=None
) -> List[GlobalState]:
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: List[int] = []
code_end = call_data.size
size = call_data.size
if isinstance(size, BitVec):
# Other size restriction checks handle this
if size.symbolic:
size = 10**4
else:
size = size.value
code_raw = []
constraints = global_state.world_state.constraints
try:
model = get_model(constraints)
except UnsatError:
model = None
if isinstance(call_data, ConcreteCalldata):
for element in call_data.concrete(model):
if isinstance(element, BitVec) and element.symbolic:
break
if isinstance(element, BitVec):
code_raw.append(element.value)
else:
code_raw.append(element)
if len(code_raw) < 1:
global_state.mstate.stack.append(1)
log.debug("No code found for trying to execute a create type instruction.")
return [global_state]
code_str = bytes.hex(bytes(code_raw))
next_transaction_id = tx_id_manager.get_next_tx_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: Union[BitVec, int] = None
Instruction._sha3_gas_helper(global_state, len(code_str[2:]) // 2)
if create2_salt:
if create2_salt.symbolic:
if create2_salt.size() != 256:
pad = symbol_factory.BitVecVal(0, 256 - create2_salt.size())
create2_salt = Concat(pad, create2_salt)
address = keccak_function_manager.create_keccak(
Concat(
symbol_factory.BitVecVal(255, 8),
caller,
create2_salt,
symbol_factory.BitVecVal(int(get_code_hash(code_str), 16), 256),
)
)
contract_address = Extract(255, 96, address)
else:
salt = hex(create2_salt.value)[2:]
salt = "0" * (64 - len(salt)) + salt
addr = hex(caller.value)[2:]
addr = "0" * (40 - len(addr)) + addr
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,
)
log.info("Raise transaction start signal")
raise TransactionStartSignal(transaction, self.op_code, global_state)
@StateTransition(is_state_mutation_instruction=True)
def create_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
call_value, mem_offset, mem_size = global_state.mstate.pop(3)
return self._create_transaction_helper(
global_state, call_value, mem_offset, mem_size
)
@StateTransition()
def create_post(self, global_state: GlobalState) -> List[GlobalState]:
return self._handle_create_type_post(global_state)
@StateTransition(is_state_mutation_instruction=True)
def create2_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
call_value, mem_offset, mem_size, salt = global_state.mstate.pop(4)
return self._create_transaction_helper(
global_state, call_value, mem_offset, mem_size, salt
)
@StateTransition()
def create2_post(self, global_state: GlobalState) -> List[GlobalState]:
return self._handle_create_type_post(global_state, opcode="create2")
@staticmethod
def _handle_create_type_post(global_state, opcode="create"):
if opcode == "create2":
global_state.mstate.pop(4)
else:
global_state.mstate.pop(3)
if global_state.last_return_data:
return_val = symbol_factory.BitVecVal(
int(global_state.last_return_data.return_data, 16), 256
)
else:
return_val = symbol_factory.BitVecVal(0, 256)
global_state.mstate.stack.append(return_val)
return [global_state]
@StateTransition()
def return_(self, global_state: GlobalState):
"""
:param global_state:
"""
state = global_state.mstate
offset, length = state.stack.pop(), state.stack.pop()
if length.symbolic:
return_data = [global_state.new_bitvec("return_data", 8)]
log.debug("Return with symbolic length or offset. Not supported")
else:
state.mem_extend(offset, length)
StateTransition.check_gas_usage_limit(global_state)
return_data = state.memory[offset : offset + length]
global_state.current_transaction.end(
global_state, ReturnData(return_data, length)
)
@StateTransition(is_state_mutation_instruction=True)
def selfdestruct_(self, global_state: GlobalState):
"""
:param global_state:
"""
target = global_state.mstate.stack.pop()
transfer_amount = global_state.environment.active_account.balance()
# Often the target of the selfdestruct instruction will be symbolic
# If it isn't then we'll transfer the balance to the indicated contract
global_state.world_state.balances[target] += transfer_amount
global_state.environment.active_account = deepcopy(
global_state.environment.active_account
)
global_state.accounts[global_state.environment.active_account.address.value] = (
global_state.environment.active_account
)
global_state.environment.active_account.set_balance(0)
global_state.environment.active_account.deleted = True
global_state.current_transaction.end(global_state)
@StateTransition()
def revert_(self, global_state: GlobalState) -> None:
"""
:param global_state:
"""
state = global_state.mstate
offset, length = state.stack.pop(), state.stack.pop()
if length.symbolic is False:
return_data = [
global_state.new_bitvec(
f"{global_state.current_transaction.id}_return_data_{i}", 8
)
for i in range(length.value)
]
else:
return_data = [
If(
i < length,
global_state.new_bitvec(
f"{global_state.current_transaction.id}_return_data_{i}", 8
),
0,
)
for i in range(300)
]
try:
return_data = state.memory[
util.get_concrete_int(offset) : util.get_concrete_int(offset + length)
]
except TypeError:
log.debug("Return with symbolic length or offset. Not supported")
global_state.current_transaction.end(
global_state, return_data=ReturnData(return_data, length), revert=True
)
@StateTransition()
def assert_fail_(self, global_state: GlobalState):
"""
:param global_state:
"""
# 0xfe: designated invalid opcode
raise InvalidInstruction
@StateTransition()
def invalid_(self, global_state: GlobalState):
"""
:param global_state:
"""
raise InvalidInstruction
@StateTransition()
def stop_(self, global_state: GlobalState):
"""
:param global_state:
"""
global_state.current_transaction.end(global_state)
@staticmethod
def _write_symbolic_returndata(
global_state: GlobalState, memory_out_offset: BitVec, memory_out_size: BitVec
):
"""
Writes symbolic return-data into memory, The memory offset and size should be concrete
:param global_state:
:param memory_out_offset:
:param memory_out_size:
:return:
"""
if memory_out_offset.symbolic is True or memory_out_size.symbolic is True:
return
return_data = []
return_data_size = global_state.new_bitvec("returndatasize", 256)
for i in range(memory_out_size.value):
data = global_state.new_bitvec(
"call_output_var({})_{}".format(
simplify(memory_out_offset + i), global_state.mstate.pc
),
8,
)
return_data.append(data)
global_state.mstate.mem_extend(memory_out_offset, memory_out_size)
for i in range(memory_out_size.value):
global_state.mstate.memory[memory_out_offset + i] = If(
i <= return_data_size,
return_data[i],
global_state.mstate.memory[memory_out_offset + i],
)
global_state.last_return_data = ReturnData(
return_data=return_data, return_data_size=return_data_size
)
@StateTransition()
def call_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
instr = global_state.get_current_instruction()
environment = global_state.environment
memory_out_size, memory_out_offset = global_state.mstate.stack[-7:-5]
try:
(
callee_address,
callee_account,
call_data,
value,
gas,
memory_out_offset,
memory_out_size,
) = 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 = environment.active_account.address
receiver = callee_account.address
transfer_ether(global_state, sender, receiver, value)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
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
)
)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
# TODO: decide what to do in this case
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
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.world_state.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(
global_state, callee_address, call_data, memory_out_offset, memory_out_size
)
if native_result:
return native_result
transaction = MessageCallTransaction(
world_state=global_state.world_state,
gas_price=environment.gasprice,
gas_limit=gas,
origin=environment.origin,
caller=environment.active_account.address,
callee_account=callee_account,
call_data=call_data,
call_value=value,
static=environment.static,
)
raise TransactionStartSignal(transaction, self.op_code, global_state)
@StateTransition()
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
memory_out_size, memory_out_offset = global_state.mstate.stack[-7:-5]
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)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
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
)
)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
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
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:
:return:
"""
instr = global_state.get_current_instruction()
memory_out_size, memory_out_offset = global_state.mstate.stack[-7:-5]
try:
(
_,
_,
_,
_,
_,
memory_out_offset,
memory_out_size,
) = get_call_parameters(global_state, self.dynamic_loader, True)
except ValueError as e:
log.debug(
"Could not determine required parameters for call, putting fresh symbol on the stack. \n{}".format(
e
)
)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
if global_state.last_return_data is None:
# Put return value on stack
return_value = global_state.new_bitvec(
"retval_" + str(instr["address"]), 256
)
global_state.mstate.stack.append(return_value)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
global_state.world_state.constraints.append(return_value == 0)
return [global_state]
try:
memory_out_offset = (
util.get_concrete_int(memory_out_offset)
if isinstance(memory_out_offset, Expression)
else memory_out_offset
)
memory_out_size = (
util.get_concrete_int(memory_out_size)
if isinstance(memory_out_size, Expression)
else memory_out_size
)
except TypeError:
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
# Copy memory
global_state.mstate.mem_extend(
memory_out_offset, min(memory_out_size, global_state.last_return_data.size)
)
if global_state.last_return_data.size.symbolic:
ret_size = 500
else:
ret_size = global_state.last_return_data.size.value
for i in range(min(memory_out_size, ret_size)):
global_state.mstate.memory[i + memory_out_offset] = (
global_state.last_return_data[i]
)
# Put return value on stack
return_value = global_state.new_bitvec("retval_" + str(instr["address"]), 256)
global_state.mstate.stack.append(return_value)
global_state.world_state.constraints.append(return_value == 1)
return [global_state]
@StateTransition()
def delegatecall_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
instr = global_state.get_current_instruction()
environment = global_state.environment
memory_out_size, memory_out_offset = global_state.mstate.stack[-6:-4]
try:
(
callee_address,
callee_account,
call_data,
value,
gas,
_,
_,
) = get_call_parameters(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)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
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
)
)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
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
transaction = MessageCallTransaction(
world_state=global_state.world_state,
gas_price=environment.gasprice,
gas_limit=gas,
origin=environment.origin,
code=callee_account.code,
caller=environment.sender,
callee_account=environment.active_account,
call_data=call_data,
call_value=environment.callvalue,
static=environment.static,
)
raise TransactionStartSignal(transaction, self.op_code, global_state)
@StateTransition()
def delegatecall_post(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
instr = global_state.get_current_instruction()
memory_out_size, memory_out_offset = global_state.mstate.stack[-6:-4]
try:
(
_,
_,
_,
_,
_,
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)
)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
return [global_state]
if global_state.last_return_data is None:
# Put return value on stack
return_value = global_state.new_bitvec(
"retval_" + str(instr["address"]), 256
)
global_state.mstate.stack.append(return_value)
global_state.world_state.constraints.append(return_value == 0)
return [global_state]
try:
memory_out_offset = (
util.get_concrete_int(memory_out_offset)
if isinstance(memory_out_offset, Expression)
else memory_out_offset
)
memory_out_size = (
util.get_concrete_int(memory_out_size)
if isinstance(memory_out_size, Expression)
else memory_out_size
)
except TypeError:
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
# Copy memory
global_state.mstate.mem_extend(
memory_out_offset, min(memory_out_size, global_state.last_return_data.size)
)
if global_state.last_return_data.size.symbolic:
ret_size = 500
else:
ret_size = global_state.last_return_data.size.value
for i in range(min(memory_out_size, ret_size)):
global_state.mstate.memory[i + memory_out_offset] = (
global_state.last_return_data[i]
)
# Put return value on stack
return_value = global_state.new_bitvec("retval_" + str(instr["address"]), 256)
global_state.mstate.stack.append(return_value)
global_state.world_state.constraints.append(return_value == 1)
return [global_state]
@StateTransition()
def staticcall_(self, global_state: GlobalState) -> List[GlobalState]:
"""
:param global_state:
:return:
"""
instr = global_state.get_current_instruction()
environment = global_state.environment
memory_out_size, memory_out_offset = global_state.mstate.stack[-6:-4]
try:
(
callee_address,
callee_account,
call_data,
value,
gas,
memory_out_offset,
memory_out_size,
) = get_call_parameters(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)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
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
)
)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
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
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=callee_account,
call_data=call_data,
call_value=value,
static=True,
)
raise TransactionStartSignal(transaction, self.op_code, global_state)
@StateTransition()
def staticcall_post(self, global_state: GlobalState) -> List[GlobalState]:
return self.post_handler(global_state, function_name="staticcall")
def post_handler(self, global_state, function_name: str):
instr = global_state.get_current_instruction()
if function_name in ("staticcall", "delegatecall"):
memory_out_size, memory_out_offset = global_state.mstate.stack[-6:-4]
else:
memory_out_size, memory_out_offset = global_state.mstate.stack[-7:-5]
try:
with_value = function_name != "staticcall"
(
_,
_,
_,
_,
_,
memory_out_offset,
memory_out_size,
) = get_call_parameters(global_state, self.dynamic_loader, with_value)
except ValueError as e:
log.debug(
"Could not determine required parameters for {}, putting fresh symbol on the stack. \n{}".format(
function_name, e
)
)
self._write_symbolic_returndata(
global_state, memory_out_offset, memory_out_size
)
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
if global_state.last_return_data is None:
# Put return value on stack
return_value = global_state.new_bitvec(
"retval_" + str(instr["address"]), 256
)
global_state.mstate.stack.append(return_value)
return [global_state]
try:
memory_out_offset = (
util.get_concrete_int(memory_out_offset)
if isinstance(memory_out_offset, Expression)
else memory_out_offset
)
memory_out_size = (
util.get_concrete_int(memory_out_size)
if isinstance(memory_out_size, Expression)
else memory_out_size
)
except TypeError:
global_state.mstate.stack.append(
global_state.new_bitvec("retval_" + str(instr["address"]), 256)
)
return [global_state]
global_state.mstate.mem_extend(
memory_out_offset, min(memory_out_size, global_state.last_return_data.size)
)
if global_state.last_return_data.size.symbolic:
ret_size = 500
else:
ret_size = global_state.last_return_data.size.value
for i in range(min(memory_out_size, ret_size)):
global_state.mstate.memory[i + memory_out_offset] = (
global_state.last_return_data[i]
)
# Put return value on stack
return_value = global_state.new_bitvec(
"retval_" + str(global_state.get_current_instruction()["address"]), 256
)
global_state.mstate.stack.append(return_value)
global_state.world_state.constraints.append(return_value == 1)
return [global_state]
================================================
FILE: mythril/laser/ethereum/natives.py
================================================
"""This nodule defines helper functions to deal with native calls."""
import hashlib
import logging
from typing import List
import blake2b
import coincurve
import py_ecc.optimized_bn128 as bn128
from eth._utils.blake2.coders import extract_blake2b_parameters
from eth._utils.bn128 import validate_point
from eth_utils import ValidationError, big_endian_to_int, int_to_big_endian
from py_ecc.secp256k1 import N as secp256k1n
from py_ecc.secp256k1 import ecdsa_raw_recover
from rlp.utils import ALL_BYTES
from mythril.laser.ethereum.state.calldata import BaseCalldata, ConcreteCalldata
from mythril.laser.ethereum.util import extract32, extract_copy
from mythril.support.support_utils import sha3, zpad
log = logging.getLogger(__name__)
def encode_int32(v):
return v.to_bytes(32, byteorder="big")
def safe_ord(value):
if isinstance(value, int):
return value
else:
return ord(value)
def int_to_32bytearray(i):
o = [0] * 32
for x in range(32):
o[31 - x] = i & 0xFF
i >>= 8
return o
def ecrecover_to_pub(rawhash, v, r, s):
if hasattr(coincurve, "PublicKey"):
try:
pk = coincurve.PublicKey.from_signature_and_message(
zpad(bytes(int_to_32bytearray(r)), 32)
+ zpad(bytes(int_to_32bytearray(s)), 32)
+ ALL_BYTES[v - 27],
rawhash,
hasher=None,
)
pub = pk.format(compressed=False)[1:]
except BaseException:
pub = b"\x00" * 64
else:
result = ecdsa_raw_recover(rawhash, (v, r, s))
if result:
x, y = result
pub = encode_int32(x) + encode_int32(y)
else:
raise ValueError("Invalid VRS")
assert len(pub) == 64
return pub
class NativeContractException(Exception):
"""An exception denoting an error during a native call."""
pass
def ecrecover(data: List[int]) -> List[int]:
"""
:param data:
:return:
"""
# TODO: Add type hints
try:
bytes_data = bytearray(data)
v = extract32(bytes_data, 32)
r = extract32(bytes_data, 64)
s = extract32(bytes_data, 96)
except TypeError:
raise NativeContractException
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:
return []
try:
pub = ecrecover_to_pub(message, v, r, s)
except Exception as e:
log.debug("An error has occurred while extracting public key: " + str(e))
return []
o = [0] * 12 + [x for x in sha3(pub)[-20:]]
return list(bytearray(o))
def sha256(data: List[int]) -> List[int]:
"""
:param data:
:return:
"""
try:
bytes_data = bytes(data)
except TypeError:
raise NativeContractException
return list(bytearray(hashlib.sha256(bytes_data).digest()))
def ripemd160(data: List[int]) -> List[int]:
"""
:param data:
:return:
"""
try:
bytes_data = bytes(data)
except TypeError:
raise NativeContractException
digest = hashlib.new("ripemd160", bytes_data).digest()
padded = 12 * [0] + list(digest)
return list(bytearray(bytes(padded)))
def identity(data: List[int]) -> List[int]:
"""
:param data:
:return:
"""
return data
def mod_exp(data: List[int]) -> List[int]:
"""
TODO: Some symbolic parts can be handled here
Modular Exponentiation
:param data: Data with
: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))
while first_exp_bytes:
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)
try:
p1 = validate_point(x1, y1)
p2 = validate_point(x2, y2)
except ValidationError:
return []
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)
try:
p = validate_point(x, y)
except ValidationError:
return []
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]
def blake2b_fcompress(data: List[int]) -> List[int]:
"""
blake2b hashing
:param data:
:return:
"""
try:
parameters = extract_blake2b_parameters(bytes(data))
except ValidationError as v:
logging.debug("Invalid blake2b params: {}".format(v))
return []
return list(bytearray(blake2b.compress(*parameters)))
PRECOMPILE_FUNCTIONS = (
ecrecover,
sha256,
ripemd160,
identity,
mod_exp,
ec_add,
ec_mul,
ec_pair,
blake2b_fcompress,
)
PRECOMPILE_COUNT = len(PRECOMPILE_FUNCTIONS)
def native_contracts(address: int, data: BaseCalldata) -> List[int]:
"""Takes integer address 1, 2, 3, 4.
:param address:
:param data:
:return:
"""
if not isinstance(data, ConcreteCalldata):
raise NativeContractException
concrete_data = data.concrete(None)
try:
return PRECOMPILE_FUNCTIONS[address - 1](concrete_data)
except TypeError:
raise NativeContractException
================================================
FILE: mythril/laser/ethereum/state/__init__.py
================================================
# Hello!
================================================
FILE: mythril/laser/ethereum/state/account.py
================================================
"""This module contains account-related functionality.
This includes classes representing accounts and their storage.
"""
import logging
from copy import copy, deepcopy
from typing import Any, Dict, Set, Union
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.smt import Array, BaseArray, BitVec, If, K, simplify, symbol_factory
from mythril.laser.smt import SMTBool as Bool
from mythril.support.support_args import args
log = logging.getLogger(__name__)
class Storage:
"""Storage class represents the storage of an Account."""
def __init__(self, concrete=False, address=None, dynamic_loader=None) -> None:
"""Constructor for Storage.
:param concrete: bool indicating whether to interpret uninitialized storage as concrete versus symbolic
"""
if concrete and args.unconstrained_storage is False:
self._standard_storage: BaseArray = K(256, 256, 0)
else:
self._standard_storage = Array(f"Storage{address}", 256, 256)
self.printable_storage: Dict[BitVec, BitVec] = {}
self.dynld = dynamic_loader
self.storage_keys_loaded: Set[int] = set()
self.address = address
# Stores all keys set in the storage
self.keys_set: Set[BitVec] = set()
# Stores all get keys in the storage
self.keys_get: Set[BitVec] = set()
def __getitem__(self, item: BitVec) -> BitVec:
storage = self._standard_storage
self.keys_get.add(item)
if (
self.address
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.active)
and args.unconstrained_storage is False
):
try:
value = symbol_factory.BitVecVal(
int(
self.dynld.read_storage(
contract_address="0x{:040X}".format(self.address.value),
index=int(item.value),
),
16,
),
256,
)
for key in self.keys_set:
value = If(key == item, storage[item], value)
storage[item] = value
self.storage_keys_loaded.add(int(item.value))
self.printable_storage[item] = storage[item]
except ValueError as e:
log.debug("Couldn't read storage at %s: %s", item, e)
return simplify(storage[item])
def __setitem__(self, key, value: Any) -> None:
if isinstance(value, Bool):
value = If(value, 1, 0)
self.printable_storage[key] = value
self._standard_storage[key] = value
self.keys_set.add(key)
if key.symbolic is False:
self.storage_keys_loaded.add(int(key.value))
def __deepcopy__(self, memodict=dict()):
concrete = isinstance(self._standard_storage, K)
storage = Storage(
concrete=concrete, address=self.address, dynamic_loader=self.dynld
)
storage._standard_storage = deepcopy(self._standard_storage)
storage.printable_storage = copy(self.printable_storage)
storage.storage_keys_loaded = copy(self.storage_keys_loaded)
storage.keys_set = deepcopy(self.keys_set)
storage.keys_get = deepcopy(self.keys_get)
return storage
def __str__(self) -> str:
# TODO: Do something better here
return str(self.printable_storage)
class Account:
"""Account class representing ethereum accounts."""
def __init__(
self,
address: Union[BitVec, str],
code=None,
contract_name=None,
balances: Array = None,
concrete_storage=False,
dynamic_loader=None,
nonce=0,
) -> None:
"""Constructor for account.
:param address: Address of the account
:param code: The contract code of the account
:param contract_name: The name associated with the account
:param balances: The balance for the account
:param concrete_storage: Interpret storage as concrete
"""
self.concrete_storage = concrete_storage
self.nonce = nonce
self.code = code or Disassembly("")
self.address = (
address
if isinstance(address, BitVec)
else symbol_factory.BitVecVal(int(address, 16), 256)
)
self.storage = Storage(
concrete_storage, address=self.address, dynamic_loader=dynamic_loader
)
# Metadata
if contract_name is None:
self.contract_name = (
"{0:#0{1}x}".format(self.address.value, 42)
if not self.address.symbolic
else "unknown"
)
else:
self.contract_name = contract_name
self.deleted = False
self._balances = balances
self.balance = lambda: self._balances[self.address]
def __str__(self) -> str:
return str(self.as_dict)
def set_balance(self, balance: Union[int, BitVec]) -> None:
"""
:param balance:
"""
balance = (
symbol_factory.BitVecVal(balance, 256)
if isinstance(balance, int)
else balance
)
assert self._balances is not None
self._balances[self.address] = balance
def set_storage(self, storage: Dict):
"""
Sets concrete storage
"""
for key, value in storage.items():
concrete_key, concrete_value = int(key, 16), int(value, 16)
self.storage[symbol_factory.BitVecVal(concrete_key, 256)] = (
symbol_factory.BitVecVal(concrete_value, 256)
)
def add_balance(self, balance: Union[int, BitVec]) -> None:
"""
:param balance:
"""
balance = (
symbol_factory.BitVecVal(balance, 256)
if isinstance(balance, int)
else balance
)
self._balances[self.address] += balance
@property
def as_dict(self) -> Dict:
"""
:return:
"""
return {
"nonce": self.nonce,
"code": self.serialised_code(),
"balance": self.balance(),
"storage": self.storage,
}
def serialised_code(self):
if isinstance(self.code.bytecode, str):
return self.code.bytecode
new_code = "0x"
for byte in self.code.bytecode:
if isinstance(byte, int):
new_code += hex(byte)
else:
new_code += ""
return new_code
def __copy__(self, memodict={}):
new_account = Account(
address=self.address,
code=self.code,
contract_name=self.contract_name,
balances=deepcopy(self._balances),
concrete_storage=self.concrete_storage,
nonce=self.nonce,
)
new_account.storage = deepcopy(self.storage)
new_account.code = self.code
return new_account
================================================
FILE: mythril/laser/ethereum/state/annotation.py
================================================
"""This module includes classes used for annotating trace information.
This includes the base StateAnnotation class, as well as an adaption,
which will not be copied on every new state.
"""
from abc import abstractmethod
class StateAnnotation:
"""The StateAnnotation class is used to persist information over traces.
This allows modules to reason about traces without the need to
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
def persist_to_world_state(self) -> bool:
"""If this function returns true then laser will also annotate the
world state.
If you want annotations to persist through different user initiated message call transactions
then this should be enabled.
The default is set to False
"""
return False
@property
def persist_over_calls(self) -> bool:
"""If this function returns true then laser will propagate the annotation between calls
The default is set to False
"""
return False
@property
def search_importance(self) -> int:
"""
Used in estimating the priority of a state annotated with the corresponding annotation.
Default is 1
"""
return 1
class MergeableStateAnnotation(StateAnnotation):
"""This class allows a base annotation class for annotations that
can be merged.
"""
@abstractmethod
def check_merge_annotation(self, annotation) -> bool:
pass
@abstractmethod
def merge_annotation(self, annotation):
pass
class NoCopyAnnotation(StateAnnotation):
"""This class provides a base annotation class for annotations that
shouldn't be copied on every new state.
Rather the same object should be propagated. This is very useful if
you are looking to analyze a property over multiple substates
"""
def __copy__(self):
return self
def __deepcopy__(self, _):
return self
================================================
FILE: mythril/laser/ethereum/state/calldata.py
================================================
"""This module declares classes to represent call data."""
from typing import Any, List, Tuple, Union, cast
from z3 import Model, unknown, unsat
from z3.z3types import Z3Exception
from mythril.laser.ethereum.util import get_concrete_int
from mythril.laser.smt import (
Array,
BitVec,
Concat,
Expression,
If,
K,
Solver,
simplify,
symbol_factory,
)
from mythril.laser.smt import (
SMTBool as Bool,
)
class BaseCalldata:
"""Base calldata class This represents the calldata provided when sending a
transaction to a contract."""
def __init__(self, tx_id: str) -> None:
"""
:param tx_id:
"""
self.tx_id = tx_id
@property
def calldatasize(self) -> BitVec:
"""
:return: Calldata size for this calldata object
"""
result = self.size
if isinstance(result, int):
return symbol_factory.BitVecVal(result, 256)
return result
def get_word_at(self, offset: int) -> Expression:
"""Gets word at offset.
:param offset:
:return:
"""
parts = self[offset : offset + 32]
return simplify(Concat(parts))
def __getitem__(self, item: Union[int, slice, BitVec]) -> Any:
"""
:param item:
:return:
"""
if isinstance(item, int) or isinstance(item, Expression):
return self._load(item)
if isinstance(item, slice):
start = 0 if item.start is None else item.start
step = 1 if item.step is None else item.step
stop = self.size if item.stop is None else item.stop
try:
current_index = (
start
if isinstance(start, BitVec)
else symbol_factory.BitVecVal(start, 256)
)
parts = []
while True:
s = Solver()
s.set_timeout(1000)
s.add(current_index != stop)
result = s.check()
if result in (unsat, unknown):
break
element = self._load(current_index)
if not isinstance(element, Expression):
element = symbol_factory.BitVecVal(element, 8)
parts.append(element)
current_index = simplify(current_index + step)
except Z3Exception:
raise IndexError("Invalid Calldata Slice")
return parts
raise ValueError
def _load(self, item: Union[int, BitVec]) -> Any:
"""
:param item:
"""
raise NotImplementedError()
@property
def size(self) -> Union[BitVec, int]:
"""Returns the exact size of this calldata, this is not normalized.
:return: unnormalized call data size
"""
raise NotImplementedError()
def concrete(self, model: Model) -> list:
"""Returns a concrete version of the calldata using the provided model.
:param model:
"""
raise NotImplementedError
class ConcreteCalldata(BaseCalldata):
"""A concrete call data representation."""
def __init__(self, tx_id: str, calldata: list) -> None:
"""Initializes the ConcreteCalldata object.
:param tx_id: Id of the transaction that the calldata is for.
:param calldata: The concrete calldata content
"""
self._concrete_calldata = calldata
self._calldata = K(256, 8, 0)
for i, element in enumerate(calldata, 0):
element = (
symbol_factory.BitVecVal(element, 8)
if isinstance(element, int)
else element
)
self._calldata[symbol_factory.BitVecVal(i, 256)] = element
super().__init__(tx_id)
def _load(self, item: Union[int, BitVec]) -> BitVec:
"""
:param item:
:return:
"""
item = symbol_factory.BitVecVal(item, 256) if isinstance(item, int) else item
return simplify(self._calldata[item])
def concrete(self, model: Model) -> list:
"""
:param model:
:return:
"""
return self._concrete_calldata
@property
def size(self) -> int:
"""
:return:
"""
return len(self._concrete_calldata)
class BasicConcreteCalldata(BaseCalldata):
"""A base class to represent concrete call data."""
def __init__(self, tx_id: str, calldata: list) -> None:
"""Initializes the ConcreteCalldata object, that doesn't use z3 arrays.
:param tx_id: Id of the transaction that the calldata is for.
:param calldata: The concrete calldata content
"""
self._calldata = calldata
super().__init__(tx_id)
def _load(self, item: Union[int, Expression]) -> Any:
"""
:param item:
:return:
"""
if isinstance(item, int):
try:
return self._calldata[item]
except IndexError:
return 0
value = symbol_factory.BitVecVal(0x0, 8)
for i in range(self.size):
value = If(cast(Union[BitVec, Bool], item) == i, self._calldata[i], value)
return value
def concrete(self, model: Model) -> list:
"""
:param model:
:return:
"""
concrete_calldata = []
for data in self._calldata:
if isinstance(data, BitVec) and data.symbolic and model is not None:
concrete_calldata.append(model.eval(data, model_completion=True))
elif isinstance(data, BitVec) and data.symbolic is False:
concrete_calldata.append(data)
else:
break
return concrete_calldata
@property
def size(self) -> int:
"""
:return:
"""
return len(self._calldata)
class SymbolicCalldata(BaseCalldata):
"""A class for representing symbolic call data."""
def __init__(self, tx_id: str) -> None:
"""Initializes the SymbolicCalldata object.
:param tx_id: Id of the transaction that the calldata is for.
"""
self._size = symbol_factory.BitVecSym(str(tx_id) + "_calldatasize", 256)
self._calldata = Array("{}_calldata".format(tx_id), 256, 8)
super().__init__(tx_id)
def _load(self, item: Union[int, BitVec]) -> Any:
"""
:param item:
:return:
"""
item = symbol_factory.BitVecVal(item, 256) if isinstance(item, int) else item
return simplify(
If(
item < self._size,
simplify(self._calldata[cast(BitVec, item)]),
symbol_factory.BitVecVal(0, 8),
)
)
def concrete(self, model: Model) -> list:
"""
:param model:
:return:
"""
concrete_length = model.eval(self.size.raw, model_completion=True).as_long()
result = []
for i in range(concrete_length):
value = self._load(i)
c_value = model.eval(value.raw, model_completion=True).as_long()
result.append(c_value)
return result
@property
def size(self) -> BitVec:
"""
:return:
"""
return self._size
class BasicSymbolicCalldata(BaseCalldata):
"""A basic class representing symbolic call data."""
def __init__(self, tx_id: str) -> None:
"""Initializes the SymbolicCalldata object.
:param tx_id: Id of the transaction that the calldata is for.
"""
self._reads: List[Tuple[Union[int, BitVec], BitVec]] = []
self._size = symbol_factory.BitVecSym(str(tx_id) + "_calldatasize", 256)
super().__init__(tx_id)
def _load(self, item: Union[int, BitVec], clean=False) -> Any:
expr_item: BitVec = (
symbol_factory.BitVecVal(item, 256) if isinstance(item, int) else item
)
symbolic_base_value = If(
expr_item >= self._size,
symbol_factory.BitVecVal(0, 8),
BitVec(
symbol_factory.BitVecSym(
"{}_calldata_{}".format(self.tx_id, str(item)), 8
)
),
)
return_value = symbolic_base_value
for r_index, r_value in self._reads:
return_value = If(r_index == expr_item, r_value, return_value)
if not clean:
self._reads.append((expr_item, symbolic_base_value))
return simplify(return_value)
def concrete(self, model: Model) -> list:
"""
:param model:
:return:
"""
concrete_length = get_concrete_int(model.eval(self.size, model_completion=True))
result = []
for i in range(concrete_length):
value = self._load(i, clean=True)
c_value = get_concrete_int(model.eval(value, model_completion=True))
result.append(c_value)
return result
@property
def size(self) -> BitVec:
"""
:return:
"""
return self._size
================================================
FILE: mythril/laser/ethereum/state/constraints.py
================================================
"""This module contains the class used to represent state-change constraints in
the call graph."""
from copy import copy
from typing import Iterable, List, Optional, Union
from mythril.exceptions import SolverTimeOutException, UnsatError
from mythril.laser.ethereum.function_managers import keccak_function_manager
from mythril.laser.smt import SMTBool as Bool
from mythril.laser.smt import simplify, symbol_factory
from mythril.laser.smt.model import Model
from mythril.support.model import get_model
class Constraints(list):
"""This class should maintain a solver and it's constraints, This class
tries to make the Constraints() object as a simple list of constraints with
some background processing.
"""
def __init__(self, constraint_list: Optional[List[Bool]] = None) -> None:
"""
:param constraint_list: List of constraints
"""
constraint_list = constraint_list or []
constraint_list = self._get_smt_bool_list(constraint_list)
super(Constraints, self).__init__(constraint_list)
def is_possible(self, solver_timeout=None) -> bool:
"""
:param solver_timeout: The default timeout uses analysis timeout from args.solver_timeout
:return: True/False based on the existence of solution of constraints
"""
try:
get_model(self, solver_timeout=solver_timeout)
except SolverTimeOutException:
# If it uses the long analysis solver timeout
if solver_timeout is None:
return False
# If it uses a short custom solver timeout
return True
except UnsatError:
return False
return True
def get_model(self, solver_timeout=None) -> Optional[Model]:
"""
:param solver_timeout: The default timeout uses analysis timeout from args.solver_timeout
:return: True/False based on the existence of solution of constraints
"""
try:
return get_model(self, solver_timeout=solver_timeout)
except SolverTimeOutException:
return None
except UnsatError:
return None
def append(self, constraint: Union[bool, Bool]) -> None:
"""
:param constraint: The constraint to be appended
"""
constraint = (
simplify(constraint)
if isinstance(constraint, Bool)
else symbol_factory.Bool(constraint)
)
super(Constraints, self).append(constraint)
@property
def as_list(self) -> List[Bool]:
"""
:return: returns the list of constraints
"""
return self[:] + [keccak_function_manager.create_conditions()]
def __copy__(self) -> "Constraints":
"""
:return: The copied constraint List
"""
constraint_list = super(Constraints, self).copy()
return Constraints(constraint_list)
def copy(self) -> "Constraints":
return self.__copy__()
def __deepcopy__(self, memodict=None) -> "Constraints":
"""
:param memodict:
:return: The copied constraint List
"""
new_constraints = Constraints()
for constraint in self:
new_constraints.append(copy(constraint))
return new_constraints
def __add__(self, constraints: List[Union[bool, Bool]]) -> "Constraints":
"""
:param constraints:
:return: the new list after the + operation
"""
constraints_list = self._get_smt_bool_list(constraints)
constraints_list = super(Constraints, self).__add__(constraints_list)
return Constraints(constraint_list=constraints_list)
def __iadd__(self, constraints: Iterable[Union[bool, Bool]]) -> "Constraints":
"""
:param constraints:
:return:
"""
list_constraints = self._get_smt_bool_list(constraints)
super(Constraints, self).__iadd__(list_constraints)
return self
@staticmethod
def _get_smt_bool_list(constraints: Iterable[Union[bool, Bool]]) -> List[Bool]:
return [
(
constraint
if isinstance(constraint, Bool)
else symbol_factory.Bool(constraint)
)
for constraint in constraints
]
def get_all_constraints(self):
return self[:] + [keccak_function_manager.create_conditions()]
def __hash__(self):
return tuple(self[:]).__hash__()
================================================
FILE: mythril/laser/ethereum/state/environment.py
================================================
"""This module contains the representation for an execution state's
environment."""
from typing import Dict
from z3 import ExprRef
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.calldata import BaseCalldata
from mythril.laser.smt import symbol_factory
class Environment:
"""The environment class represents the current execution environment for
the symbolic executor."""
def __init__(
self,
active_account: Account,
sender: ExprRef,
calldata: BaseCalldata,
gasprice: ExprRef,
callvalue: ExprRef,
origin: ExprRef,
basefee: ExprRef,
code=None,
static=False,
) -> None:
"""
:param active_account:
:param sender:
:param calldata:
:param gasprice:
:param callvalue:
:param origin:
:param code:
:param static: Makes the environment static.
"""
# Metadata
self.active_account = active_account
self.active_function_name = ""
self.address = active_account.address
# TODO: Add tx_2 > tx_1 then block_no(tx_2) > block_no(tx_1)
self.block_number = symbol_factory.BitVecSym("block_number", 256)
self.chainid = symbol_factory.BitVecSym("chain_id", 256)
# Ib
self.code = active_account.code if code is None else code
self.sender = sender
self.calldata = calldata
self.gasprice = gasprice
self.origin = origin
self.callvalue = callvalue
self.static = static
self.basefee = basefee
def __str__(self) -> str:
"""
:return:
"""
return str(self.as_dict)
@property
def as_dict(self) -> Dict:
"""
:return:
"""
return dict(
active_account=self.active_account,
sender=self.sender,
calldata=self.calldata,
gasprice=self.gasprice,
callvalue=self.callvalue,
origin=self.origin,
)
================================================
FILE: mythril/laser/ethereum/state/global_state.py
================================================
"""This module contains a representation of the global execution state."""
from copy import copy, deepcopy
from typing import TYPE_CHECKING, Dict, Iterable, List, Union
from z3 import BitVec
from mythril.laser.ethereum.cfg import Node
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.machine_state import MachineState
from mythril.laser.smt import symbol_factory
if TYPE_CHECKING:
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction,
MessageCallTransaction,
)
class GlobalState:
"""GlobalState represents the current globalstate."""
def __init__(
self,
world_state: "WorldState",
environment: Environment,
node: Node,
machine_state=None,
transaction_stack=None,
last_return_data=None,
annotations=None,
) -> None:
"""Constructor for GlobalState.
:param world_state:
:param environment:
:param node:
:param machine_state:
:param transaction_stack:
:param last_return_data:
:param annotations:
"""
self.node = node
self.world_state = world_state
self.environment = environment
self.mstate = (
machine_state if machine_state else MachineState(gas_limit=1000000000)
)
self.transaction_stack = transaction_stack if transaction_stack else []
self.op_code = ""
self.last_return_data = last_return_data
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":
"""
:return:
"""
world_state = copy(self.world_state)
environment = copy(self.environment)
mstate = deepcopy(self.mstate)
transaction_stack = copy(self.transaction_stack)
environment.active_account = world_state[environment.active_account.address]
return GlobalState(
world_state,
environment,
self.node,
mstate,
transaction_stack=transaction_stack,
last_return_data=self.last_return_data,
annotations=[copy(a) for a in self._annotations],
)
def __deepcopy__(self, _) -> "GlobalState":
"""
Deepcopy is much slower than copy, since it deepcopies constraints.
:return:
"""
world_state = deepcopy(self.world_state)
environment = copy(self.environment)
mstate = deepcopy(self.mstate)
transaction_stack = copy(self.transaction_stack)
environment.active_account = world_state[environment.active_account.address]
return GlobalState(
world_state,
environment,
self.node,
mstate,
transaction_stack=transaction_stack,
last_return_data=self.last_return_data,
annotations=[copy(a) for a in self._annotations],
)
@property
def accounts(self) -> Dict:
"""
:return:
"""
return self.world_state._accounts
# TODO: remove this, as two instructions are confusing
def get_current_instruction(self) -> Dict:
"""Gets the current instruction for this GlobalState.
:return:
"""
instructions = self.environment.code.instruction_list
try:
return instructions[self.mstate.pc]
except IndexError:
return {"address": self.mstate.pc, "opcode": "STOP"}
@property
def current_transaction(
self,
) -> Union["MessageCallTransaction", "ContractCreationTransaction", None]:
"""
:return:
"""
# TODO: Remove circular to transaction package to import Transaction classes
try:
return self.transaction_stack[-1][0]
except IndexError:
return None
@property
def instruction(self) -> Dict:
"""
:return:
"""
return self.get_current_instruction()
def new_bitvec(self, name: str, size=256, annotations=None) -> BitVec:
"""
:param name:
:param size:
:return:
"""
transaction_id = self.current_transaction.id
return symbol_factory.BitVecSym(
"{}_{}".format(transaction_id, name), size, annotations=annotations
)
def annotate(self, annotation: StateAnnotation) -> None:
"""
:param annotation:
"""
self._annotations.append(annotation)
if annotation.persist_to_world_state:
self.world_state.annotate(annotation)
@property
def annotations(self) -> List[StateAnnotation]:
"""
:return:
"""
return self._annotations
def get_annotations(self, annotation_type: type) -> Iterable[StateAnnotation]:
"""Filters annotations for the queried annotation type. Designed
particularly for modules with annotations:
globalstate.get_annotations(MySpecificModuleAnnotation)
:param annotation_type: The type to filter annotations for
:return: filter of matching annotations
"""
return filter(lambda x: isinstance(x, annotation_type), self.annotations)
================================================
FILE: mythril/laser/ethereum/state/machine_state.py
================================================
"""This module contains a representation of the EVM's machine state and its
stack."""
from copy import copy
from typing import Any, Dict, List, Optional, Sized, Union, cast
from eth._utils.numeric import ceil32
from eth.constants import GAS_MEMORY, GAS_MEMORY_QUADRATIC_DENOMINATOR
from mythril.laser.ethereum.evm_exceptions import (
OutOfGasException,
StackOverflowException,
StackUnderflowException,
)
from mythril.laser.ethereum.state.memory import Memory
from mythril.laser.smt import BitVec, Expression, If, symbol_factory
from mythril.laser.smt import SMTBool as Bool
class MachineStack(list):
"""Defines EVM stack, overrides the default list to handle overflows."""
STACK_LIMIT = 1024
def __init__(self, default_list=None) -> None:
"""
:param default_list:
"""
super(MachineStack, self).__init__(default_list or [])
def append(self, element: Union[int, Expression]) -> None:
"""
This function ensures the following properties when appending to a list:
- Element appended to this list should be a BitVec
- Ensures stack overflow bound
:param element: element to be appended to the list
:function: appends the element to list if the size is less than STACK_LIMIT, else throws an error
"""
if isinstance(element, int):
element = symbol_factory.BitVecVal(element, 256)
if isinstance(element, Bool):
element = If(
element,
symbol_factory.BitVecVal(1, 256),
symbol_factory.BitVecVal(0, 256),
)
if super(MachineStack, self).__len__() >= self.STACK_LIMIT:
raise StackOverflowException(
"Reached the EVM stack limit of {}, you can't append more "
"elements".format(self.STACK_LIMIT)
)
super(MachineStack, self).append(element)
def pop(self, index=-1) -> Union[int, Expression]:
"""
This function ensures stack underflow bound
:param index:index to be popped, same as the list() class.
:returns popped value
:function: same as list() class but throws StackUnderflowException for popping from an empty list
"""
try:
return super(MachineStack, self).pop(index)
except IndexError:
raise StackUnderflowException("Trying to pop from an empty stack")
def __getitem__(self, item: Union[int, slice]) -> Any:
"""
:param item:
:return:
"""
try:
return super(MachineStack, self).__getitem__(item)
except IndexError:
raise StackUnderflowException(
"Trying to access a stack element which doesn't exist"
)
def __add__(self, other):
"""Implement list concatenation if needed.
:param other:
"""
raise NotImplementedError("Implement this if needed")
def __iadd__(self, other):
"""Implement list concatenation if needed.
:param other:
"""
raise NotImplementedError("Implement this if needed")
class MachineState:
"""
MachineState represents current machine state also referenced to as \mu.
"""
def __init__(
self,
gas_limit: int,
pc=0,
stack=None,
subroutine_stack=None,
memory: Optional[Memory] = None,
depth=0,
max_gas_used=0,
min_gas_used=0,
) -> None:
"""Constructor for machineState.
:param gas_limit:
:param pc:
:param stack:
:param memory:
:param depth:
:param max_gas_used:
:param min_gas_used:
"""
self.pc = pc
self.stack = MachineStack(stack)
self.subroutine_stack = MachineStack(subroutine_stack)
self.memory = memory or Memory()
self.gas_limit = gas_limit
self.min_gas_used = min_gas_used # lower gas usage bound
self.max_gas_used = max_gas_used # upper gas usage bound
self.depth = depth
def calculate_extension_size(self, start: int, size: int) -> int:
"""
:param start:
:param size:
:return:
"""
if self.memory_size > start + size:
return 0
# The extension size is calculated based on the word length
new_size = ceil32(start + size) // 32
old_size = self.memory_size // 32
return (new_size - old_size) * 32
def calculate_memory_gas(self, start: int, size: int):
"""
:param start:
:param size:
:return:
"""
# https://github.com/ethereum/pyethereum/blob/develop/ethereum/vm.py#L148
oldsize = self.memory_size // 32
old_totalfee = (
oldsize * GAS_MEMORY + oldsize**2 // GAS_MEMORY_QUADRATIC_DENOMINATOR
)
newsize = ceil32(start + size) // 32
new_totalfee = (
newsize * GAS_MEMORY + newsize**2 // GAS_MEMORY_QUADRATIC_DENOMINATOR
)
return new_totalfee - old_totalfee
def check_gas(self):
"""Check whether the machine is out of gas."""
if self.min_gas_used > self.gas_limit:
raise OutOfGasException()
def mem_extend(self, start: Union[int, BitVec], size: Union[int, BitVec]) -> None:
"""Extends the memory of this machine state.
:param start: Start of memory extension
:param size: Size of memory extension
"""
if (isinstance(start, BitVec) and start.symbolic) or (
isinstance(size, BitVec) and size.symbolic
):
return
if isinstance(start, BitVec):
start = start.value
if isinstance(size, BitVec):
size = size.value
m_extend = self.calculate_extension_size(start, size)
if m_extend:
extend_gas = self.calculate_memory_gas(start, size)
self.min_gas_used += extend_gas
self.max_gas_used += extend_gas
self.check_gas()
self.memory.extend(m_extend)
def memory_write(self, offset: int, data: List[Union[int, BitVec]]) -> None:
"""Writes data to memory starting at offset.
:param offset:
:param data:
"""
self.mem_extend(offset, len(data))
self.memory[offset : offset + len(data)] = data
def pop(self, amount=1) -> Union[BitVec, List[BitVec]]:
"""Pops amount elements from the stack.
:param amount:
:return:
"""
if amount > len(self.stack):
raise StackUnderflowException
values = self.stack[-amount:][::-1]
del self.stack[-amount:]
return values[0] if amount == 1 else values
def __deepcopy__(self, memodict=None):
"""
:param memodict:
:return:
"""
memodict = {} if memodict is None else memodict
return MachineState(
gas_limit=self.gas_limit,
max_gas_used=self.max_gas_used,
min_gas_used=self.min_gas_used,
pc=self.pc,
stack=copy(self.stack),
memory=copy(self.memory),
depth=self.depth,
subroutine_stack=copy(self.subroutine_stack),
)
def __str__(self):
"""
:return:
"""
return str(self.as_dict)
@property
def memory_size(self) -> int:
"""
:return:
"""
return len(cast(Sized, self.memory))
@property
def as_dict(self) -> Dict:
"""
:return:
"""
return dict(
pc=self.pc,
stack=self.stack,
subroutine_stack=self.subroutine_stack,
memory=self.memory,
memsize=self.memory_size,
gas=self.gas_limit,
max_gas_used=self.max_gas_used,
min_gas_used=self.min_gas_used,
)
================================================
FILE: mythril/laser/ethereum/state/memory.py
================================================
"""This module contains a representation of a smart contract's memory."""
from copy import copy
from typing import Dict, List, Union, cast, overload
from z3 import Z3Exception
from mythril.laser.ethereum import util
from mythril.laser.smt import (
BitVec,
Concat,
Extract,
If,
simplify,
symbol_factory,
)
from mythril.laser.smt import (
SMTBool as Bool,
)
def convert_bv(val: Union[int, BitVec]) -> BitVec:
if isinstance(val, BitVec):
return val
return symbol_factory.BitVecVal(val, 256)
# No of iterations to perform when iteration size is symbolic
APPROX_ITR = 100
class Memory:
"""A class representing contract memory with random access."""
def __init__(self):
""""""
self._msize = 0
self._memory: Dict[BitVec, Union[int, BitVec]] = {}
def __len__(self):
"""
:return:
"""
return self._msize
def __copy__(self):
new_memory = Memory()
new_memory._memory = copy(self._memory)
new_memory._msize = self._msize
return new_memory
def extend(self, size: int):
"""
:param size:
"""
self._msize += size
def get_word_at(self, index: int) -> Union[int, BitVec]:
"""Access a word from a specified memory index.
:param index: integer representing the index to access
:return: 32 byte word at the specified index
"""
try:
return symbol_factory.BitVecVal(
util.concrete_int_from_bytes(
bytes([util.get_concrete_int(b) for b in self[index : index + 32]]),
0,
),
256,
)
except TypeError:
result = simplify(
Concat(
[
b if isinstance(b, BitVec) else symbol_factory.BitVecVal(b, 8)
for b in cast(
List[Union[int, BitVec]], self[index : index + 32]
)
]
)
)
assert result.size() == 256
return result
def write_word_at(self, index: int, value: Union[int, BitVec, bool, Bool]) -> None:
"""Writes a 32 byte word to memory at the specified index`
:param index: index to write to
:param value: the value to write to memory
"""
try:
# Attempt to concretize value
if isinstance(value, bool):
_bytes = (
int(1).to_bytes(32, byteorder="big")
if value
else int(0).to_bytes(32, byteorder="big")
)
else:
_bytes = util.concrete_int_to_bytes(value)
assert len(_bytes) == 32
self[index : index + 32] = list(bytearray(_bytes))
except (Z3Exception, AttributeError): # BitVector or BoolRef
value = cast(Union[BitVec, Bool], value)
if isinstance(value, Bool):
value_to_write = If(
value,
symbol_factory.BitVecVal(1, 256),
symbol_factory.BitVecVal(0, 256),
)
else:
value_to_write = value
assert value_to_write.size() == 256
for i in range(0, value_to_write.size(), 8):
self[index + 31 - (i // 8)] = Extract(i + 7, i, value_to_write)
@overload
def __getitem__(self, item: BitVec) -> Union[int, BitVec]: ...
@overload
def __getitem__(self, item: slice) -> List[Union[int, BitVec]]: ...
def __getitem__(
self, item: Union[BitVec, slice]
) -> Union[BitVec, int, List[Union[int, BitVec]]]:
"""
:param item:
:return:
"""
if isinstance(item, slice):
start, step, stop = item.start, item.step, item.stop
if start is None:
start = 0
if stop is None: # 2**256 is just a bit too big
raise IndexError("Invalid Memory Slice")
if step is None:
step = 1
bvstart, bvstop, bvstep = (
convert_bv(start),
convert_bv(stop),
convert_bv(step),
)
ret_lis = []
symbolic_len = False
itr = symbol_factory.BitVecVal(0, 256)
if (bvstop - bvstart).symbolic:
symbolic_len = True
while simplify(bvstep * itr != simplify(bvstop - bvstart)) and (
not symbolic_len or itr <= APPROX_ITR
):
ret_lis.append(self[bvstart + bvstep * itr])
itr += 1
return ret_lis
item = simplify(convert_bv(item))
return self._memory.get(item, 0)
def __setitem__(
self,
key: Union[int, BitVec, slice],
value: Union[BitVec, int, List[Union[int, BitVec]]],
):
"""
:param key:
:param value:
"""
if isinstance(key, slice):
start, step, stop = key.start, key.step, key.stop
if start is None:
start = 0
if stop is None:
raise IndexError("Invalid Memory Slice")
if step is None:
step = 1
else:
assert False, "Currently mentioning step size is not supported"
assert isinstance(value, list)
bvstart, bvstop, bvstep = (
convert_bv(start),
convert_bv(stop),
convert_bv(step),
)
symbolic_len = False
itr = symbol_factory.BitVecVal(0, 256)
if (bvstop - bvstart).symbolic:
symbolic_len = True
while simplify(bvstep * itr != simplify(bvstop - bvstart)) and (
not symbolic_len or itr <= APPROX_ITR
):
self[bvstart + itr * bvstep] = cast(List[Union[int, BitVec]], value)[
itr.value
]
itr += 1
else:
bv_key = simplify(convert_bv(key))
if bv_key >= len(self):
return
if isinstance(value, int):
assert 0 <= value <= 0xFF
if isinstance(value, BitVec):
assert value.size() == 8
self._memory[bv_key] = cast(Union[int, BitVec], value)
================================================
FILE: mythril/laser/ethereum/state/return_data.py
================================================
"""This module declares classes to represent call data."""
from typing import List
from mythril.laser.smt import (
BitVec,
)
class ReturnData:
"""Base returndata class."""
def __init__(self, return_data: List[BitVec], return_data_size: BitVec) -> None:
"""
:param tx_id:
"""
self.return_data = return_data
self.return_data_size = return_data_size
@property
def size(self) -> BitVec:
"""
:return: Calldata size for this calldata object
"""
return self.return_data_size
def __getitem__(self, index):
if index < self.size:
return self.return_data[index]
else:
return 0
================================================
FILE: mythril/laser/ethereum/state/transient_storage.py
================================================
from copy import copy, deepcopy
from mythril.laser.smt import Concat, K, simplify
class TransientStorage:
"""
Implements transient storage using an SMT Array.
This class tracks set operations in a journal and dynamically constructs SMT queries based on the journal entries.
"""
def __init__(self, journal=None):
"""
Initializes the TransientStorage object.
Args:
journal (list): A list to track set operations. Defaults to an empty list.
"""
self.journal = journal or []
def get(self, addr, index):
"""
Constructs and returns an SMT query using the journal.
Args:
addr: Address component of the key.
index: Index component of the key.
Returns:
An SMT query representing the value associated with the given key.
"""
key = Concat(addr, index) # Size: 256 + 256
dynamic_storage = K(512, 256, 0)
# Construct an SMT array based on journal entries
for entry in self.journal:
current_key, current_value = entry["key"], entry["value"]
dynamic_storage[current_key] = current_value
return dynamic_storage[key]
def set(self, addr, index, value):
"""
Logs the set operation in the journal.
Args:
addr: Address component of the key.
index: Index component of the key.
value: Value to be associated with the key.
"""
key = simplify(Concat(addr, index))
self.journal.append({"key": key, "value": value})
def clear(self):
"""
Clears the journal.
This method should be called before user transactions.
"""
self.journal = []
def __copy__(self):
"""
Returns a shallow copy of the TransientStorage object.
"""
return TransientStorage(copy(self.journal))
def __deepcopy__(self):
"""
Returns a deep copy of the TransientStorage object.
"""
return TransientStorage(deepcopy(self.journal))
================================================
FILE: mythril/laser/ethereum/state/world_state.py
================================================
"""This module contains a representation of the EVM's world state."""
from copy import copy, deepcopy
from random import randint
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional
from eth._utils.address import generate_contract_address
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.state.transient_storage import TransientStorage
from mythril.laser.smt import Array, BitVec, symbol_factory
from mythril.support.loader import DynLoader
if TYPE_CHECKING:
from mythril.laser.ethereum.cfg import Node
class WorldState:
"""The WorldState class represents the world state as described in the
yellow paper."""
def __init__(
self,
transaction_sequence=None,
annotations: List[StateAnnotation] = None,
constraints: Constraints = None,
transient_storage: TransientStorage = None,
) -> None:
"""Constructor for the world state. Initializes the accounts record.
:param transaction_sequence:
:param annotations:
"""
self._accounts: Dict[int, Account] = {}
self.balances = Array("balance", 256, 256)
self.starting_balances = deepcopy(self.balances)
self.constraints = constraints or Constraints()
self.node: Optional["Node"] = None
self.transaction_sequence = transaction_sequence or []
self._annotations = annotations or []
self.transient_storage = transient_storage or TransientStorage()
@property
def accounts(self):
return self._accounts
def __getitem__(self, item: BitVec) -> Account:
"""Gets an account from the worldstate using item as key.
:param item: Address of the account to get
:return: Account associated with the address
"""
try:
return self._accounts[item.value]
except KeyError:
new_account = Account(address=item, code=None, balances=self.balances)
self._accounts[item.value] = new_account
return new_account
def __copy__(self) -> "WorldState":
"""
:return:
"""
new_annotations = [copy(a) for a in self._annotations]
new_world_state = WorldState(
transaction_sequence=self.transaction_sequence[:],
annotations=new_annotations,
)
new_world_state.balances = copy(self.balances)
new_world_state.starting_balances = copy(self.starting_balances)
for account in self._accounts.values():
new_world_state.put_account(copy(account))
new_world_state.node = self.node
new_world_state.constraints = copy(self.constraints)
new_world_state.transient_storage = copy(self.transient_storage)
return new_world_state
def __deepcopy__(self, _) -> "WorldState":
"""
:return:
"""
new_annotations = [copy(a) for a in self._annotations]
new_world_state = WorldState(
transaction_sequence=self.transaction_sequence[:],
annotations=new_annotations,
)
new_world_state.balances = copy(self.balances)
new_world_state.starting_balances = copy(self.starting_balances)
for account in self._accounts.values():
new_world_state.put_account(copy(account))
new_world_state.node = self.node
new_world_state.constraints = deepcopy(self.constraints)
new_world_state.transient_storage = copy(self.transient_storage)
return new_world_state
def accounts_exist_or_load(self, addr, dynamic_loader: DynLoader) -> Account:
"""
returns account if it exists, else it loads from the dynamic loader
:param addr: address
:param dynamic_loader: Dynamic Loader
:return: The code
"""
if isinstance(addr, str):
addr = int(addr, 16)
if isinstance(addr, int):
addr_bitvec = symbol_factory.BitVecVal(addr, 256)
elif not isinstance(addr, BitVec):
addr_bitvec = symbol_factory.BitVecVal(int(addr, 16), 256)
else:
addr_bitvec = addr
if addr_bitvec.value in self.accounts:
return self.accounts[addr_bitvec.value]
if dynamic_loader is None:
raise ValueError("dynamic_loader is None")
if dynamic_loader.active is False:
raise ValueError("Dynamic Loader is deactivated. Use a symbol.")
if isinstance(addr, int):
try:
balance = dynamic_loader.read_balance("{0:#0{1}x}".format(addr, 42))
return self.create_account(
balance=balance,
address=addr_bitvec.value,
dynamic_loader=dynamic_loader,
code=dynamic_loader.dynld(addr),
concrete_storage=True,
)
except ValueError:
# Initial balance will be a symbolic variable
pass
try:
code = dynamic_loader.dynld(addr)
except ValueError:
code = None
return self.create_account(
address=addr_bitvec.value, dynamic_loader=dynamic_loader, code=code
)
def create_account(
self,
balance=0,
address=None,
concrete_storage=False,
dynamic_loader=None,
creator=None,
code=None,
nonce=0,
) -> Account:
"""Create non-contract account.
:param address: The account's address
:param balance: Initial balance for the account
:param concrete_storage: Interpret account storage as concrete
:param dynamic_loader: used for dynamically loading storage from the block chain
:param creator: The address of the creator of the contract if it's a contract
:param code: The code of the contract, if it's a contract
:param nonce: Nonce of the account
:return: The new account
"""
if creator in self.accounts:
nonce = self.accounts[creator].nonce
elif creator:
self.create_account(address=creator)
address = (
symbol_factory.BitVecVal(address, 256)
if address is not None
else self._generate_new_address(creator, nonce=self.accounts[creator].nonce)
)
if creator:
self.accounts[creator].nonce += 1
new_account = Account(
address=address,
balances=self.balances,
dynamic_loader=dynamic_loader,
concrete_storage=concrete_storage,
)
if code:
new_account.code = code
new_account.nonce = nonce
if balance is not None:
new_account.set_balance(symbol_factory.BitVecVal(balance, 256))
self.put_account(new_account)
return new_account
def create_initialized_contract_account(self, contract_code, storage) -> None:
"""Creates a new contract account, based on the contract code and
storage provided The contract code only includes the runtime contract
bytecode.
:param contract_code: Runtime bytecode for the contract
:param storage: Initial storage for the contract
:return: The new account
"""
# TODO: Add type hints
new_account = Account(
self._generate_new_address(), code=contract_code, balances=self.balances
)
new_account.storage = storage
self.put_account(new_account)
def annotate(self, annotation: StateAnnotation) -> None:
"""
:param annotation:
"""
self._annotations.append(annotation)
@property
def annotations(self) -> List[StateAnnotation]:
"""
:return:
"""
return self._annotations
def get_annotations(self, annotation_type: type) -> Iterator[StateAnnotation]:
"""Filters annotations for the queried annotation type. Designed
particularly for modules with annotations:
worldstate.get_annotations(MySpecificModuleAnnotation)
:param annotation_type: The type to filter annotations for
:return: filter of matching annotations
"""
return filter(lambda x: isinstance(x, annotation_type), self.annotations)
def _generate_new_address(self, creator=None, nonce=0) -> BitVec:
"""Generates a new address for the global state.
:return:
"""
if creator:
# TODO: Use nounce
address = "0x" + str(generate_contract_address(creator, nonce).hex())
return symbol_factory.BitVecVal(int(address, 16), 256)
while True:
address = "0x" + "".join([str(hex(randint(0, 16)))[-1] for _ in range(40)])
if address not in self._accounts.keys():
return symbol_factory.BitVecVal(int(address, 16), 256)
def put_account(self, account: Account) -> None:
"""
:param account:
"""
self._accounts[account.address.value] = account
account._balances = self.balances
================================================
FILE: mythril/laser/ethereum/strategy/__init__.py
================================================
from abc import ABC, abstractmethod
from typing import List
from mythril.laser.ethereum.state.global_state import GlobalState
class BasicSearchStrategy(ABC):
"""
A basic search strategy which halts based on depth
"""
def __init__(self, work_list, max_depth, **kwargs):
self.work_list: List[GlobalState] = work_list
self.max_depth: int = max_depth
def __iter__(self):
return self
@abstractmethod
def get_strategic_global_state(self):
""""""
raise NotImplementedError("Must be implemented by a subclass")
def run_check(self):
return True
def __next__(self):
try:
global_state = self.get_strategic_global_state()
if global_state.mstate.depth >= self.max_depth:
return self.__next__()
return global_state
except (IndexError, StopIteration):
raise StopIteration
class CriterionSearchStrategy(BasicSearchStrategy):
"""
If a criterion is satisfied, the search halts
"""
def __init__(self, work_list, max_depth, **kwargs):
super().__init__(work_list, max_depth, **kwargs)
self._satisfied_criterion = False
def get_strategic_global_state(self):
if self._satisfied_criterion:
raise StopIteration
try:
return self.get_strategic_global_state()
except StopIteration:
raise StopIteration
def set_criterion_satisfied(self):
self._satisfied_criterion = True
================================================
FILE: mythril/laser/ethereum/strategy/basic.py
================================================
"""This module implements basic symbolic execution search strategies."""
from random import choices, randrange
from mythril.laser.ethereum.state.global_state import GlobalState
from . import BasicSearchStrategy
class DepthFirstSearchStrategy(BasicSearchStrategy):
"""Implements a depth first search strategy I.E.
Follow one path to a leaf, and then continue to the next one
"""
def get_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
return self.work_list.pop()
def view_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
return self.work_list[-1]
class BreadthFirstSearchStrategy(BasicSearchStrategy):
"""Implements a breadth first search strategy I.E.
Execute all states of a "level" before continuing
"""
def get_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
return self.work_list.pop(0)
def view_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
return self.work_list[0]
class ReturnRandomNaivelyStrategy(BasicSearchStrategy):
"""chooses a random state from the worklist with equal likelihood."""
def __init__(self, work_list, max_depth, **kwargs):
super().__init__(work_list, max_depth, **kwargs)
self.previous_random_value = -1
def get_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
if len(self.work_list) > 0:
if self.previous_random_value == -1:
return self.work_list.pop(randrange(len(self.work_list)))
else:
new_state = self.work_list.pop(self.previous_random_value)
self.previous_random_value = -1
return new_state
else:
raise IndexError
def view_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
if len(self.work_list) > 0:
self.previous_random_value = randrange(len(self.work_list))
return self.work_list[self.previous_random_value]
else:
raise IndexError
class ReturnWeightedRandomStrategy(BasicSearchStrategy):
"""chooses a random state from the worklist with likelihood based on
inverse proportion to depth."""
def __init__(self, work_list, max_depth, **kwargs):
super().__init__(work_list, max_depth, **kwargs)
self.previous_random_value = -1
def get_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
probability_distribution = [
1 / (global_state.mstate.depth + 1) for global_state in self.work_list
]
if self.previous_random_value != -1:
ns = self.work_list.pop(self.previous_random_value)
self.previous_random_value = -1
return ns
else:
return self.work_list.pop(
choices(range(len(self.work_list)), probability_distribution)[0]
)
def view_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
probability_distribution = [
1 / (global_state.mstate.depth + 1) for global_state in self.work_list
]
self.previous_random_value = choices(
range(len(self.work_list)), probability_distribution
)[0]
return self.work_list[self.previous_random_value]
================================================
FILE: mythril/laser/ethereum/strategy/beam.py
================================================
from mythril.laser.ethereum.state.global_state import GlobalState
from . import BasicSearchStrategy
class BeamSearch(BasicSearchStrategy):
"""chooses a random state from the worklist with equal likelihood."""
def __init__(self, work_list, max_depth, beam_width, **kwargs):
super().__init__(work_list, max_depth)
self.beam_width = beam_width
@staticmethod
def beam_priority(state):
return sum([annotation.search_importance for annotation in state._annotations])
def sort_and_eliminate_states(self):
self.work_list.sort(key=lambda state: self.beam_priority(state), reverse=True)
del self.work_list[self.beam_width :]
def view_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
self.sort_and_eliminate_states()
if len(self.work_list) > 0:
return self.work_list[0]
else:
raise IndexError
def get_strategic_global_state(self) -> GlobalState:
"""
:return:
"""
self.sort_and_eliminate_states()
if len(self.work_list) > 0:
return self.work_list.pop(0)
else:
raise IndexError
================================================
FILE: mythril/laser/ethereum/strategy/concolic.py
================================================
import logging
import operator
from copy import copy
from functools import reduce
from typing import Any, Dict, List, Tuple, cast
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.constraints import Constraints
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import Not
from . import CriterionSearchStrategy
log = logging.getLogger(__name__)
class TraceAnnotation(StateAnnotation):
"""
This is the annotation used by the ConcolicStrategy to store concolic traces.
"""
def __init__(self, trace=None):
self.trace = trace or []
@property
def persist_over_calls(self) -> bool:
return True
def __copy__(self):
return TraceAnnotation(copy(self.trace))
class ConcolicStrategy(CriterionSearchStrategy):
"""
Executes program concolically using the input trace till a specific branch
"""
def __init__(
self,
work_list: List[GlobalState],
max_depth: int,
trace: List[List[Tuple[int, str]]],
flip_branch_addresses: List[str],
):
"""
work_list: The work-list of states
max_depth: The maximum depth for the strategy to go through
trace: This is the trace to be followed, each element is of the type Tuple(program counter, tx_id)
flip_branch_addresses: Branch addresses to be flipped.
"""
super().__init__(work_list, max_depth)
self.trace: List[Tuple[int, str]] = reduce(operator.iconcat, trace, [])
self.last_tx_count: int = len(trace)
self.flip_branch_addresses: List[str] = flip_branch_addresses
self.results: Dict[str, Dict[str, Any]] = {}
def check_completion_criterion(self):
if len(self.flip_branch_addresses) == len(self.results):
self.set_criterion_satisfied()
def get_strategic_global_state(self) -> GlobalState:
"""
This function does the following:-
1) Choose the states by following the concolic trace.
2) In case we have an executed JUMPI that is in flip_branch_addresses, flip that branch.
:return:
"""
while len(self.work_list) > 0:
state = self.work_list.pop()
seq_id = len(state.world_state.transaction_sequence)
trace_annotations = cast(
List[TraceAnnotation],
list(state.world_state.get_annotations(TraceAnnotation)),
)
if len(trace_annotations) == 0:
annotation = TraceAnnotation()
state.world_state.annotate(annotation)
else:
annotation = trace_annotations[0]
# Appends trace
annotation.trace.append((state.mstate.pc, state.current_transaction.id))
# If length of trace is 1 then it is not a JUMPI
if len(annotation.trace) < 2:
# If trace does not follow the specified path, ignore the state
if annotation.trace != self.trace[: len(annotation.trace)]:
continue
return state
# Get the address of the previous pc
addr: str = str(
state.environment.code.instruction_list[annotation.trace[-2][0]][
"address"
]
)
if (
annotation.trace == self.trace[: len(annotation.trace)]
and seq_id == self.last_tx_count
and addr in self.flip_branch_addresses
and addr not in self.results
):
if (
state.environment.code.instruction_list[annotation.trace[-2][0]][
"opcode"
]
!= "JUMPI"
):
log.error(
f"The branch {addr} does not lead "
"to a jump address, skipping this branch"
)
continue
constraints = Constraints(state.world_state.constraints[:-1])
constraints.append(Not(state.world_state.constraints[-1]))
try:
self.results[addr] = get_transaction_sequence(state, constraints)
except UnsatError:
self.results[addr] = None
elif annotation.trace != self.trace[: len(annotation.trace)]:
continue
self.check_completion_criterion()
return state
raise StopIteration
================================================
FILE: mythril/laser/ethereum/strategy/constraint_strategy.py
================================================
import logging
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.strategy.basic import BasicSearchStrategy
from mythril.support.support_utils import ModelCache
log = logging.getLogger(__name__)
class DelayConstraintStrategy(BasicSearchStrategy):
def __init__(self, work_list, max_depth, **kwargs):
super().__init__(work_list, max_depth)
self.model_cache = ModelCache()
self.pending_worklist = []
log.info("Loaded search strategy extension: DelayConstraintStrategy")
def get_strategic_global_state(self) -> GlobalState:
"""Returns the next state
:return: Global state
"""
while len(self.work_list) == 0:
state = self.pending_worklist.pop(0)
model = state.world_state.constraints.get_model()
if model is not None:
self.model_cache.put(model, 1)
self.work_list.append(state)
state = self.work_list.pop(0)
return state
================================================
FILE: mythril/laser/ethereum/strategy/extensions/__init__.py
================================================
================================================
FILE: mythril/laser/ethereum/strategy/extensions/bounded_loops.py
================================================
import logging
from copy import copy
from typing import Dict, List, cast
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.strategy.basic import BasicSearchStrategy
from mythril.laser.ethereum.transaction import ContractCreationTransaction
log = logging.getLogger(__name__)
class JumpdestCountAnnotation(StateAnnotation):
"""State annotation that counts the number of jumps per destination."""
def __init__(self) -> None:
self._reached_count: Dict[int, int] = {}
self.trace: List[int] = []
def __copy__(self):
result = JumpdestCountAnnotation()
result._reached_count = copy(self._reached_count)
result.trace = copy(self.trace)
return result
class BoundedLoopsStrategy(BasicSearchStrategy):
"""Adds loop pruning to the search strategy.
Ignores JUMPI instruction if the destination was targeted >JUMPDEST_LIMIT times.
"""
def __init__(self, super_strategy: BasicSearchStrategy, **kwargs) -> None:
""""""
self.super_strategy = super_strategy
self.bound = kwargs["loop_bound"]
log.info(
"Loaded search strategy extension: Loop bounds (limit = {})".format(
self.bound
)
)
BasicSearchStrategy.__init__(
self, super_strategy.work_list, super_strategy.max_depth, **kwargs
)
@staticmethod
def calculate_hash(i: int, j: int, trace: List[int]) -> int:
"""
calculate hash(trace[i: j])
:param i:
:param j:
:param trace:
:return: hash(trace[i: j])
"""
key = 0
size = 0
for itr in range(i, j):
key |= trace[itr] << ((itr - i) * 8)
size += 1
return key
@staticmethod
def count_key(trace: List[int], key: int, start: int, size: int) -> int:
"""
Count continuous loops in the trace.
:param trace:
:param key:
:param size:
:return:
"""
count = 1
i = start
while i >= 0:
if BoundedLoopsStrategy.calculate_hash(i, i + size, trace) != key:
break
count += 1
i -= size
return count
@staticmethod
def get_loop_count(trace: List[int]) -> int:
"""
Gets the loop count
:param trace: annotation trace
:return:
"""
found = False
for i in range(len(trace) - 3, 0, -1):
if trace[i] == trace[-2] and trace[i + 1] == trace[-1]:
found = True
break
if found:
key = BoundedLoopsStrategy.calculate_hash(i + 1, len(trace) - 1, trace)
size = len(trace) - i - 2
count = BoundedLoopsStrategy.count_key(trace, key, i + 1, size)
else:
count = 0
return count
def get_strategic_global_state(self) -> GlobalState:
"""Returns the next state
:return: Global state
"""
while True:
state = self.super_strategy.get_strategic_global_state()
annotations = cast(
List[JumpdestCountAnnotation],
list(state.get_annotations(JumpdestCountAnnotation)),
)
if len(annotations) == 0:
annotation = JumpdestCountAnnotation()
state.annotate(annotation)
else:
annotation = annotations[0]
cur_instr = state.get_current_instruction()
annotation.trace.append(cur_instr["address"])
if cur_instr["opcode"].upper() != "JUMPDEST":
return state
# create unique instruction identifier
count = BoundedLoopsStrategy.get_loop_count(annotation.trace)
# The creation transaction gets a higher loop bound to give it a better chance of success.
# TODO: There's probably a nicer way to do this
if isinstance(
state.current_transaction, ContractCreationTransaction
) and count < max(128, self.bound):
return state
elif count > self.bound:
log.debug("Loop bound reached, skipping state")
continue
return state
================================================
FILE: mythril/laser/ethereum/svm.py
================================================
"""This module implements the main symbolic execution engine."""
import logging
import random
from abc import ABCMeta
from collections import defaultdict
from copy import copy
from datetime import datetime, timedelta
from typing import Callable, DefaultDict, Dict, List, Optional, Tuple
from mythril.analysis.potential_issues import check_potential_issues
from mythril.laser.ethereum.cfg import Edge, JumpType, Node, NodeFlags
from mythril.laser.ethereum.evm_exceptions import StackUnderflowException, VmException
from mythril.laser.ethereum.instruction_data import get_required_stack_elements
from mythril.laser.ethereum.instructions import Instruction
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy
from mythril.laser.ethereum.strategy.constraint_strategy import DelayConstraintStrategy
from mythril.laser.ethereum.time_handler import time_handler
from mythril.laser.ethereum.transaction import (
ContractCreationTransaction,
TransactionEndSignal,
TransactionStartSignal,
execute_contract_creation,
execute_message_call,
)
from mythril.laser.execution_info import ExecutionInfo
from mythril.laser.plugin.signals import PluginSkipState, PluginSkipWorldState
from mythril.laser.smt import And, simplify, symbol_factory
from mythril.support.opcodes import OPCODES
from mythril.support.support_args import args
log = logging.getLogger(__name__)
class SVMError(Exception):
"""An exception denoting an unexpected state in symbolic execution."""
pass
class LaserEVM:
"""The LASER EVM.
Just as Mithril had to be mined at great efforts to provide the
Dwarves with their exceptional armour, LASER stands at the heart of
Mythril, digging deep in the depths of call graphs, unearthing the
most precious symbolic call data, that is then hand-forged into
beautiful and strong security issues by the experienced smiths we
call detection modules. It is truly a magnificent symbiosis.
"""
def __init__(
self,
dynamic_loader=None,
max_depth=float("inf"),
execution_timeout=60,
create_timeout=10,
strategy=DepthFirstSearchStrategy,
transaction_count=2,
requires_statespace=True,
iprof=None,
use_reachability_check=True,
beam_width=None,
tx_strategy=None,
) -> None:
"""
Initializes the laser evm object
:param dynamic_loader: Loads data from chain
:param max_depth: Maximum execution depth this vm should execute
:param execution_timeout: Time to take for execution
:param create_timeout: Time to take for contract creation
:param strategy: Execution search strategy
:param transaction_count: The amount of transactions to execute
:param requires_statespace: Variable indicating whether the statespace should be recorded
:param iprof: Instruction Profiler
:param use_reachability_check: Runs reachability check by solving constraints
:param beam_width: The beam width for search strategies
:param tx_strategy: A global tx prioritisation strategy
"""
self.execution_info: List[ExecutionInfo] = []
self.open_states: List[WorldState] = []
self.total_states = 0
self.dynamic_loader = dynamic_loader
self.use_reachability_check = use_reachability_check
self.work_list: List[GlobalState] = []
self.strategy = strategy(self.work_list, max_depth, beam_width=beam_width)
self.max_depth = max_depth
self.transaction_count = transaction_count
self.tx_strategy = tx_strategy
self.execution_timeout = execution_timeout or 0
self.create_timeout = create_timeout or 0
self.requires_statespace = requires_statespace
if self.requires_statespace:
self.nodes: Dict[int, Node] = {}
self.edges: List[Edge] = []
self.time: datetime = None
self.executed_transactions: bool = False
self.pre_hooks: DefaultDict[str, List[Callable]] = defaultdict(list)
self.post_hooks: DefaultDict[str, List[Callable]] = defaultdict(list)
self._add_world_state_hooks: List[Callable] = []
self._execute_state_hooks: List[Callable] = []
self._start_exec_trans_hooks: List[Callable] = []
self._stop_exec_trans_hooks: List[Callable] = []
self._start_sym_trans_hooks: List[Callable] = []
self._stop_sym_trans_hooks: List[Callable] = []
self._start_sym_exec_hooks: List[Callable] = []
self._stop_sym_exec_hooks: List[Callable] = []
self._start_exec_hooks: List[Callable] = []
self._stop_exec_hooks: List[Callable] = []
self._transaction_end_hooks: List[Callable] = []
self.iprof = iprof
self.instr_pre_hook: Dict[str, List[Callable]] = {}
self.instr_post_hook: Dict[str, List[Callable]] = {}
for op in OPCODES:
self.instr_pre_hook[op] = []
self.instr_post_hook[op] = []
self.hook_type_map = {
"start_execute_transactions": self._start_exec_trans_hooks,
"stop_execute_transactions": self._stop_exec_trans_hooks,
"add_world_state": self._add_world_state_hooks,
"execute_state": self._execute_state_hooks,
"start_sym_exec": self._start_sym_exec_hooks,
"stop_sym_exec": self._stop_sym_exec_hooks,
"start_sym_trans": self._start_sym_trans_hooks,
"stop_sym_trans": self._stop_sym_trans_hooks,
"start_exec": self._start_exec_hooks,
"stop_exec": self._stop_exec_hooks,
"transaction_end": self._transaction_end_hooks,
}
log.info("LASER EVM initialized with dynamic loader: " + str(dynamic_loader))
def extend_strategy(self, extension: ABCMeta, **kwargs) -> None:
self.strategy = extension(self.strategy, **kwargs)
def sym_exec(
self,
world_state: WorldState = None,
target_address: int = None,
creation_code: str = None,
contract_name: str = None,
) -> None:
"""Starts symbolic execution
There are two modes of execution.
Either we analyze a preconfigured configuration, in which case the world_state and target_address variables
must be supplied.
Or we execute the creation code of a contract, in which case the creation code and desired name of that
contract should be provided.
:param world_state The world state configuration from which to perform analysis
:param target_address The address of the contract account in the world state which analysis should target
:param creation_code The creation code to create the target contract in the symbolic environment
:param contract_name The name that the created account should be associated with
"""
pre_configuration_mode = target_address is not None
scratch_mode = creation_code is not None and contract_name is not None
if pre_configuration_mode == scratch_mode:
raise ValueError("Symbolic execution started with invalid parameters")
log.debug("Starting LASER execution")
for hook in self._start_sym_exec_hooks:
hook()
time_handler.start_execution(self.execution_timeout)
self.time = datetime.now()
if pre_configuration_mode:
self.open_states = [world_state]
log.info("Starting message call transaction to {}".format(target_address))
self.execute_transactions(symbol_factory.BitVecVal(target_address, 256))
elif scratch_mode:
log.info("Starting contract creation transaction")
created_account = execute_contract_creation(
self, creation_code, contract_name, world_state=world_state
)
log.info(
"Finished contract creation, found {} open states".format(
len(self.open_states)
)
)
if len(self.open_states) == 0:
log.warning(
"No contract was created during the execution of contract creation "
"Increase the resources for creation execution (--max-depth or --create-timeout) "
"Check whether the bytecode is indeed the creation code, otherwise use the --bin-runtime flag"
)
self.execute_transactions(created_account.address)
log.info("Finished symbolic execution")
if self.requires_statespace:
log.info(
"%d nodes, %d edges, %d total states",
len(self.nodes),
len(self.edges),
self.total_states,
)
for hook in self._stop_sym_exec_hooks:
hook()
def execute_transactions(self, address) -> None:
"""This function helps runs plugins that can order transactions.
Such plugins should set self.executed_transactions as True after its execution
:param address: Address of the contract
:return: None
"""
for hook in self._start_exec_trans_hooks:
hook()
if self.tx_strategy is None:
if self.executed_transactions is False:
self.time = datetime.now()
self._execute_transactions_incremental(
address, txs=args.transaction_sequences
)
else:
self.time = datetime.now()
self._execute_transactions_non_ordered(address)
for hook in self._stop_exec_trans_hooks:
hook()
def _execute_transactions_non_ordered(self, address):
"""
This function executes multiple transactions non-incrementally, using some type priority ordering
:param address: Address of the contract
:return:
"""
for txs in self.strategy:
log.info(f"Executing the sequence: {txs}")
self._execute_transactions_incremental(address, txs=txs)
def _execute_transactions_incremental(self, address, txs=None):
"""This function executes multiple transactions incrementally on the address
:param address: Address of the contract
:return:
"""
for i in range(self.transaction_count):
if len(self.open_states) == 0:
break
old_states_count = len(self.open_states)
# Clear journal at every new user transaction
for state in self.open_states:
state.transient_storage.clear()
if self.use_reachability_check:
if isinstance(self.strategy, DelayConstraintStrategy):
open_states = []
for state in self.open_states:
c_val = self.strategy.model_cache.check_quick_sat(
simplify(And(*state.world_state.constraints)).raw
)
if c_val:
open_states.append(self.open_states)
else:
self.strategy.pending_worklist.append(state)
else:
self.open_states = [
state
for state in self.open_states
if state.constraints.is_possible()
]
prune_count = old_states_count - len(self.open_states)
if prune_count:
log.info("Pruned {} unreachable states".format(prune_count))
log.info(
"Starting message call transaction, iteration: {}, {} initial states".format(
i, len(self.open_states)
)
)
func_hashes = txs[i] if txs else None
if func_hashes:
for itr, func_hash in enumerate(func_hashes):
if func_hash in (-1, -2):
func_hashes[itr] = func_hash
else:
func_hashes[itr] = bytes.fromhex(hex(func_hash)[2:].zfill(8))
for hook in self._start_sym_trans_hooks:
hook()
execute_message_call(self, address, func_hashes=func_hashes)
for hook in self._stop_sym_trans_hooks:
hook()
self.executed_transactions = True
def _check_create_termination(self) -> bool:
if len(self.open_states) != 0:
return (
self.create_timeout > 0
and self.time + timedelta(seconds=self.create_timeout) <= datetime.now()
)
return self._check_execution_termination()
def _check_execution_termination(self) -> bool:
return (
self.execution_timeout > 0
and self.time + timedelta(seconds=self.execution_timeout) <= datetime.now()
)
def exec(self, create=False, track_gas=False) -> Optional[List[GlobalState]]:
"""
:param create:
:param track_gas:
:return:
"""
final_states: List[GlobalState] = []
for hook in self._start_exec_hooks:
hook()
for global_state in self.strategy:
if create and self._check_create_termination():
log.debug("Hit create timeout, returning.")
return final_states + [global_state] if track_gas else None
if not create and self._check_execution_termination():
log.debug("Hit execution timeout, returning.")
return final_states + [global_state] if track_gas else None
try:
new_states, op_code = self.execute_state(global_state)
except NotImplementedError:
log.debug("Encountered unimplemented instruction")
continue
if self.strategy.run_check() and (
len(new_states) > 1 and random.uniform(0, 1) < args.pruning_factor
):
new_states = [
state
for state in new_states
if state.world_state.constraints.is_possible()
]
self.manage_cfg(op_code, new_states) # TODO: What about op_code is None?
if new_states:
self.work_list += new_states
elif track_gas:
final_states.append(global_state)
self.total_states += len(new_states)
for hook in self._stop_exec_hooks:
hook()
return final_states if track_gas else None
def _add_world_state(self, global_state: GlobalState):
"""Stores the world_state of the passed global state in the open states"""
for hook in self._add_world_state_hooks:
try:
hook(global_state)
except PluginSkipWorldState:
return
self.open_states.append(global_state.world_state)
def handle_vm_exception(
self, global_state: GlobalState, op_code: str, error_msg: str
) -> List[GlobalState]:
_, return_global_state = global_state.transaction_stack.pop()
if return_global_state is None:
# In this case we don't put an unmodified world state in the open_states list Since in the case of an
# exceptional halt all changes should be discarded, and this world state would not provide us with a
# previously unseen world state
log.debug("Encountered a VmException, ending path: `{}`".format(error_msg))
new_global_states: List[GlobalState] = []
else:
# First execute the post hook for the transaction ending instruction
self._execute_post_hook(op_code, [global_state])
new_global_states = self._end_message_call(
return_global_state, global_state, revert_changes=True, return_data=None
)
return new_global_states
def execute_state(
self, global_state: GlobalState
) -> Tuple[List[GlobalState], Optional[str]]:
"""Execute a single instruction in global_state.
:param global_state:
:return: A list of successor states.
"""
# Execute hooks
try:
for hook in self._execute_state_hooks:
hook(global_state)
except PluginSkipState:
return [], None
instructions = global_state.environment.code.instruction_list
try:
op_code = instructions[global_state.mstate.pc]["opcode"]
except IndexError:
self._add_world_state(global_state)
return [], None
if len(global_state.mstate.stack) < get_required_stack_elements(op_code):
error_msg = (
"Stack Underflow Exception due to insufficient "
"stack elements for the address {}".format(
instructions[global_state.mstate.pc]["address"]
)
)
new_global_states = self.handle_vm_exception(
global_state, op_code, error_msg
)
self._execute_post_hook(op_code, new_global_states)
return new_global_states, op_code
try:
self._execute_pre_hook(op_code, global_state)
except PluginSkipState:
return [], None
try:
new_global_states = Instruction(
op_code,
self.dynamic_loader,
pre_hooks=self.instr_pre_hook[op_code],
post_hooks=self.instr_post_hook[op_code],
).evaluate(global_state)
except VmException as e:
for hook in self._transaction_end_hooks:
hook(
global_state,
global_state.current_transaction,
None,
False,
)
new_global_states = self.handle_vm_exception(global_state, op_code, str(e))
except TransactionStartSignal as start_signal:
# Setup new global state
new_global_state = start_signal.transaction.initial_global_state()
new_global_state.transaction_stack = copy(
global_state.transaction_stack
) + [(start_signal.transaction, global_state)]
new_global_state.node = global_state.node
new_global_state.world_state.constraints = (
start_signal.global_state.world_state.constraints
)
log.debug("Starting new transaction %s", start_signal.transaction)
return [new_global_state], op_code
except TransactionEndSignal as end_signal:
(
transaction,
return_global_state,
) = end_signal.global_state.transaction_stack[-1]
log.debug("Ending transaction %s.", transaction)
for hook in self._transaction_end_hooks:
hook(
end_signal.global_state,
transaction,
return_global_state,
end_signal.revert,
)
if return_global_state is None:
if (
not isinstance(transaction, ContractCreationTransaction)
or transaction.return_data
) and not end_signal.revert:
check_potential_issues(global_state)
end_signal.global_state.world_state.node = global_state.node
self._add_world_state(end_signal.global_state)
new_global_states = []
else:
# First execute the post hook for the transaction ending instruction
self._execute_post_hook(op_code, [end_signal.global_state])
# Propagate annotations
new_annotations = [
annotation
for annotation in global_state.annotations
if annotation.persist_over_calls
]
return_global_state.add_annotations(new_annotations)
new_global_states = self._end_message_call(
copy(return_global_state),
global_state,
revert_changes=False or end_signal.revert,
return_data=transaction.return_data,
)
self._execute_post_hook(op_code, new_global_states)
return new_global_states, op_code
def _end_message_call(
self,
return_global_state: GlobalState,
global_state: GlobalState,
revert_changes=False,
return_data=None,
) -> List[GlobalState]:
"""
:param return_global_state:
:param global_state:
:param revert_changes:
:param return_data:
:return:
"""
return_global_state.world_state.constraints += (
global_state.world_state.constraints
)
# Resume execution of the transaction initializing instruction
op_code = return_global_state.environment.code.instruction_list[
return_global_state.mstate.pc
]["opcode"]
# Set execution result in the return_state
return_global_state.last_return_data = return_data
if not revert_changes:
return_global_state.world_state = copy(global_state.world_state)
return_global_state.environment.active_account = global_state.accounts[
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
)
try:
# Execute the post instruction handler
new_global_states = Instruction(
op_code,
self.dynamic_loader,
pre_hooks=self.instr_pre_hook[op_code],
post_hooks=self.instr_post_hook[op_code],
).evaluate(return_global_state, True)
except VmException:
new_global_states = []
# In order to get a nice call graph we need to set the nodes here
for state in new_global_states:
state.node = global_state.node
return new_global_states
def manage_cfg(self, opcode: str, new_states: List[GlobalState]) -> None:
"""
:param opcode:
:param new_states:
"""
if opcode == "JUMP":
assert len(new_states) <= 1
for state in new_states:
self._new_node_state(state)
elif opcode == "JUMPI":
assert len(new_states) <= 2
for state in new_states:
self._new_node_state(
state, JumpType.CONDITIONAL, state.world_state.constraints[-1]
)
elif opcode == "RETURN":
for state in new_states:
self._new_node_state(state, JumpType.RETURN)
for state in new_states:
state.node.states.append(state)
def _new_node_state(
self, state: GlobalState, edge_type=JumpType.UNCONDITIONAL, condition=None
) -> None:
"""
:param state:
:param edge_type:
:param condition:
"""
try:
address = state.environment.code.instruction_list[state.mstate.pc][
"address"
]
except IndexError:
return
new_node = Node(state.environment.active_account.contract_name)
old_node = state.node
state.node = new_node
new_node.constraints = state.world_state.constraints
if self.requires_statespace:
self.nodes[new_node.uid] = new_node
self.edges.append(
Edge(
old_node.uid, new_node.uid, edge_type=edge_type, condition=condition
)
)
if edge_type == JumpType.RETURN:
new_node.flags |= NodeFlags.CALL_RETURN
elif edge_type == JumpType.CALL:
try:
if "retval" in str(state.mstate.stack[-1]):
new_node.flags |= NodeFlags.CALL_RETURN
else:
new_node.flags |= NodeFlags.FUNC_ENTRY
except StackUnderflowException:
new_node.flags |= NodeFlags.FUNC_ENTRY
environment = state.environment
disassembly = environment.code
# Only set when encountering new JUMPI
if edge_type == JumpType.CONDITIONAL:
if isinstance(
state.world_state.transaction_sequence[-1], ContractCreationTransaction
):
environment.active_function_name = "constructor"
elif address in disassembly.address_to_function_name:
# Enter a new function
environment.active_function_name = disassembly.address_to_function_name[
address
]
new_node.flags |= NodeFlags.FUNC_ENTRY
log.debug(
"- Entering function "
+ environment.active_account.contract_name
+ ":"
+ new_node.function_name
)
elif address == 0:
environment.active_function_name = "fallback"
new_node.function_name = environment.active_function_name
def register_hooks(self, hook_type: str, hook_dict: Dict[str, List[Callable]]):
"""
:param hook_type:
:param hook_dict:
"""
if hook_type == "pre":
entrypoint = self.pre_hooks
elif hook_type == "post":
entrypoint = self.post_hooks
else:
raise ValueError(
"Invalid hook type %s. Must be one of {pre, post}", hook_type
)
for op_code, funcs in hook_dict.items():
entrypoint[op_code].extend(funcs)
def register_laser_hooks(self, hook_type: str, hook: Callable):
"""registers the hook with this Laser VM"""
if hook_type in self.hook_type_map:
self.hook_type_map[hook_type].append(hook)
else:
raise ValueError(f"Invalid hook type {hook_type}")
def register_instr_hooks(self, hook_type: str, opcode: str, hook: Callable):
"""Registers instructions hooks from plugins"""
if hook_type == "pre":
if opcode is None:
for op in OPCODES:
self.instr_pre_hook[op].append(hook(op))
else:
self.instr_pre_hook[opcode].append(hook)
else:
if opcode is None:
for op in OPCODES:
self.instr_post_hook[op].append(hook(op))
else:
self.instr_post_hook[opcode].append(hook)
def instr_hook(self, hook_type, opcode) -> Callable:
"""Registers the annotated function with register_instr_hooks
:param hook_type: Type of hook pre/post
:param opcode: The opcode related to the function
"""
def hook_decorator(func: Callable):
"""Hook decorator generated by laser_hook
:param func: Decorated function
"""
self.register_instr_hooks(hook_type, opcode, func)
return hook_decorator
def laser_hook(self, hook_type: str) -> Callable:
"""Registers the annotated function with register_laser_hooks
:param hook_type:
:return: hook decorator
"""
def hook_decorator(func: Callable):
"""Hook decorator generated by laser_hook
:param func: Decorated function
"""
self.register_laser_hooks(hook_type, func)
return func
return hook_decorator
def _execute_pre_hook(self, op_code: str, global_state: GlobalState) -> None:
"""
:param op_code:
:param global_state:
:return:
"""
if op_code not in self.pre_hooks.keys():
return
for hook in self.pre_hooks[op_code]:
hook(global_state)
def _execute_post_hook(
self, op_code: str, global_states: List[GlobalState]
) -> None:
"""
:param op_code:
:param global_states:
:return:
"""
if op_code not in self.post_hooks.keys():
return
for hook in self.post_hooks[op_code]:
for global_state in global_states:
try:
hook(global_state)
except PluginSkipState:
global_states.remove(global_state)
def pre_hook(self, op_code: str) -> Callable:
"""
:param op_code:
:return:
"""
def hook_decorator(func: Callable):
"""
:param func:
:return:
"""
if op_code not in self.pre_hooks.keys():
self.pre_hooks[op_code] = []
self.pre_hooks[op_code].append(func)
return func
return hook_decorator
def post_hook(self, op_code: str) -> Callable:
"""
:param op_code:
:return:
"""
def hook_decorator(func: Callable):
"""
:param func:
:return:
"""
if op_code not in self.post_hooks.keys():
self.post_hooks[op_code] = []
self.post_hooks[op_code].append(func)
return func
return hook_decorator
================================================
FILE: mythril/laser/ethereum/time_handler.py
================================================
import time
from mythril.support.support_utils import Singleton
class TimeHandler(object, metaclass=Singleton):
def __init__(self):
self._start_time = None
self._execution_time = None
def start_execution(self, execution_time):
self._start_time = int(time.time() * 1000)
self._execution_time = execution_time * 1000
def time_remaining(self):
return self._execution_time - (int(time.time() * 1000) - self._start_time)
time_handler = TimeHandler()
================================================
FILE: mythril/laser/ethereum/transaction/__init__.py
================================================
from mythril.laser.ethereum.transaction.symbolic import (
execute_contract_creation,
execute_message_call,
)
from mythril.laser.ethereum.transaction.transaction_models import *
================================================
FILE: mythril/laser/ethereum/transaction/concolic.py
================================================
"""This module contains functions to set up and execute concolic message
calls."""
import binascii
from typing import List, Union
from mythril.disassembler.disassembly import Disassembly
from mythril.exceptions import IllegalArgumentError
from mythril.laser.ethereum.cfg import Edge, JumpType, Node
from mythril.laser.ethereum.state.calldata import ConcreteCalldata
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction,
MessageCallTransaction,
tx_id_manager,
)
from mythril.laser.smt import symbol_factory
def execute_contract_creation(
laser_evm,
callee_address,
caller_address,
origin_address,
data,
gas_limit,
gas_price,
value,
code=None,
track_gas=False,
contract_name=None,
):
"""Executes a contract creation transaction concretely.
:param laser_evm:
:param callee_address:
:param caller_address:
:param origin_address:
:param code:
:param data:
:param gas_limit:
:param gas_price:
:param value:
:param track_gas:
:return:
"""
open_states: List[WorldState] = laser_evm.open_states[:]
del laser_evm.open_states[:]
data = binascii.b2a_hex(data).decode("utf-8")
for open_world_state in open_states:
next_transaction_id = tx_id_manager.get_next_tx_id()
transaction = ContractCreationTransaction(
world_state=open_world_state,
identifier=next_transaction_id,
gas_price=gas_price,
gas_limit=gas_limit, # block gas limit
origin=origin_address,
code=Disassembly(data),
caller=caller_address,
contract_name=contract_name,
call_data=None,
call_value=value,
)
_setup_global_state_for_execution(laser_evm, transaction)
return laser_evm.exec(True, track_gas=track_gas)
def execute_message_call(
laser_evm,
callee_address,
caller_address,
origin_address,
data,
gas_limit,
gas_price,
value,
code=None,
track_gas=False,
) -> Union[None, List[GlobalState]]:
"""Execute a message call transaction from all open states.
:param laser_evm:
:param callee_address:
:param caller_address:
:param origin_address:
:param code:
:param data:
:param gas_limit:
:param gas_price:
:param value:
:param track_gas:
:return:
"""
open_states: List[WorldState] = laser_evm.open_states[:]
del laser_evm.open_states[:]
for open_world_state in open_states:
next_transaction_id = tx_id_manager.get_next_tx_id()
code = code or open_world_state[callee_address].code.bytecode
transaction = MessageCallTransaction(
world_state=open_world_state,
identifier=next_transaction_id,
gas_price=gas_price,
gas_limit=gas_limit,
origin=origin_address,
code=Disassembly(code),
caller=caller_address,
callee_account=open_world_state[callee_address],
call_data=ConcreteCalldata(next_transaction_id, data),
call_value=value,
)
_setup_global_state_for_execution(laser_evm, transaction)
return laser_evm.exec(track_gas=track_gas)
def _setup_global_state_for_execution(laser_evm, transaction) -> None:
"""Set up global state and cfg for a transactions execution.
:param laser_evm:
:param transaction:
"""
# TODO: Resolve circular import between .transaction and ..svm to import LaserEVM here
global_state = transaction.initial_global_state()
global_state.transaction_stack.append((transaction, None))
new_node = Node(
global_state.environment.active_account.contract_name,
function_name=global_state.environment.active_function_name,
)
if laser_evm.requires_statespace:
laser_evm.nodes[new_node.uid] = new_node
if transaction.world_state.node and laser_evm.requires_statespace:
laser_evm.edges.append(
Edge(
transaction.world_state.node.uid,
new_node.uid,
edge_type=JumpType.Transaction,
condition=None,
)
)
new_node.constraints = global_state.world_state.constraints
global_state.world_state.transaction_sequence.append(transaction)
global_state.node = new_node
new_node.states.append(global_state)
laser_evm.work_list.append(global_state)
def execute_transaction(*args, **kwargs) -> Union[None, List[GlobalState]]:
"""
Chooses the transaction type based on callee address and
executes the transaction
"""
try:
if kwargs["callee_address"] == "":
if kwargs["caller_address"] == "":
kwargs["caller_address"] = kwargs["origin"]
return execute_contract_creation(*args, **kwargs)
kwargs["callee_address"] = symbol_factory.BitVecVal(
int(kwargs["callee_address"], 16), 256
)
except KeyError as k:
raise IllegalArgumentError(f"Argument not found: {k}")
return execute_message_call(*args, **kwargs)
================================================
FILE: mythril/laser/ethereum/transaction/symbolic.py
================================================
"""This module contains functions setting up and executing transactions with
symbolic values."""
import logging
from typing import List, Optional
from mythril.disassembler.disassembly import Disassembly
from mythril.laser.ethereum.cfg import Edge, JumpType, Node
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.calldata import SymbolicCalldata
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.transaction.transaction_models import (
BaseTransaction,
ContractCreationTransaction,
MessageCallTransaction,
tx_id_manager,
)
from mythril.laser.smt import BitVec, Or, symbol_factory
from mythril.laser.smt import SMTBool as Bool
FUNCTION_HASH_BYTE_LENGTH = 4
log = logging.getLogger(__name__)
class Actors:
def __init__(
self,
creator=0xAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFEAFFE,
attacker=0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF,
someguy=0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,
):
self.addresses = {
"CREATOR": symbol_factory.BitVecVal(creator, 256),
"ATTACKER": symbol_factory.BitVecVal(attacker, 256),
"SOMEGUY": symbol_factory.BitVecVal(someguy, 256),
}
def __setitem__(self, actor: str, address: Optional[str]):
"""
Sets an actor to a desired address
:param actor: Name of the actor to set
:param address: Address to set the actor to. None to delete the actor
"""
if address is None:
if actor in ("CREATOR", "ATTACKER"):
raise ValueError("Can't delete creator or attacker address")
del self.addresses[actor]
else:
if address[0:2] != "0x":
raise ValueError("Actor address not in valid format")
self.addresses[actor] = symbol_factory.BitVecVal(int(address[2:], 16), 256)
def __getitem__(self, actor: str):
return self.addresses[actor]
@property
def creator(self):
return self.addresses["CREATOR"]
@property
def attacker(self):
return self.addresses["ATTACKER"]
def __len__(self):
return len(self.addresses)
ACTORS = Actors()
def generate_function_constraints(
calldata: SymbolicCalldata, func_hashes: List[List[int]]
) -> List[Bool]:
"""
This will generate constraints for fixing the function call part of calldata
:param calldata: Calldata
:param func_hashes: The list of function hashes allowed for this transaction
:return: Constraints List
"""
if len(func_hashes) == 0:
return []
constraints = []
for i in range(FUNCTION_HASH_BYTE_LENGTH):
constraint = Bool(False)
for func_hash in func_hashes:
if func_hash == -1:
# Fallback function without calldata
constraint = Or(constraint, calldata.size < 4)
elif func_hash == -2:
# Receive function
constraint = Or(constraint, calldata.size == 0)
else:
constraint = Or(
constraint, calldata[i] == symbol_factory.BitVecVal(func_hash[i], 8)
)
constraints.append(constraint)
return constraints
def execute_message_call(
laser_evm, callee_address: BitVec, func_hashes: List[List[int]] = None
) -> None:
"""Executes a message call transaction from all open states.
:param laser_evm:
:param callee_address:
"""
# TODO: Resolve circular import between .transaction and ..svm to import LaserEVM here
open_states = laser_evm.open_states[:]
del laser_evm.open_states[:]
for open_world_state in open_states:
if open_world_state[callee_address].deleted:
log.debug("Can not execute dead contract, skipping.")
continue
next_transaction_id = tx_id_manager.get_next_tx_id()
external_sender = symbol_factory.BitVecSym(
"sender_{}".format(next_transaction_id), 256
)
calldata = SymbolicCalldata(next_transaction_id)
transaction = MessageCallTransaction(
world_state=open_world_state,
identifier=next_transaction_id,
gas_price=symbol_factory.BitVecSym(
"gas_price{}".format(next_transaction_id), 256
),
gas_limit=8000000, # block gas limit
origin=external_sender,
caller=external_sender,
callee_account=open_world_state[callee_address],
call_data=calldata,
call_value=symbol_factory.BitVecSym(
"call_value{}".format(next_transaction_id), 256
),
)
constraints = (
generate_function_constraints(calldata, func_hashes)
if func_hashes
else None
)
_setup_global_state_for_execution(laser_evm, transaction, constraints)
laser_evm.exec()
def execute_contract_creation(
laser_evm,
contract_initialization_code,
contract_name=None,
world_state=None,
origin=ACTORS["CREATOR"],
caller=ACTORS["CREATOR"],
) -> Account:
"""Executes a contract creation transaction from all open states.
:param laser_evm:
:param contract_initialization_code:
:param contract_name:
:return:
"""
world_state = world_state or WorldState()
open_states = [world_state]
del laser_evm.open_states[:]
new_account = None
for open_world_state in open_states:
next_transaction_id = tx_id_manager.get_next_tx_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(
world_state=open_world_state,
identifier=next_transaction_id,
gas_price=symbol_factory.BitVecSym(
"gas_price{}".format(next_transaction_id), 256
),
gas_limit=8000000, # block gas limit
origin=origin,
code=Disassembly(contract_initialization_code),
caller=caller,
contract_name=contract_name,
call_data=None,
call_value=symbol_factory.BitVecSym(
"call_value{}".format(next_transaction_id), 256
),
)
_setup_global_state_for_execution(laser_evm, transaction)
new_account = new_account or transaction.callee_account
laser_evm.exec(True)
return new_account
def _setup_global_state_for_execution(
laser_evm,
transaction: BaseTransaction,
initial_constraints: Optional[List[Bool]] = None,
) -> None:
"""Sets up global state and cfg for a transactions execution.
:param laser_evm:
:param transaction:
"""
# TODO: Resolve circular import between .transaction and ..svm to import LaserEVM here
global_state = transaction.initial_global_state()
global_state.transaction_stack.append((transaction, None))
global_state.world_state.constraints += initial_constraints or []
global_state.world_state.constraints.append(
Or(*[transaction.caller == actor for actor in ACTORS.addresses.values()])
)
new_node = Node(
global_state.environment.active_account.contract_name,
function_name=global_state.environment.active_function_name,
)
if laser_evm.requires_statespace:
laser_evm.nodes[new_node.uid] = new_node
if transaction.world_state.node:
if laser_evm.requires_statespace:
laser_evm.edges.append(
Edge(
transaction.world_state.node.uid,
new_node.uid,
edge_type=JumpType.Transaction,
condition=None,
)
)
new_node.constraints = global_state.world_state.constraints
global_state.world_state.transaction_sequence.append(transaction)
global_state.node = new_node
new_node.states.append(global_state)
laser_evm.work_list.append(global_state)
def execute_transaction(*args, **kwargs):
"""
Chooses the transaction type based on callee address and
executes the transaction
"""
laser_evm = args[0]
if kwargs["callee_address"] == "":
for ws in laser_evm.open_states[:]:
execute_contract_creation(
laser_evm=laser_evm,
contract_initialization_code=kwargs["data"],
world_state=ws,
)
return
execute_message_call(
laser_evm=laser_evm,
callee_address=symbol_factory.BitVecVal(int(kwargs["callee_address"], 16), 256),
)
================================================
FILE: mythril/laser/ethereum/transaction/transaction_models.py
================================================
"""This module contains the transaction models used throughout LASER's symbolic
execution."""
import logging
from copy import deepcopy
from typing import Optional, Union
from z3 import ExprRef
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.calldata import (
BaseCalldata,
ConcreteCalldata,
SymbolicCalldata,
)
from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.return_data import ReturnData
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.smt import UGE, BitVec, symbol_factory
from mythril.support.support_utils import Singleton
log = logging.getLogger(__name__)
class TxIdManager(object, metaclass=Singleton):
def __init__(self):
self._next_transaction_id = 0
def get_next_tx_id(self):
self._next_transaction_id += 1
return str(self._next_transaction_id)
def restart_counter(self):
self._next_transaction_id = 0
def set_counter(self, tx_id):
self._next_transaction_id = tx_id
tx_id_manager = TxIdManager()
class TransactionEndSignal(Exception):
"""Exception raised when a transaction is finalized."""
def __init__(self, global_state: GlobalState, revert=False) -> None:
self.global_state = global_state
self.revert = revert
class TransactionStartSignal(Exception):
"""Exception raised when a new transaction is started."""
def __init__(
self,
transaction: Union["MessageCallTransaction", "ContractCreationTransaction"],
op_code: str,
global_state: GlobalState,
) -> None:
self.transaction = transaction
self.op_code = op_code
self.global_state = global_state
class BaseTransaction:
"""Basic transaction class holding common data."""
def __init__(
self,
world_state: WorldState,
callee_account: Account = None,
caller: ExprRef = None,
call_data=None,
identifier: Optional[str] = None,
gas_price=None,
gas_limit=None,
origin=None,
code=None,
call_value=None,
init_call_data=True,
static=False,
base_fee=None,
) -> None:
assert isinstance(world_state, WorldState)
self.world_state = world_state
self.id = identifier or tx_id_manager.get_next_tx_id()
self.gas_price = (
gas_price
if gas_price is not None
else symbol_factory.BitVecSym(f"gasprice{identifier}", 256)
)
self.base_fee = (
base_fee
if base_fee is not None
else symbol_factory.BitVecSym(f"basefee{identifier}", 256)
)
self.gas_limit = gas_limit
self.origin = (
origin
if origin is not None
else symbol_factory.BitVecSym(f"origin{identifier}", 256)
)
self.code = code
self.caller = caller
self.callee_account = callee_account
if call_data is None and init_call_data:
self.call_data: BaseCalldata = SymbolicCalldata(self.id)
else:
self.call_data = (
call_data
if isinstance(call_data, BaseCalldata)
else ConcreteCalldata(self.id, [])
)
self.call_value = (
call_value
if call_value is not None
else symbol_factory.BitVecSym(f"callvalue{identifier}", 256)
)
self.static = static
self.return_data: Optional[ReturnData] = None
def initial_global_state_from_environment(self, environment, active_function):
"""
:param environment:
:param active_function:
:return:
"""
# Initialize the execution environment
global_state = GlobalState(self.world_state, environment, None)
global_state.environment.active_function_name = active_function
sender = environment.sender
receiver = environment.active_account.address
value = (
environment.callvalue
if isinstance(environment.callvalue, BitVec)
else symbol_factory.BitVecVal(environment.callvalue, 256)
)
global_state.world_state.constraints.append(
UGE(global_state.world_state.balances[sender], value)
)
global_state.world_state.balances[receiver] += value
global_state.world_state.balances[sender] -= value
return global_state
def initial_global_state(self) -> GlobalState:
raise NotImplementedError
def __str__(self) -> str:
if self.callee_account is None or self.callee_account.address.symbolic is False:
return "{} {} from {} to {:#42x}".format(
self.__class__.__name__,
self.id,
self.caller,
int(str(self.callee_account.address)) if self.callee_account else -1,
)
else:
return "{} {} from {} to {}".format(
self.__class__.__name__,
self.id,
self.caller,
str(self.callee_account.address),
)
class MessageCallTransaction(BaseTransaction):
"""Transaction object models an transaction."""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
def initial_global_state(self) -> GlobalState:
"""Initialize the execution environment."""
environment = Environment(
self.callee_account,
self.caller,
self.call_data,
self.gas_price,
self.call_value,
self.origin,
self.base_fee,
code=self.code or self.callee_account.code,
static=self.static,
)
return super().initial_global_state_from_environment(
environment, active_function="fallback"
)
def end(self, global_state: GlobalState, return_data=None, revert=False) -> None:
"""
:param global_state:
:param return_data:
:param revert:
"""
self.return_data = return_data
raise TransactionEndSignal(global_state, revert)
class ContractCreationTransaction(BaseTransaction):
"""Transaction object models an transaction."""
def __init__(
self,
world_state: WorldState,
caller: ExprRef = None,
call_data=None,
identifier: Optional[str] = None,
gas_price=None,
gas_limit=None,
origin=None,
code=None,
call_value=None,
contract_name=None,
contract_address=None,
base_fee=None,
) -> None:
self.prev_world_state = deepcopy(world_state)
contract_address = (
contract_address if isinstance(contract_address, int) else None
)
# Balance set to None to allow for pre-existing ether.
callee_account = world_state.create_account(
None, concrete_storage=True, creator=caller.value, address=contract_address
)
callee_account.contract_name = contract_name or callee_account.contract_name
# 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__(
world_state=world_state,
callee_account=callee_account,
caller=caller,
call_data=call_data,
identifier=identifier,
gas_price=gas_price,
gas_limit=gas_limit,
origin=origin,
code=code,
call_value=call_value,
init_call_data=True,
base_fee=base_fee,
)
def initial_global_state(self) -> GlobalState:
"""Initialize the execution environment."""
environment = Environment(
active_account=self.callee_account,
sender=self.caller,
calldata=self.call_data,
gasprice=self.gas_price,
callvalue=self.call_value,
origin=self.origin,
basefee=self.base_fee,
code=self.code,
)
return super().initial_global_state_from_environment(
environment, active_function="constructor"
)
def end(self, global_state: GlobalState, return_data=None, revert=False):
"""
:param global_state:
:param return_data:
:param revert:
"""
if return_data is None or return_data.size == 0:
self.return_data = None
raise TransactionEndSignal(global_state, revert=revert)
global_state.environment.active_account.code.assign_bytecode(
tuple(return_data.return_data)
)
return_data = str(hex(global_state.environment.active_account.address.value))
self.return_data: Optional[ReturnData] = ReturnData(
return_data, symbol_factory.BitVecVal(len(return_data) // 2, 256)
)
assert global_state.environment.active_account.code.instruction_list != []
raise TransactionEndSignal(global_state, revert=revert)
================================================
FILE: mythril/laser/ethereum/tx_prioritiser/__init__.py
================================================
from .rf_prioritiser import RfTxPrioritiser
================================================
FILE: mythril/laser/ethereum/tx_prioritiser/rf_prioritiser.py
================================================
import logging
import pickle
import numpy as np
log = logging.getLogger(__name__)
class RfTxPrioritiser:
def __init__(self, contract, depth=3, model_path=None):
self.rf_path = None
self.contract = contract
self.depth = depth
with open(model_path, "rb") as file:
self.model = pickle.load(file)
if self.contract.features is None:
log.info(
"There are no available features. Rf based Tx Prioritisation turned off."
)
return None
self.preprocessed_features = self.preprocess_features(self.contract.features)
self.recent_predictions = []
def preprocess_features(self, features_dict):
flat_features = []
for function_name, function_features in features_dict.items():
function_features_values = list(function_features.values())
flat_features.extend(function_features_values)
return np.array(flat_features).reshape(1, -1)
def __next__(self, address):
predictions_sequence = []
current_features = np.concatenate(
[
self.preprocessed_features,
np.array(self.recent_predictions).reshape(1, -1),
],
axis=1,
)
for i in range(self.depth):
predictions = self.model.predict_proba(current_features)
most_likely_next_tx = np.argmax(predictions, axis=1)[0]
predictions_sequence.append(most_likely_next_tx)
current_features = np.concatenate(
[
self.preprocessed_features,
np.array(
self.recent_predictions + predictions_sequence[: i + 1]
).reshape(1, -1),
],
axis=1,
)
self.recent_predictions.extend(predictions_sequence)
while len(self.recent_predictions) > self.depth:
self.recent_predictions.pop(0)
return predictions_sequence
================================================
FILE: mythril/laser/ethereum/util.py
================================================
"""This module contains various utility conversion functions and constants for
LASER."""
import re
from typing import TYPE_CHECKING, Dict, List, Union, cast
if TYPE_CHECKING:
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.machine_state import MachineState
from mythril.laser.smt import (
BitVec,
Expression,
If,
simplify,
symbol_factory,
)
from mythril.laser.smt import (
SMTBool as Bool,
)
TT256 = 2**256
TT256M1 = 2**256 - 1
TT255 = 2**255
def safe_decode(hex_encoded_string: str) -> bytes:
"""
:param hex_encoded_string:
:return:
"""
if hex_encoded_string.startswith("0x"):
return bytes.fromhex(hex_encoded_string[2:])
else:
return bytes.fromhex(hex_encoded_string)
def insert_ret_val(global_state: "GlobalState"):
retval = global_state.new_bitvec(
"retval_" + str(global_state.get_current_instruction()["address"]), 256
)
global_state.mstate.stack.append(retval)
global_state.world_state.constraints.append(retval == 1)
def to_signed(i: int) -> int:
"""
:param i:
:return:
"""
return i if i < TT255 else i - TT256
def get_instruction_index(
instruction_list: List[Dict], address: int
) -> Union[int, None]:
"""
:param instruction_list:
:param address:
:return:
"""
index = 0
for instr in instruction_list:
if instr["address"] >= address:
return index
index += 1
return None
def get_trace_line(instr: Dict, state: "MachineState") -> str:
"""
:param instr:
:param state:
:return:
"""
stack = str(state.stack[::-1])
# stack = re.sub("(\d+)", lambda m: hex(int(m.group(1))), stack)
stack = re.sub("\n", "", stack)
return str(instr["address"]) + " " + instr["opcode"] + "\tSTACK: " + stack
def pop_bitvec(state: "MachineState") -> BitVec:
"""
:param state:
:return:
"""
# pop one element from stack, converting boolean expressions and
# concrete Python variables to BitVecVal
item = state.stack.pop()
if isinstance(item, Bool):
return If(
cast(Bool, item),
symbol_factory.BitVecVal(1, 256),
symbol_factory.BitVecVal(0, 256),
)
elif isinstance(item, int):
return symbol_factory.BitVecVal(item, 256)
else:
item = cast(BitVec, item)
return simplify(item)
def get_concrete_int(item: Union[int, Expression]) -> int:
"""
:param item:
:return:
"""
if isinstance(item, int):
return item
elif isinstance(item, BitVec):
if item.symbolic:
raise TypeError("Got a symbolic BitVecRef")
return item.value
elif isinstance(item, Bool):
value = item.value
if value is None:
raise TypeError("Symbolic boolref encountered")
return value
assert False, "Unhandled type {} encountered".format(str(type(item)))
def concrete_int_from_bytes(
concrete_bytes: Union[List[Union[BitVec, int]], bytes], start_index: int
) -> int:
"""
:param concrete_bytes:
:param start_index:
:return:
"""
concrete_bytes = [
byte.value if isinstance(byte, BitVec) and not byte.symbolic else byte
for byte in concrete_bytes
]
integer_bytes = concrete_bytes[start_index : start_index + 32]
# The below statement is expected to fail in some circumstances whose error is caught
return int.from_bytes(integer_bytes, byteorder="big") # type: ignore
def concrete_int_to_bytes(val):
"""
:param val:
:return:
"""
# logging.debug("concrete_int_to_bytes " + str(val))
if isinstance(val, int):
return val.to_bytes(32, byteorder="big")
return simplify(val).value.to_bytes(32, byteorder="big")
def bytearray_to_int(arr):
"""
:param arr:
:return:
"""
o = 0
for a in arr:
o = (o << 8) + a
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)
================================================
FILE: mythril/laser/execution_info.py
================================================
from abc import ABC, abstractmethod
class ExecutionInfo(ABC):
@abstractmethod
def as_dict(self):
"""Returns a dictionary with the execution info contained in this object
The returned dictionary only uses primitive types.
"""
pass
================================================
FILE: mythril/laser/plugin/__init__.py
================================================
"""Laser plugins
This module contains everything to do with laser plugins
Laser plugins are a way of extending laser's functionality without complicating the core business logic.
Different features that have been implemented in the form of plugins are:
- benchmarking
- path pruning
Plugins also provide a way to implement optimisations outside of the mythril code base and to inject them.
The api that laser currently provides is still unstable and will probably change to suit our needs
as more plugins get developed.
For the implementation of plugins the following modules are of interest:
- laser.plugins.plugin
- laser.plugins.signals
- laser.svm
Which show the basic interfaces with which plugins are able to interact
"""
================================================
FILE: mythril/laser/plugin/builder.py
================================================
from abc import ABC, abstractmethod
from mythril.laser.plugin.interface import LaserPlugin
class PluginBuilder(ABC):
"""PluginBuilder
The plugin builder interface enables construction of Laser plugins
"""
name = "Default Plugin Name"
def __init__(self):
self.enabled = True
@abstractmethod
def __call__(self, *args, **kwargs) -> LaserPlugin:
"""Constructs the plugin"""
pass
================================================
FILE: mythril/laser/plugin/interface.py
================================================
from mythril.laser.ethereum.svm import LaserEVM
class LaserPlugin:
"""Base class for laser plugins
Functionality in laser that the symbolic execution process does not need to depend on
can be implemented in the form of a laser plugin.
Laser plugins implement the function initialize(symbolic_vm) which is called with the laser virtual machine
when they are loaded.
Regularly a plugin will introduce several hooks into laser in this function
Plugins can direct actions by raising Signals defined in mythril.laser.ethereum.plugins.signals
For example, a pruning plugin might raise the PluginSkipWorldState signal.
"""
def initialize(self, symbolic_vm: LaserEVM) -> None:
"""Initializes this plugin on the symbolic virtual machine
:param symbolic_vm: symbolic virtual machine to initialize the laser plugin on
"""
raise NotImplementedError
================================================
FILE: mythril/laser/plugin/loader.py
================================================
import logging
from typing import Dict, List, Optional
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
from mythril.support.support_utils import Singleton
log = logging.getLogger(__name__)
class LaserPluginLoader(object, metaclass=Singleton):
"""
The LaserPluginLoader is used to abstract the logic relating to plugins.
Components outside of laser thus don't have to be aware of the interface that plugins provide
"""
def __init__(self) -> None:
"""Initializes the plugin loader"""
self.laser_plugin_builders: Dict[str, PluginBuilder] = {}
self.plugin_args: Dict[str, Dict] = {}
self.plugin_list: Dict[str, LaserPlugin] = {}
def add_args(self, plugin_name, **kwargs):
self.plugin_args[plugin_name] = kwargs
def load(self, plugin_builder: PluginBuilder) -> None:
"""Enables a Laser Plugin
:param plugin_builder: Builder that constructs the plugin
"""
log.info(f"Loading laser plugin: {plugin_builder.name}")
if plugin_builder.name in self.laser_plugin_builders:
log.warning(
f"Laser plugin with name {plugin_builder.name} was already loaded, skipping..."
)
return
self.laser_plugin_builders[plugin_builder.name] = plugin_builder
def is_enabled(self, plugin_name: str) -> bool:
"""Returns whether the plugin is loaded in the symbolic_vm
:param plugin_name: Name of the plugin to check
"""
if plugin_name not in self.laser_plugin_builders:
return False
else:
return self.laser_plugin_builders[plugin_name].enabled
def enable(self, plugin_name: str):
if plugin_name not in self.laser_plugin_builders:
return ValueError(f"Plugin with name: {plugin_name} was not loaded")
self.laser_plugin_builders[plugin_name].enabled = True
def instrument_virtual_machine(
self, symbolic_vm: LaserEVM, with_plugins: Optional[List[str]]
):
"""Load enabled plugins into the passed symbolic virtual machine
:param symbolic_vm: The virtual machine to instrument the plugins with
:param with_plugins: Override the globally enabled/disabled builders and load all plugins in the list
"""
for plugin_name, plugin_builder in self.laser_plugin_builders.items():
enabled = (
plugin_builder.enabled
if not with_plugins
else plugin_name in with_plugins
)
if not enabled:
continue
log.info(f"Instrumenting symbolic vm with plugin: {plugin_name}")
plugin = plugin_builder(**self.plugin_args.get(plugin_name, {}))
plugin.initialize(symbolic_vm)
self.plugin_list[plugin_name] = plugin
================================================
FILE: mythril/laser/plugin/plugins/__init__.py
================================================
"""Plugin implementations
This module contains the implementation of some features
- benchmarking
- pruning
"""
from mythril.laser.plugin.plugins.benchmark import BenchmarkPluginBuilder
from mythril.laser.plugin.plugins.call_depth_limiter import CallDepthLimitBuilder
from mythril.laser.plugin.plugins.coverage.coverage_plugin import CoveragePluginBuilder
from mythril.laser.plugin.plugins.coverage_metrics import CoverageMetricsPluginBuilder
from mythril.laser.plugin.plugins.dependency_pruner import DependencyPrunerBuilder
from mythril.laser.plugin.plugins.instruction_profiler import InstructionProfilerBuilder
from mythril.laser.plugin.plugins.mutation_pruner import MutationPrunerBuilder
from mythril.laser.plugin.plugins.state_merge import StateMergePluginBuilder
from mythril.laser.plugin.plugins.summary import SymbolicSummaryPluginBuilder
from mythril.laser.plugin.plugins.trace import TraceFinderBuilder
================================================
FILE: mythril/laser/plugin/plugins/benchmark.py
================================================
import logging
from time import time
import matplotlib.pyplot as plt
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
log = logging.getLogger(__name__)
class BenchmarkPluginBuilder(PluginBuilder):
name = "benchmark"
def __call__(self, *args, **kwargs):
return BenchmarkPlugin()
# TODO: introduce dependency on coverage plugin
class BenchmarkPlugin(LaserPlugin):
"""Benchmark Plugin
This plugin aggregates the following information:
- duration
- code coverage over time
- final code coverage
- total number of executed instructions
"""
def __init__(self, name=None):
"""Creates BenchmarkPlugin
:param name: name of this benchmark, used for storing the results
"""
self.nr_of_executed_insns = 0
self.begin = None
self.end = None
self.coverage = {}
self.name = name
def initialize(self, symbolic_vm: LaserEVM):
"""Initializes the BenchmarkPlugin
Introduces hooks in symbolic_vm to track the desired values
:param symbolic_vm: Symbolic virtual machine to analyze
"""
self._reset()
@symbolic_vm.laser_hook("execute_state")
def execute_state_hook(_):
current_time = time() - self.begin
self.nr_of_executed_insns += 1
for key, value in symbolic_vm.coverage.items():
try:
self.coverage[key][current_time] = sum(value[1]) * 100 / value[0]
except KeyError:
self.coverage[key] = {}
self.coverage[key][current_time] = sum(value[1]) * 100 / value[0]
@symbolic_vm.laser_hook("start_sym_exec")
def start_sym_exec_hook():
self.begin = time()
@symbolic_vm.laser_hook("stop_sym_exec")
def stop_sym_exec_hook():
self.end = time()
self._write_to_graph()
self._store_report()
def _reset(self):
"""Reset this plugin"""
self.nr_of_executed_insns = 0
self.begin = None
self.end = None
self.coverage = {}
def _store_report(self):
"""Store the results of this plugin"""
pass
def _write_to_graph(self):
"""Write the coverage results to a graph"""
traces = []
for _, trace_data in self.coverage.items():
traces += [list(trace_data.keys()), list(trace_data.values()), "r--"]
plt.plot(*traces)
plt.axis([0, self.end - self.begin, 0, 100])
plt.xlabel("Duration (seconds)")
plt.ylabel("Coverage (percentage)")
plt.savefig("{}.png".format(self.name))
================================================
FILE: mythril/laser/plugin/plugins/call_depth_limiter.py
================================================
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
from mythril.laser.plugin.signals import PluginSkipState
class CallDepthLimitBuilder(PluginBuilder):
name = "call-depth-limit"
def __call__(self, *args, **kwargs):
return CallDepthLimit(kwargs["call_depth_limit"])
class CallDepthLimit(LaserPlugin):
def __init__(self, call_depth_limit: int):
self.call_depth_limit = call_depth_limit
def initialize(self, symbolic_vm: LaserEVM):
"""Initializes the mutation pruner
Introduces hooks for SSTORE operations
:param symbolic_vm:
:return:
"""
@symbolic_vm.pre_hook("CALL")
def sstore_mutator_hook(global_state: GlobalState):
if len(global_state.transaction_stack) - 1 == self.call_depth_limit:
raise PluginSkipState
================================================
FILE: mythril/laser/plugin/plugins/coverage/__init__.py
================================================
from mythril.laser.plugin.plugins.coverage.coverage_plugin import (
InstructionCoveragePlugin,
)
================================================
FILE: mythril/laser/plugin/plugins/coverage/coverage_plugin.py
================================================
import logging
from typing import Dict, List, Tuple
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
log = logging.getLogger(__name__)
class CoveragePluginBuilder(PluginBuilder):
name = "coverage"
def __call__(self, *args, **kwargs):
return InstructionCoveragePlugin()
class InstructionCoveragePlugin(LaserPlugin):
"""InstructionCoveragePlugin
This plugin measures the instruction coverage of mythril.
The instruction coverage is the ratio between the instructions that have been executed
and the total amount of instructions.
Note that with lazy constraint solving enabled that this metric will be "unsound" as
reachability will not be considered for the calculation of instruction coverage.
"""
def __init__(self):
self.coverage: Dict[str, Tuple[int, List[bool]]] = {}
self.initial_coverage = 0
self.tx_id = 0
def initialize(self, symbolic_vm: LaserEVM):
"""Initializes the instruction coverage plugin
Introduces hooks for each instruction
:param symbolic_vm:
:return:
"""
self.coverage = {}
self.initial_coverage = 0
self.tx_id = 0
@symbolic_vm.laser_hook("stop_sym_exec")
def stop_sym_exec_hook():
# Print results
for code, code_cov in self.coverage.items():
if sum(code_cov[1]) == 0 and code_cov[0] == 0:
cov_percentage = 0
else:
cov_percentage = sum(code_cov[1]) / float(code_cov[0]) * 100
string_code = code
if isinstance(code, tuple):
try:
string_code = bytearray(code).hex()
except TypeError:
string_code = ""
log.info(
"Achieved {:.2f}% coverage for code: {}".format(
cov_percentage, string_code
)
)
@symbolic_vm.laser_hook("execute_state")
def execute_state_hook(global_state: GlobalState):
# Record coverage
code = global_state.environment.code.bytecode
if code not in self.coverage.keys():
number_of_instructions = len(
global_state.environment.code.instruction_list
)
self.coverage[code] = (
number_of_instructions,
[False] * number_of_instructions,
)
if global_state.mstate.pc >= len(self.coverage[code][1]):
# Instruction beyond the instruction list are considered as STOP by EVM
# and can be ignored
return
self.coverage[code][1][global_state.mstate.pc] = True
@symbolic_vm.laser_hook("start_sym_trans")
def execute_start_sym_trans_hook():
self.initial_coverage = self._get_covered_instructions()
@symbolic_vm.laser_hook("stop_sym_trans")
def execute_stop_sym_trans_hook():
end_coverage = self._get_covered_instructions()
log.info(
"Number of new instructions covered in tx %d: %d"
% (self.tx_id, end_coverage - self.initial_coverage)
)
self.tx_id += 1
def _get_covered_instructions(self) -> int:
"""Gets the total number of covered instructions for all accounts in
the svm.
:return:
"""
total_covered_instructions = 0
for _, cv in self.coverage.items():
total_covered_instructions += sum(cv[1])
return total_covered_instructions
def is_instruction_covered(self, bytecode, index):
if bytecode not in self.coverage.keys():
return False
try:
return self.coverage[bytecode][index]
except IndexError:
return False
================================================
FILE: mythril/laser/plugin/plugins/coverage/coverage_strategy.py
================================================
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.strategy import BasicSearchStrategy
from mythril.laser.plugin.plugins.coverage import InstructionCoveragePlugin
class CoverageStrategy(BasicSearchStrategy):
"""Implements a instruction coverage based search strategy
This strategy is quite simple and effective, it prioritizes the execution of instructions that have previously been
uncovered. Once there is no such global state left in the work list, it will resort to using the super_strategy.
This strategy is intended to be used "on top of" another one
"""
def __init__(
self,
super_strategy: BasicSearchStrategy,
instruction_coverage_plugin: InstructionCoveragePlugin,
):
self.super_strategy = super_strategy
self.instruction_coverage_plugin = instruction_coverage_plugin
BasicSearchStrategy.__init__(
self, super_strategy.work_list, super_strategy.max_depth
)
def get_strategic_global_state(self) -> GlobalState:
"""
Returns the first uncovered global state in the work list if it exists,
otherwise super_strategy.get_strategic_global_state() is returned.
"""
for global_state in self.work_list:
if not self._is_covered(global_state):
self.work_list.remove(global_state)
return global_state
return self.super_strategy.get_strategic_global_state()
def _is_covered(self, global_state: GlobalState) -> bool:
"""Checks if the instruction for the given global state is already covered"""
bytecode = global_state.environment.code.bytecode
index = global_state.mstate.pc
return self.instruction_coverage_plugin.is_instruction_covered(bytecode, index)
================================================
FILE: mythril/laser/plugin/plugins/coverage_metrics/__init__.py
================================================
from .metrics_plugin import CoverageMetricsPluginBuilder
================================================
FILE: mythril/laser/plugin/plugins/coverage_metrics/constants.py
================================================
BATCH_OF_STATES = 5
================================================
FILE: mythril/laser/plugin/plugins/coverage_metrics/coverage_data.py
================================================
import json
from mythril.laser.execution_info import ExecutionInfo
from mythril.support.support_utils import get_code_hash
class InstructionCoverageInfo(ExecutionInfo):
def __init__(self):
self._instruction_coverage = {} # type: Dict[str, int]
def as_dict(self):
return dict(instruction_discovery_time=self._instruction_coverage)
def get_code_instr_hex(self, code: str, instruction: int):
code_hash = get_code_hash(code)[2:]
instruction_hex = hex(instruction)[2:]
return "{}:{}".format(code_hash, instruction_hex)
def is_covered(self, code: str, instruction: int):
code_instr_hex = self.get_code_instr_hex(code, instruction)
return code_instr_hex in self._instruction_coverage
def add_data(self, code: str, instruction: int, discovery_time: int):
code_instr_hex = self.get_code_instr_hex(code, instruction)
self._instruction_coverage[code_instr_hex] = discovery_time
def output(self, filename: str):
with open(filename, "w") as outfile:
json.dump(
self._instruction_coverage, default=lambda o: o.__dict__, fp=outfile
)
class CoverageData:
def __init__(
self,
instructions_covered: int,
total_instructions: int,
branches_covered: int,
tx_id: int,
total_branches: int,
state_counter: int,
code: str,
time_elapsed: int,
):
self.instructions_covered = instructions_covered
self.total_instructions = total_instructions
self.branches_covered = branches_covered
self.tx_id = tx_id
self.total_branches = total_branches
self.state_counter = state_counter
self.code_hash = get_code_hash(code)[2:]
self.time_elapsed = time_elapsed
def as_dict(self):
return self.__dict__
class CoverageTimeSeries(ExecutionInfo):
def __init__(self):
self.coverage = [] # type: List[CoverageData]
def output(self, filename: str):
with open(filename, "w") as outfile:
json.dump(self.coverage, default=lambda o: o.__dict__, fp=outfile)
def as_dict(self):
return dict(coverage=self.coverage)
def add_data(self, *args, **kwargs):
cov_data = CoverageData(*args, **kwargs)
self.coverage.append(cov_data.as_dict())
================================================
FILE: mythril/laser/plugin/plugins/coverage_metrics/metrics_plugin.py
================================================
import logging
import time
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
from .constants import BATCH_OF_STATES
from .coverage_data import (
CoverageTimeSeries,
InstructionCoverageInfo,
)
log = logging.getLogger(__name__)
class CoverageMetricsPluginBuilder(PluginBuilder):
"""CoveragePlugin
Checks Instruction and branch coverage and puts it to data.json file
which appears in the directory in which mythril is run.
"""
plugin_default_enabled = True
enabled = True
author = "MythX Development Team"
plugin_name = "MythX Coverage Metrics"
plugin_license = "All rights reserved."
plugin_type = "Laser Plugin"
plugin_description = (
"This plugin measures coverage throughout symbolic execution,"
" reporting it at the end in the MythX coverage format."
)
def __call__(self, *args, **kwargs):
"""Constructs the plugin"""
return LaserCoveragePlugin()
class LaserCoveragePlugin(LaserPlugin):
def __init__(self):
self.instruction_coverage_data = {} # type: Dict[str, Tuple[int, Dict[bool]]]
self.branch_possibilities = {} # type: Dict[str, Dict[int, List]]
self.tx_id = 0
self.state_counter = 0
self.coverage = CoverageTimeSeries()
self.instruction_coverage_info = InstructionCoverageInfo()
self.start_time = time.time_ns()
def initialize(self, symbolic_vm: LaserEVM) -> None:
"""Initializes the instruction coverage plugin
Introduces hooks for each instruction
:param symbolic_vm: The symbolic virtual machine to initialise this plugin for
"""
log.info("Initializing coverage metrics plugin")
self.instruction_coverage_data = {}
self.branch_possibilities = {}
self.tx_id = 0
# Add the instruction coverage ExecutionInfo to laser vm for use in reporting
symbolic_vm.execution_info.append(self.instruction_coverage_info)
symbolic_vm.execution_info.append(self.coverage)
@symbolic_vm.laser_hook("execute_state")
def execute_state_hook(global_state: GlobalState):
self._update_instruction_coverage_data(global_state)
self._update_branch_coverage_data(global_state)
self.state_counter += 1
if self.state_counter == BATCH_OF_STATES:
self._record_coverage()
self.state_counter = 0
@symbolic_vm.laser_hook("stop_sym_trans")
def execute_stop_sym_trans_hook():
self.tx_id += 1
# The following is useful for debugging
# @symbolic_vm.laser_hook("stop_sym_exec")
# def execute_stop_sym_exec_hook():
# self.coverage.output("coverage_data.json")
# self.instruction_coverage_info.output("instruction_discovery_data.json")
def _update_instruction_coverage_data(self, global_state: GlobalState):
"""Records instruction coverage"""
code = global_state.environment.code.bytecode
if code not in self.instruction_coverage_data.keys():
number_of_instructions = len(global_state.environment.code.instruction_list)
self.instruction_coverage_data[code] = (number_of_instructions, {})
current_instr = global_state.get_current_instruction()["address"]
if self.instruction_coverage_info.is_covered(code, current_instr) is False:
self.instruction_coverage_info.add_data(
code, current_instr, time.time_ns() - self.start_time
)
self.instruction_coverage_data[code][1][current_instr] = True
def _update_branch_coverage_data(self, global_state: GlobalState):
"""Records branch coverage"""
code = global_state.environment.code.bytecode
if code not in self.branch_possibilities:
self.branch_possibilities[code] = {}
if global_state.get_current_instruction()["opcode"] != "JUMPI":
return
addr = global_state.get_current_instruction()["address"]
jump_addr = global_state.mstate.stack[-1]
if jump_addr.symbolic:
log.debug("Encountered a symbolic jump, ignoring it for branch coverage")
return
self.branch_possibilities[code][addr] = [addr + 1, jump_addr.value]
def _record_coverage(self):
for code, code_cov in self.instruction_coverage_data.items():
total_branches = 0
branches_covered = 0
for jumps, branches in self.branch_possibilities[code].items():
for branch in branches:
total_branches += 1
branches_covered += branch in code_cov[1]
self.coverage.add_data(
code=code,
instructions_covered=len(code_cov[1]),
total_instructions=code_cov[0],
branches_covered=branches_covered,
tx_id=self.tx_id,
total_branches=total_branches,
state_counter=self.state_counter,
time_elapsed=time.time_ns() - self.start_time,
)
================================================
FILE: mythril/laser/plugin/plugins/dependency_pruner.py
================================================
import logging
from typing import Dict, List, Set, cast
from mythril.analysis import solver
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction,
)
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
from mythril.laser.plugin.plugins.plugin_annotations import (
DependencyAnnotation,
WSDependencyAnnotation,
)
from mythril.laser.plugin.signals import PluginSkipState
log = logging.getLogger(__name__)
def get_dependency_annotation(state: GlobalState) -> DependencyAnnotation:
"""Returns a dependency annotation
:param state: A global state object
"""
annotations = cast(
List[DependencyAnnotation], list(state.get_annotations(DependencyAnnotation))
)
if len(annotations) == 0:
"""FIXME: Hack for carrying over state annotations from the STOP and RETURN states of
the previous states. The states are pushed on a stack in the world state annotation
and popped off the stack in the subsequent iteration. This might break if any
other strategy than bfs is used (?).
"""
try:
world_state_annotation = get_ws_dependency_annotation(state)
annotation = world_state_annotation.annotations_stack.pop()
except IndexError:
annotation = DependencyAnnotation()
state.annotate(annotation)
else:
annotation = annotations[0]
return annotation
def get_ws_dependency_annotation(state: GlobalState) -> WSDependencyAnnotation:
"""Returns the world state annotation
:param state: A global state object
"""
annotations = cast(
List[WSDependencyAnnotation],
list(state.world_state.get_annotations(WSDependencyAnnotation)),
)
if len(annotations) == 0:
annotation = WSDependencyAnnotation()
state.world_state.annotate(annotation)
else:
annotation = annotations[0]
return annotation
class DependencyPrunerBuilder(PluginBuilder):
name = "dependency-pruner"
def __call__(self, *args, **kwargs):
return DependencyPruner()
class DependencyPruner(LaserPlugin):
"""Dependency Pruner Plugin
For every basic block, this plugin keeps a list of storage locations that
are accessed (read) in the execution path containing that block. This map
is built up over the whole symbolic execution run.
After the initial build up of the map in the first transaction, blocks are
executed only if any of the storage locations written to in the previous
transaction can have an effect on that block or any of its successors.
"""
def __init__(self):
"""Creates DependencyPruner"""
self._reset()
def _reset(self):
self.iteration = 0
self.calls_on_path: Dict[int, bool] = {}
self.sloads_on_path: Dict[int, List[object]] = {}
self.sstores_on_path: Dict[int, List[object]] = {}
self.storage_accessed_global: Set = set()
def update_sloads(self, path: List[int], target_location: object) -> 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.sloads_on_path:
if target_location not in self.sloads_on_path[address]:
self.sloads_on_path[address].append(target_location)
else:
self.sloads_on_path[address] = [target_location]
def update_sstores(self, path: List[int], target_location: object) -> 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:
if target_location not in self.sstores_on_path[address]:
self.sstores_on_path[address].append(target_location)
else:
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:
"""Decide whether the basic block starting at 'address' should be executed.
:param address
:param storage_write_cache
"""
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.
if address not in self.sloads_on_path:
return False
# Execute the path if there are state modifications along it that *could* be relevant
if address in self.storage_accessed_global:
for location in self.sstores_on_path:
try:
solver.get_model((location == address,))
return True
except UnsatError:
continue
dependencies = self.sloads_on_path[address]
# Execute the path if there's any dependency on state modified in the previous transaction
for location in storage_write_cache:
for dependency in dependencies:
# Is there a known read operation along this path that matches a write in the previous transaction?
try:
solver.get_model((location == dependency,))
return True
except UnsatError:
continue
# Has the *currently executed* path been influenced by a write operation in the previous transaction?
for dependency in annotation.storage_loaded:
try:
solver.get_model((location == dependency,))
return True
except UnsatError:
continue
return False
def initialize(self, symbolic_vm: LaserEVM) -> None:
"""Initializes the DependencyPruner
:param symbolic_vm
"""
self._reset()
@symbolic_vm.laser_hook("start_sym_trans")
def start_sym_trans_hook():
self.iteration += 1
@symbolic_vm.post_hook("JUMP")
def jump_hook(state: GlobalState):
try:
address = state.get_current_instruction()["address"]
except IndexError:
raise PluginSkipState
annotation = get_dependency_annotation(state)
annotation.path.append(address)
_check_basic_block(address, annotation)
@symbolic_vm.post_hook("JUMPI")
def jumpi_hook(state: GlobalState):
try:
address = state.get_current_instruction()["address"]
except IndexError:
raise PluginSkipState
annotation = get_dependency_annotation(state)
annotation.path.append(address)
_check_basic_block(address, annotation)
@symbolic_vm.pre_hook("SSTORE")
def sstore_hook(state: GlobalState):
annotation = get_dependency_annotation(state)
location = state.mstate.stack[-1]
self.update_sstores(annotation.path, location)
annotation.extend_storage_write_cache(self.iteration, location)
@symbolic_vm.pre_hook("SLOAD")
def sload_hook(state: GlobalState):
annotation = get_dependency_annotation(state)
location = state.mstate.stack[-1]
if location not in annotation.storage_loaded:
annotation.storage_loaded.add(location)
# We backwards-annotate the path here as sometimes execution never reaches a stop or return
# (and this may change in a future transaction).
self.update_sloads(annotation.path, 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("STATICCALL")
def staticcall_hook(state: GlobalState):
annotation = get_dependency_annotation(state)
self.update_calls(annotation.path)
annotation.has_call = True
@symbolic_vm.pre_hook("STOP")
def stop_hook(state: GlobalState):
_transaction_end(state)
@symbolic_vm.pre_hook("RETURN")
def return_hook(state: GlobalState):
_transaction_end(state)
def _transaction_end(state: GlobalState) -> None:
"""When a stop or return is reached, the storage locations read along the path are entered into
the dependency map for all nodes encountered in this path.
:param state:
"""
annotation = get_dependency_annotation(state)
for index in annotation.storage_loaded:
self.update_sloads(annotation.path, index)
for index in annotation.storage_written:
self.update_sstores(annotation.path, index)
if annotation.has_call:
self.update_calls(annotation.path)
def _check_basic_block(address: int, annotation: DependencyAnnotation):
"""This method is where the actual pruning happens.
:param address: Start address (bytecode offset) of the block
:param annotation:
"""
# Don't skip any blocks in the contract creation transaction
if self.iteration < 2:
return
# Don't skip newly discovered blocks
if address not in annotation.blocks_seen:
annotation.blocks_seen.add(address)
return
if self.wanna_execute(address, annotation):
return
else:
log.debug(
"Skipping state: Storage slots {} not read in block at address {}, function".format(
annotation.get_storage_write_cache(self.iteration - 1), address
)
)
raise PluginSkipState
@symbolic_vm.laser_hook("add_world_state")
def world_state_filter_hook(state: GlobalState):
if isinstance(state.current_transaction, ContractCreationTransaction):
# Reset iteration variable
self.iteration = 0
return
world_state_annotation = get_ws_dependency_annotation(state)
annotation = get_dependency_annotation(state)
# Reset the state annotation except for storage written which is carried on to
# the next transaction
annotation.path = [0]
annotation.storage_loaded = set()
world_state_annotation.annotations_stack.append(annotation)
================================================
FILE: mythril/laser/plugin/plugins/instruction_profiler.py
================================================
import logging
from collections import namedtuple
from datetime import datetime
from typing import Dict, List, Tuple
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
# Type annotations:
# start_time: datetime
# end_time: datetime
_InstrExecRecord = namedtuple("_InstrExecRecord", ["start_time", "end_time"])
# Type annotations:
# total_time: float
# total_nr: float
# min_time: float
# max_time: float
_InstrExecStatistic = namedtuple(
"_InstrExecStatistic", ["total_time", "total_nr", "min_time", "max_time"]
)
# Map the instruction opcode to its records if all execution times
_InstrExecRecords = Dict[str, List[_InstrExecRecord]]
# Map the instruction opcode to the statistic of its execution times
_InstrExecStatistics = Dict[str, _InstrExecStatistic]
log = logging.getLogger(__name__)
class InstructionProfilerBuilder(PluginBuilder):
name = "instruction-profiler"
def __call__(self, *args, **kwargs):
return InstructionProfiler()
class InstructionProfiler(LaserPlugin):
"""Performance profile for the execution of each instruction."""
def __init__(self):
self._reset()
def _reset(self):
self.records = dict()
self.start_time = None
def initialize(self, symbolic_vm: LaserEVM) -> None:
@symbolic_vm.instr_hook("pre", None)
def get_start_time(op_code: str):
def start_time_wrapper(global_state: GlobalState):
self.start_time = datetime.now()
return start_time_wrapper
@symbolic_vm.instr_hook("post", None)
def record(op_code: str):
def record_opcode(global_state: GlobalState):
end_time = datetime.now()
try:
self.records[op_code].append(
_InstrExecRecord(self.start_time, end_time)
)
except KeyError:
self.records[op_code] = [
_InstrExecRecord(self.start_time, end_time)
]
return record_opcode
@symbolic_vm.laser_hook("stop_sym_exec")
def print_stats():
total, stats = self._make_stats()
s = "Total: {} s\n".format(total)
for op in sorted(stats):
stat = stats[op]
s += "[{:12s}] {:>8.4f} %, nr {:>6}, total {:>8.4f} s, avg {:>8.4f} s, min {:>8.4f} s, max {:>8.4f} s\n".format(
op,
stat.total_time * 100 / total,
stat.total_nr,
stat.total_time,
stat.total_time / stat.total_nr,
stat.min_time,
stat.max_time,
)
log.info(s)
def _make_stats(self) -> Tuple[float, _InstrExecStatistics]:
periods = {
op: list(
map(lambda r: r.end_time.timestamp() - r.start_time.timestamp(), rs)
)
for op, rs in self.records.items()
}
stats = dict()
total_time = 0
for _, (op, times) in enumerate(periods.items()):
stat = _InstrExecStatistic(
total_time=sum(times),
total_nr=len(times),
min_time=min(times),
max_time=max(times),
)
total_time += stat.total_time
stats[op] = stat
return total_time, stats
================================================
FILE: mythril/laser/plugin/plugins/mutation_pruner.py
================================================
from mythril.analysis import solver
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction,
)
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
from mythril.laser.plugin.plugins.plugin_annotations import MutationAnnotation
from mythril.laser.plugin.signals import PluginSkipWorldState
from mythril.laser.smt import UGT, symbol_factory
class MutationPrunerBuilder(PluginBuilder):
name = "mutation-pruner"
def __call__(self, *args, **kwargs):
return MutationPruner()
class MutationPruner(LaserPlugin):
"""Mutation pruner plugin
Let S be a world state from which T is a symbolic transaction, and S' is the resulting world state.
In a situation where T does not execute any mutating instructions we can safely abandon further analysis on top of
state S'. This is for the reason that we already performed analysis on S, which is equivalent.
This optimization inhibits path explosion caused by "clean" behaviour
The basic operation of this plugin is as follows:
- Hook all mutating operations and introduce a MutationAnnotation to the global state on execution of the hook
- Hook the svm EndTransaction on execution filter the states that do not have a mutation annotation
"""
def initialize(self, symbolic_vm: LaserEVM):
"""Initializes the mutation pruner
Introduces hooks for SSTORE operations
:param symbolic_vm:
:return:
"""
@symbolic_vm.pre_hook("SSTORE")
def sstore_mutator_hook(global_state: GlobalState):
global_state.annotate(MutationAnnotation())
"""FIXME: Check for changes in world_state.balances instead of adding MutationAnnotation for all calls.
Requires world_state.starting_balances to be initialized at every tx start *after* call value has been added.
"""
@symbolic_vm.pre_hook("CALL")
def call_mutator_hook(global_state: GlobalState):
global_state.annotate(MutationAnnotation())
@symbolic_vm.pre_hook("STATICCALL")
def staticcall_mutator_hook(global_state: GlobalState):
global_state.annotate(MutationAnnotation())
@symbolic_vm.laser_hook("add_world_state")
def world_state_filter_hook(global_state: GlobalState):
if isinstance(
global_state.current_transaction, ContractCreationTransaction
):
return
if isinstance(global_state.environment.callvalue, int):
callvalue = symbol_factory.BitVecVal(
global_state.environment.callvalue, 256
)
else:
callvalue = global_state.environment.callvalue
try:
constraints = global_state.world_state.constraints + [
UGT(callvalue, symbol_factory.BitVecVal(0, 256))
]
solver.get_model(constraints)
return
except UnsatError:
# callvalue is constrained to 0, therefore there is no balance based world state mutation
pass
if len(list(global_state.get_annotations(MutationAnnotation))) == 0:
raise PluginSkipWorldState
================================================
FILE: mythril/laser/plugin/plugins/plugin_annotations.py
================================================
import logging
from copy import copy
from typing import Dict, List, Set
from mythril.laser.ethereum.state.annotation import (
MergeableStateAnnotation,
StateAnnotation,
)
log = logging.getLogger(__name__)
class MutationAnnotation(StateAnnotation):
"""Mutation Annotation
This is the annotation used by the MutationPruner plugin to record mutations
"""
def __init__(self):
pass
@property
def persist_over_calls(self) -> bool:
return True
class DependencyAnnotation(MergeableStateAnnotation):
"""Dependency Annotation
This annotation tracks read and write access to the state during each transaction.
"""
def __init__(self):
self.storage_loaded: Set = set()
self.storage_written: Dict[int, Set] = {}
self.has_call: bool = False
self.path: List = [0]
self.blocks_seen: Set[int] = set()
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):
return self.storage_written.get(iteration, set())
def extend_storage_write_cache(self, iteration: int, value: object):
if iteration not in self.storage_written:
self.storage_written[iteration] = set()
self.storage_written[iteration].add(value)
def check_merge_annotation(self, other: "DependencyAnnotation"):
if not isinstance(other, DependencyAnnotation):
raise TypeError("Expected an instance of DependencyAnnotation")
return self.has_call == other.has_call and self.path == other.path
def merge_annotation(self, other: "DependencyAnnotation"):
merged_annotation = DependencyAnnotation()
merged_annotation.blocks_seen = self.blocks_seen.union(other.blocks_seen)
merged_annotation.has_call = self.has_call
merged_annotation.path = copy(self.path)
merged_annotation.storage_loaded = self.storage_loaded.union(
other.storage_loaded
)
keys = set(
list(other.storage_written.keys()) + list(self.storage_written.keys())
)
for key in keys:
other_set = other.storage_written.get(key, set())
merged_annotation.storage_written[key] = self.storage_written.get(
key, set()
).union(other_set)
return merged_annotation
class WSDependencyAnnotation(MergeableStateAnnotation):
"""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: List[DependencyAnnotation] = []
def __copy__(self):
result = WSDependencyAnnotation()
result.annotations_stack = copy(self.annotations_stack)
return result
def check_merge_annotation(self, annotation: "WSDependencyAnnotation") -> bool:
if len(self.annotations_stack) != len(annotation.annotations_stack):
# We can only merge worldstate annotations that have seen an equal amount of transactions
# since the beginning of symbolic execution
return False
for a1, a2 in zip(self.annotations_stack, annotation.annotations_stack):
if a1 == a2:
continue
if (
isinstance(a1, MergeableStateAnnotation)
and isinstance(a2, MergeableStateAnnotation)
and a1.check_merge_annotation(a2) is True
):
continue
log.debug("Aborting merge between annotations {} and {}".format(a1, a2))
return False
return True
def merge_annotation(
self, annotation: "WSDependencyAnnotation"
) -> "WSDependencyAnnotation":
merged_annotation = WSDependencyAnnotation()
for a1, a2 in zip(self.annotations_stack, annotation.annotations_stack):
if a1 == a2:
merged_annotation.annotations_stack.append(copy(a1))
merged_annotation.annotations_stack.append(a1.merge_annotation(a2))
return merged_annotation
================================================
FILE: mythril/laser/plugin/plugins/state_merge/__init__.py
================================================
from .state_merge_plugin import StateMergePluginBuilder
================================================
FILE: mythril/laser/plugin/plugins/state_merge/check_mergeability.py
================================================
import logging
from mythril.laser.ethereum.cfg import Node
from mythril.laser.ethereum.state.account import Account
from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.smt import Not
CONSTRAINT_DIFFERENCE_LIMIT = 15
log = logging.getLogger(__name__)
def check_node_merge_condition(node1: Node, node2: Node):
"""
Checks whether two nodes are merge-able
:param node1: The node to be merged
:param node2: The other node to be merged
:return: Boolean, True if we can merge
"""
return all(
[
node1.function_name == node2.function_name,
node1.contract_name == node2.contract_name,
node1.start_addr == node2.start_addr,
]
)
def check_account_merge_condition(account1: Account, account2: Account):
"""
Checks whether we can merge accounts
"""
return all(
[
account1.nonce == account2.nonce,
account1.deleted == account2.deleted,
account1.code.bytecode == account2.code.bytecode,
]
)
def check_ws_merge_condition(state1: WorldState, state2: WorldState):
"""
Checks whether we can merge these states
"""
if state1.node and not check_node_merge_condition(state1.node, state2.node):
return False
for address, account in state2.accounts.items():
if (
address in state1._accounts
and check_account_merge_condition(state1._accounts[address], account)
is False
):
return False
if not _check_merge_annotations(state1, state2):
return False
return True
def _check_merge_annotations(state1: WorldState, state2: WorldState):
"""
Checks whether two annotations can be merged
:param state:
:return:
"""
if len(state2.annotations) != len(state1.annotations):
return False
if _check_constraint_merge(state1.constraints, state2.constraints) is False:
return False
for v1, v2 in zip(state2.annotations, state1.annotations):
if type(v1) != type(v2):
return False
try:
if v1.check_merge_annotation(v2) is False: # type: ignore
return False
except AttributeError:
log.error(
f"check_merge_annotation() method doesn't exist "
f"for the annotation {type(v1)}. Aborting merge for the state"
)
return False
return True
def _check_constraint_merge(
constraints1: Constraints, constraints2: Constraints
) -> bool:
"""
We are merging the states which have a no more than CONSTRAINT_DIFFERENCE_LIMIT
different constraints. This helps in merging states which are not too different
"""
dict1 = {c: True for c in constraints1}
dict2 = {c: True for c in constraints2}
c1, c2 = 0, 0
for key in dict1:
if key not in dict2 and Not(key) not in dict2:
c1 += 1
for key in dict2:
if key not in dict1 and Not(key) not in dict1:
c2 += 1
if c1 + c2 > CONSTRAINT_DIFFERENCE_LIMIT:
return False
return True
================================================
FILE: mythril/laser/plugin/plugins/state_merge/merge_states.py
================================================
import logging
from typing import Tuple, cast
from mythril.laser.ethereum.cfg import Node
from mythril.laser.ethereum.state.account import Account, Storage
from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.smt import And, Array, If, Not, Or, symbol_factory
from mythril.laser.smt import SMTBool as Bool
log = logging.getLogger(__name__)
def merge_states(state1: WorldState, state2: WorldState):
"""
Merge state2 into state1
:param state1: The state to be merged into
:param state2: The state which is merged into state1
:return:
"""
# Merge constraints
state1.constraints, condition1, _ = _merge_constraints(
state1.constraints, state2.constraints
)
# Merge balances
state1.balances = cast(Array, If(condition1, state1.balances, state2.balances))
state1.starting_balances = cast(
Array, If(condition1, state1.starting_balances, state2.starting_balances)
)
# Merge accounts
for address, account in state2.accounts.items():
if address not in state1._accounts:
state1.put_account(account)
else:
merge_accounts(
state1._accounts[address], account, condition1, state1.balances
)
# Merge annotations
_merge_annotations(state1, state2)
# Merge Node
merge_nodes(state1.node, state2.node, state1.constraints)
def merge_nodes(node1: Node, node2: Node, constraints: Constraints):
"""
Merges node2 into node1
:param node1: The node to be merged
:param node2: The other node to be merged
:param constraints: The merged constraints
:return:
"""
node1.states += node2.states
node1.uid = hash(node1)
node1.flags |= node2.flags
node1.constraints = constraints
def merge_accounts(
account1: Account,
account2: Account,
path_condition: Bool,
merged_balance: Array,
):
"""
Merges account2 into account1
:param account1: The account to merge with
:param account2: The second account to merge
:param path_condition: The constraint for this account
:param merged_balance: The merged balance
:return:
"""
if (
account1.nonce != account2.nonce
or account1.deleted != account2.deleted
or account1.code.bytecode != account2.code.bytecode
):
raise ValueError("Un-Mergeable accounts are given to be merged")
account1._balances = merged_balance
merge_storage(account1.storage, account2.storage, path_condition)
def merge_storage(storage1: Storage, storage2: Storage, path_condition: Bool):
"""
Merge storage2 into storage1
:param storage1: To storage to merge into
:param storage2: To storage to merge with
:param path_condition: The constraint for this storage to be executed
:return:
"""
storage1._standard_storage = If(
path_condition, storage1._standard_storage, storage2._standard_storage
)
storage1.storage_keys_loaded = storage1.storage_keys_loaded.union(
storage2.storage_keys_loaded
)
for key, value in storage2.printable_storage.items():
if key in storage1.printable_storage:
storage1.printable_storage[key] = If(
path_condition, storage1.printable_storage[key], value
)
else:
storage1.printable_storage[key] = If(path_condition, 0, value)
def _merge_annotations(state1: "WorldState", state2: "WorldState"):
"""
Merges the annotations of the two states into state1
:param state1:
:param state2:
:return:
"""
for v1, v2 in zip(state1.annotations, state2.annotations):
try:
v1.merge_annotation(v2) # type: ignore
except AttributeError:
log.error(
f"merge_annotation() method doesn't exist for the annotation {type(v1)}. "
"Aborting merge for the state"
)
return False
def _merge_constraints(
constraints1: Constraints, constraints2: Constraints
) -> Tuple[Constraints, Bool, Bool]:
"""
Merges constraints
:param constraints1: Constraint2 of state1
:param constraints2: Constraints of state2
:return: A Tuple of merged constraints,
conjunction of constraints in state 1 not in state 2, conjunction of constraints
in state2 not in state1
"""
dict1 = {c: True for c in constraints1}
dict2 = {c: True for c in constraints2}
c1, c2 = symbol_factory.Bool(True), symbol_factory.Bool(True)
new_constraint1, new_constraint2 = (
symbol_factory.Bool(True),
symbol_factory.Bool(True),
)
same_constraints = Constraints()
for key in dict1:
if key not in dict2:
c1 = And(c1, key)
if Not(key) not in dict2:
new_constraint1 = And(new_constraint1, key)
else:
same_constraints.append(key)
for key in dict2:
if key not in dict1:
c2 = And(c2, key)
if Not(key) not in dict1:
new_constraint2 = And(new_constraint2, key)
else:
same_constraints.append(key)
merge_constraints = same_constraints + [Or(new_constraint1, new_constraint2)]
return merge_constraints, c1, c2
================================================
FILE: mythril/laser/plugin/plugins/state_merge/state_merge_plugin.py
================================================
import logging
from copy import copy
from typing import List, Set
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.plugin.interface import LaserPlugin
from .check_mergeability import check_ws_merge_condition
from .merge_states import merge_states
log = logging.getLogger(__name__)
class MergeAnnotation(StateAnnotation):
pass
class StateMergePluginBuilder(LaserPlugin):
plugin_default_enabled = True
enabled = True
author = "MythX Development Team"
name = "MythX State Merge"
plugin_license = "All rights reserved."
plugin_type = "Laser Plugin"
plugin_version = "0.0.1 "
plugin_description = "This plugin merges states after the end of a transaction"
def __call__(self, *args, **kwargs):
return StateMergePlugin()
class StateMergePlugin(LaserPlugin):
"""
Tries to merge states based on their similarity.
Currently it only tries to merge if everything is same
except constraints and storage. And there is some tolerance level
to the constraints.
A state can be merged only once --> avoids segfaults + better performance
"""
def initialize(self, symbolic_vm: LaserEVM):
"""Initializes the State merging plugin
Introduces hooks for stop_sym_trans function
:param symbolic_vm:
:return:
"""
@symbolic_vm.laser_hook("stop_sym_trans")
def execute_stop_sym_trans_hook():
open_states = symbolic_vm.open_states
if len(open_states) <= 1:
return
num_old_states = len(open_states)
new_states = [] # type: List[WorldState]
old_size = len(open_states)
old_states = copy(open_states)
while old_size != len(new_states):
old_size = len(new_states)
new_states = []
merged_set = set() # type: Set[int]
for i, state in enumerate(old_states):
if i in merged_set:
continue
if len(list(state.get_annotations(MergeAnnotation))) > 0:
new_states.append(state)
continue
new_states.append(self._look_for_merges(i, old_states, merged_set))
old_states = copy(new_states)
log.info(f"States reduced from {num_old_states} to {len(new_states)}")
symbolic_vm.open_states = new_states
def _look_for_merges(
self,
offset: int,
states: List[WorldState],
merged_set: Set[int],
) -> WorldState:
"""
Tries to merge states[offset] with any of the states in states[offset+1:]
:param offset: The offset of state
:param states: The List of states
:param merged_set: Set indicating which states are excluded from merging
:return: Returns a state
"""
state = states[offset]
for j in range(offset + 1, len(states)):
if j in merged_set or not check_ws_merge_condition(state, states[j]):
continue
merge_states(state, states[j])
merged_set.add(j)
state.annotations.append(MergeAnnotation())
return state
return state
================================================
FILE: mythril/laser/plugin/plugins/summary/__init__.py
================================================
from .core import SymbolicSummaryPluginBuilder
================================================
FILE: mythril/laser/plugin/plugins/summary/annotations.py
================================================
from copy import deepcopy
from typing import List, Tuple
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.smt import BaseArray
from mythril.laser.smt import SMTBool as Bool
class SummaryTrackingAnnotation(StateAnnotation):
"""SummaryTrackingAnnotation
This annotation is used by the symbolic summary plugin to keep track of data related to a summary that
will be computed during the future exploration of the annotated world state.
"""
def __init__(
self,
entry: GlobalState,
storage_pairs: List[Tuple[BaseArray, BaseArray]],
storage_constraints: List[Bool],
environment_pair: Tuple[Environment, Environment],
balance_pair: Tuple[BaseArray, BaseArray],
code: str,
):
self.entry = entry
self.trace = []
self.storage_pairs = storage_pairs
self.storage_constraints = storage_constraints
self.environment_pair = environment_pair
self.balance_pair = balance_pair
self.code = code
def __copy__(self):
annotation = SummaryTrackingAnnotation(
entry=self.entry,
storage_pairs=deepcopy(self.storage_pairs),
storage_constraints=deepcopy(self.storage_constraints),
environment_pair=deepcopy(self.environment_pair),
balance_pair=deepcopy(self.balance_pair),
code=self.code,
)
annotation.trace = self.trace
return annotation
================================================
FILE: mythril/laser/plugin/plugins/summary/core.py
================================================
import logging
from copy import copy, deepcopy
from typing import List, Optional, Set, Tuple
import z3
from mythril.analysis.issue_annotation import IssueAnnotation
from mythril.analysis.potential_issues import check_potential_issues
from mythril.analysis.solver import get_transaction_sequence
from mythril.exceptions import UnsatError
from mythril.laser.ethereum.function_managers import (
keccak_function_manager,
)
from mythril.laser.ethereum.state.calldata import SymbolicCalldata
from mythril.laser.ethereum.state.constraints import Constraints
from mythril.laser.ethereum.state.environment import Environment
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.ethereum.transaction.transaction_models import (
BaseTransaction,
ContractCreationTransaction,
)
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
from mythril.laser.plugin.plugins.plugin_annotations import MutationAnnotation
from mythril.laser.plugin.signals import PluginSkipState
from mythril.laser.smt import (
Array,
Expression,
Solver,
symbol_factory,
)
from mythril.laser.smt import (
SMTBool as Bool,
)
from mythril.support.support_args import args
from mythril.support.support_utils import get_code_hash
from .annotations import SummaryTrackingAnnotation
from .summary import SymbolicSummary, substitute_exprs
log = logging.getLogger(__name__)
class SymbolicSummaryPluginBuilder(PluginBuilder):
name = "Symbolic Summaries"
def __call__(self, *args, **kwargs):
return SymbolicSummaryPlugin()
class SymbolicSummaryPlugin(LaserPlugin):
def __init__(self):
self.summaries = []
args.use_issue_annotations = True
self.issue_cache: Set[Tuple[str, str, int]] = set()
self.init_save_states = []
self.save_init_balance = None
def initialize(self, symbolic_vm: LaserEVM):
"""Initializes the symbolic summary generating plugin
Introduces hooks for each instruction
:param symbolic_vm: the symbolic virtual machine to initialize this plugin for
"""
"""
@symbolic_vm.laser_hook("start_execute_transactions"):
def start_exec_txs_hook():
log.info(f"Started executing transactions")
symbolic_vm.executed_transactions = True
"""
@symbolic_vm.laser_hook("stop_sym_exec")
def stop_sym_exec_hook():
# Print results
log.info(f"Generated {len(self.summaries)} summaries")
@symbolic_vm.laser_hook("execute_state")
def execute_start_sym_trans_hook(global_state: GlobalState):
if global_state.mstate.pc == 0:
if len(global_state.world_state.transaction_sequence) == 2:
self.init_save_states.append(deepcopy(global_state))
self._apply_summaries(laser_evm=symbolic_vm, global_state=global_state)
self.save_init_balance = deepcopy(global_state.world_state.balances)
self._summary_entry(global_state)
@symbolic_vm.post_hook("JUMPI")
@symbolic_vm.post_hook("JUMP")
def call_mutator_hook(global_state: GlobalState):
for annotation in global_state.get_annotations(SummaryTrackingAnnotation):
annotation.trace.append(global_state.instruction["address"])
@symbolic_vm.laser_hook("transaction_end")
def transaction_end_hook(
global_state: GlobalState,
transaction: BaseTransaction,
return_global_state: Optional[GlobalState],
revert: bool = True,
):
if return_global_state is not None:
return
if (
not isinstance(transaction, ContractCreationTransaction)
or transaction.return_data
) and (not revert or list(global_state.get_annotations(IssueAnnotation))):
check_potential_issues(global_state)
self._summary_exit(global_state, transaction, revert)
def _summary_entry(self, global_state: GlobalState):
"""Handles logic for when the analysis reaches an entry point of a to-be recorded symbolic summary
:param global_state: The state at the entry of the symbolic summary
"""
summary_constraints = []
storage_pairs = []
# Rewrite storage
for index, account in global_state.world_state.accounts.items():
actual_storage = deepcopy(account.storage._standard_storage)
symbolic_storage = Array(f"{index}_symbolic_storage", 256, 256)
account.storage._standard_storage = symbolic_storage
storage_pairs.append((actual_storage, symbolic_storage))
account.storage.keys_get = set()
account.storage.keys_set = set()
# Rewrite balances
previous_balances, summary_balances = (
global_state.world_state.balances,
Array("summary_balance", 256, 256),
)
global_state.world_state.balances = summary_balances
balances_pair = (previous_balances, summary_balances)
# Rewrite environment
previous_environment = global_state.environment
summary_environment = self._create_summary_environment(previous_environment)
environment_pair = (previous_environment, summary_environment)
# Construct the summary tracking annotation
summary_annotation = SummaryTrackingAnnotation(
global_state,
storage_pairs,
summary_constraints,
environment_pair,
balances_pair,
global_state.environment.code.bytecode,
)
# Introduce annotation and constraints to the global state
for constraint in summary_constraints:
global_state.world_state.constraints.append(constraint)
global_state.annotate(summary_annotation)
def _create_summary_environment(self, base_environment: Environment) -> Environment:
return Environment(
# No need to rewrite, accounts are handled in other procedure
active_account=base_environment.active_account,
# Need to rewrite, different symbol for each transaction
sender=symbol_factory.BitVecSym("summary_sender", 256),
# Need to rewrite, different symbol for each transaction
origin=symbol_factory.BitVecSym("summary_origin", 256),
# Need to rewrite, different symbol for each transaction
calldata=SymbolicCalldata("summary"),
# Need to rewrite, different symbol for each transaction
gasprice=symbol_factory.BitVecSym("summary_origin", 256),
# Need to rewrite, different symbol for each transaction
callvalue=symbol_factory.BitVecSym("summary_callvalue", 256),
# No need to rewrite, this can be inherited from the original environment
static=base_environment.static,
# No need to rewrite, this can be inherited from the original environment
code=base_environment.code,
basefee=base_environment.basefee,
)
@classmethod
def _restore_environment(
cls,
summary_tracking_annotation: SummaryTrackingAnnotation,
global_state: GlobalState,
):
global_state.environment = summary_tracking_annotation.environment_pair[0]
original, summary = summary_tracking_annotation.environment_pair
# Rewrite sender
cls._rewrite(global_state, summary.sender, original.sender)
# Rewrite origin
cls._rewrite(global_state, summary.origin, original.origin)
# Rewrite calldata
cls._rewrite(
global_state, summary.calldata.calldatasize, original.calldata.calldatasize
)
cls._rewrite(
global_state, summary.calldata._calldata, original.calldata._calldata
)
# Rewrite gasprice
cls._rewrite(global_state, summary.gasprice, original.gasprice)
# Rewrite callvalue
cls._rewrite(global_state, summary.callvalue, original.callvalue)
def check_for_issues(self, global_state):
for summary in self.summaries:
for issue in summary.issues:
self._check_issue(global_state, issue)
def storage_dependent(self, summary, global_state: GlobalState) -> bool:
"""
Checks if storage of summary depends on global state's previous storage stores
"""
total_key_set = set()
for index in global_state.accounts:
total_key_set = total_key_set.union(
global_state.accounts[index].storage.keys_set
)
if len(global_state.world_state.transaction_sequence) <= 3:
return True
for index, storage_get in summary.get_map.items():
for key in storage_get:
if key.symbolic is False:
if key in global_state.accounts[index].storage.keys_set:
return True
else:
for state_key in global_state.accounts[index].storage.keys_set:
s = Solver()
s.set_timeout(3000)
s.add(state_key == key)
s.add(keccak_function_manager.create_conditions())
sat = s.check() == z3.sat
if sat:
return True
return False
def _apply_summaries(self, laser_evm: LaserEVM, global_state: GlobalState):
"""
Applies summaries on the EVM
"""
pc = global_state.instruction["address"]
self.check_for_issues(global_state)
summaries = [
summary
for summary in self.summaries
if summary.entry == pc
and summary.code == global_state.environment.code.bytecode
and not summary.revert
and summary.storage_effect
]
for summary in summaries:
resulting_state = summary.apply_summary(global_state)
if resulting_state:
laser_evm._add_world_state(resulting_state[0])
if summaries:
raise PluginSkipState
def issue_in_cache(
self, global_state: GlobalState, issue_annotation: IssueAnnotation
) -> bool:
address = (
issue_annotation.issue.source_location or issue_annotation.issue.address
)
return (
issue_annotation.detector.swc_id,
address,
get_code_hash(global_state.environment.code.bytecode),
) in self.issue_cache
def _check_issue(
self, global_state: GlobalState, issue_annotation: IssueAnnotation
):
if self.issue_in_cache(global_state, issue_annotation):
return
success = 0
tx_seq = []
for constraint in issue_annotation.conditions:
condition = self._translate_condition(
global_state,
[constraint, deepcopy(keccak_function_manager.create_conditions())],
)
condition = [
expr
for expr in global_state.world_state.constraints.as_list + condition
]
try:
tx_seq = get_transaction_sequence(global_state, Constraints(condition))
success += 1
except UnsatError:
break
if success == len(issue_annotation.conditions):
log.info("Found an issue")
new_issue = copy(issue_annotation.issue)
new_issue.transaction_sequence = tx_seq
issue_annotation.detector.issues += [new_issue]
address = (
issue_annotation.issue.source_location or issue_annotation.issue.address
)
self.issue_cache.add(
(
issue_annotation.detector.swc_id,
address,
get_code_hash(global_state.environment.code.bytecode),
)
)
def _translate_condition(self, global_state: GlobalState, condition: List[Bool]):
condition = deepcopy(condition)
for account_id, account in global_state.world_state.accounts.items():
for expression in condition:
substitute_exprs(expression, account_id, account, global_state)
return condition
def _summary_exit(
self, global_state: GlobalState, transaction: BaseTransaction, revert: bool
):
"""Handles logic for when the analysis reaches the summary exit
This function populates self.summaries with the discovered symbolic summaries
:param global_state: The state at the exit of the discovered symbolic summary
"""
summary_annotation = self._get_and_remove_summary_tracking_annotation(
global_state
)
if not summary_annotation:
log.error("Missing Annotation")
return
self._record_symbolic_summary(
global_state, summary_annotation, transaction, revert
)
self._restore_previous_state(global_state, summary_annotation)
@staticmethod
def _get_and_remove_summary_tracking_annotation(
global_state: GlobalState,
) -> Optional[SummaryTrackingAnnotation]:
"""Retrieves symbolic summary from the global state"""
summary_annotation: List[SummaryTrackingAnnotation] = list(
global_state.get_annotations(SummaryTrackingAnnotation)
)
if len(summary_annotation) != 1:
logging.warning(
f"Unexpected number of summary tracking annotations found: {len(summary_annotation)}\nSkipping..."
)
summary_annotation: SummaryTrackingAnnotation = summary_annotation[0]
global_state.annotations.remove(summary_annotation)
return summary_annotation
def _record_symbolic_summary(
self,
global_state: GlobalState,
tracking_annotation: SummaryTrackingAnnotation,
transaction: BaseTransaction,
revert,
):
"""Records a summary between tracking_annotation.entry and global_state"""
if (
len(list(global_state.get_annotations(MutationAnnotation))) == 0
and list(global_state.get_annotations(IssueAnnotation)) == 0
):
return
storage_mutations = []
return_value = transaction.return_data
set_map = {}
get_map = {}
for index, account in global_state.world_state.accounts.items():
if account.storage._standard_storage not in [
p[1] for p in tracking_annotation.storage_pairs
]:
get_map[account.address] = account.storage.keys_get
set_map[account.address] = account.storage.keys_set
storage_mutations.append(
(index, copy(account.storage._standard_storage))
)
condition = global_state.world_state.constraints.get_all_constraints()
for constraint in tracking_annotation.entry.world_state.constraints:
condition.remove(constraint)
annotations = list(global_state.get_annotations(IssueAnnotation))
summary = SymbolicSummary(
storage_effect=deepcopy(storage_mutations),
balance_effect=copy(global_state.world_state.balances),
condition=deepcopy(condition),
return_value=return_value,
entry=tracking_annotation.entry.mstate.pc,
exit=global_state.mstate.pc,
trace=tracking_annotation.trace,
code=tracking_annotation.code,
issues=annotations,
revert=revert,
get_map=get_map,
set_map=set_map,
)
log.debug(annotations)
# Calculate issues for the first transaction
if len(global_state.world_state.transaction_sequence) == 2:
for state in self.init_save_states:
for issue in summary.issues:
self._check_issue(state, issue)
self.summaries.append(summary)
@classmethod
def _restore_previous_state(
cls, global_state: GlobalState, tracking_annotation: SummaryTrackingAnnotation
):
"""Restores the previous persistent variables to the global state"""
for og_storage, sym_storage in tracking_annotation.storage_pairs:
cls._rewrite(global_state, sym_storage, og_storage)
cls._rewrite(
global_state,
tracking_annotation.balance_pair[1],
tracking_annotation.balance_pair[0],
)
cls._restore_environment(tracking_annotation, global_state)
@staticmethod
def _rewrite(global_state: GlobalState, original: Expression, new: Expression):
for account in global_state.world_state.accounts.values():
account.storage._standard_storage.substitute(original, new)
global_state.world_state.balances.substitute(original, new)
for constraint in global_state.world_state.constraints:
constraint.substitute(original, new)
================================================
FILE: mythril/laser/plugin/plugins/summary/summary.py
================================================
import logging
from copy import deepcopy
import z3
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.plugin.plugins.plugin_annotations import MutationAnnotation
from mythril.laser.smt import Array, Solver, symbol_factory
from mythril.support.support_args import args
log = logging.getLogger(__name__)
class SymbolicSummary:
"""Symbolic Summary
A symbolic summary is an awesome construct that allows mythril to record and re-use partial analysis results
"""
def __init__(
self,
storage_effect,
balance_effect,
condition,
return_value,
entry,
exit,
trace,
code,
issues,
revert,
set_map=None,
get_map=None,
):
self.storage_effect = storage_effect
self.balance_effect = balance_effect
self.condition = condition
self.return_value = return_value
self.entry = entry
self.exit = exit
self.trace = trace
self.code = code
self.issues = issues
self.revert = revert
self.set_map = set_map
self.get_map = get_map
@property
def as_csv(self, delimiter=",", sub_array_delimiter=";", tuple_delimiter=":"):
condition = sub_array_delimiter.join(map(str, self.condition))
storage_effect = sub_array_delimiter.join(
[f"{ap[0]}{tuple_delimiter}{ap[1]}" for ap in self.storage_effect]
)
return_value = None
trace = sub_array_delimiter.join(map(str, self.trace))
return (
delimiter.join(
map(
str,
[
self.entry,
condition,
self.exit,
storage_effect,
return_value,
trace,
],
)
)
.replace("\n", "")
.replace(" ", "")
)
@property
def as_dict(self):
return dict(
entry=self.entry,
condition=list(map(str, self.condition)),
exit=self.exit,
storage_effect=list(map(str, self.storage_effect)),
balance_effect=str(self.balance_effect),
return_value=self.return_value,
trace=self.trace[:],
code=self.code,
issues=len(self.issues),
revert=self.revert,
)
def apply_summary(self, global_state: GlobalState):
# Copy and apply summary
global_state = deepcopy(global_state)
conditions = deepcopy(self.condition)
for account_id, account in global_state.world_state.accounts.items():
for expression in conditions:
substitute_exprs(expression, account_id, account, global_state)
for account_id, effect in self.storage_effect:
account = global_state.world_state.accounts[account_id]
new_storage = deepcopy(effect)
substitute_exprs(new_storage, account_id, account, global_state)
account.storage._standard_storage = new_storage
new_balances = deepcopy(self.balance_effect)
new_balances.substitute(
Array("summary_balance", 256, 256), global_state.world_state.balances
)
global_state.world_state.balances = new_balances
# Set constraints
global_state.world_state.constraints += [c for c in conditions]
# Check Condition
solver = Solver()
solver.set_timeout(args.solver_timeout)
solver.add(*(global_state.world_state.constraints.as_list))
sat = solver.check() == z3.sat
if not sat:
return []
global_state.node.constraints = global_state.world_state.constraints
global_state.world_state.node = global_state.node
global_state.annotate(MutationAnnotation())
return [global_state]
def substitute_exprs(expression, account_id, account, global_state):
a = Array("2_calldata", 256, 8)
b = Array(f"{global_state.current_transaction.id}_calldata", 256, 8)
expression.substitute(a, b)
a = symbol_factory.BitVecSym("2_calldatasize", 256)
b = symbol_factory.BitVecSym(
f"{global_state.current_transaction.id}_calldatasize", 256
)
expression.substitute(a, b)
a = symbol_factory.BitVecSym("sender_2", 256)
b = symbol_factory.BitVecSym(f"sender_{global_state.current_transaction.id}", 256)
expression.substitute(a, b)
a = symbol_factory.BitVecSym("call_value2", 256)
b = symbol_factory.BitVecSym(
f"call_value{global_state.current_transaction.id}", 256
)
expression.substitute(a, b)
a = Array(f"{account_id}_symbolic_storage", 256, 256)
b = account.storage._standard_storage
expression.substitute(a, b)
a = Array("summary_balance", 256, 256)
b = global_state.world_state.balances
expression.substitute(a, b)
================================================
FILE: mythril/laser/plugin/plugins/summary_backup/__init__.py
================================================
================================================
FILE: mythril/laser/plugin/plugins/trace.py
================================================
from typing import List, Tuple
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.plugin.builder import PluginBuilder
from mythril.laser.plugin.interface import LaserPlugin
class TraceFinderBuilder(PluginBuilder):
name = "trace-finder"
plugin_default_enabled = True
enabled = True
author = "MythX Development Team"
name = "MythX Trace Finder"
plugin_license = "All rights reserved."
plugin_type = "Laser Plugin"
plugin_version = "0.0.1 "
plugin_description = "This plugin merges states after the end of a transaction"
def __call__(self, *args, **kwargs):
return TraceFinder()
class TraceFinder(LaserPlugin):
def __init__(self):
self._reset()
def _reset(self):
self.tx_trace: List[List[Tuple[int, str]]] = []
def initialize(self, symbolic_vm: LaserEVM):
"""Initializes Trace Finder
Introduces hooks during the start of the execution and each execution state
:param symbolic_vm:
:return:
"""
self._reset()
@symbolic_vm.laser_hook("start_exec")
def start_sym_trans_hook():
self.tx_trace.append([])
@symbolic_vm.laser_hook("execute_state")
def trace_jumpi_hook(global_state: GlobalState):
self.tx_trace[-1].append(
(global_state.mstate.pc, global_state.current_transaction.id)
)
================================================
FILE: mythril/laser/plugin/signals.py
================================================
class PluginSignal(Exception):
"""Base plugin signal
These signals are used by the laser plugins to create intent for certain actions in the symbolic virtual machine
"""
pass
class PluginSkipWorldState(PluginSignal):
"""Plugin to skip world state
Plugins that raise this signal while the add_world_state hook is being executed
will force laser to abandon that world state.
"""
pass
class PluginSkipState(PluginSignal):
"""Plugin to skip world state
Plugins that raise this signal while the add_world_state hook is being executed
will force laser to abandon that world state.
"""
pass
================================================
FILE: mythril/laser/smt/__init__.py
================================================
from typing import Any, Generic, Optional, Set, TypeVar, Union
import z3
from mythril.laser.smt.array import Array, BaseArray, K
from mythril.laser.smt.bitvec import BitVec
from mythril.laser.smt.bitvec_helper import (
UGE,
UGT,
ULE,
ULT,
BVAddNoOverflow,
BVMulNoOverflow,
BVSubNoUnderflow,
Concat,
Extract,
If,
LShR,
SRem,
Sum,
UDiv,
URem,
)
from mythril.laser.smt.bool import And, Not, Or, is_false, is_true
from mythril.laser.smt.bool import Bool as SMTBool
from mythril.laser.smt.expression import Expression, simplify
from mythril.laser.smt.function import Function
from mythril.laser.smt.model import Model
from mythril.laser.smt.solver import Optimize, Solver, SolverStatistics
Annotations = Optional[Set[Any]]
T = TypeVar("T", bound=Union[SMTBool, z3.BoolRef])
U = TypeVar("U", bound=Union[BitVec, z3.BitVecRef])
class SymbolFactory(Generic[T, U]):
"""A symbol factory provides a default interface for all the components of mythril to create symbols"""
@staticmethod
def Bool(value: "__builtins__.bool", annotations: Annotations = None) -> T:
"""
Creates a Bool with concrete value
:param value: The boolean value
:param annotations: The annotations to initialize the bool with
:return: The freshly created Bool()
"""
raise NotImplementedError
@staticmethod
def BoolSym(name: str, annotations: Annotations = None) -> T:
"""
Creates a boolean symbol
:param name: The name of the Bool variable
:param annotations: The annotations to initialize the bool with
:return: The freshly created Bool()
"""
raise NotImplementedError
@staticmethod
def BitVecVal(value: int, size: int, annotations: Annotations = None) -> U:
"""Creates a new bit vector with a concrete value.
:param value: The concrete value to set the bit vector to
:param size: The size of the bit vector
:param annotations: The annotations to initialize the bit vector with
:return: The freshly created bit vector
"""
raise NotImplementedError()
@staticmethod
def BitVecSym(name: str, size: int, annotations: Annotations = None) -> U:
"""Creates a new bit vector with a symbolic value.
:param name: The name of the symbolic bit vector
:param size: The size of the bit vector
:param annotations: The annotations to initialize the bit vector with
:return: The freshly created bit vector
"""
raise NotImplementedError()
class _SmtSymbolFactory(SymbolFactory[SMTBool, BitVec]):
"""
An implementation of a SymbolFactory that creates symbols using
the classes in: mythril.laser.smt
"""
@staticmethod
def Bool(value: "__builtins__.bool", annotations: Annotations = None) -> SMTBool:
"""
Creates a Bool with concrete value
:param value: The boolean value
:param annotations: The annotations to initialize the bool with
:return: The freshly created Bool()
"""
raw = z3.BoolVal(value)
return SMTBool(raw, annotations)
@staticmethod
def BoolSym(name: str, annotations: Annotations = None) -> SMTBool:
"""
Creates a boolean symbol
:param name: The name of the Bool variable
:param annotations: The annotations to initialize the bool with
:return: The freshly created Bool()
"""
raw = z3.Bool(name)
return SMTBool(raw, annotations)
@staticmethod
def BitVecVal(value: int, size: int, annotations: Annotations = None) -> BitVec:
"""Creates a new bit vector with a concrete value."""
raw = z3.BitVecVal(value, size)
return BitVec(raw, annotations)
@staticmethod
def BitVecSym(name: str, size: int, annotations: Annotations = None) -> BitVec:
"""Creates a new bit vector with a symbolic value."""
raw = z3.BitVec(name, size)
return BitVec(raw, annotations)
class _Z3SymbolFactory(SymbolFactory[z3.BoolRef, z3.BitVecRef]):
"""
An implementation of a SymbolFactory that directly returns
z3 symbols
"""
@staticmethod
def Bool(value: "__builtins__.bool", annotations: Annotations = None) -> z3.BoolRef:
"""Creates a new bit vector with a concrete value"""
return z3.BoolVal(value)
@staticmethod
def BitVecVal(
value: int, size: int, annotations: Annotations = None
) -> z3.BitVecRef:
"""Creates a new bit vector with a concrete value."""
return z3.BitVecVal(value, size)
@staticmethod
def BitVecSym(
name: str, size: int, annotations: Annotations = None
) -> z3.BitVecRef:
"""Creates a new bit vector with a symbolic value."""
return z3.BitVec(name, size)
# This is the instance that other parts of mythril should use
# Type hints are not allowed here in 3.5
# symbol_factory: SymbolFactory = _SmtSymbolFactory()
symbol_factory = _SmtSymbolFactory()
================================================
FILE: mythril/laser/smt/array.py
================================================
"""This module contains an SMT abstraction of arrays.
This includes an Array class to implement basic store and set
operations, as well as as a K-array, which can be initialized with
default values over a certain range.
"""
from typing import cast
import z3
from mythril.laser.smt.bitvec import BitVec
class BaseArray:
"""Base array type, which implements basic store and set operations."""
def __init__(self, raw):
self.raw = raw
def __getitem__(self, item: BitVec) -> BitVec:
"""Gets item from the array, item can be symbolic."""
if isinstance(item, slice):
raise ValueError(
"Instance of BaseArray, does not support getitem with slices"
)
return BitVec(cast(z3.BitVecRef, z3.Select(self.raw, item.raw)))
def __setitem__(self, key: BitVec, value: BitVec) -> None:
"""Sets an item in the array, key can be symbolic."""
self.raw = z3.Store(self.raw, key.raw, value.raw)
def substitute(self, original_expression, new_expression):
"""
:param original_expression:
:param new_expression:
"""
if self.raw is None:
return
original_z3 = original_expression.raw
new_z3 = new_expression.raw
self.raw = z3.substitute(self.raw, (original_z3, new_z3))
class Array(BaseArray):
"""A basic symbolic array."""
def __init__(self, name: str, domain: int, value_range: int):
"""Initializes a symbolic array.
:param name: Name of the array
:param domain: The domain for the array (10 -> all the values that a bv of size 10 could take)
:param value_range: The range for the values in the array (10 -> all the values that a bv of size 10 could take)
"""
self.domain = z3.BitVecSort(domain)
self.range = z3.BitVecSort(value_range)
super(Array, self).__init__(z3.Array(name, self.domain, self.range))
class K(BaseArray):
"""A basic symbolic array, which can be initialized with a default
value."""
def __init__(self, domain: int, value_range: int, value: int):
"""Initializes an array with a default value.
:param domain: The domain for the array (10 -> all the values that a bv of size 10 could take)
:param value_range: The range for the values in the array (10 -> all the values that a bv of size 10 could take)
:param value: The default value to use for this array
"""
self.domain = z3.BitVecSort(domain)
self.value = z3.BitVecVal(value, value_range)
self.raw = z3.K(self.domain, self.value)
================================================
FILE: mythril/laser/smt/bitvec.py
================================================
"""This module provides classes for an SMT abstraction of bit vectors."""
from operator import eq, lshift, ne, rshift
from typing import Any, Callable, Optional, Set, Union, cast
import z3
from mythril.laser.smt.bool import Bool
from mythril.laser.smt.expression import Expression
Annotations = Set[Any]
# 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]):
"""A bit vector symbol."""
def __init__(self, raw: z3.BitVecRef, annotations: Optional[Annotations] = None):
"""
:param raw:
:param annotations:
"""
super().__init__(raw, annotations)
def size(self) -> int:
"""TODO: documentation
:return:
"""
return self.raw.size()
@property
def symbolic(self) -> bool:
"""Returns whether this symbol doesn't have a concrete value.
:return:
"""
self.simplify()
return not isinstance(self.raw, z3.BitVecNumRef)
@property
def value(self) -> Optional[int]:
"""Returns the value of this symbol if concrete, otherwise None.
:return:
"""
if self.symbolic:
return None
assert isinstance(self.raw, z3.BitVecNumRef)
return self.raw.as_long()
def __add__(self, other: Union[int, "BitVec"]) -> "BitVec":
"""Create an addition expression.
:param other:
:return:
"""
if isinstance(other, int):
return BitVec(self.raw + other, annotations=self.annotations)
union = self.annotations.union(other.annotations)
return BitVec(self.raw + other.raw, annotations=union)
def __sub__(self, other: Union[int, "BitVec"]) -> "BitVec":
"""Create a subtraction expression.
:param other:
:return:
"""
if isinstance(other, int):
return BitVec(self.raw - other, annotations=self.annotations)
union = self.annotations.union(other.annotations)
return BitVec(self.raw - other.raw, annotations=union)
def __mul__(self, other: "BitVec") -> "BitVec":
"""Create a multiplication expression.
:param other:
:return:
"""
union = self.annotations.union(other.annotations)
return BitVec(self.raw * other.raw, annotations=union)
def __truediv__(self, other: "BitVec") -> "BitVec":
"""Create a signed division expression.
:param other:
:return:
"""
union = self.annotations.union(other.annotations)
return BitVec(self.raw / other.raw, annotations=union)
def __and__(self, other: Union[int, "BitVec"]) -> "BitVec":
"""Create an and expression.
:param other:
:return:
"""
if not isinstance(other, BitVec):
other = BitVec(z3.BitVecVal(other, self.size()))
union = self.annotations.union(other.annotations)
return BitVec(self.raw & other.raw, annotations=union)
def __or__(self, other: Union[int, "BitVec"]) -> "BitVec":
"""Create an or expression.
:param other:
:return:
"""
if not isinstance(other, BitVec):
other = BitVec(z3.BitVecVal(other, self.size()))
union = self.annotations.union(other.annotations)
return BitVec(self.raw | other.raw, annotations=union)
def __xor__(self, other: Union[int, "BitVec"]) -> "BitVec":
"""Create a xor expression.
:param other:
:return:
"""
if not isinstance(other, BitVec):
other = BitVec(z3.BitVecVal(other, self.size()))
union = self.annotations.union(other.annotations)
return BitVec(self.raw ^ other.raw, annotations=union)
def __lt__(self, other: Union[int, "BitVec"]) -> Bool:
"""Create a signed less than expression.
:param other:
:return:
"""
if not isinstance(other, BitVec):
other = BitVec(z3.BitVecVal(other, self.size()))
union = self.annotations.union(other.annotations)
return Bool(self.raw < other.raw, annotations=union)
def __gt__(self, other: Union[int, "BitVec"]) -> Bool:
"""Create a signed greater than expression.
:param other:
:return:
"""
if not isinstance(other, BitVec):
other = BitVec(z3.BitVecVal(other, self.size()))
union = self.annotations.union(other.annotations)
return Bool(self.raw > other.raw, annotations=union)
def __le__(self, other: Union[int, "BitVec"]) -> Bool:
"""Create a signed less than expression.
:param other:
:return:
"""
if not isinstance(other, BitVec):
other = BitVec(z3.BitVecVal(other, self.size()))
union = self.annotations.union(other.annotations)
return Bool(self.raw <= other.raw, annotations=union)
def __ge__(self, other: Union[int, "BitVec"]) -> Bool:
"""Create a signed greater than expression.
:param other:
:return:
"""
if not isinstance(other, BitVec):
other = BitVec(z3.BitVecVal(other, self.size()))
union = self.annotations.union(other.annotations)
return Bool(self.raw >= other.raw, annotations=union)
# MYPY: fix complains about overriding __eq__
def __eq__(self, other: Union[int, "BitVec"]) -> Bool: # type: ignore
"""Create an equality expression.
:param other:
:return:
"""
if not isinstance(other, BitVec):
return Bool(
cast(z3.BoolRef, self.raw == other), annotations=self.annotations
)
union = self.annotations.union(other.annotations)
# Some of the BitVecs can be 512 bit due to sha3()
eq_check = _padded_operation(self.raw, other.raw, eq)
# MYPY: fix complaints due to z3 overriding __eq__
return Bool(cast(z3.BoolRef, eq_check), annotations=union)
# MYPY: fix complains about overriding __ne__
def __ne__(self, other: Union[int, "BitVec"]) -> Bool: # type: ignore
"""Create an inequality expression.
:param other:
:return:
"""
if not isinstance(other, BitVec):
return Bool(
cast(z3.BoolRef, self.raw != other), annotations=self.annotations
)
union = self.annotations.union(other.annotations)
# Some of the BitVecs can be 512 bit due to sha3()
neq_check = _padded_operation(self.raw, other.raw, ne)
# MYPY: fix complaints due to z3 overriding __eq__
return Bool(cast(z3.BoolRef, neq_check), annotations=union)
def _handle_shift(self, other: Union[int, "BitVec"], operator: Callable) -> "BitVec":
"""
Handles shift
:param other: The other BitVector
:param operator: The shift operator
:return: the resulting output
"""
if not isinstance(other, BitVec):
return BitVec(
operator(self.raw, other), annotations=self.annotations
)
union = self.annotations.union(other.annotations)
return BitVec(operator(self.raw, other.raw), annotations=union)
def __lshift__(self, other: Union[int, "BitVec"]) -> "BitVec":
"""
:param other:
:return:
"""
return self._handle_shift(other, lshift)
def __rshift__(self, other: Union[int, "BitVec"]) -> "BitVec":
"""
:param other:
:return:
"""
return self._handle_shift(other, rshift)
def __hash__(self) -> int:
"""
:return:
"""
return self.raw.__hash__()
================================================
FILE: mythril/laser/smt/bitvec_helper.py
================================================
from typing import Any, Callable, List, Set, Union, cast, overload
import z3
from mythril.laser.smt.array import Array, BaseArray
from mythril.laser.smt.bitvec import BitVec
from mythril.laser.smt.bool import Bool, Or
Annotations = Set[Any]
def _z3_array_converter(array: Union[z3.Array, z3.K]) -> Array:
new_array = Array(
"name_to_be_overwritten", array.domain().size(), array.range().size()
)
new_array.raw = array
return new_array
def _comparison_helper(a: BitVec, b: BitVec, operation: Callable) -> Bool:
annotations = a.annotations.union(b.annotations)
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)
return BitVec(raw, annotations=union)
def LShR(a: BitVec, b: BitVec):
return _arithmetic_helper(a, b, z3.LShR)
@overload
def If(
a: Union[Bool, bool], b: Union[BitVec, int], c: Union[BitVec, int]
) -> BitVec: ...
@overload
def If(a: Union[Bool, bool], b: BaseArray, c: BaseArray) -> BaseArray: ...
def If(
a: Union[Bool, bool],
b: Union[BaseArray, BitVec, int],
c: Union[BaseArray, BitVec, int],
) -> Union[BitVec, BaseArray]:
"""Create an if-then-else expression.
:param a:
:param b:
:param c:
:return:
"""
if not isinstance(a, Bool):
a = Bool(z3.BoolVal(a))
if isinstance(b, BaseArray) and isinstance(c, BaseArray):
array = z3.If(a.raw, b.raw, c.raw)
return _z3_array_converter(array)
default_sort_size = 256
if isinstance(b, BitVec):
default_sort_size = b.size()
if isinstance(c, BitVec):
default_sort_size = c.size()
if not isinstance(b, BitVec):
b = BitVec(z3.BitVecVal(b, default_sort_size))
if not isinstance(c, BitVec):
c = BitVec(z3.BitVecVal(c, default_sort_size))
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)
def UGE(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned greater than or equal to 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)
def ULE(a: BitVec, b: BitVec) -> Bool:
"""Create an unsigned less than or equal to 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: List[BitVec] = args[0]
else:
bvs = cast(List[BitVec], args)
nraw = z3.Concat([a.raw for a in bvs])
annotations: Annotations = set()
for bv in bvs:
annotations = annotations.union(bv.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)
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: Annotations = set()
for bv in args:
annotations = annotations.union(bv.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))
================================================
FILE: mythril/laser/smt/bool.py
================================================
"""This module provides classes for an SMT abstraction of boolean
expressions."""
from typing import Set, Union, cast
import z3
from mythril.laser.smt.expression import Expression
# fmt: off
class Bool(Expression[z3.BoolRef]):
"""This is a Bool expression."""
@property
def is_false(self) -> bool:
"""Specifies whether this variable can be simplified to false.
:return:
"""
self.simplify()
return z3.is_false(self.raw)
@property
def is_true(self) -> bool:
"""Specifies whether this variable can be simplified to true.
:return:
"""
self.simplify()
return z3.is_true(self.raw)
@property
def value(self) -> Union[bool, None]:
"""Returns the concrete value of this bool if concrete, otherwise None.
:return: Concrete value or None
"""
self.simplify()
if self.is_true:
return True
elif self.is_false:
return False
else:
return None
# MYPY: complains about overloading __eq__ # noqa
def __eq__(self, other: object) -> "Bool": # type: ignore
"""
:param other:
:return:
"""
if isinstance(other, Expression):
return Bool(cast(z3.BoolRef, self.raw == other.raw),
self.annotations.union(other.annotations))
return Bool(cast(z3.BoolRef, self.raw == other), self.annotations)
# MYPY: complains about overloading __ne__ # noqa
def __ne__(self, other: object) -> "Bool": # type: ignore
"""
:param other:
:return:
"""
if isinstance(other, Expression):
return Bool(cast(z3.BoolRef, self.raw != other.raw),
self.annotations.union(other.annotations))
return Bool(cast(z3.BoolRef, self.raw != other), self.annotations)
def __bool__(self) -> bool:
"""
:return:
"""
if self.value is not None:
return self.value
else:
return False
def substitute(self, original_expression, new_expression):
"""
:param original_expression:
:param new_expression:
"""
if self.raw is None:
return
original_z3 = original_expression.raw
new_z3 = new_expression.raw
self.raw = z3.substitute(self.raw, (original_z3, new_z3))
def __hash__(self) -> int:
return self.raw.__hash__()
def And(*args: Union[Bool, bool]) -> Bool:
"""Create an And expression."""
annotations: Set = set()
args_list = [arg if isinstance(arg, Bool) else Bool(arg) for arg in args]
for arg in args_list:
annotations = annotations.union(arg.annotations)
return Bool(z3.And([a.raw for a in args_list]), annotations)
def Xor(a: Bool, b: Bool) -> Bool:
"""Create an And expression."""
union = a.annotations.union(b.annotations)
return Bool(z3.Xor(a.raw, b.raw), union)
def Or(*args: Union[Bool, bool]) -> Bool:
"""Create an or expression.
:param a:
:param b:
:return:
"""
args_list = [arg if isinstance(arg, Bool) else Bool(arg) for arg in args]
annotations: Set = set()
for arg in args_list:
annotations = annotations.union(arg.annotations)
return Bool(z3.Or([a.raw for a in args_list]), annotations=annotations)
def Not(a: Bool) -> Bool:
"""Create a Not expression.
:param a:
:return:
"""
return Bool(z3.Not(a.raw), a.annotations)
def is_false(a: Bool) -> bool:
"""Returns whether the provided bool can be simplified to false.
:param a:
:return:
"""
return z3.is_false(a.raw)
def is_true(a: Bool) -> bool:
"""Returns whether the provided bool can be simplified to true.
:param a:
:return:
"""
return z3.is_true(a.raw)
================================================
FILE: mythril/laser/smt/expression.py
================================================
"""This module contains the SMT abstraction for a basic symbol expression."""
from typing import Any, Generic, Optional, Set, TypeVar, cast
import z3
Annotations = Set[Any]
T = TypeVar("T", bound=z3.ExprRef)
class Expression(Generic[T]):
"""This is the base symbol class and maintains functionality for
simplification and annotations."""
def __init__(self, raw: T, annotations: Optional[Annotations] = None):
"""
:param raw:
:param annotations:
"""
self.raw = raw
if annotations:
assert isinstance(annotations, set)
self._annotations = annotations or set()
@property
def annotations(self) -> Annotations:
"""Gets the annotations for this expression.
:return:
"""
return self._annotations
def annotate(self, annotation: Any) -> None:
"""Annotates this expression with the given annotation.
:param annotation:
"""
self._annotations.add(annotation)
def simplify(self) -> None:
"""Simplify this expression."""
self.raw = cast(T, z3.simplify(self.raw))
def __repr__(self) -> str:
return repr(self.raw)
def size(self):
return self.raw.size()
def __hash__(self) -> int:
return self.raw.__hash__()
def get_annotations(self, annotation: Any):
return list(filter(lambda x: isinstance(x, annotation), self.annotations))
G = TypeVar("G", bound=Expression)
def simplify(expression: G) -> G:
"""Simplify the expression .
:param expression:
:return:
"""
expression.simplify()
return expression
================================================
FILE: mythril/laser/smt/function.py
================================================
from typing import Any, List, Set, cast
import z3
from mythril.laser.smt.bitvec import BitVec
class Function:
"""An uninterpreted function."""
def __init__(self, name: str, domain: List[int], value_range: int):
"""Initializes an uninterpreted function.
:param name: Name of the Function
:param domain: The domain for the Function (10 -> all the values that a bv of size 10 could take)
:param value_range: The range for the values of the function (10 -> all the values that a bv of size 10 could take)
"""
self.domain = []
for element in domain:
self.domain.append(z3.BitVecSort(element))
self.range = z3.BitVecSort(value_range)
self.raw = z3.Function(name, *self.domain, self.range)
def __call__(self, *items) -> BitVec:
"""Function accessor, item can be symbolic."""
annotations: Set[Any] = set().union(*[item.annotations for item in items])
return BitVec(
cast(z3.BitVecRef, self.raw(*[item.raw for item in items])),
annotations=annotations,
)
================================================
FILE: mythril/laser/smt/model.py
================================================
from typing import List, Union
import z3
class Model:
"""The model class wraps a z3 model
This implementation allows for multiple internal models, this is required for the use of an independence solver
which has models for multiple queries which need an uniform output.
"""
def __init__(self, models: List[z3.ModelRef] = None):
"""
Initialize a model object
:param models: the internal z3 models that this model should use
"""
self.raw = models or []
def decls(self) -> List[z3.ExprRef]:
"""Get the declarations for this model"""
result: List[z3.ExprRef] = []
for internal_model in self.raw:
result.extend(internal_model.decls())
return result
def __getitem__(self, item) -> Union[None, z3.ExprRef]:
"""Get declaration from model
If item is an int, then the declaration at offset item is returned
If item is a declaration, then the interpretation is returned
"""
for internal_model in self.raw:
is_last_model = self.raw.index(internal_model) == len(self.raw) - 1
try:
result = internal_model[item]
if result is not None:
return result
except IndexError:
if is_last_model:
raise
continue
return None
def eval(
self, expression: z3.ExprRef, model_completion: bool = False
) -> Union[None, z3.ExprRef]:
"""Evaluate the expression using this model
:param expression: The expression to evaluate
:param model_completion: Use the default value if the model has no interpretation of the given expression
:return: The evaluated expression
"""
for internal_model in self.raw:
is_last_model = self.raw.index(internal_model) == len(self.raw) - 1
is_relevant_model = expression.decl() in list(internal_model.decls())
if is_relevant_model or is_last_model:
return internal_model.eval(expression, model_completion)
return None
================================================
FILE: mythril/laser/smt/solver/__init__.py
================================================
import z3
from mythril.laser.smt.solver.independence_solver import IndependenceSolver
from mythril.laser.smt.solver.solver import BaseSolver, Optimize, Solver
from mythril.laser.smt.solver.solver_statistics import SolverStatistics
from mythril.support.support_args import args
if args.parallel_solving:
z3.set_param("parallel.enable", True)
================================================
FILE: mythril/laser/smt/solver/independence_solver.py
================================================
from typing import Dict, List, Set, Tuple, cast
import z3
from mythril.laser.smt.bool import Bool
from mythril.laser.smt.model import Model
from mythril.laser.smt.solver.solver_statistics import stat_smt_query
def _get_expr_variables(expression: z3.ExprRef) -> List[z3.ExprRef]:
"""
Gets the variables that make up the current expression
:param expression:
:return:
"""
result = []
if not expression.children() and not isinstance(expression, z3.BitVecNumRef):
result.append(expression)
for child in expression.children():
c_children = _get_expr_variables(child)
result.extend(c_children)
return result
class DependenceBucket:
"""Bucket object to contain a set of conditions that are dependent on each other"""
def __init__(self, variables=None, conditions=None):
"""
Initializes a DependenceBucket object
:param variables: Variables contained in the conditions
:param conditions: The conditions that are dependent on each other
"""
self.variables: List[z3.ExprRef] = variables or []
self.conditions: List[z3.ExprRef] = conditions or []
class DependenceMap:
"""DependenceMap object that maintains a set of dependence buckets, used to separate independent smt queries"""
def __init__(self):
"""Initializes a DependenceMap object"""
self.buckets: List[DependenceBucket] = []
self.variable_map: Dict[str, DependenceBucket] = {}
def add_condition(self, condition: z3.BoolRef) -> None:
"""
Add condition to the dependence map
:param condition: The condition that is to be added to the dependence map
"""
variables = set(_get_expr_variables(condition))
relevant_buckets = set()
for variable in variables:
try:
bucket = self.variable_map[str(variable)]
relevant_buckets.add(bucket)
except KeyError:
continue
new_bucket = DependenceBucket(variables, [condition])
self.buckets.append(new_bucket)
if relevant_buckets:
# Merge buckets, and rewrite variable map accordingly
relevant_buckets.add(new_bucket)
new_bucket = self._merge_buckets(relevant_buckets)
for variable in new_bucket.variables:
self.variable_map[str(variable)] = new_bucket
def _merge_buckets(self, bucket_list: Set[DependenceBucket]) -> DependenceBucket:
"""Merges the buckets in bucket list"""
variables: List[str] = []
conditions: List[z3.BoolRef] = []
for bucket in bucket_list:
self.buckets.remove(bucket)
variables += bucket.variables
conditions += bucket.conditions
new_bucket = DependenceBucket(variables, conditions)
self.buckets.append(new_bucket)
return new_bucket
class IndependenceSolver:
"""An SMT solver object that uses independence optimization"""
def __init__(self):
""""""
self.raw = z3.Solver()
self.constraints = []
self.models = []
def set_timeout(self, timeout: int) -> None:
"""Sets the timeout that will be used by this solver, timeout is in
milliseconds.
:param timeout:
"""
self.raw.set(timeout=timeout)
def add(self, *constraints: Bool) -> None:
"""Adds the constraints to this solver.
:param constraints: constraints to add
"""
raw_constraints: List[z3.BoolRef] = [
c.raw for c in cast(Tuple[Bool], constraints)
]
self.constraints.extend(raw_constraints)
def append(self, *constraints: Tuple[Bool]) -> None:
"""Adds the constraints to this solver.
:param constraints: constraints to add
"""
raw_constraints: List[z3.BoolRef] = [
c.raw for c in cast(Tuple[Bool], constraints)
]
self.constraints.extend(raw_constraints)
@stat_smt_query
def check(self) -> z3.CheckSatResult:
"""Returns z3 smt check result."""
dependence_map = DependenceMap()
for constraint in self.constraints:
dependence_map.add_condition(constraint)
self.models = []
for bucket in dependence_map.buckets:
self.raw.reset()
self.raw.append(*bucket.conditions)
check_result = self.raw.check()
if check_result == z3.sat:
self.models.append(self.raw.model())
else:
return check_result
return z3.sat
def model(self) -> Model:
"""Returns z3 model for a solution."""
return Model(self.models)
def reset(self) -> None:
"""Reset this solver."""
self.constraints = []
def pop(self, num) -> None:
"""Pop num constraints from this solver."""
self.constraints.pop(num)
================================================
FILE: mythril/laser/smt/solver/solver.py
================================================
"""This module contains an abstract SMT representation of an SMT solver."""
import logging
import os
import sys
from typing import Generic, List, Sequence, TypeVar, Union, cast
import z3
from mythril.laser.smt.bool import Bool
from mythril.laser.smt.expression import Expression
from mythril.laser.smt.model import Model
from mythril.laser.smt.solver.solver_statistics import stat_smt_query
T = TypeVar("T", bound=Union[z3.Solver, z3.Optimize])
log = logging.getLogger(__name__)
class BaseSolver(Generic[T]):
def __init__(self, raw: T) -> None:
""""""
self.raw = raw
def set_timeout(self, timeout: int) -> None:
"""Sets the timeout that will be used by this solver, timeout is in
milliseconds.
:param timeout:
"""
self.raw.set(timeout=timeout)
def set_unsat_core(self) -> None:
"""
Enables the generation of unsatisfiable cores in the solver. This option must be activated
if you intend to identify and extract the minimal set of conflicting constraints that make
a problem unsolvable. Useful for diagnosing and debugging unsatisfiable conditions within
constraint sets.
"""
self.raw.set(unsat_core=True)
def add(self, *constraints: Bool) -> None:
"""Adds the constraints to this solver.
:param constraints:
:return:
"""
z3_constraints: Sequence[z3.BoolRef] = [
c.raw for c in cast(List[Bool], constraints)
]
self.raw.add(z3_constraints)
def assert_and_track(self, constraints: Bool, name: str) -> None:
"""
Adds a constraint to the solver with an associated name, allowing the constraint to be tracked.
This is particularly useful for identifying specific constraints contributing to an unsat.
:param constraints: The constraints.
:param name: A unique identifier for the constraint, used for tracking purposes in unsat core extraction.
:return: None
"""
self.raw.assert_and_track(constraints.raw, name)
def append(self, *constraints: Bool) -> None:
"""Adds the constraints to this solver.
:param constraints:
:return:
"""
self.add(*constraints)
@stat_smt_query
def check(self, *args) -> z3.CheckSatResult:
"""Returns z3 smt check result.
Also suppresses the stdout when running z3 library's check() to avoid unnecessary output
:return: The evaluated result which is either of sat, unsat or unknown
"""
old_stdout = sys.stdout
with open(os.devnull, "w") as dev_null_fd:
sys.stdout = dev_null_fd
try:
evaluate = self.raw.check(args)
except z3.z3types.Z3Exception as e:
# Some requests crash the solver
evaluate = z3.unknown
log.info(f"Encountered Z3 exception when checking the constraints: {e}")
sys.stdout = old_stdout
return evaluate
def model(self) -> Model:
"""Returns z3 model for a solution.
:return:
"""
try:
return Model([self.raw.model()])
except z3.z3types.Z3Exception as e:
log.info(f"Encountered a Z3 exception while querying for the model: {e}")
return Model()
def sexpr(self):
return self.raw.sexpr()
class Solver(BaseSolver[z3.Solver]):
"""An SMT solver object."""
def __init__(self) -> None:
""""""
super().__init__(z3.Solver())
def reset(self) -> None:
"""Reset this solver."""
self.raw.reset()
def pop(self, num: int) -> None:
"""Pop num constraints from this solver.
:param num:
"""
self.raw.pop(num)
class Optimize(BaseSolver[z3.Optimize]):
"""An optimizing smt solver."""
def __init__(self) -> None:
"""Create a new optimizing solver instance."""
super().__init__(z3.Optimize())
def minimize(self, element: Expression[z3.ExprRef]) -> None:
"""In solving this solver will try to minimize the passed expression.
:param element:
"""
self.raw.minimize(element.raw)
def maximize(self, element: Expression[z3.ExprRef]) -> None:
"""In solving this solver will try to maximize the passed expression.
:param element:
"""
self.raw.maximize(element.raw)
================================================
FILE: mythril/laser/smt/solver/solver_statistics.py
================================================
from time import time
from typing import Callable
from mythril.support.support_utils import Singleton
def stat_smt_query(func: Callable):
"""Measures statistics for annotated smt query check function"""
stat_store = SolverStatistics()
def function_wrapper(*args, **kwargs):
if not stat_store.enabled:
return func(*args, **kwargs)
stat_store.query_count += 1
begin = time()
result = func(*args, **kwargs)
end = time()
stat_store.solver_time += end - begin
return result
return function_wrapper
class SolverStatistics(object, metaclass=Singleton):
"""Solver Statistics Class
Keeps track of the important statistics around smt queries
"""
def __init__(self):
self.enabled = False
self.query_count = 0
self.solver_time = 0
def __repr__(self):
return "Query count: {} \nSolver time: {}".format(
self.query_count, self.solver_time
)
================================================
FILE: mythril/mythril/__init__.py
================================================
from .mythril_analyzer import MythrilAnalyzer
from .mythril_config import MythrilConfig
from .mythril_disassembler import MythrilDisassembler
================================================
FILE: mythril/mythril/mythril_analyzer.py
================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import traceback
from argparse import Namespace
from typing import List, Optional
from mythril.analysis.callgraph import generate_graph
from mythril.analysis.report import Issue, Report
from mythril.analysis.security import fire_lasers, retrieve_callback_issues
from mythril.analysis.symbolic import SymExecWrapper
from mythril.analysis.traceexplore import get_serializable_statespace
from mythril.ethereum.evmcontract import EVMContract
from mythril.exceptions import DetectorNotFoundError
from mythril.laser.execution_info import ExecutionInfo
from mythril.laser.smt import SolverStatistics
from mythril.support.loader import DynLoader
from mythril.support.source_support import Source
from mythril.support.start_time import StartTime
from mythril.support.support_args import args
from .mythril_disassembler import MythrilDisassembler
log = logging.getLogger(__name__)
LARGE_TIME = 300
class MythrilAnalyzer:
"""
The Mythril Analyzer class
Responsible for the analysis of the smart contracts
"""
def __init__(
self,
disassembler: MythrilDisassembler,
cmd_args: Namespace,
strategy: str = "dfs",
address: Optional[str] = None,
):
"""
:param disassembler: The MythrilDisassembler class
:param cmd_args: The command line args Namespace
:param strategy: Search strategy
:param address: Address of the contract
"""
self.eth = disassembler.eth
self.contracts: List[EVMContract] = disassembler.contracts or []
self.use_onchain_data = not cmd_args.no_onchain_data
self.strategy = strategy
self.address = address
self.max_depth = cmd_args.max_depth
self.execution_timeout = cmd_args.execution_timeout
self.loop_bound = cmd_args.loop_bound
self.create_timeout = cmd_args.create_timeout
self.disable_dependency_pruning = cmd_args.disable_dependency_pruning
self.custom_modules_directory = (
cmd_args.custom_modules_directory
if cmd_args.custom_modules_directory
else ""
)
args.pruning_factor = cmd_args.pruning_factor
args.solver_timeout = cmd_args.solver_timeout
args.parallel_solving = cmd_args.parallel_solving
args.unconstrained_storage = cmd_args.unconstrained_storage
args.call_depth_limit = cmd_args.call_depth_limit
args.disable_iprof = cmd_args.disable_iprof
args.solver_log = cmd_args.solver_log
args.transaction_sequences = cmd_args.transaction_sequences
args.disable_coverage_strategy = cmd_args.disable_coverage_strategy
args.disable_mutation_pruner = cmd_args.disable_mutation_pruner
args.enable_summaries = cmd_args.enable_summaries
args.enable_state_merge = cmd_args.enable_state_merging
if args.pruning_factor is None:
if self.execution_timeout > LARGE_TIME:
args.pruning_factor = 1
else:
args.pruning_factor = 0
def dump_statespace(self, contract: EVMContract = None) -> str:
"""
Returns serializable statespace of the contract
:param contract: The Contract on which the analysis should be done
:return: The serialized state space
"""
sym = SymExecWrapper(
contract or self.contracts[0],
self.address,
self.strategy,
dynloader=DynLoader(self.eth, active=self.use_onchain_data),
max_depth=self.max_depth,
execution_timeout=self.execution_timeout,
create_timeout=self.create_timeout,
disable_dependency_pruning=self.disable_dependency_pruning,
run_analysis_modules=False,
custom_modules_directory=self.custom_modules_directory,
)
return get_serializable_statespace(sym)
def graph_html(
self,
contract: EVMContract = None,
enable_physics: bool = False,
phrackify: bool = False,
transaction_count: Optional[int] = None,
) -> str:
"""
:param contract: The Contract on which the analysis should be done
:param enable_physics: If true then enables the graph physics simulation
:param phrackify: If true generates Phrack-style call graph
:param transaction_count: The amount of transactions to be executed
:return: The generated graph in html format
"""
sym = SymExecWrapper(
contract or self.contracts[0],
self.address,
self.strategy,
dynloader=DynLoader(self.eth, active=self.use_onchain_data),
max_depth=self.max_depth,
execution_timeout=self.execution_timeout,
transaction_count=transaction_count,
create_timeout=self.create_timeout,
disable_dependency_pruning=self.disable_dependency_pruning,
run_analysis_modules=False,
custom_modules_directory=self.custom_modules_directory,
)
return generate_graph(sym, physics=enable_physics, phrackify=phrackify)
def fire_lasers(
self,
modules: Optional[List[str]] = None,
transaction_count: Optional[int] = None,
) -> Report:
"""
:param modules: The analysis modules which should be executed
:param transaction_count: The amount of transactions to be executed
:return: The Report class which contains the all the issues/vulnerabilities
"""
all_issues: List[Issue] = []
SolverStatistics().enabled = True
exceptions = []
execution_info: Optional[List[ExecutionInfo]] = None
for contract in self.contracts:
StartTime() # Reinitialize start time for new contracts
try:
sym = SymExecWrapper(
contract,
self.address,
self.strategy,
dynloader=DynLoader(self.eth, active=self.use_onchain_data),
max_depth=self.max_depth,
execution_timeout=self.execution_timeout,
loop_bound=self.loop_bound,
create_timeout=self.create_timeout,
transaction_count=transaction_count,
modules=modules,
compulsory_statespace=False,
disable_dependency_pruning=self.disable_dependency_pruning,
custom_modules_directory=self.custom_modules_directory,
)
issues = fire_lasers(sym, modules)
execution_info = sym.execution_info
except DetectorNotFoundError as e:
# Bubble up
raise e
except KeyboardInterrupt:
log.critical("Keyboard Interrupt")
issues = retrieve_callback_issues(modules)
except Exception:
log.critical(
"Exception occurred, aborting analysis. Please report this issue to the Mythril GitHub page.\n"
+ traceback.format_exc()
)
issues = retrieve_callback_issues(modules)
exceptions.append(traceback.format_exc())
for issue in issues:
issue.add_code_info(contract)
all_issues += issues
log.info("Solver statistics: \n{}".format(str(SolverStatistics())))
source_data = Source()
source_data.get_source_from_contracts_list(self.contracts)
# Finally, output the results
report = Report(
contracts=self.contracts,
exceptions=exceptions,
execution_info=execution_info,
)
for issue in all_issues:
report.append_issue(issue)
return report
================================================
FILE: mythril/mythril/mythril_config.py
================================================
import codecs
import logging
import os
from configparser import ConfigParser
from pathlib import Path
from shutil import copyfile
from typing import Optional
from mythril.ethereum.interface.rpc.client import EthJsonRpc
from mythril.exceptions import CriticalError
from mythril.support.lock import LockFile
log = logging.getLogger(__name__)
class MythrilConfig:
"""
The Mythril Analyzer class
Responsible for setup of the mythril environment
"""
def __init__(self):
self.infura_id: str = os.getenv("INFURA_ID")
self.mythril_dir = self.init_mythril_dir()
self.config_path = os.path.join(self.mythril_dir, "config.ini")
self._init_config()
self.eth: Optional[EthJsonRpc] = None
def set_api_infura_id(self, id):
self.infura_id = id
@staticmethod
def init_mythril_dir() -> str:
"""
Initializes the mythril dir and config.ini file
:return: The mythril dir's path
"""
try:
mythril_dir = os.environ["MYTHRIL_DIR"]
except KeyError:
mythril_dir = os.path.join(os.path.expanduser("~"), ".mythril")
if not os.path.exists(mythril_dir):
# Initialize data directory
log.info("Creating mythril data directory")
os.mkdir(mythril_dir)
db_path = str(Path(mythril_dir) / "signatures.db")
if not os.path.exists(db_path):
# if the default mythril dir doesn't contain a signature DB
# initialize it with the default one from the project root
asset_dir = Path(__file__).parent.parent / "support" / "assets"
copyfile(str(asset_dir / "signatures.db"), db_path)
return mythril_dir
def _init_config(self):
"""If no config file exists, create it and add default options.
Defaults:-
- dynamic loading is set to infura by default in the file
"""
if not os.path.exists(self.config_path):
log.info("No config file found. Creating default: " + self.config_path)
open(self.config_path, "a").close()
config = ConfigParser(allow_no_value=True)
config.optionxform = str
with LockFile(self.config_path):
config.read(self.config_path, "utf-8")
if "defaults" not in config.sections():
self._add_default_options(config)
if not config.has_option("defaults", "dynamic_loading"):
self._add_dynamic_loading_option(config)
if not config.has_option("defaults", "infura_id"):
config.set("defaults", "infura_id", "")
with codecs.open(self.config_path, "w", "utf-8") as fp:
config.write(fp)
if not self.infura_id:
self.infura_id = config.get("defaults", "infura_id", fallback="")
@staticmethod
def _add_default_options(config: ConfigParser) -> None:
"""
Adds defaults option to config.ini
:param config: The config file object
:return: None
"""
config.add_section("defaults")
@staticmethod
def _add_dynamic_loading_option(config: ConfigParser) -> None:
"""
Sets the dynamic loading config option in .mythril/config.ini file
:param config: The config file object
:return: None
"""
config.set(
"defaults", "#– To connect to Infura use dynamic_loading: infura", ""
)
config.set(
"defaults",
"#– To connect to Rpc use "
"dynamic_loading: HOST:PORT / ganache / infura-[network_name]",
"",
)
config.set(
"defaults", "#– To connect to local host use dynamic_loading: localhost", ""
)
config.set("defaults", "dynamic_loading", "infura")
def set_api_rpc_infura(self) -> None:
"""Set the RPC mode to INFURA on Mainnet."""
log.info("Using INFURA Main Net for RPC queries")
self.eth = EthJsonRpc(
"mainnet.infura.io/v3/{}".format(self.infura_id), None, True
)
def set_api_rpc(self, rpc: str = None, rpctls: bool = False) -> None:
"""
Sets the RPC mode to either of ganache or infura
:param rpc: either of the strings - ganache, infura-mainnet, infura-rinkeby, infura-kovan, infura-ropsten,
infura-goerli, avalanche, arbitrum, bsc, optimism, polygon
"""
if rpc == "ganache":
rpcconfig = ("localhost", 7545, False)
elif rpc.startswith("infura-"):
network = rpc.replace("infura-", "")
layer_one = ["mainnet", "rinkeby", "kovan", "ropsten", "goerli", "sepolia"]
layer_two = [
"avalanche",
"arbitrum",
"bsc",
"optimism",
"polygon",
"celo",
"starknet",
"aurora",
"near",
"palm",
]
if network in layer_one + layer_two:
if self.infura_id in (None, ""):
log.info(
"Infura key not provided, so onchain access is disabled. "
"Use --infura-id "
"or set it in the environment variable INFURA_ID "
"or in the ~/.mythril/config.ini file"
)
self.eth = None
return
if network in layer_one:
rpcconfig = (
f"{network}.infura.io/v3/{self.infura_id}",
None,
True,
)
else:
rpcconfig = (
f"{network}-mainnet.infura.io/v3/{self.infura_id}",
None,
True,
)
else:
raise CriticalError(
f"Invalid network {network}, use 'mainnet', 'rinkeby', 'kovan', 'ropsten', 'goerli', 'avalanche', 'arbitrum', 'optimism', or 'polygon'"
)
else:
try:
host, port = rpc.split(":")
rpcconfig = (host, int(port), rpctls)
except ValueError:
raise CriticalError(
"Invalid RPC argument, use 'ganache', 'infura-[network]', or 'HOST:PORT'"
)
if rpcconfig:
log.info("Using RPC settings: %s" % str(rpcconfig))
self.eth = EthJsonRpc(rpcconfig[0], rpcconfig[1], rpcconfig[2])
else:
raise CriticalError("Invalid RPC settings, check help for details.")
def set_api_rpc_localhost(self) -> None:
"""Set the RPC mode to a local instance."""
log.info("Using default RPC settings: http://localhost:8545")
self.eth = EthJsonRpc("localhost", 8545)
def set_api_from_config_path(self) -> None:
"""Set the RPC mode based on a given config file."""
config = ConfigParser(allow_no_value=False)
config.optionxform = str
config.read(self.config_path, "utf-8")
if config.has_option("defaults", "dynamic_loading"):
dynamic_loading = config.get("defaults", "dynamic_loading")
else:
dynamic_loading = "infura"
self._set_rpc(dynamic_loading)
def _set_rpc(self, rpc_type: str) -> None:
"""
Sets rpc based on the type
:param rpc_type: The type of connection: like infura, ganache, localhost
:return:
"""
if rpc_type == "infura":
self.set_api_rpc_infura()
elif rpc_type == "localhost":
self.set_api_rpc_localhost()
else:
self.set_api_rpc(rpc_type)
================================================
FILE: mythril/mythril/mythril_disassembler.py
================================================
import json
import logging
import os
import re
import shutil
import subprocess
import warnings
from pathlib import Path
from typing import List, Optional, Tuple
import solc
from eth_utils import int_to_big_endian
from semantic_version import NpmSpec, Version
from mythril.ethereum import util
from mythril.ethereum.evmcontract import EVMContract
from mythril.ethereum.interface.rpc.client import EthJsonRpc
from mythril.ethereum.interface.rpc.exceptions import ConnectionError
from mythril.exceptions import CompilerError, CriticalError, NoContractFoundError
from mythril.solidity.soliditycontract import (
SolidityContract,
get_contracts_from_file,
get_contracts_from_foundry,
)
from mythril.support import signatures
from mythril.support.support_args import args
from mythril.support.support_utils import rzpad, sha3, zpad
def format_warning(message, category, filename, lineno, line=""):
return "{}: {}\n\n".format(str(filename), str(message))
warnings.formatwarning = format_warning
log = logging.getLogger(__name__)
class MythrilDisassembler:
"""
The Mythril Disassembler class
Responsible for generating disassembly of smart contracts:
- Compiles solc code from file/onchain
- Can also be used to access onchain storage data
"""
def __init__(
self,
eth: Optional[EthJsonRpc] = None,
solc_version: str = None,
solc_settings_json: str = None,
solc_args=None,
) -> None:
args.solc_args = solc_args
self.solc_version = solc_version
self.solc_binary = self._init_solc_binary(solc_version)
self.solc_settings_json = solc_settings_json
self.eth = eth
self.sigs = signatures.SignatureDB()
self.contracts: List[EVMContract] = []
@staticmethod
def _init_solc_binary(version: str) -> Optional[str]:
"""
Only proper versions are supported. No nightlies, commits etc (such as available in remix).
This functions extracts
:param version: Version of the solc binary required
:return: AThe solc binary of the corresponding version
"""
if not version:
return None
# tried converting input to semver, seemed not necessary so just slicing for now
try:
main_version = solc.get_solc_version_string()
except:
main_version = "" # allow missing solc will download instead
main_version_number = re.search(r"\d+.\d+.\d+", main_version)
if version.startswith("v"):
version = version[1:]
if version == main_version_number:
log.info("Given version matches installed version")
solc_binary = os.environ.get("SOLC") or "solc"
else:
solc_binary = util.solc_exists(version)
if solc_binary is None:
raise CriticalError(
"The version of solc that is needed cannot be installed automatically"
)
else:
log.info("Setting the compiler to %s", solc_binary)
return solc_binary
def load_from_bytecode(
self, code: str, bin_runtime: bool = False, address: Optional[str] = None
) -> Tuple[str, EVMContract]:
"""
Returns the address and the contract class for the given bytecode
:param code: Bytecode
:param bin_runtime: Whether the code is runtime code or creation code
:param address: address of contract
:return: tuple(address, Contract class)
"""
if address is None:
address = util.get_indexed_address(0)
if bin_runtime:
self.contracts.append(
EVMContract(
code=code,
name="MAIN",
)
)
else:
self.contracts.append(
EVMContract(
creation_code=code,
name="MAIN",
)
)
return address, self.contracts[-1] # return address and contract object
def load_from_address(self, address: str) -> Tuple[str, EVMContract]:
"""
Returns the contract given it's on chain address
:param address: The on chain address of a contract
:return: tuple(address, contract)
"""
if not re.match(r"0x[a-fA-F0-9]{40}", address):
raise CriticalError("Invalid contract address. Expected format is '0x...'.")
if self.eth is None:
raise CriticalError(
"Please check whether the Infura key is set or use a different RPC method."
)
try:
code = self.eth.eth_getCode(address)
except FileNotFoundError as e:
raise CriticalError("IPC error: " + str(e))
except ConnectionError:
raise CriticalError(
"Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly."
)
except Exception as e:
raise CriticalError("IPC / RPC error: " + str(e))
if code == "0x" or code == "0x0":
raise CriticalError(
"Received an empty response from eth_getCode. Check the contract address and verify that you are on the correct chain."
)
else:
self.contracts.append(EVMContract(code, name=address))
return address, self.contracts[-1] # return address and contract object
def load_from_foundry(self):
project_root = os.getcwd()
cmd = ["forge", "build", "--build-info", "--force"]
with subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=project_root,
executable=shutil.which(cmd[0]),
) as p:
stdout, stderr = p.communicate()
stdout, stderr = (stdout.decode(), stderr.decode())
if stderr:
log.error(stderr)
build_dir = Path(project_root, "artifacts", "contracts", "build-info")
build_dir = os.path.join(project_root, "artifacts", "contracts", "build-info")
address = util.get_indexed_address(0)
files = sorted(
os.listdir(build_dir), key=lambda x: os.path.getmtime(Path(build_dir, x))
)
files = [str(f) for f in files if str(f).endswith(".json")]
if not files:
txt = f"`compile` failed. Can you run it?\n{build_dir} is empty"
raise Exception(txt)
contracts = []
for file in files:
build_info = Path(build_dir, file)
with open(build_info, encoding="utf8") as file_desc:
loaded_json = json.load(file_desc)
targets_json = loaded_json["output"]
input_json = loaded_json["input"]
compiler = "solc" if input_json["language"] == "Solidity" else "vyper"
if compiler == "vyper":
raise NotImplementedError("Support for Vyper is not implemented.")
if "contracts" in targets_json:
for original_filename, contracts_info in targets_json[
"contracts"
].items():
for contract in get_contracts_from_foundry(
original_filename, targets_json
):
self.contracts.append(contract)
contracts.append(contract)
self.sigs.add_sigs(original_filename, targets_json)
return address, contracts
def check_run_integer_module(self, source_file):
with open(source_file, "r") as f:
for line in f:
if "unchecked" in line:
return True
if self.solc_version is None:
# Runs the version installed in the system (likely 0.8.0+)
# Post 0.8.0 versions automatically add assertions to sanity check arithmetic
return False
# Strip leading 'v' from version if it's there
normalized_version = self.solc_version.lstrip("v")
# Check if solc_version is not provided or doesn't match the required version
# As post 0.8.0 solc versions automatically add assertions to sanity check arithmetic
if not self.solc_version or not NpmSpec("^0.8.0").match(
Version(normalized_version)
):
return True
return False
def load_from_solidity(
self, solidity_files: List[str]
) -> Tuple[str, List[SolidityContract]]:
"""
:param solidity_files: List of solidity_files
:return: tuple of address, contract class list
"""
address = util.get_indexed_address(0)
contracts = []
for file in solidity_files:
if ":" in file:
file, contract_name = file.split(":")
else:
contract_name = None
file = os.path.expanduser(file)
solc_binary = self.solc_binary
if solc_binary is None:
solc_binary, self.solc_version = util.extract_binary(file)
if self.check_run_integer_module(file) is False:
args.use_integer_module = False
try:
# import signatures from solidity source
self.sigs.import_solidity_file(
file,
solc_binary=solc_binary,
solc_settings_json=self.solc_settings_json,
)
if contract_name is not None:
contract = SolidityContract(
input_file=file,
name=contract_name,
solc_settings_json=self.solc_settings_json,
solc_binary=solc_binary,
)
self.contracts.append(contract)
contracts.append(contract)
else:
for contract in get_contracts_from_file(
input_file=file,
solc_settings_json=self.solc_settings_json,
solc_binary=solc_binary,
):
self.contracts.append(contract)
contracts.append(contract)
except FileNotFoundError as e:
raise CriticalError(f"Input file not found {e}")
except CompilerError as 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 ...", 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 = (
"" 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:
log.error(
"The file " + file + " does not contain a compilable contract."
)
return address, contracts
@staticmethod
def hash_for_function_signature(func: str) -> str:
"""
Return function nadmes corresponding signature hash
:param func: function name
:return: Its hash signature
"""
return "0x%s" % sha3(func)[:4].hex()
def get_state_variable_from_storage(
self, address: str, params: Optional[List[str]] = None
) -> str:
"""
Get variables from the storage
:param address: The contract address
:param params: The list of parameters param types: [position, length] or ["mapping", position, key1, key2, ... ]
or [position, length, array]
:return: The corresponding storage slot and its value
"""
params = params or []
(position, length, mappings) = (0, 1, [])
try:
if params[0] == "mapping":
if len(params) < 3:
raise CriticalError("Invalid number of parameters.")
position = int(params[1])
position_formatted = zpad(int_to_big_endian(position), 32)
for i in range(2, len(params)):
key = bytes(params[i], "utf8")
key_formatted = rzpad(key, 32)
mappings.append(
int.from_bytes(
sha3(key_formatted + position_formatted), byteorder="big"
)
)
length = len(mappings)
if length == 1:
position = mappings[0]
else:
if len(params) >= 4:
raise CriticalError("Invalid number of parameters.")
if len(params) >= 1:
position = int(params[0])
if len(params) >= 2:
length = int(params[1])
if len(params) == 3 and params[2] == "array":
position_formatted = zpad(int_to_big_endian(position), 32)
position = int.from_bytes(sha3(position_formatted), byteorder="big")
except ValueError:
raise CriticalError(
"Invalid storage index. Please provide a numeric value."
)
outtxt = []
try:
if length == 1:
outtxt.append(
"{}: {}".format(
position, self.eth.eth_getStorageAt(address, position)
)
)
else:
if len(mappings) > 0:
for i in range(0, len(mappings)):
position = mappings[i]
outtxt.append(
"{}: {}".format(
hex(position),
self.eth.eth_getStorageAt(address, position),
)
)
else:
for i in range(position, position + length):
outtxt.append(
"{}: {}".format(
hex(i), self.eth.eth_getStorageAt(address, i)
)
)
except FileNotFoundError as e:
raise CriticalError("IPC error: " + str(e))
except ConnectionError:
raise CriticalError(
"Could not connect to RPC server. "
"Make sure that your node is running and that RPC parameters are set correctly."
)
return "\n".join(outtxt)
================================================
FILE: mythril/plugin/__init__.py
================================================
from mythril.plugin.interface import MythrilCLIPlugin, MythrilPlugin
from mythril.plugin.loader import MythrilPluginLoader
================================================
FILE: mythril/plugin/discovery.py
================================================
try:
# Older python versions
import pkg_resources
except Exception:
pkg_resources = None
from importlib.metadata import entry_points
from typing import Any, Dict, List, Optional
from mythril.plugin.interface import MythrilPlugin
from mythril.support.support_utils import Singleton
class PluginDiscovery(object, metaclass=Singleton):
"""PluginDiscovery class
This plugin implements the logic to discover and build plugins in installed python packages
"""
# Installed plugins structure. Retrieves all modules that have an entry point for mythril.plugins
_installed_plugins: Optional[Dict[str, Any]] = None
def init_installed_plugins(self):
if pkg_resources:
self._installed_plugins = {
entry_point.name: entry_point.load()
for entry_point in pkg_resources.iter_entry_points("mythril.plugins")
}
else:
all_entry_points = entry_points()
mythril_plugins = [
ep for ep in all_entry_points if ep.group == "mythril.plugins"
]
self._installed_plugins = {
entry_point.name: entry_point.load() for entry_point in mythril_plugins
}
@property
def installed_plugins(self):
if self._installed_plugins is None:
self.init_installed_plugins()
return self._installed_plugins
def is_installed(self, plugin_name: str) -> bool:
"""Returns whether there is python package with a plugin with plugin_name"""
return plugin_name in self.installed_plugins.keys()
def build_plugin(self, plugin_name: str, plugin_args: Dict) -> MythrilPlugin:
"""Returns the plugin for the given plugin_name if it is installed"""
if not self.is_installed(plugin_name):
raise ValueError(f"Plugin with name: `{plugin_name}` is not installed")
plugin = self.installed_plugins.get(plugin_name)
if plugin is None or not issubclass(plugin, MythrilPlugin):
raise ValueError(f"No valid plugin was found for {plugin_name}")
return plugin(**plugin_args)
def get_plugins(self, default_enabled=None) -> List[str]:
"""Gets a list of installed mythril plugins
:param default_enabled: Select plugins that are enabled by default
:return: List of plugin names
"""
if default_enabled is None:
return list(self.installed_plugins.keys())
return [
plugin_name
for plugin_name, plugin_class in self.installed_plugins.items()
if plugin_class.plugin_default_enabled == default_enabled
]
================================================
FILE: mythril/plugin/interface.py
================================================
from abc import ABC
from mythril.laser.plugin.builder import PluginBuilder as LaserPluginBuilder
class MythrilPlugin:
"""MythrilPlugin interface
Mythril Plugins can be used to extend Mythril in different ways:
1. Extend Laser, in which case the LaserPlugin interface must also be extended
2. Extend Laser with a new search strategy in which case the SearchStrategy needs to be implemented
3. Add an analysis module, in this case the AnalysisModule interface needs to be implemented
4. Add new commands to the Mythril cli, using the MythrilCLIPlugin Interface
"""
author = "Default Author"
name = "Plugin Name"
plugin_license = "All rights reserved."
plugin_type = "Mythril Plugin"
plugin_version = "0.0.1 "
plugin_description = "This is an example plugin description"
def __init__(self, **kwargs):
pass
def __repr__(self):
plugin_name = type(self).__name__
return f"{plugin_name} - {self.plugin_version} - {self.author}"
class MythrilCLIPlugin(MythrilPlugin):
"""MythrilCLIPlugin interface
This interface should be implemented by mythril plugins that aim to add commands to the mythril cli
"""
pass
class MythrilLaserPlugin(MythrilPlugin, LaserPluginBuilder, ABC):
"""Mythril Laser Plugin interface
Plugins of this type are used to instrument the laser EVM
"""
pass
================================================
FILE: mythril/plugin/loader.py
================================================
import logging
from typing import Dict
from mythril.analysis.module import DetectionModule
from mythril.analysis.module.loader import ModuleLoader
from mythril.laser.plugin.loader import LaserPluginLoader
from mythril.plugin.discovery import PluginDiscovery
from mythril.plugin.interface import MythrilLaserPlugin, MythrilPlugin
from mythril.support.support_utils import Singleton
log = logging.getLogger(__name__)
class UnsupportedPluginType(Exception):
"""Raised when a plugin with an unsupported type is loaded"""
pass
class MythrilPluginLoader(object, metaclass=Singleton):
"""MythrilPluginLoader singleton
This object permits loading MythrilPlugin's
"""
def __init__(self):
log.info("Initializing mythril plugin loader")
self.loaded_plugins = []
self.plugin_args: Dict[str, Dict] = dict()
self._load_default_enabled()
def set_args(self, plugin_name: str, **kwargs):
self.plugin_args[plugin_name] = kwargs
def load(self, plugin: MythrilPlugin):
"""Loads the passed plugin
This function handles input validation and dispatches loading to type specific loaders.
Supported plugin types:
- laser plugins
- detection modules
"""
if not isinstance(plugin, MythrilPlugin):
raise ValueError("Passed plugin is not of type MythrilPlugin")
logging.info(f"Loading plugin: {plugin.name}")
log.info(f"Loading plugin: {str(plugin)}")
if isinstance(plugin, DetectionModule):
self._load_detection_module(plugin)
elif isinstance(plugin, MythrilLaserPlugin):
self._load_laser_plugin(plugin)
else:
raise UnsupportedPluginType("Passed plugin type is not yet supported")
self.loaded_plugins.append(plugin)
log.info(f"Finished loading plugin: {plugin.name}")
@staticmethod
def _load_detection_module(plugin: DetectionModule):
"""Loads the passed detection module"""
log.info(f"Loading detection module: {plugin.name}")
ModuleLoader().register_module(plugin)
@staticmethod
def _load_laser_plugin(plugin: MythrilLaserPlugin):
"""Loads the laser plugin"""
log.info(f"Loading laser plugin: {plugin.name}")
LaserPluginLoader().load(plugin)
def _load_default_enabled(self):
"""Loads the plugins that have the default enabled flag"""
log.info("Loading installed analysis modules that are enabled by default")
for plugin_name in PluginDiscovery().get_plugins(default_enabled=True):
plugin = PluginDiscovery().build_plugin(
plugin_name, self.plugin_args.get(plugin_name, {})
)
self.load(plugin)
================================================
FILE: mythril/solidity/__init__.py
================================================
================================================
FILE: mythril/solidity/features.py
================================================
TRANSFER_METHODS = ["transfer", "send"]
class SolidityFeatureExtractor:
def __init__(self, ast):
self.ast = ast
def extract_features(self):
function_features = {}
function_nodes = self.find_function_nodes(self.ast)
modifier_vars = {}
for modifier_node in self.find_modifier_nodes(self.ast):
modifier_vars[modifier_node["name"]] = self.find_variables_in_require(
modifier_node
)
modifier_vars[modifier_node["name"]].update(
self.find_variables_in_if(modifier_node)
)
for node in function_nodes:
function_name = self.get_function_name(node)
contains_selfdestruct = self.contains_selfdestruct(node)
contains_call = self.contains_call(node)
contains_delegatecall = self.contains_delegatecall(node)
contains_callcode = self.contains_callcode(node)
contains_staticcall = self.contains_staticcall(node)
all_require_vars = self.find_variables_in_require(node)
ether_vars = self.extract_address_variable(node)
for potential_modifier in node.get("modifiers", []):
# Issues with AST, sometimes, non-modifiers pop up here
if potential_modifier["modifierName"]["name"] in modifier_vars:
all_require_vars.update(
modifier_vars[potential_modifier["modifierName"]["name"]]
)
is_payable = self.is_function_payable(node)
has_isowner_modifier = self.has_isowner_modifier(node)
contains_assert = self.contains_assert(node)
function_features[function_name] = {
"contains_selfdestruct": contains_selfdestruct,
"contains_call": contains_call,
"is_payable": is_payable,
"has_owner_modifier": has_isowner_modifier,
"contains_assert": contains_assert,
"contains_callcode": contains_callcode,
"contains_delegatecall": contains_delegatecall,
"contains_staticcall": contains_staticcall,
"all_require_vars": all_require_vars,
"transfer_vars": ether_vars,
}
return function_features
def find_function_nodes(self, node):
if node["nodeType"] == "FunctionDefinition":
yield node
if "nodes" in node:
for child_node in node["nodes"]:
yield from self.find_function_nodes(child_node)
def find_modifier_nodes(self, node):
if node["nodeType"] == "ModifierDefinition":
yield node
if "nodes" in node:
for child_node in node["nodes"]:
yield from self.find_modifier_nodes(child_node)
def get_function_name(self, node):
return node["name"]
def contains_command(self, node, command):
if isinstance(node, dict):
if command in node.values():
return True
for value in node.values():
if isinstance(value, (dict, list)):
if self.contains_command(value, command):
return True
elif isinstance(node, list):
for item in node:
if self.contains_command(item, command):
return True
return False
def contains_call(self, node):
return self.contains_command(node, "call")
def is_function_payable(self, node):
return node.get("stateMutability") == "payable"
def has_isowner_modifier(self, node):
if "modifiers" in node:
for modifier in node["modifiers"]:
if modifier["modifierName"]["name"].lower() in ("isowner", "onlyowner"):
return True
return False
def contains_assert(self, node):
return self.contains_command(node, "assert")
def contains_selfdestruct(self, node):
return self.contains_command(node, "selfdestruct")
def contains_delegatecall(self, node):
return self.contains_command(node, "delegatecall")
def contains_callcode(self, node):
return self.contains_command(node, "callcode")
def contains_staticcall(self, node):
return self.contains_command(node, "staticcall")
def contains_require(self, node):
return self.contains_command(node, "require")
def extract_nodes(self, node, command, parent=None):
node_list = []
if isinstance(node, dict):
if command in node.values():
node_list.append((parent, node))
for key, value in node.items():
if isinstance(value, (dict, list)):
node_list.extend(self.extract_nodes(value, command, parent=node))
elif isinstance(node, list):
for item in node:
node_list.extend(self.extract_nodes(item, command, parent=node))
return node_list
def find_all_variables(self, node):
variables = set()
def traverse(node):
if isinstance(node, dict):
for key, value in node.items():
if key == "nodeType" and value == "Identifier":
if "name" in node:
variables.add(node["name"])
elif isinstance(value, (dict, list)):
traverse(value)
elif isinstance(node, list):
for item in node:
traverse(item)
traverse(node)
return variables
def find_variables_in_require(self, node):
nodes = self.extract_nodes(node, "require")
variables = set()
for parent, _ in nodes:
if "arguments" in parent:
arguments = parent["arguments"]
for argument in arguments:
variables.update(self.find_all_variables(argument))
return variables
def find_variables_in_if(self, node):
variables = []
def traverse(node):
if "condition" in node:
condition = node["condition"]
if (
"leftExpression" in condition
and condition["leftExpression"]["nodeType"] == "Identifier"
):
variables.append(condition["leftExpression"]["name"])
if (
"rightExpression" in condition
and condition["rightExpression"]["nodeType"] == "Identifier"
):
variables.append(condition["rightExpression"]["name"])
traverse(condition)
if "falseBody" in node and node["falseBody"]:
traverse(node["falseBody"])
if "trueBody" in node and node["trueBody"]:
if (
"nodeType" in node["trueBody"]
and node["trueBody"]["nodeType"] == "Block"
):
statements = node["trueBody"].get("statements", [])
for statement in statements:
traverse(statement)
else:
traverse(node["trueBody"])
if "body" in node and node["body"]:
if "nodeType" in node["body"] and node["body"]["nodeType"] == "Block":
statements = node["body"].get("statements", [])
for statement in statements:
traverse(statement)
else:
traverse(node["body"])
traverse(node)
return variables
def extract_address_variable(self, node):
if node is None or isinstance(node, (int, str)):
return set([])
transfer_vars = set([])
if (
node.get("nodeType", "") == "ExpressionStatement"
and node.get("expression", {}).get("nodeType") == "FunctionCall"
):
expression = node["expression"].get("expression", None)
if expression is not None and (
expression["nodeType"] == "MemberAccess"
and expression["memberName"] in TRANSFER_METHODS
):
address_variable = expression["expression"].get("name")
if address_variable:
transfer_vars.update(set([address_variable]))
for key, value in node.items():
if isinstance(value, dict):
transfer_vars.update(self.extract_address_variable(value))
elif isinstance(value, list):
for item in value:
transfer_vars.update(self.extract_address_variable(item))
return transfer_vars
================================================
FILE: mythril/solidity/soliditycontract.py
================================================
"""This module contains representation classes for Solidity files, contracts
and source mappings."""
import logging
from typing import Dict, Set
import mythril.laser.ethereum.util as helper
from mythril.ethereum.evmcontract import EVMContract
from mythril.ethereum.util import get_solc_json
from mythril.exceptions import NoContractFoundError
from mythril.solidity.features import SolidityFeatureExtractor
log = logging.getLogger(__name__)
class SolcAST:
def __init__(self, ast):
self.ast = ast
@property
def node_type(self):
if "nodeType" in self.ast:
return self.ast["nodeType"]
if "name" in self.ast:
return self.ast["name"]
assert False, "Unknown AST type has been fed to SolcAST"
@property
def abs_path(self):
if "absolutePath" in self.ast:
return self.ast["absolutePath"]
else:
return None
@property
def nodes(self):
if "nodes" in self.ast:
return self.ast["nodes"]
if "children" in self.ast:
return self.ast["children"]
assert False, "Unknown AST type has been fed to SolcAST"
def __next__(self):
yield self.ast.__next__()
def __getitem__(self, item):
return self.ast[item]
class SolcSource:
def __init__(self, source):
self.source = source
@property
def ast(self):
if "ast" in self.source:
return SolcAST(self.source["ast"])
if "legacyAST" in self.source:
return SolcAST(self.source["legacyAST"])
assert False, "Unknown source type has been fed to SolcSource"
@property
def id(self):
return self.source["id"]
@property
def name(self):
return self.source["name"]
@property
def contents(self):
return self.source["contents"]
class SourceMapping:
def __init__(self, solidity_file_idx, offset, length, lineno, mapping):
"""Representation of a source mapping for a Solidity file."""
self.solidity_file_idx = solidity_file_idx
self.offset = offset
self.length = length
self.lineno = lineno
self.solc_mapping = mapping
class SolidityFile:
"""Representation of a file containing Solidity code."""
def __init__(self, filename: str, data: str, full_contract_src_maps: Set[str]):
"""
Metadata class containing data regarding a specific solidity file
:param filename: The filename of the solidity file
:param data: The code of the solidity file
:param full_contract_src_maps: The set of contract source mappings of all the contracts in the file
"""
self.filename = filename
self.data = data
self.full_contract_src_maps = full_contract_src_maps
class SourceCodeInfo:
def __init__(self, filename, lineno, code, mapping):
"""Metadata class containing a code reference for a specific file."""
self.filename = filename
self.lineno = lineno
self.code = code
self.solc_mapping = mapping
def get_contracts_from_file(input_file, solc_settings_json=None, solc_binary="solc"):
"""
:param input_file:
:param solc_settings_json:
:param solc_binary:
"""
data = get_solc_json(
input_file, solc_settings_json=solc_settings_json, solc_binary=solc_binary
)
try:
contract_names = data["contracts"][input_file].keys()
except KeyError:
raise NoContractFoundError
for contract_name in contract_names:
if len(
data["contracts"][input_file][contract_name]["evm"]["deployedBytecode"][
"object"
]
):
yield SolidityContract(
input_file=input_file,
name=contract_name,
solc_settings_json=solc_settings_json,
solc_binary=solc_binary,
)
def get_contracts_from_foundry(input_file, foundry_json):
"""
:param input_file:
:param solc_settings_json:
:param solc_binary:
"""
try:
contract_names = foundry_json["contracts"][input_file].keys()
except KeyError:
raise NoContractFoundError
for contract_name in contract_names:
if len(
foundry_json["contracts"][input_file][contract_name]["evm"][
"deployedBytecode"
]["object"]
):
yield SolidityContract(
input_file=input_file,
name=contract_name,
solc_settings_json=None,
solc_binary=None,
solc_data=foundry_json,
)
class SolidityContract(EVMContract):
"""Representation of a Solidity contract."""
def __init__(
self,
input_file,
name=None,
solc_settings_json=None,
solc_binary="solc",
solc_data=None,
):
if solc_data is None:
data = get_solc_json(
input_file,
solc_settings_json=solc_settings_json,
solc_binary=solc_binary,
)
else:
data = solc_data
self.solc_indices = self.get_solc_indices(input_file, data)
self.solc_json = data
self.input_file = input_file
if "ast" in data["sources"][str(input_file)]:
# Not available in old solidity versions, around ~0.4.11
self.features = SolidityFeatureExtractor(
data["sources"][str(input_file)]["ast"]
).extract_features()
else:
self.features = None
has_contract = False
# If a contract name has been specified, find the bytecode of that specific contract
srcmap_constructor = []
srcmap = []
if name:
contract = data["contracts"][input_file][name]
if len(contract["evm"]["deployedBytecode"]["object"]):
code = contract["evm"]["deployedBytecode"]["object"]
creation_code = contract["evm"]["bytecode"]["object"]
srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";")
srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split(";")
has_contract = True
# If no contract name is specified, get the last bytecode entry for the input file
else:
for contract_name, contract in sorted(
data["contracts"][input_file].items()
):
if len(contract["evm"]["deployedBytecode"]["object"]):
name = contract_name
code = contract["evm"]["deployedBytecode"]["object"]
creation_code = contract["evm"]["bytecode"]["object"]
srcmap = contract["evm"]["deployedBytecode"]["sourceMap"].split(";")
srcmap_constructor = contract["evm"]["bytecode"]["sourceMap"].split(
";"
)
has_contract = True
if not has_contract:
raise NoContractFoundError
self.mappings = []
self.constructor_mappings = []
self._get_solc_mappings(srcmap)
self._get_solc_mappings(srcmap_constructor, constructor=True)
super().__init__(code, creation_code, name=name)
@staticmethod
def get_sources(indices_data: Dict, source_data: Dict) -> None:
"""
Get source indices mapping. Function not needed for older solc versions.
"""
if "generatedSources" not in source_data:
return
sources = source_data["generatedSources"]
for source in sources:
full_contract_src_maps = SolidityContract.get_full_contract_src_maps(
SolcAST(source["ast"])
)
indices_data[source["id"]] = SolidityFile(
source["name"], source["contents"], full_contract_src_maps
)
@staticmethod
def get_solc_indices(input_file: str, data: Dict) -> Dict:
"""
Returns solc file indices
"""
indices: Dict = {}
for contract_data in data["contracts"].values():
for source_data in contract_data.values():
SolidityContract.get_sources(indices, source_data["evm"]["bytecode"])
SolidityContract.get_sources(
indices, source_data["evm"]["deployedBytecode"]
)
for source in data["sources"].values():
source = SolcSource(source)
full_contract_src_maps = SolidityContract.get_full_contract_src_maps(
source.ast
)
if source.ast.abs_path is not None:
abs_path = source.ast.abs_path
else:
abs_path = input_file
with open(abs_path, "rb") as f:
code = f.read()
indices[source.id] = SolidityFile(
abs_path,
code.decode("utf-8"),
full_contract_src_maps,
)
return indices
@staticmethod
def get_full_contract_src_maps(ast: SolcAST) -> Set[str]:
"""
Takes a solc AST and gets the src mappings for all the contracts defined in the top level of the ast
:param ast: AST of the contract
:return: The source maps
"""
source_maps = set()
if ast.node_type == "SourceUnit":
for child in ast.nodes:
if child.get("contractKind"):
source_maps.add(child["src"])
elif ast.node_type == "YulBlock":
for child in ast["statements"]:
source_maps.add(child["src"])
return source_maps
def get_source_info(self, address, constructor=False):
"""
:param address:
:param constructor:
:return:
"""
disassembly = self.creation_disassembly if constructor else self.disassembly
mappings = self.constructor_mappings if constructor else self.mappings
index = helper.get_instruction_index(disassembly.instruction_list, address)
if index is None or index >= len(mappings):
# TODO: Find why this scenario happens. Possibly an external call
return None
file_index = mappings[index].solidity_file_idx
if file_index == -1:
# If issue is detected in an internal file
return None
solidity_file = self.solc_indices[file_index]
filename = solidity_file.filename
offset = mappings[index].offset
length = mappings[index].length
code = solidity_file.data.encode("utf-8")[offset : offset + length].decode(
"utf-8", errors="ignore"
)
lineno = mappings[index].lineno
return SourceCodeInfo(filename, lineno, code, mappings[index].solc_mapping)
def _is_autogenerated_code(self, offset: int, length: int, file_index: int) -> bool:
"""
Checks whether the code is autogenerated or not
:param offset: offset of the code
:param length: length of the code
:param file_index: file the code corresponds to
:return: True if the code is internally generated, else false
"""
if file_index == -1:
return True
# Handle the common code src map for the entire code.
if (
"{}:{}:{}".format(offset, length, file_index)
in self.solc_indices[file_index].full_contract_src_maps
):
return True
return False
def _get_solc_mappings(self, srcmap, constructor=False):
"""
:param srcmap:
:param constructor:
"""
mappings = self.constructor_mappings if constructor else self.mappings
prev_item = ""
for item in srcmap:
if item == "":
item = prev_item
mapping = item.split(":")
if len(mapping) > 0 and len(mapping[0]) > 0:
offset = int(mapping[0])
if len(mapping) > 1 and len(mapping[1]) > 0:
length = int(mapping[1])
if len(mapping) > 2 and len(mapping[2]) > 0:
idx = int(mapping[2])
if self._is_autogenerated_code(offset, length, idx):
lineno = None
else:
lineno = (
self.solc_indices[idx]
.data.encode("utf-8")[0:offset]
.count("\n".encode("utf-8"))
+ 1
)
prev_item = item
mappings.append(SourceMapping(idx, offset, length, lineno, item))
================================================
FILE: mythril/support/__init__.py
================================================
================================================
FILE: mythril/support/loader.py
================================================
"""This module contains the dynamic loader logic to get on-chain storage data
and dependencies."""
import functools
import logging
import re
from typing import Optional
from mythril.disassembler.disassembly import Disassembly
from mythril.ethereum.interface.rpc.client import EthJsonRpc
LRU_CACHE_SIZE = 4096
log = logging.getLogger(__name__)
class DynLoader:
"""The dynamic loader class."""
def __init__(self, eth: Optional[EthJsonRpc], active=True):
"""
:param eth:
:param active:
"""
self.eth = eth
self.active = active
@functools.lru_cache(LRU_CACHE_SIZE)
def read_storage(self, contract_address: str, index: int) -> str:
"""
:param contract_address:
:param index:
:return:
"""
if not self.active:
raise ValueError("Loader is disabled")
if not self.eth:
raise ValueError("Cannot load from the storage when eth is None")
value = self.eth.eth_getStorageAt(
contract_address, position=index, block="latest"
)
if value.startswith("0x"):
value = "0x0000000000000000000000000000000000000000000000000000000000000000"
return value
@functools.lru_cache(LRU_CACHE_SIZE)
def read_balance(self, address: str) -> str:
"""
:param address:
:return:
"""
if not self.active:
raise ValueError("Cannot load from storage when the loader is disabled")
if not self.eth:
raise ValueError(
"Cannot load from the chain when eth is None, please use rpc, or specify infura-id"
)
return self.eth.eth_getBalance(address)
@functools.lru_cache(LRU_CACHE_SIZE)
def dynld(self, dependency_address: str) -> Optional[Disassembly]:
"""
:param dependency_address:
:return:
"""
if not self.active:
raise ValueError("Loader is disabled")
if not self.eth:
raise ValueError(
"Cannot load from the chain when eth is None, please use rpc, or specify infura-id"
)
log.debug("Dynld at contract %s", dependency_address)
# Ensure that dependency_address is the correct length, with 0s prepended as needed.
if isinstance(dependency_address, int):
dependency_address = "0x{:040X}".format(dependency_address)
else:
dependency_address = (
"0x" + "0" * (42 - len(dependency_address)) + dependency_address[2:]
)
m = re.match(r"^(0x[0-9a-fA-F]{40})$", dependency_address)
if m:
dependency_address = m.group(1)
else:
return None
log.debug("Dependency address: %s", dependency_address)
code = self.eth.eth_getCode(dependency_address)
if code.startswith("0x"):
return None
else:
return Disassembly(code)
================================================
FILE: mythril/support/lock.py
================================================
import errno
import os
import time
"""
credits: https://github.com/dmfrey/FileLock
"""
class LockFileException(Exception):
pass
class LockFile(object):
"""
Locks files.
"""
def __init__(self, file_name, timeout=100, delay=0.05):
"""
Initialises the file locker
"""
if timeout is not None and delay is None:
raise ValueError("If timeout is not None, then delay must not be None.")
self.is_locked = False
self.lockfile = os.path.join(os.getcwd(), f"{file_name}.lock")
self.file_name = file_name
self.timeout = timeout
self.delay = delay
def acquire(self):
"""
Acquires a lock when possible.
"""
start_time = time.time()
while True:
try:
self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
self.is_locked = True
break
except OSError as e:
if e.errno != errno.EEXIST:
raise
if (time.time() - start_time) >= self.timeout:
raise LockFileException(
f"Stuck for more than {self.timeout} seconds waiting to unlock the file {self.filename}."
)
time.sleep(self.delay)
def release(self):
"""
Releases the lock
"""
if self.is_locked:
os.close(self.fd)
os.unlink(self.lockfile)
self.is_locked = False
def __enter__(self):
"""
Lock gets acquired at the `with` statement.
"""
if not self.is_locked:
self.acquire()
return self
def __exit__(self, type, value, traceback):
"""
Lock gets released at the end of the `with` block
"""
if self.is_locked:
self.release()
def __del__(self):
"""
Releases the lock during deletion.
"""
self.release()
================================================
FILE: mythril/support/model.py
================================================
import logging
import os
import sys
from functools import lru_cache
from multiprocessing import TimeoutError
from multiprocessing.pool import ThreadPool
from pathlib import Path
from z3 import sat, unknown
from mythril.exceptions import SolverTimeOutException, UnsatError
from mythril.laser.ethereum.time_handler import time_handler
from mythril.laser.smt import And, Optimize, simplify
from mythril.support.support_args import args
from mythril.support.support_utils import ModelCache
log = logging.getLogger(__name__)
model_cache = ModelCache()
def solver_worker(
constraints,
minimize=(),
maximize=(),
solver_timeout=None,
):
"""
Returns a model based on given constraints as a tuple
:param constraints: Tuple of constraints
:param minimize: Tuple of minimization conditions
:param maximize: Tuple of maximization conditions
:param solver_timeout: The timeout for solver
:return:
"""
s = Optimize()
s.set_timeout(solver_timeout)
for constraint in constraints:
s.add(constraint)
for e in minimize:
s.minimize(e)
for e in maximize:
s.maximize(e)
if args.solver_log:
Path(args.solver_log).mkdir(parents=True, exist_ok=True)
constraint_hash_input = tuple(
list(constraints)
+ list(minimize)
+ list(maximize)
+ [len(constraints), len(minimize), len(maximize)]
)
with open(
args.solver_log + f"/{abs(hash(constraint_hash_input))}.smt2", "w"
) as f:
f.write(s.sexpr())
result = s.check()
return result, s
@lru_cache(maxsize=2**23)
def get_model(
constraints,
minimize=(),
maximize=(),
solver_timeout=None,
):
"""
Returns a model based on given constraints as a tuple
:param constraints: Tuple of constraints
:param minimize: Tuple of minimization conditions
:param maximize: Tuple of maximization conditions
:param solver_timeout: The solver timeout
:return:
"""
solver_timeout = solver_timeout or args.solver_timeout
solver_timeout = min(solver_timeout, time_handler.time_remaining())
if solver_timeout <= 0:
raise SolverTimeOutException
for constraint in constraints:
if isinstance(constraint, bool) and not constraint:
raise UnsatError
if isinstance(constraints, tuple) is False:
constraints = constraints.get_all_constraints()
constraints = [
constraint
for constraint in constraints
if isinstance(constraint, bool) is False
]
if len(maximize) + len(minimize) == 0:
ret_model = model_cache.check_quick_sat(simplify(And(*constraints)).raw)
if ret_model:
return ret_model
pool = ThreadPool(1)
try:
thread_result = pool.apply_async(
solver_worker, args=(constraints, minimize, maximize, solver_timeout)
)
try:
result, s = thread_result.get(solver_timeout)
except TimeoutError:
result = unknown
except Exception:
log.warning("Encountered an exception while solving expression using z3")
result = unknown
finally:
# This is to prevent any segmentation faults from being displayed from z3
sys.stdout = open(os.devnull, "w")
sys.stderr = open(os.devnull, "w")
pool.terminate()
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
if result == sat:
model_cache.model_cache.put(s.model(), 1)
return s.model()
elif result == unknown:
log.debug("Timeout/Error encountered while solving expression using z3")
raise SolverTimeOutException
raise UnsatError
================================================
FILE: mythril/support/opcodes.py
================================================
from typing import Dict
Z_OPERATOR_TUPLE = (0, 1)
UN_OPERATOR_TUPLE = (1, 1)
BIN_OPERATOR_TUPLE = (2, 1)
T_OPERATOR_TUPLE = (3, 1)
GAS = "gas"
STACK = "stack"
ADDRESS = "address"
# Gas tuple contains (min_gas, max_gas)
# stack tuple contains (no_of_elements_popped, no_of_elements_pushed)
# TODO: Make this more specific when TypedDict supports key re-usage.
OPCODES: Dict = {
"STOP": {GAS: (0, 0), STACK: (0, 0), ADDRESS: 0x00},
"ADD": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x01},
"MUL": {GAS: (5, 5), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x02},
"SUB": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x03},
"DIV": {GAS: (5, 5), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x04},
"SDIV": {GAS: (5, 5), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x05},
"MOD": {GAS: (5, 5), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x06},
"SMOD": {GAS: (5, 5), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x07},
"ADDMOD": {GAS: (8, 8), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x08},
"MULMOD": {GAS: (8, 8), STACK: T_OPERATOR_TUPLE, ADDRESS: 0x09},
"EXP": {
GAS: (10, 340),
STACK: BIN_OPERATOR_TUPLE,
ADDRESS: 0x0A,
}, # exponent max 2^32
"SIGNEXTEND": {GAS: (5, 5), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x0B},
"LT": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x10},
"GT": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x11},
"SLT": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x12},
"SGT": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x13},
"EQ": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x14},
"ISZERO": {GAS: (3, 3), STACK: UN_OPERATOR_TUPLE, ADDRESS: 0x15},
"AND": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x16},
"OR": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x17},
"XOR": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x18},
"NOT": {GAS: (3, 3), STACK: UN_OPERATOR_TUPLE, ADDRESS: 0x19},
"BYTE": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x1A},
"SHL": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x1B},
"SHR": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x1C},
"SAR": {GAS: (3, 3), STACK: BIN_OPERATOR_TUPLE, ADDRESS: 0x1D},
"SHA3": {
GAS: (
30,
30 + 6 * 8,
), # max can be larger, but usually storage location with 8 words
STACK: BIN_OPERATOR_TUPLE,
ADDRESS: 0x20,
},
"ADDRESS": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x30},
"BALANCE": {GAS: (700, 700), STACK: UN_OPERATOR_TUPLE, ADDRESS: 0x31},
"ORIGIN": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x32},
"CALLER": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x33},
"CALLVALUE": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x34},
"CALLDATALOAD": {GAS: (3, 3), STACK: UN_OPERATOR_TUPLE, ADDRESS: 0x35},
"CALLDATASIZE": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x36},
"CALLDATACOPY": {
GAS: (2, 2 + 3 * 768), # https://ethereum.stackexchange.com/a/47556
STACK: (3, 0),
ADDRESS: 0x37,
},
"CODESIZE": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x38},
"CODECOPY": {
GAS: (2, 2 + 3 * 768), # https://ethereum.stackexchange.com/a/47556,
STACK: (3, 0),
ADDRESS: 0x39,
},
"GASPRICE": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x3A},
"EXTCODESIZE": {GAS: (700, 700), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x3B},
"EXTCODECOPY": {
GAS: (700, 700 + 3 * 768), # https://ethereum.stackexchange.com/a/47556
STACK: (4, 0),
ADDRESS: 0x3C,
},
"RETURNDATASIZE": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x3D},
"RETURNDATACOPY": {GAS: (3, 3), STACK: (3, 0), ADDRESS: 0x3E},
"EXTCODEHASH": {GAS: (700, 700), STACK: UN_OPERATOR_TUPLE, ADDRESS: 0x3F},
"BLOCKHASH": {GAS: (20, 20), STACK: UN_OPERATOR_TUPLE, ADDRESS: 0x40},
"COINBASE": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x41},
"TIMESTAMP": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x42},
"NUMBER": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x43},
"DIFFICULTY": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x44},
"GASLIMIT": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x45},
"CHAINID": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x46},
"SELFBALANCE": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x47},
"BASEFEE": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x48},
"POP": {GAS: (2, 2), STACK: (1, 0), ADDRESS: 0x50},
# assume 1KB memory r/w cost as upper bound
"MLOAD": {GAS: (3, 96), STACK: UN_OPERATOR_TUPLE, ADDRESS: 0x51},
"MSTORE": {GAS: (3, 98), STACK: (2, 0), ADDRESS: 0x52},
"MSTORE8": {GAS: (3, 98), STACK: (2, 0), ADDRESS: 0x53},
# assume 64 byte r/w cost as upper bound
"SLOAD": {GAS: (800, 800), STACK: UN_OPERATOR_TUPLE, ADDRESS: 0x54},
"SSTORE": {GAS: (5000, 25000), STACK: (2, 0), ADDRESS: 0x55},
"JUMP": {GAS: (8, 8), STACK: (1, 0), ADDRESS: 0x56},
"JUMPI": {GAS: (10, 10), STACK: (2, 0), ADDRESS: 0x57},
"PC": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x58},
"MSIZE": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x59},
"GAS": {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x5A},
"JUMPDEST": {GAS: (1, 1), STACK: (0, 0), ADDRESS: 0x5B},
"TLOAD": {GAS: (100, 100), STACK: UN_OPERATOR_TUPLE, ADDRESS: 0x5C},
"TSTORE": {GAS: (100, 100), STACK: (2, 0), ADDRESS: 0x5D},
# apparently Solidity only allows byte32 as input to the log
# function. Virtually it could be as large as the block gas limit
# allows, but let's stick to the reasonable standard here.
# https://ethereum.stackexchange.com/a/1691
"LOG0": {GAS: (375, 375 + 8 * 32), STACK: (2, 0), ADDRESS: 0xA0},
"LOG1": {GAS: (2 * 375, 2 * 375 + 8 * 32), STACK: (3, 0), ADDRESS: 0xA1},
"LOG2": {GAS: (3 * 375, 3 * 375 + 8 * 32), STACK: (4, 0), ADDRESS: 0xA2},
"LOG3": {GAS: (4 * 375, 4 * 375 + 8 * 32), STACK: (5, 0), ADDRESS: 0xA3},
"LOG4": {GAS: (5 * 375, 5 * 375 + 8 * 32), STACK: (6, 0), ADDRESS: 0xA4},
"CREATE": {GAS: (32000, 32000), STACK: T_OPERATOR_TUPLE, ADDRESS: 0xF0},
"CREATE2": {
GAS: (32000, 32000), # TODO: Make the gas values dynamic
STACK: (4, 1),
ADDRESS: 0xF5,
},
"CALL": {GAS: (700, 700 + 9000 + 25000), STACK: (7, 1), ADDRESS: 0xF1},
"CALLCODE": {GAS: (700, 700 + 9000 + 25000), STACK: (7, 1), ADDRESS: 0xF2},
"RETURN": {GAS: (0, 0), STACK: (2, 0), ADDRESS: 0xF3},
"DELEGATECALL": {GAS: (700, 700 + 9000 + 25000), STACK: (6, 1), ADDRESS: 0xF4},
"STATICCALL": {GAS: (700, 700 + 9000 + 25000), STACK: (6, 1), ADDRESS: 0xFA},
"REVERT": {GAS: (0, 0), STACK: (2, 0), ADDRESS: 0xFD},
"SELFDESTRUCT": {GAS: (5000, 30000), STACK: (1, 0), ADDRESS: 0xFF},
"INVALID": {GAS: (0, 0), STACK: (0, 0), ADDRESS: 0xFE},
}
OPCODES["PUSH0"] = {GAS: (2, 2), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x5F}
for i in range(1, 33):
OPCODES[f"PUSH{i}"] = {GAS: (3, 3), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x5F + i}
for i in range(1, 17):
OPCODES[f"DUP{i}"] = {GAS: (3, 3), STACK: (0, 0), ADDRESS: 0x7F + i}
OPCODES[f"SWAP{i}"] = {GAS: (3, 3), STACK: Z_OPERATOR_TUPLE, ADDRESS: 0x8F + i}
ADDRESS_OPCODE_MAPPING = {}
for opcode, opcode_data in OPCODES.items():
ADDRESS_OPCODE_MAPPING[opcode_data[ADDRESS]] = opcode
================================================
FILE: mythril/support/signatures.py
================================================
"""The Mythril function signature database."""
import functools
import logging
import multiprocessing
import os
import sqlite3
from collections import defaultdict
from typing import DefaultDict, Dict, List
from mythril.ethereum.util import get_solc_json
log = logging.getLogger(__name__)
lock = multiprocessing.Lock()
def synchronized(sync_lock):
"""A decorator synchronizing multi-process access to a resource."""
def wrapper(f):
"""The decorator's core function.
:param f:
:return:
"""
@functools.wraps(f)
def inner_wrapper(*args, **kw):
"""
:param args:
:param kw:
:return:
"""
with sync_lock:
return f(*args, **kw)
return inner_wrapper
return wrapper
class Singleton(type):
"""A metaclass type implementing the singleton pattern."""
_instances: Dict["Singleton", "Singleton"] = dict()
@synchronized(lock)
def __call__(cls, *args, **kwargs):
"""Delegate the call to an existing resource or a new one.
This is not thread- or process-safe by default. It must be protected with
a lock.
:param args:
:param kwargs:
:return:
"""
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class SQLiteDB(object):
"""Simple context manager for sqlite3 databases.
Commits everything at exit.
"""
def __init__(self, path):
"""
:param path:
"""
self.path = path
self.conn = None
self.cursor = None
def __enter__(self):
"""
:return:
"""
try:
self.conn = sqlite3.connect(self.path)
except sqlite3.OperationalError:
raise sqlite3.OperationalError(f"Unable to Connect to path {self.path}")
self.cursor = self.conn.cursor()
return self.cursor
def __exit__(self, exc_class, exc, traceback):
"""
:param exc_class:
:param exc:
:param traceback:
"""
self.conn.commit()
self.conn.close()
def __repr__(self):
return "".format(self.path)
class SignatureDB(object, metaclass=Singleton):
""""""
def __init__(self, path: str = None) -> None:
"""
:param path:
"""
# if we're analysing a Solidity file, store its hashes
# here to prevent unnecessary lookups
self.solidity_sigs: DefaultDict[str, List[str]] = defaultdict(list)
if path is None:
self.path = os.environ.get("MYTHRIL_DIR") or os.path.join(
os.path.expanduser("~"), ".mythril"
)
self.path = os.path.join(self.path, "signatures.db")
log.info("Using signature database at %s", self.path)
# NOTE: Creates a new DB file if it doesn't exist already
with SQLiteDB(self.path) as cur:
cur.execute(
(
"CREATE TABLE IF NOT EXISTS signatures"
"(byte_sig VARCHAR(10), text_sig VARCHAR(255),"
"PRIMARY KEY (byte_sig, text_sig))"
)
)
def __getitem__(self, item: str) -> List[str]:
"""Provide dict interface db[sighash]
:param item: 4-byte signature string
:return: list of matching text signature strings
"""
return self.get(byte_sig=item)
@staticmethod
def _normalize_byte_sig(byte_sig: str) -> str:
"""Adds a leading 0x to the byte signature if it's not already there.
:param byte_sig: 4-byte signature string
:return: normalized byte signature string
"""
if not byte_sig.startswith("0x"):
byte_sig = "0x" + byte_sig
if not len(byte_sig) == 10:
raise ValueError(
"Invalid byte signature %s, must have 10 characters", byte_sig
)
return byte_sig
def add(self, byte_sig: str, text_sig: str) -> None:
"""
Adds a new byte - text signature pair to the database.
:param byte_sig: 4-byte signature string
:param text_sig: resolved text signature
:return:
"""
byte_sig = self._normalize_byte_sig(byte_sig)
with SQLiteDB(self.path) as cur:
# ignore new row if it's already in the DB (and would cause a unique constraint error)
cur.execute(
"INSERT OR IGNORE INTO signatures (byte_sig, text_sig) VALUES (?,?)",
(byte_sig, text_sig),
)
def get(self, byte_sig: str, online_timeout: int = 2) -> List[str]:
"""Get a function text signature for a byte signature 1) try local
cache 2) try online lookup (if enabled; if not flagged as unavailable)
:param byte_sig: function signature hash as hexstr
:param online_timeout: online lookup timeout
:return: list of matching function text signatures
"""
byte_sig = self._normalize_byte_sig(byte_sig)
# check if we have any Solidity signatures to look up
text_sigs = self.solidity_sigs.get(byte_sig)
if text_sigs is not None:
return text_sigs
# try lookup in the local DB
with SQLiteDB(self.path) as cur:
cur.execute("SELECT text_sig FROM signatures WHERE byte_sig=?", (byte_sig,))
text_sigs = cur.fetchall()
if text_sigs:
return [t[0] for t in text_sigs]
return []
def import_solidity_file(
self, file_path: str, solc_binary: str = "solc", solc_settings_json: str = None
):
"""Import Function Signatures from solidity source files.
:param solc_binary:
:param solc_settings_json:
:param file_path: solidity source code file path
:return:
"""
solc_json = get_solc_json(file_path, solc_binary, solc_settings_json)
self.add_sigs(file_path, solc_json)
def add_sigs(self, file_path: str, solc_json):
for contract in solc_json["contracts"][file_path].values():
if "methodIdentifiers" not in contract["evm"]:
continue
for name, hash_ in contract["evm"]["methodIdentifiers"].items():
sig = "0x{}".format(hash_)
if sig in self.solidity_sigs:
self.solidity_sigs[sig].append(name)
else:
self.solidity_sigs[sig] = [name]
self.add(sig, name)
def __repr__(self):
return "".format(self.path)
================================================
FILE: mythril/support/source_support.py
================================================
from mythril.ethereum.evmcontract import EVMContract
from mythril.solidity.soliditycontract import SolidityContract
class Source:
"""Class to handle to source data"""
def __init__(self, source_type=None, source_format=None, source_list=None):
"""
:param source_type: whether it is a solidity-file or evm-bytecode
:param source_format: whether it is bytecode, ethereum-address or text
:param source_list: List of files
:param meta: meta data
"""
self.source_type = source_type
self.source_format = source_format
self.source_list = source_list or []
self._source_hash = []
def get_source_from_contracts_list(self, contracts):
"""
get the source data from the contracts list
:param contracts: the list of contracts
:return:
"""
if contracts is None or len(contracts) == 0:
return
if isinstance(contracts[0], SolidityContract):
self.source_type = "solidity-file"
self.source_format = "text"
for contract in contracts:
self.source_list += [
file.filename for file in contract.solc_indices.values()
]
self._source_hash.append(contract.bytecode_hash)
self._source_hash.append(contract.creation_bytecode_hash)
elif isinstance(contracts[0], EVMContract):
self.source_format = "evm-byzantium-bytecode"
self.source_type = (
"ethereum-address"
if len(contracts[0].name) == 42 and contracts[0].name.startswith("0x")
else "raw-bytecode"
)
for contract in contracts:
if contract.creation_code:
self.source_list.append(contract.creation_bytecode_hash)
if contract.code:
self.source_list.append(contract.bytecode_hash)
self._source_hash = self.source_list
else:
assert False # Fail hard
def get_source_index(self, bytecode_hash: str) -> int:
"""
Find the contract index in the list
:param bytecode_hash: The contract hash
:return: The index of the contract in the _source_hash list
"""
# TODO: Add this part to exception logs
try:
return self._source_hash.index(bytecode_hash)
except ValueError:
self._source_hash.append(bytecode_hash)
return len(self._source_hash) - 1
================================================
FILE: mythril/support/start_time.py
================================================
from time import time
from mythril.support.support_utils import Singleton
class StartTime(metaclass=Singleton):
"""Maintains the start time of the current contract in execution"""
def __init__(self):
self.global_start_time = time()
================================================
FILE: mythril/support/support_args.py
================================================
from typing import List
from mythril.support.support_utils import Singleton
class Args(object, metaclass=Singleton):
"""
This module helps in preventing args being sent through multiple of classes to reach
any analysis/laser module
"""
def __init__(self):
self.solver_timeout = 10000
self.pruning_factor = None
self.unconstrained_storage = False
self.parallel_solving = False
self.call_depth_limit = 3
self.disable_iprof = False
self.solver_log = None
self.transaction_sequences: List[List[str]] = None
self.use_integer_module = True
self.use_issue_annotations = False
self.solc_args = None
self.disable_coverage_strategy = False
self.disable_mutation_pruner = False
self.incremental_txs = True
self.enable_summaries = False
self.enable_state_merge = False
args = Args()
================================================
FILE: mythril/support/support_utils.py
================================================
"""This module contains utility functions for the Mythril support package."""
import logging
from collections import OrderedDict
from copy import deepcopy
from functools import lru_cache
from typing import Dict
from eth_hash.auto import keccak
from z3 import is_true
log = logging.getLogger(__name__)
class Singleton(type):
"""A metaclass type implementing the singleton pattern."""
_instances: Dict = {}
def __call__(cls, *args, **kwargs):
"""Delegate the call to an existing resource or a new one.
This is not thread- or process-safe by default. It must be protected with
a lock.
:param args:
:param kwargs:
:return:
"""
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class LRUCache:
def __init__(self, size):
self.size = size
self.lru_cache = OrderedDict()
def get(self, key):
try:
value = self.lru_cache.pop(key)
self.lru_cache[key] = value
return value
except KeyError:
return -1
def put(self, key, value):
try:
self.lru_cache.pop(key)
except KeyError:
if len(self.lru_cache) >= self.size:
self.lru_cache.popitem(last=False)
self.lru_cache[key] = value
class ModelCache:
def __init__(self):
self.model_cache = LRUCache(size=100)
@lru_cache(maxsize=2**10)
def check_quick_sat(self, constraints) -> bool:
for model in reversed(self.model_cache.lru_cache.keys()):
model_copy = deepcopy(model)
if is_true(model_copy.eval(constraints, model_completion=True)):
self.model_cache.put(model, self.model_cache.get(model) + 1)
return model
return False
def put(self, key, value):
self.model_cache.put(key, value)
@lru_cache(maxsize=2**10)
def get_code_hash(code) -> str:
"""
:param code: bytecode
:return: Returns hash of the given bytecode
"""
if isinstance(code, tuple):
# Temporary hack, since we cannot find symbols of sha3
return str(hash(code))
code = code[2:] if code.startswith("0x") else code
try:
hash_ = keccak(bytes.fromhex(code))
return "0x" + hash_.hex()
except ValueError:
log.debug("Unable to change the bytecode to bytes. Bytecode: {}".format(code))
return ""
def sha3(value):
if isinstance(value, str):
if value.startswith("0x"):
new_hash = keccak(bytes.fromhex(value))
else:
new_hash = keccak(value.encode())
else:
new_hash = keccak(value)
return new_hash
def zpad(x, l):
"""
Left zero pad value `x` at least to length `l`.
"""
return b"\x00" * max(0, l - len(x)) + x
def rzpad(value, total_length):
"""
Right zero pad value `x` at least to length `l`.
"""
return value + b"\x00" * max(0, total_length - len(value))
================================================
FILE: pyproject.toml
================================================
[tool.ruff]
target-version = "py37"
[tool.ruff.lint]
fixable = ["ALL"]
# List of rules https://docs.astral.sh/ruff/rules/
select = ["F", "I"]
[tool.ruff.lint.per-file-ignores]
# Fine for __init__.py files:
# * F401: Module imported but unused
# * F403: 'from .module import *'
# (resolvable via __all__ list,
# see https://docs.python.org/3/tutorial/modules.html#importing-from-a-package)
"mythril/**/__init__.py" = ["F401", "F403"]
================================================
FILE: requirements.txt
================================================
blake2b-py>=0.2.0,<1
coloredlogs>=10.0
coincurve>=13.0.0
cytoolz>=0.12.0
asn1crypto>=0.22.0
configparser>=3.5.0
py_ecc>=5.0.0
eth-abi>=5.1.0
eth-hash>=0.3.1,<0.8.0
eth-utils>=2.0.0
hexbytes<1.4.0
jinja2>=2.9
MarkupSafe<3.1.0
mypy-extensions==1.0.0
numpy
persistent>=4.2.0
py-flags
py-evm==0.10.1b2
py-solc-x<3.0.0
py-solc
pyparsing>=2.0.2,<4
requests
rlp>=3,<5
semantic_version
z3-solver>=4.8.8.0,<=4.13.4.0
matplotlib
certifi>=2020.06.20
================================================
FILE: setup.py
================================================
# -*- coding: utf-8 -*-
"""install mythril and deploy source-dist and wheel to pypi.python.org.
deps (requires up2date version):
*) pip install --upgrade pip wheel setuptools twine
publish to pypi w/o having to convert Readme.md to RST:
1) #> python setup.py sdist bdist_wheel
2) #> twine upload dist/* #; #optional --repository or --repository-url
"""
import io
import os
import sys
from setuptools import find_packages, setup
from setuptools.command.install import install as _install
# Package meta-data.
NAME = "mythril"
DESCRIPTION = "Security analysis tool for Ethereum smart contracts"
URL = "https://github.com/ConsenSys/mythril"
AUTHOR = "ConsenSys Dilligence"
AUTHOR_MAIL = None
REQUIRES_PYTHON = ">=3.7.0"
here = os.path.abspath(os.path.dirname(__file__))
# What packages are required for this module to be executed?
def get_requirements():
"""
Return requirements as list.
Handles cases where git links are used.
"""
with open(os.path.join(here, "requirements.txt")) as f:
packages = []
for line in f:
line = line.strip()
# let's also ignore empty lines and comments
if not line or line.startswith("#"):
continue
if "https://" not in line:
packages.append(line)
continue
rest, package_name = line.split("#egg=")[0], line.split("#egg=")[1]
if "-e" in rest:
rest = rest.split("-e")[1]
package_name = package_name + "@" + rest
packages.append(package_name)
return packages
REQUIRED = get_requirements()
TESTS_REQUIRE = ["mypy==0.782", "pytest>=3.6.0", "pytest_mock", "pytest-cov"]
# What packages are optional?
EXTRAS = {}
# If version is set to None then it will be fetched from __version__.py
VERSION = None
# Import the README and use it as the long-description.
# Note: this will only work if 'README.md' is present in your MANIFEST.in file!
try:
with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
long_description = "\n" + f.read()
except FileNotFoundError:
long_description = DESCRIPTION
# Load the package's __version__.py module as a dictionary.
about = {}
if not VERSION:
project_slug = NAME.lower().replace("-", "_").replace(" ", "_")
with open(os.path.join(here, project_slug, "__version__.py")) as f:
exec(f.read(), about)
else:
about["__version__"] = VERSION
# Package version (vX.Y.Z). It must match git tag being used for CircleCI
# deployment; otherwise the build will failed.
class VerifyVersionCommand(_install):
"""Custom command to verify that the git tag matches our version."""
description = "verify that the git tag matches our version"
def run(self):
""""""
tag = os.getenv("CIRCLE_TAG")
if tag != about["__version__"]:
info = "Git tag: {0} does not match the version of this app: {1}".format(
tag, about["__version__"]
)
sys.exit(info)
setup(
name=NAME,
version=about["__version__"][1:],
description=DESCRIPTION,
long_description=long_description,
long_description_content_type="text/markdown", # requires twine and recent setuptools
url=URL,
author=AUTHOR,
author_mail=AUTHOR_MAIL,
license="MIT",
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Science/Research",
"Topic :: Software Development :: Disassemblers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
],
keywords="hacking disassembler security ethereum",
packages=find_packages(exclude=["contrib", "docs", "tests"]),
install_requires=REQUIRED,
tests_require=TESTS_REQUIRE,
python_requires=REQUIRES_PYTHON,
extras_require=EXTRAS,
package_data={"mythril.analysis.templates": ["*"], "mythril.support.assets": ["*"]},
include_package_data=True,
entry_points={"console_scripts": ["myth=mythril.interfaces.cli:main"]},
cmdclass={"verify": VerifyVersionCommand},
)
================================================
FILE: solidity_examples/BECToken.sol
================================================
/**
* @title SafeMath
* @dev Math operations with safety checks that throw on error
*/
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
/**
* @title ERC20Basic
* @dev Simpler version of ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/179
*/
contract ERC20Basic {
uint256 public totalSupply;
function balanceOf(address who) public returns (uint256);
function transfer(address to, uint256 value) public returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
}
/**
* @title Basic token
* @dev Basic version of StandardToken, with no allowances.
*/
contract BasicToken is ERC20Basic {
using SafeMath for uint256;
mapping(address => uint256) balances;
/**
* @dev transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value > 0 && _value <= balances[msg.sender]);
// SafeMath.sub will throw if there is not enough balance.
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
emit Transfer(msg.sender, _to, _value);
return true;
}
/**
* @dev Gets the balance of the specified address.
* @param _owner The address to query the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address _owner) public returns (uint256 balance) {
return balances[_owner];
}
}
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 is ERC20Basic {
function allowance(address owner, address spender) public returns (uint256);
function transferFrom(address from, address to, uint256 value) public returns (bool);
function approve(address spender, uint256 value) public returns (bool);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* @dev https://github.com/ethereum/EIPs/issues/20
* @dev Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract StandardToken is ERC20, BasicToken {
mapping (address => mapping (address => uint256)) internal allowed;
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value > 0 && _value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
*
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(address _owner, address _spender) public returns (uint256 remaining) {
return allowed[_owner][_spender];
}
}
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor() public {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) onlyOwner public {
require(newOwner != address(0));
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
/**
* @title Pausable
* @dev Base contract which allows children to implement an emergency stop mechanism.
*/
contract Pausable is Ownable {
event Pause();
event Unpause();
bool public paused = false;
/**
* @dev Modifier to make a function callable only when the contract is not paused.
*/
modifier whenNotPaused() {
require(!paused);
_;
}
/**
* @dev Modifier to make a function callable only when the contract is paused.
*/
modifier whenPaused() {
require(paused);
_;
}
/**
* @dev called by the owner to pause, triggers stopped state
*/
function pause() onlyOwner whenNotPaused public {
paused = true;
emit Pause();
}
/**
* @dev called by the owner to unpause, returns to normal state
*/
function unpause() onlyOwner whenPaused public {
paused = false;
emit Unpause();
}
}
/**
* @title Pausable token
*
* @dev StandardToken modified with pausable transfers.
**/
contract PausableToken is StandardToken, Pausable {
function transfer(address _to, uint256 _value) public whenNotPaused returns (bool) {
return super.transfer(_to, _value);
}
function transferFrom(address _from, address _to, uint256 _value) public whenNotPaused returns (bool) {
return super.transferFrom(_from, _to, _value);
}
function approve(address _spender, uint256 _value) public whenNotPaused returns (bool) {
return super.approve(_spender, _value);
}
function batchTransfer(address[] memory _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length;
uint256 amount = uint256(cnt) * _value;
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
emit Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
}
/**
* @title Bec Token
*
* @dev Implementation of Bec Token based on the basic standard token.
*/
contract BecToken is PausableToken {
/**
* Public variables of the token
* The following variables are OPTIONAL vanities. One does not have to include them.
* They allow one to customise the token contract & in no way influences the core functionality.
* Some wallets/interfaces might not even bother to look at this information.
*/
string public name = "BeautyChain";
string public symbol = "BEC";
string public version = '1.0.0';
uint8 public decimals = 18;
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
*/
constructor() public {
totalSupply = 7000000000 * (10**(uint256(decimals)));
balances[msg.sender] = totalSupply; // Give the creator all initial tokens
}
function () external {
//if ether is sent to this address, send it back.
revert();
}
}
================================================
FILE: solidity_examples/WalletLibrary.sol
================================================
//sol Wallet
// Multi-sig, daily-limited account proxy/wallet.
// @authors:
// Gav Wood
// inheritable "property" contract that enables methods to be protected by requiring the acquiescence of either a
// single, or, crucially, each of a number of, designated owners.
// usage:
// use modifiers onlyowner (just own owned) or onlymanyowners(hash), whereby the same hash must be provided by
// some number (specified in constructor) of the set of owners (specified in the constructor, modifiable) before the
// interior is executed.
pragma solidity 0.5.0;
contract WalletEvents {
// EVENTS
// this contract only has six types of events: it can accept a confirmation, in which case
// we record owner and operation (hash) alongside it.
event Confirmation(address owner, bytes32 operation);
event Revoke(address owner, bytes32 operation);
// some others are in the case of an owner changing.
event OwnerChanged(address oldOwner, address newOwner);
event OwnerAdded(address newOwner);
event OwnerRemoved(address oldOwner);
// the last one is emitted if the required signatures change
event RequirementChanged(uint newRequirement);
// Funds has arrived into the wallet (record how much).
event Deposit(address _from, uint value);
// Single transaction going out of the wallet (record who signed for it, how much, and to whom it's going).
event SingleTransact(address owner, uint value, address to, bytes data, address created);
// Multi-sig transaction going out of the wallet (record who signed for it last, the operation hash, how much, and to whom it's going).
event MultiTransact(address owner, bytes32 operation, uint value, address to, bytes data, address created);
// Confirmation still needed for a transaction.
event ConfirmationNeeded(bytes32 operation, address initiator, uint value, address to, bytes data);
}
contract WalletAbi {
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) external;
// Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) external;
function addOwner(address _owner) external;
function removeOwner(address _owner) external;
function changeRequirement(uint _newRequired) external;
function isOwner(address _addr) public returns (bool);
function hasConfirmed(bytes32 _operation, address _owner) external returns (bool);
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit) external;
function execute(address _to, uint _value, bytes calldata _data) external returns (bytes32 o_hash);
function confirm(bytes32 _h) public returns (bool o_success);
}
contract WalletLibrary is WalletEvents {
// TYPES
// struct for the status of a pending operation.
struct PendingState {
uint yetNeeded;
uint ownersDone;
uint index;
}
// Transaction structure to remember details of transaction lest it need be saved for a later call.
struct Transaction {
address to;
uint value;
bytes data;
}
// MODIFIERS
// simple single-sig function modifier.
modifier onlyowner {
if (isOwner(msg.sender))
_;
}
// multi-sig function modifier: the operation must have an intrinsic hash in order
// that later attempts can be realised as the same underlying operation and
// thus count as confirmations.
modifier onlymanyowners(bytes32 _operation) {
if (confirmAndCheck(_operation))
_;
}
// METHODS
// gets called when no other function matches
function() external payable {
// just being sent some cash?
if (msg.value > 0)
emit Deposit(msg.sender, msg.value);
}
// constructor is given number of sigs required to do protected "onlymanyowners" transactions
// as well as the selection of addresses capable of confirming them.
function initMultiowned(address[] memory _owners, uint _required) public only_uninitialized {
m_numOwners = _owners.length + 1;
m_owners[1] = uint(msg.sender);
m_ownerIndex[uint(msg.sender)] = 1;
for (uint i = 0; i < _owners.length; ++i)
{
m_owners[2 + i] = uint(_owners[i]);
m_ownerIndex[uint(_owners[i])] = 2 + i;
}
m_required = _required;
}
// Revokes a prior confirmation of the given operation
function revoke(bytes32 _operation) external {
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return;
uint ownerIndexBit = 2**ownerIndex;
PendingState memory pending = m_pending[_operation];
if (pending.ownersDone & ownerIndexBit > 0) {
pending.yetNeeded++;
pending.ownersDone -= ownerIndexBit;
emit Revoke(msg.sender, _operation);
}
}
// Replaces an owner `_from` with another `_to`.
function changeOwner(address _from, address _to) onlymanyowners(keccak256(msg.data)) external {
if (isOwner(_to)) return;
uint ownerIndex = m_ownerIndex[uint(_from)];
if (ownerIndex == 0) return;
clearPending();
m_owners[ownerIndex] = uint(_to);
m_ownerIndex[uint(_from)] = 0;
m_ownerIndex[uint(_to)] = ownerIndex;
emit OwnerChanged(_from, _to);
}
function addOwner(address _owner) onlymanyowners(keccak256(msg.data)) external {
if (isOwner(_owner)) return;
clearPending();
if (m_numOwners >= c_maxOwners)
reorganizeOwners();
if (m_numOwners >= c_maxOwners)
return;
m_numOwners++;
m_owners[m_numOwners] = uint(_owner);
m_ownerIndex[uint(_owner)] = m_numOwners;
emit OwnerAdded(_owner);
}
function removeOwner(address _owner) onlymanyowners(keccak256(msg.data)) external {
uint ownerIndex = m_ownerIndex[uint(_owner)];
if (ownerIndex == 0) return;
if (m_required > m_numOwners - 1) return;
m_owners[ownerIndex] = 0;
m_ownerIndex[uint(_owner)] = 0;
clearPending();
reorganizeOwners(); //make sure m_numOwner is equal to the number of owners and always points to the optimal free slot
emit OwnerRemoved(_owner);
}
function changeRequirement(uint _newRequired) onlymanyowners(keccak256(msg.data)) external {
if (_newRequired > m_numOwners) return;
m_required = _newRequired;
clearPending();
emit RequirementChanged(_newRequired);
}
// Gets an owner by 0-indexed position (using numOwners as the count)
function getOwner(uint ownerIndex) external view returns (address) {
return address(m_owners[ownerIndex + 1]);
}
function isOwner(address _addr) public view returns (bool) {
return m_ownerIndex[uint(_addr)] > 0;
}
function hasConfirmed(bytes32 _operation, address _owner) external view returns (bool) {
PendingState memory pending = m_pending[_operation];
uint ownerIndex = m_ownerIndex[uint(_owner)];
// make sure they're an owner
if (ownerIndex == 0) return false;
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
return !(pending.ownersDone & ownerIndexBit == 0);
}
// constructor - stores initial daily limit and records the present day's index.
function initDaylimit(uint _limit) public only_uninitialized {
m_dailyLimit = _limit;
m_lastDay = today();
}
// (re)sets the daily limit. needs many of the owners to confirm. doesn't alter the amount already spent today.
function setDailyLimit(uint _newLimit) onlymanyowners(keccak256(msg.data)) external {
m_dailyLimit = _newLimit;
}
// resets the amount already spent today. needs many of the owners to confirm.
function resetSpentToday() onlymanyowners(keccak256(msg.data)) external {
m_spentToday = 0;
}
// throw unless the contract is not yet initialized.
modifier only_uninitialized { require(m_numOwners == 0); _; }
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] memory _owners, uint _required, uint _daylimit) public only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
// kills the contract sending everything to `_to`.
function kill(address payable _to) onlymanyowners(keccak256(msg.data)) external {
selfdestruct(_to);
}
// Outside-visible transact entry point. Executes transaction immediately if below daily spend limit.
// If not, goes into multisig process. We provide a hash on return to allow the sender to provide
// shortcuts for the other confirmations (allowing them to avoid replicating the _to, _value
// and _data arguments). They still get the option of using them if they want, anyways.
function execute(address _to, uint _value, bytes calldata _data) external onlyowner returns (bytes32 o_hash) {
// first, take the opportunity to check that we're under the daily limit.
if ((_data.length == 0 && underLimit(_value)) || m_required == 1) {
// yes - just execute the call.
address created;
if (_to == address(0)) {
created = create(_value, _data);
} else {
(bool success, bytes memory data) = _to.call.value(_value)(_data);
require(success);
}
emit SingleTransact(msg.sender, _value, _to, _data, created);
} else {
// determine our operation hash.
o_hash = keccak256(abi.encode(msg.data, block.number));
// store if it's new
if (m_txs[o_hash].to == address(0) && m_txs[o_hash].value == 0 && m_txs[o_hash].data.length == 0) {
m_txs[o_hash].to = _to;
m_txs[o_hash].value = _value;
m_txs[o_hash].data = _data;
}
if (!confirm(o_hash)) {
emit ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data);
}
}
}
function create(uint _value, bytes memory _code) internal returns (address o_addr) {
uint256 o_size;
assembly {
o_addr := create(_value, add(_code, 0x20), mload(_code))
o_size := extcodesize(o_addr)
}
require(o_size != 0);
}
// confirm a transaction through just the hash. we use the previous transactions map, m_txs, in order
// to determine the body of the transaction from the hash provided.
function confirm(bytes32 _h) public onlymanyowners(_h) returns (bool o_success) {
if (m_txs[_h].to != address(0) || m_txs[_h].value != 0 || m_txs[_h].data.length != 0) {
address created;
if (m_txs[_h].to == address(0)) {
created = create(m_txs[_h].value, m_txs[_h].data);
} else {
(bool success, bytes memory data) = m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data);
require(success);
}
emit MultiTransact(msg.sender, _h, m_txs[_h].value, m_txs[_h].to, m_txs[_h].data, created);
delete m_txs[_h];
return true;
}
}
// INTERNAL METHODS
function confirmAndCheck(bytes32 _operation) internal returns (bool) {
// determine what index the present sender is:
uint ownerIndex = m_ownerIndex[uint(msg.sender)];
// make sure they're an owner
if (ownerIndex == 0) return false;
PendingState memory pending = m_pending[_operation];
// if we're not yet working on this operation, switch over and reset the confirmation status.
if (pending.yetNeeded == 0) {
// reset count of confirmations needed.
pending.yetNeeded = m_required;
// reset which owners have confirmed (none) - set our bitmap to 0.
pending.ownersDone = 0;
pending.index = m_pendingIndex.length++;
m_pendingIndex[pending.index] = _operation;
}
// determine the bit to set for this owner.
uint ownerIndexBit = 2**ownerIndex;
// make sure we (the message sender) haven't confirmed this operation previously.
if (pending.ownersDone & ownerIndexBit == 0) {
emit Confirmation(msg.sender, _operation);
// ok - check if count is enough to go ahead.
if (pending.yetNeeded <= 1) {
// enough confirmations: reset and run interior.
delete m_pendingIndex[m_pending[_operation].index];
delete m_pending[_operation];
return true;
}
else
{
// not enough: record that this owner in particular confirmed.
pending.yetNeeded--;
pending.ownersDone |= ownerIndexBit;
}
}
}
function reorganizeOwners() private {
uint free = 1;
while (free < m_numOwners)
{
while (free < m_numOwners && m_owners[free] != 0) free++;
while (m_numOwners > 1 && m_owners[m_numOwners] == 0) m_numOwners--;
if (free < m_numOwners && m_owners[m_numOwners] != 0 && m_owners[free] == 0)
{
m_owners[free] = m_owners[m_numOwners];
m_ownerIndex[m_owners[free]] = free;
m_owners[m_numOwners] = 0;
}
}
}
// checks to see if there is at least `_value` left from the daily limit today. if there is, subtracts it and
// returns true. otherwise just returns false.
function underLimit(uint _value) internal onlyowner returns (bool) {
// reset the spend limit if we're on a different day to last time.
if (today() > m_lastDay) {
m_spentToday = 0;
m_lastDay = today();
}
// check to see if there's enough left - if so, subtract and return true.
// overflow protection // dailyLimit check
if (m_spentToday + _value >= m_spentToday && m_spentToday + _value <= m_dailyLimit) {
m_spentToday += _value;
return true;
}
return false;
}
// determines today's index.
function today() private view returns (uint) { return now / 1 days; }
function clearPending() internal {
uint length = m_pendingIndex.length;
for (uint i = 0; i < length; ++i) {
delete m_txs[m_pendingIndex[i]];
if (m_pendingIndex[i] != 0)
delete m_pending[m_pendingIndex[i]];
}
delete m_pendingIndex;
}
// FIELDS
address _walletLibrary = 0xCAfEcAfeCAfECaFeCaFecaFecaFECafECafeCaFe;
// the number of owners that must confirm the same operation before it is run.
uint public m_required;
// pointer used to find a free slot in m_owners
uint public m_numOwners;
uint public m_dailyLimit;
uint public m_spentToday;
uint public m_lastDay;
// list of owners
uint[256] m_owners;
uint c_maxOwners = 250;
// index on the list of owners to allow reverse lookup
mapping(uint => uint) m_ownerIndex;
// the ongoing operations.
mapping(bytes32 => PendingState) m_pending;
bytes32[] m_pendingIndex;
// pending transactions we have at present.
mapping (bytes32 => Transaction) m_txs;
}
================================================
FILE: solidity_examples/calls.sol
================================================
contract Caller {
address public fixed_address;
address public stored_address;
uint256 statevar;
constructor(address addr) public {
fixed_address = addr;
}
function thisisfine() public {
fixed_address.call("");
}
function reentrancy() public {
fixed_address.call("");
statevar = 0;
}
function calluseraddress(address addr) public {
addr.call("");
}
function callstoredaddress() public {
stored_address.call("");
}
function setstoredaddress(address addr) public {
stored_address = addr;
}
}
================================================
FILE: solidity_examples/etherstore.sol
================================================
pragma solidity 0.5.0;
contract EtherStore {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
(bool success, bytes memory data) = msg.sender.call.value(_weiToWithdraw)("");
require(success);
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
}
}
================================================
FILE: solidity_examples/exceptions.sol
================================================
contract Exceptions {
uint256[8] myarray;
function assert1() public pure {
uint256 i = 1;
assert(i == 0);
}
function assert2() public pure {
uint256 i = 1;
assert(i > 0);
}
function assert3(uint256 input) public pure {
assert(input != 23);
}
function requireisfine(uint256 input) public pure {
require(input != 23);
}
function divisionby0(uint256 input) public pure {
uint256 i = 1/input;
}
function thisisfine(uint256 input) public pure {
if (input > 0) {
uint256 i = 1/input;
}
}
function arrayaccess(uint256 index) public view {
uint256 i = myarray[index];
}
function thisisalsofind(uint256 index) public view {
if (index < 8) {
uint256 i = myarray[index];
}
}
}
================================================
FILE: solidity_examples/hashforether.sol
================================================
pragma solidity 0.5.0;
contract HashForEther {
function withdrawWinnings() public payable {
// Winner if the last 8 hex characters of the address are 0.
if (uint32(msg.sender) == 0)
_sendWinnings();
}
function _sendWinnings() public {
msg.sender.transfer(address(this).balance);
}
}
================================================
FILE: solidity_examples/killbilly.sol
================================================
pragma solidity 0.5.7;
contract KillBilly {
bool public is_killable;
mapping (address => bool) public approved_killers;
constructor() public {
is_killable = false;
}
function killerize(address addr) public {
approved_killers[addr] = true;
}
function activatekillability() public {
require(approved_killers[msg.sender] == true);
is_killable = true;
}
function commencekilling() public {
require(is_killable);
selfdestruct(msg.sender);
}
}
================================================
FILE: solidity_examples/origin.sol
================================================
pragma solidity 0.5.0;
contract Origin {
address public owner;
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor() public {
owner = msg.sender;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(tx.origin != owner);
_;
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
if (newOwner != address(0)) {
owner = newOwner;
}
}
}
================================================
FILE: solidity_examples/returnvalue.sol
================================================
pragma solidity 0.5.0;
contract ReturnValue {
address public callee = 0xE0f7e56E62b4267062172495D7506087205A4229;
function callnotchecked() public {
callee.call("");
}
function callchecked() public {
(bool success, bytes memory data) = callee.call("");
require(success);
}
}
================================================
FILE: solidity_examples/rubixi.sol
================================================
pragma solidity 0.5.0;
contract Rubixi {
//Declare variables for storage critical to contract
uint private balance = 0;
uint private collectedFees = 0;
uint private feePercent = 10;
uint private pyramidMultiplier = 300;
uint private payoutOrder = 0;
address payable private creator;
modifier onlyowner {
if (msg.sender == creator) _;
}
struct Participant {
address payable etherAddress;
uint payout;
}
//Fallback function
function() external payable {
init();
}
//Sets creator
function dynamicPyramid() public {
creator = msg.sender;
}
Participant[] private participants;
//Fee functions for creator
function collectAllFees() public onlyowner {
require(collectedFees > 0);
creator.transfer(collectedFees);
collectedFees = 0;
}
function collectFeesInEther(uint _amt) public onlyowner {
_amt *= 1 ether;
if (_amt > collectedFees) collectAllFees();
require(collectedFees > 0);
creator.transfer(_amt);
collectedFees -= _amt;
}
function collectPercentOfFees(uint _pcent) public onlyowner {
require(collectedFees > 0 && _pcent <= 100);
uint feesToCollect = collectedFees / 100 * _pcent;
creator.transfer(feesToCollect);
collectedFees -= feesToCollect;
}
//Functions for changing variables related to the contract
function changeOwner(address payable _owner) public onlyowner {
creator = _owner;
}
function changeMultiplier(uint _mult) public onlyowner {
require(_mult <= 300 && _mult >= 120);
pyramidMultiplier = _mult;
}
function changeFeePercentage(uint _fee) public onlyowner {
require(_fee <= 10);
feePercent = _fee;
}
//Functions to provide information to end-user using JSON interface or other interfaces
function currentMultiplier() public view returns (uint multiplier, string memory info) {
multiplier = pyramidMultiplier;
info = "This multiplier applies to you as soon as transaction is received, may be lowered to hasten payouts or increased if payouts are fast enough. Due to no float or decimals, multiplier is x100 for a fractional multiplier e.g. 250 is actually a 2.5x multiplier. Capped at 3x max and 1.2x min.";
}
function currentFeePercentage() public view returns (uint fee, string memory info) {
fee = feePercent;
info = "Shown in % form. Fee is halved(50%) for amounts equal or greater than 50 ethers. (Fee may change, but is capped to a maximum of 10%)";
}
function currentPyramidBalanceApproximately() public view returns (uint pyramidBalance, string memory info) {
pyramidBalance = balance / 1 ether;
info = "All balance values are measured in Ethers, note that due to no decimal placing, these values show up as integers only, within the contract itself you will get the exact decimal value you are supposed to";
}
function nextPayoutWhenPyramidBalanceTotalsApproximately() public view returns (uint balancePayout) {
balancePayout = participants[payoutOrder].payout / 1 ether;
}
function feesSeperateFromBalanceApproximately() public view returns (uint fees) {
fees = collectedFees / 1 ether;
}
function totalParticipants() public view returns (uint count) {
count = participants.length;
}
function numberOfParticipantsWaitingForPayout() public view returns (uint count) {
count = participants.length - payoutOrder;
}
function participantDetails(uint orderInPyramid) public view returns (address addr, uint payout) {
if (orderInPyramid <= participants.length) {
addr = participants[orderInPyramid].etherAddress;
payout = participants[orderInPyramid].payout / 1 ether;
}
}
//init function run on fallback
function init() private {
//Ensures only tx with value of 1 ether or greater are processed and added to pyramid
if (msg.value < 1 ether) {
collectedFees += msg.value;
return;
}
uint _fee = feePercent;
// 50% fee rebate on any ether value of 50 or greater
if (msg.value >= 50 ether) _fee /= 2;
addPayout(_fee);
}
//Function called for valid tx to the contract
function addPayout(uint _fee) private {
//Adds new address to participant array
participants.push(Participant(msg.sender, (msg.value * pyramidMultiplier) / 100));
// These statements ensure a quicker payout system to
// later pyramid entrants, so the pyramid has a longer lifespan
if (participants.length == 10) pyramidMultiplier = 200;
else if (participants.length == 25) pyramidMultiplier = 150;
// collect fees and update contract balance
balance += (msg.value * (100 - _fee)) / 100;
collectedFees += (msg.value * _fee) / 100;
//Pays earlier participants if balance sufficient
while (balance > participants[payoutOrder].payout) {
uint payoutToSend = participants[payoutOrder].payout;
participants[payoutOrder].etherAddress.transfer(payoutToSend);
balance -= participants[payoutOrder].payout;
payoutOrder += 1;
}
}
}
================================================
FILE: solidity_examples/suicide.sol
================================================
contract Suicide {
function kill(address payable addr) public {
if (addr == address(0x0)) {
selfdestruct(addr);
}
}
}
================================================
FILE: solidity_examples/timelock.sol
================================================
pragma solidity 0.5.0;
contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() public payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = now + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
balances[msg.sender] = 0;
msg.sender.transfer(balances[msg.sender]);
}
}
================================================
FILE: solidity_examples/token.sol
================================================
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
================================================
FILE: solidity_examples/weak_random.sol
================================================
pragma solidity 0.5.0;
contract WeakRandom {
struct Contestant {
address payable addr;
uint gameId;
}
uint public prize = 2.5 ether;
uint public totalTickets = 50;
uint public pricePerTicket = prize / totalTickets;
uint public gameId = 1;
uint public nextTicket = 0;
mapping (uint => Contestant) public contestants;
function () payable external {
uint moneySent = msg.value;
while (moneySent >= pricePerTicket && nextTicket < totalTickets) {
uint currTicket = nextTicket++;
contestants[currTicket] = Contestant(msg.sender, gameId);
moneySent -= pricePerTicket;
}
if (nextTicket == totalTickets) {
chooseWinner();
}
// Send back leftover money
if (moneySent > 0) {
msg.sender.transfer(moneySent);
}
}
function chooseWinner() private {
address seed1 = contestants[uint(block.coinbase) % totalTickets].addr;
address seed2 = contestants[uint(msg.sender) % totalTickets].addr;
uint seed3 = block.difficulty;
bytes32 randHash = keccak256(abi.encode(seed1, seed2, seed3));
uint winningNumber = uint(randHash) % totalTickets;
address payable winningAddress = contestants[winningNumber].addr;
gameId++;
nextTicket = 0;
winningAddress.transfer(prize);
}
}
================================================
FILE: static/Ownable.html
================================================
Call Graph