diff --git a/.gitignore b/.gitignore
index fad4b73b..f8d1539d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,5 @@ dist
*.lock
*.svg
laser*
-lol.py
+lol*
+.idea*
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index 7125d68d..6b2dd63b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2015-present Dan Abramov
+Copyright (c) 2017-present Bernhard Mueller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index d61ae7fe..b25ea349 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,15 @@
# Mythril
-
+
-Mythril is a reverse engineering and bug hunting framework for the Ethereum blockchain.
-
- * [Installation and setup](#installation-and-setup)
- * [Command line usage](#command-line-usage)
- + [Input formats](#input-formats)
- - [Working with on-chain contracts](#working-with-on-chain-contracts)
- - [Working with Solidity files](#working-with-solidity-files)
- + [Disassembler](#disassembler)
- + [Control flow graph](#control-flow-graph)
- + [Contract search](#contract-search)
- - [Searching from the command line](#searching-from-the-command-line)
- - [Finding cross-references](#finding-cross-references)
+Mythril is a security analysis tool for Ethereum smart contracts. It uses concolic analysis to detect various types of issues. Use it to analyze source code or as a nmap-style black-box blockchain scanner (an "ethermap" if you will).
## Installation and setup
Install from Pypi:
```bash
-$ pip install mythril
+$ pip3 install mythril
```
Or, clone the GitHub repo to install the newest master branch:
@@ -28,78 +17,99 @@ Or, clone the GitHub repo to install the newest master branch:
```bash
$ git clone https://github.com/b-mueller/mythril/
$ cd mythril
-$ python setup.py install
+$ python3 setup.py install
```
Note that Mythril requires Python 3.5 to work.
-## Command line usage
+### Function signatures
-The Mythril command line tool (aptly named `myth`) allows you to conveniently access most of Mythril's functionality.
+Whenever you disassemble or analyze binary code, Mythril will try to resolve function names using its local signature database. The database must be provided at `~/.mythril/signatures.json`. You can start out with the [default file](signatures.json) as follows:
-### Input formats
+```
+$ mkdir ~/.mythril
+$ cd ~/.mythril
+$ wget https://raw.githubusercontent.com/b-mueller/mythril/master/signatures.json
+```
-Mythril can handle various sources and input formats, including bytecode, addresses of contracts on the blockchain, and Solidity source code files.
+When you analyze Solidity code, new function signatures are added to the database automatically.
-#### Working with on-chain contracts
+## Security analysis
-To pull contracts from the blockchain you need an Ethereum node that is synced with the network. By default, Mythril will query a local node via RPC. Alternatively, you can connect to a remote service such as [INFURA](https://infura.io):
+Run `myth -x` with one of the input options described below to run the analysis. This will run the Python modules in the [/analysis/modules](https://github.com/b-mueller/mythril/tree/master/mythril/analysis/modules) directory.
-```
-$ myth --rpchost=mainnet.infura.io/{API-KEY} --rpcport=443 --rpctls=True (... etc ...)
-```
+Mythril detects a range of [security issues](security_checks.md), including integer underflows, owner-overwrite-to-Ether-withdrawal, and others. However, the analysis will not detect business logic issues and is not equivalent to formal verification.
-The recommended way is to use [go-ethereum](https://github.com/ethereum/go-ethereum). Start your local node as follows:
+### Analyzing a Truffle project
-```bash
-$ geth --rpc --rpcapi eth,debug --syncmode fast
-```
+[Truffle Suite](http://truffleframework.com) is a popular development framework for Ethereum. To analyze the smart contracts in a Truffle project, change in the project root directory and make run `truffle compile` followed by `myth --truffle`.
-#### Working with Solidity files
+### Analyzing Solidity code
In order to work with Solidity source code files, the [solc command line compiler](http://solidity.readthedocs.io/en/develop/using-the-compiler.html) needs to be installed and in path. You can then provide the source file(s) as positional arguments, e.g.:
```bash
-$ myth -g ./graph.html myContract.sol
+$ myth -x myContract.sol
```
-### Disassembler
+Alternatively, compile the code on [Remix](http://remix.ethereum.org) and pass the runtime binary code to Mythril:
-Use the `-d` flag to disassemble code. The disassembler accepts a bytecode string or a contract address as its input.
+```bash
+$ myth -x -c "0x5060(...)"
+```
+
+If you have multiple interdependent contracts, pass them to Mythril as separate input files. Mythril will map the first contract to address "0x0000(..)", the second one to "0x1111(...)", and so forth (make sure that contract addresses are set accordingly in the source). The contract passed as the first argument will be used as analysis entrypoint.
```bash
-$ myth -d -c "0x6060"
-0 PUSH1 0x60
+$ myth -x myContract.sol myLibrary.sol
+```
+
+### Working with contracts on the mainnet and testnets
+
+When analyzing contracts on the blockchain, Mythril will by default query a local node via IPC. If you want to analyze contracts on the live Ethereum network, you can also use the built-in [INFURA](https://infura.io) support. Alternatively, you can override the RPC settings with the `--rpc` argument.
+
+To analyze a contract on the mainnet, run:
+
+
+```
+$ myth --infura-mainnet -x -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd
```
-Specifying an address via `-a ADDRESS` will download the contract code from your node. Mythril will try to resolve function names using the signatures in `database/signature.json`:
+Adding the `-l` flag will cause Mythril to automatically retrieve dependencies, such as dynamically linked library contracts:
```bash
-$ myth -d -a "0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208"
-0 PUSH1 0x60
-2 PUSH1 0x40
-4 MSTORE
-(...)
-1135 - FUNCTION safeAdd(uint256,uint256) -
-1136 CALLVALUE
-1137 ISZERO
+$ myth --infura-mainnet -x -a 0xEbFD99838cb0c132016B9E117563CB41f2B02264 -l -v1
```
-### Control flow graph
+### Speed vs. Coverage
+
+The maximum recursion depth for the symbolic execution engine can be controlled with the `--max-depth` argument. The default value is 12. Lowering this value reduces the analysis time as well as the coverage / number of explored states.
-Mythril integrates the LASER symbolic virtual machine. Right now, this is mainly used for CFG generation. The `-g FILENAME` option generates an [interactive jsViz graph](http://htmlpreview.github.io/?https://github.com/b-mueller/mythril/blob/master/static/mythril.html):
+```
+$ myth --infura-mainnet -x -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd --max-depth 8
+```
+
+## Control flow graph
+
+The `-g FILENAME` option generates an [interactive jsViz graph](http://htmlpreview.github.io/?https://github.com/b-mueller/mythril/blob/master/static/mythril.html):
```bash
-$ myth -g ./graph.html -a "0xFa52274DD61E1643d2205169732f29114BC240b3"
+$ myth --infura-mainnet -g ./graph.html -a 0x5c436ff914c458983414019195e0f4ecbef9e6dd --max-depth 8
```
![callgraph](https://raw.githubusercontent.com/b-mueller/mythril/master/static/callgraph7.png "Call graph")
~~The "bounce" effect, while awesome (and thus enabled by default), sometimes messes up the graph layout.~~ Try adding the `--enable-physics` flag for a very entertaining "bounce" effect that unfortunately completely destroys usability.
-### Contract search
+## Blockchain exploration
-Mythril builds its own contract database to enable fast search operations. This is to enable operations like those described in the [legendary "Mitch Brenner" blog post](https://medium.com/@rtaylor30/how-i-snatched-your-153-037-eth-after-a-bad-tinder-date-d1d84422a50b) in ~~seconds~~ minutes instead of days. Unfortunately, the initial sync process is slow. You don't need to sync the whole blockchain right away though: If you abort the syncing process with `ctrl+c`, it will be auto-resumed the next time you run the `--init-db` command.
+If you are planning to do batch operations or use the contract search features, running a [go-ethereum](https://github.com/ethereum/go-ethereum) node is recommended. Start your local node as follows:
+
+```bash
+$ geth --syncmode fast
+```
+
+Mythril builds its own contract database to enable fast search operations. This enables operations like those described in the [legendary "Mitch Brenner" blog post](https://medium.com/@rtaylor30/how-i-snatched-your-153-037-eth-after-a-bad-tinder-date-d1d84422a50b) in ~~seconds~~ minutes instead of days. Unfortunately, the initial sync process is slow. You don't need to sync the whole blockchain right away though: If you abort the syncing process with `ctrl+c`, it will be auto-resumed the next time you run the `--init-db` command.
```bash
$ myth --init-db
@@ -110,7 +120,7 @@ Processing block 4323000, 3 individual contracts in database
The default behavior is to only sync contracts with a non-zero balance. You can disable this behavior with the `--sync-all` flag, but be aware that this will result in a huge (as in: dozens of GB) database.
-#### Searching from the command line
+### Searching from the command line
The search feature allows you to find contract instances that contain specific function calls and opcode sequences. It supports simple boolean expressions, such as:
@@ -120,7 +130,40 @@ $ myth --search "code#PUSH1 0x50,POP#"
$ myth --search "func#changeMultisig(address)# and code#PUSH1 0x50#"
```
-#### Finding cross-references
+### Reading contract storage
+
+You can read the contents of storage slots from a deployed contract as follows.
+
+```bash
+$ myth --storage 0,1 -a "0x76799f77587738bfeef09452df215b63d2cfb08a"
+0x0000000000000000000000000000000000000000000000000000000000000003
+```
+
+## Utilities
+
+### Disassembler
+
+Use the `-d` flag to disassemble code. The disassembler accepts a bytecode string or a contract address as its input.
+
+```bash
+$ myth -d -c "0x6060"
+0 PUSH1 0x60
+```
+
+Specifying an address via `-a ADDRESS` will download the contract code from your node.
+
+```bash
+$ myth -d -a "0x2a0c0dbecc7e4d658f48e01e3fa353f44050c208"
+0 PUSH1 0x60
+2 PUSH1 0x40
+4 MSTORE
+(...)
+1135 - FUNCTION safeAdd(uint256,uint256) -
+1136 CALLVALUE
+1137 ISZERO
+```
+
+### Finding cross-references
It is often useful to find other contracts referenced by a particular contract. E.g.:
@@ -132,12 +175,19 @@ $ myth --xrefs -a 0x76799f77587738bfeef09452df215b63d2cfb08a
5b9e8728e316bbeb692d22daaab74f6cbf2c4691
```
+### Calculating function hashes
+
+To print the Keccak hash for a given function signature:
+
+```bash
+$ myth --hash "setOwner(address)"
+0x13af4035
+```
+
## Credit
- JSON RPC library is adapted from [ethjsonrpc](https://github.com/ConsenSys/ethjsonrpc) (it doesn't seem to be maintained anymore, and I needed to make some changes to it).
-- The signature data in `signatures.json` has been obtained from the [Ethereum Function Signature Database](https://www.4byte.directory).
-
-## Disclaimer: Act responsibly!
+- The signature data in `signatures.json` was initially obtained from the [Ethereum Function Signature Database](https://www.4byte.directory).
-The purpose of project is to aid discovery of vulnerable smart contracts on the Ethereum mainnet and support research for novel security flaws. If you do find an exploitable issue or vulnerable contract instances, please [do the right thing](https://en.wikipedia.org/wiki/Responsible_disclosure). Also, note that vulnerability branding ("etherbleed", "chainshock",...) is highly discouraged as it will annoy the author and others in the security community.
+- Many features, bugfixes and analysis modules have been added by [contributors](https://github.com/b-mueller/mythril/graphs/contributors).
diff --git a/examples/discover_writes.py b/examples/discover_writes.py
deleted file mode 100644
index 50e7378c..00000000
--- a/examples/discover_writes.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from mythril.ether import util
-from mythril.rpc.client import EthJsonRpc
-from mythril.ether.contractstorage import get_persistent_storage
-from mythril.disassembler.disassembly import Disassembly
-from ethereum.abi import encode_abi
-import re
-import os
-
-
-# Discover contract functions that write the sender address, or an address passed as an argument, to storage.
-# Needs testrpc running on port 8546
-
-# testrpc --port 8546 --gasLimit 0xFFFFFF --account 0x0b6f3fd29ca0e570faf9d0bb8945858b9c337cd2a2ff89d65013eec412a4a811,500000000000000000000 --account 0x2194ac1cd3b9ca6cccc1a90aa2c6f944994b80bb50c82b973adce7f288734d5c,500000000000000000000
-
-
-addr_knupper= "0xe2beffc4bc7ebb9eae43d59d2b555749d9ce7c54"
-addr_schnupper = "0xadc2f8617191ff60a36c3c136170cc69c03e64cd"
-
-contract_storage = get_persistent_storage(os.path.join(os.path.expanduser('~'), ".mythril"))
-testrpc = EthJsonRpc("localhost", 8546)
-
-testargs1 = [
- ([], []),
- (['address'], [addr_schnupper]),
- (['address', 'uint256'], [addr_schnupper, 1 ]),
- (['address', 'uint256', 'uint256'], [addr_schnupper, 1, 1]),
- (['address[]'], [[addr_schnupper]]),
- (['address[]', 'uint256'], [[addr_schnupper], 1 ]),
- (['address[]', 'uint256', 'uint256'], [[addr_schnupper], 1, 1]),
-]
-
-
-def testCase(contract_addr, function_selector, arg_types, args):
-
- if re.match(r'^UNK_0x', function_selector):
- args = encode_abi(['address'], [addr_schnupper])
- data= function_selector[4:] + args.hex()
- else:
- data = util.encode_calldata(function_selector, arg_types, args)
-
- tx = testrpc.eth_sendTransaction(to_address=contract_addr, from_address=addr_schnupper, gas=5000000, value=0, data=data)
-
- trace = testrpc.traceTransaction(tx)
-
- if trace:
- for t in trace['structLogs']:
- if t['op'] == 'SSTORE':
- if addr_schnupper[2:] in t['stack'][-2]:
- return True
-
- return False
-
-
-def testDynamic(contract_hash, contract, addresses, balances):
-
- ret = testrpc.eth_sendTransaction(from_address=addr_knupper, gas=5000000, value=0, data=contract.creation_code)
- receipt = testrpc.eth_getTransactionReceipt(ret)
- contract_addr = receipt['contractAddress']
-
- try:
- disas = Disassembly(contract.code)
- except:
- return
-
- found = False
-
- for function_selector in disas.func_to_addr:
-
- try:
- for t in testargs1:
- if(testCase(contract_addr, function_selector, t[0], t[1])):
- print("Possible write!")
- print("Contract hash: " + contract_hash)
- print("Selector: " + function_selector)
- print("Input data: " + str(t[1]))
-
- for i in range(0, len(addresses)):
- print("Address: " + addresses[i] + ", balance: " + str(balances[i]))
-
- found = True
- break
-
- if found:
- break
- except:
- break
-
-
-print("Searching " +str(len(list(contract_storage.contracts))) + " contracts...")
-
-contract_storage.search("code#PUSH#", testDynamic) # Returns all contracts
diff --git a/examples/find-fallback-dcl.py b/examples/find-fallback-dcl.py
deleted file mode 100644
index fc242eee..00000000
--- a/examples/find-fallback-dcl.py
+++ /dev/null
@@ -1,72 +0,0 @@
-from mythril.ether import evm
-from mythril.ether.contractstorage import get_persistent_storage
-from mythril.disassembler.disassembly import Disassembly
-from mythril.rpc.client import EthJsonRpc
-from mythril.disassembler.callgraph import generate_callgraph
-import os
-
-
-contract_storage = get_persistent_storage()
-contract_keys = list(contract_storage.contracts)
-homestead = EthJsonRpc()
-
-# Iterate over all contracts in the database
-
-for k in contract_keys:
-
- contract = contract_storage.contracts[k]
-
- # Run each contract in the PyEthereum EVM trace to check whether DELEGATECALL is reached
- # To execute the fallback function, we don't provide any input data
-
- ret = evm.trace(contract.code)
-
- if 'DELEGATECALL' in ret:
-
- print("DELEGATECALL in fallback function: Contract 0x" + k.hex() + " deployed at: ")
-
- instance_list = contract_storage.instance_lists[k]
-
- for i in range(1, len(instance_list.addresses)):
- print("Address: " + instance_list.addresses[i] + ", balance: " + str(instance_list.balances[i]))
-
- # contract.get_xrefs() should contain the delegateCall() target (library contract)
-
- print("Referenced contracts:")
-
- xrefs = contract.get_xrefs()
-
- print ("\n".join(xrefs))
-
- '''
- from here on are many different options!
-
- In this example, we'll only check one of the refernced contracts contains the initWallet() function
- If it does, we save the disassembly and callgraph for further analysis
-
- '''
-
- for xref in xrefs:
- code = homestead.eth_getCode(xref)
-
- disassembly = Disassembly(code)
-
- if contract.matches_expression("func#initWallet(address[],uint256,uint256)#"):
- print ("initWallet() in referenced library contract: " + xref)
-
- # Save list of contracts that forward calls to this library contract
-
- cwd = os.getcwd()
-
- with open("contracts_calling_" + xref + ".txt", "w") as f:
- addresses = contract_storage.instance_lists[k].addresses
-
- f.write("\n".join(addresses))
-
- easm = disassembly.get_easm()
-
- with open("library_" + xref + ".easm", "w") as f:
- f.write(easm)
-
- generate_callgraph(disassembly, os.path.join(cwd, "library_" + xref))
-
diff --git a/examples/wallet.sol b/examples/wallet.sol
deleted file mode 100644
index c3bf86e0..00000000
--- a/examples/wallet.sol
+++ /dev/null
@@ -1,85 +0,0 @@
-//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.4.9;
-
-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 Wallet is WalletEvents {
-
- // METHODS
-
- // gets called when no other function matches
- function() payable {
- // just being sent some cash?
- if (msg.value > 0)
- Deposit(msg.sender, msg.value);
- else if (msg.data.length > 0)
- _walletLibrary.delegatecall(msg.data);
- }
-
- // Gets an owner by 0-indexed position (using numOwners as the count)
- function getOwner(uint ownerIndex) constant returns (address) {
- return address(m_owners[ownerIndex + 1]);
- }
-
- // As return statement unavailable in fallback, explicit the method here
-
- function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) {
- return _walletLibrary.delegatecall(msg.data);
- }
-
- function isOwner(address _addr) constant returns (bool) {
- return _walletLibrary.delegatecall(msg.data);
- }
-
- // FIELDS
-
-
-
- address constant _walletLibrary = 0x1111111111111111111111111111111111111111;
-
- // 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;
-}
\ No newline at end of file
diff --git a/examples/walletlibrary.sol b/examples/walletlibrary.sol
deleted file mode 100644
index 88b846af..00000000
--- a/examples/walletlibrary.sol
+++ /dev/null
@@ -1,357 +0,0 @@
-pragma solidity ^0.4.9;
-
-contract WalletLibrary {
- // 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);
-
-
- // 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() payable {
- // just being sent some cash?
- if (msg.value > 0)
- 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[] _owners, uint _required) {
- 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;
- var pending = m_pending[_operation];
- if (pending.ownersDone & ownerIndexBit > 0) {
- pending.yetNeeded++;
- pending.ownersDone -= ownerIndexBit;
- Revoke(msg.sender, _operation);
- }
- }
-
- // Replaces an owner `_from` with another `_to`.
- function changeOwner(address _from, address _to) onlymanyowners(sha3(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;
- OwnerChanged(_from, _to);
- }
-
- function addOwner(address _owner) onlymanyowners(sha3(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;
- OwnerAdded(_owner);
- }
-
- function removeOwner(address _owner) onlymanyowners(sha3(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
- OwnerRemoved(_owner);
- }
-
- function changeRequirement(uint _newRequired) onlymanyowners(sha3(msg.data)) external {
- if (_newRequired > m_numOwners) return;
- m_required = _newRequired;
- clearPending();
- RequirementChanged(_newRequired);
- }
-
- // Gets an owner by 0-indexed position (using numOwners as the count)
- function getOwner(uint ownerIndex) external constant returns (address) {
- return address(m_owners[ownerIndex + 1]);
- }
-
- function isOwner(address _addr) constant returns (bool) {
- return m_ownerIndex[uint(_addr)] > 0;
- }
-
- function hasConfirmed(bytes32 _operation, address _owner) external constant returns (bool) {
- var 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) {
- 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(sha3(msg.data)) external {
- m_dailyLimit = _newLimit;
- }
- // resets the amount already spent today. needs many of the owners to confirm.
- function resetSpentToday() onlymanyowners(sha3(msg.data)) external {
- m_spentToday = 0;
- }
-
- // constructor - just pass on the owner array to the multiowned and
- // the limit to daylimit
- function initWallet(address[] _owners, uint _required, uint _daylimit) {
- initDaylimit(_daylimit);
- initMultiowned(_owners, _required);
- }
-
- // kills the contract sending everything to `_to`.
- function kill(address _to) onlymanyowners(sha3(msg.data)) external {
- suicide(_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 _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 == 0) {
- created = create(_value, _data);
- } else {
- if (!_to.call.value(_value)(_data))
- throw;
- }
- SingleTransact(msg.sender, _value, _to, _data, created);
- } else {
- // determine our operation hash.
- o_hash = sha3(msg.data, block.number);
- // store if it's new
- if (m_txs[o_hash].to == 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)) {
- ConfirmationNeeded(o_hash, msg.sender, _value, _to, _data);
- }
- }
- }
-
- function create(uint _value, bytes _code) internal returns (address o_addr) {
- assembly {
- o_addr := create(_value, add(_code, 0x20), mload(_code))
- // jumpi(invalidJumpLabel, iszero(extcodesize(o_addr)))
- }
- }
-
- // 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) onlymanyowners(_h) returns (bool o_success) {
- if (m_txs[_h].to != 0 || m_txs[_h].value != 0 || m_txs[_h].data.length != 0) {
- address created;
- if (m_txs[_h].to == 0) {
- created = create(m_txs[_h].value, m_txs[_h].data);
- } else {
- if (!m_txs[_h].to.call.value(m_txs[_h].value)(m_txs[_h].data))
- throw;
- }
-
- 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;
-
- var 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) {
- 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 constant 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 constant _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 constant 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;
-}
\ No newline at end of file
diff --git a/myth b/myth
index 3c5b9017..bd1fbbf8 100755
--- a/myth
+++ b/myth
@@ -5,18 +5,25 @@
"""
from mythril.ether import evm, util
-from mythril.disassembler.callgraph import generate_callgraph
from mythril.ether.contractstorage import get_persistent_storage
from mythril.ether.ethcontract import ETHContract
from mythril.ether.util import compile_solidity
from mythril.rpc.client import EthJsonRpc
from mythril.ipc.client import EthIpc
+from mythril.rpc.exceptions import ConnectionError
+from mythril.support import signatures
+from mythril.support.truffle import analyze_truffle_project
from mythril.support.loader import DynLoader
from mythril.exceptions import CompilerError
+from mythril.analysis.symbolic import StateSpace
+from mythril.analysis.callgraph import generate_graph
+from mythril.analysis.security import fire_lasers
+from web3 import Web3
from ethereum import utils
-from laser.ethereum import svm, laserfree
from pathlib import Path
+from json.decoder import JSONDecodeError
import logging
+import json
import sys
import argparse
import os
@@ -35,56 +42,91 @@ def exitWithError(message):
sys.exit()
-parser = argparse.ArgumentParser(description='Bug hunting on the Ethereum blockchain')
+parser = argparse.ArgumentParser(description='Security analysis of Ethereum smart contracts')
parser.add_argument("solidity_file", nargs='*')
-
commands = parser.add_argument_group('commands')
-commands.add_argument('-d', '--disassemble', action='store_true', help='disassemble')
commands.add_argument('-g', '--graph', help='generate a control flow graph', metavar='OUTPUT_FILE')
-commands.add_argument('-x', '--fire-lasers', action='store_true', help='detect vulnerabilities')
-commands.add_argument('-t', '--trace', action='store_true', help='trace contract, use with --data (optional)')
-commands.add_argument('-s', '--search', help='search the contract database', metavar='EXPRESSION')
-commands.add_argument('--xrefs', action='store_true', help='get xrefs from a contract')
-commands.add_argument('--hash', help='calculate function signature hash', metavar='SIGNATURE')
-commands.add_argument('--init-db', action='store_true', help='initialize the contract database')
+commands.add_argument('-x', '--fire-lasers', action='store_true', help='detect vulnerabilities, use with -c, -a or solidity file(s)')
+commands.add_argument('-t', '--truffle', action='store_true', help='analyze a truffle project (run from project dir)')
inputs = parser.add_argument_group('input arguments')
inputs.add_argument('-c', '--code', help='hex-encoded bytecode string ("6060604052...")', metavar='BYTECODE')
inputs.add_argument('-a', '--address', help='pull contract from the blockchain', metavar='CONTRACT_ADDRESS')
-inputs.add_argument('-l', '--dynld', action='store_true', help='auto-load dependencies (experimental)')
-inputs.add_argument('--data', help='message call input data for tracing')
+inputs.add_argument('-l', '--dynld', action='store_true', help='auto-load dependencies from the blockchain')
+
+database = parser.add_argument_group('local contracts database')
+database.add_argument('--init-db', action='store_true', help='initialize the contract database')
+database.add_argument('-s', '--search', help='search the contract database', metavar='EXPRESSION')
+
+utils = parser.add_argument_group('utilities')
+utils.add_argument('-d', '--disassemble', action='store_true', help='print disassembly')
+utils.add_argument('--xrefs', action='store_true', help='get xrefs from a contract')
+utils.add_argument('--hash', help='calculate function signature hash', metavar='SIGNATURE')
+utils.add_argument('--storage', help='read state variables from storage index, use with -a', metavar='INDEX,NUM_SLOTS,[array]')
options = parser.add_argument_group('options')
options.add_argument('--sync-all', action='store_true', help='Also sync contracts with zero balance')
-options.add_argument('--infura-mainnet', action='store_true', help='Use Infura Node service, equivalent to: --rpchost=mainnet.infura.io --rpcport=443 --rpctls="True"')
-options.add_argument('--infura-rinkeby', action='store_true', help='Use Infura Node service, equivalent to: --rpchost=rinkeby.infura.io --rpcport=443 --rpctls="True"')
-options.add_argument('--infura-kovan', action='store_true', help='Use Infura Node service, equivalent to: --rpchost=kovan.infura.io --rpcport=443 --rpctls="True"')
-options.add_argument('--infura-ropsten', action='store_true', help='Use Infura Node service, equivalent to: --rpchost=ropsten.infura.io --rpcport=443 --rpctls="True"')
-options.add_argument('--rpchost', default='127.0.0.1', help='RPC host')
-options.add_argument('--rpcport', type=int, default=8545, help='RPC port')
-options.add_argument('--rpctls', type=bool, default=False, help='RPC port')
-options.add_argument('--ipc', help='use IPC interface instead of RPC', action='store_true')
+options.add_argument('--max-depth', type=int, default=12, help='Maximum recursion depth for symbolic execution')
options.add_argument('--enable-physics', type=bool, default=False, help='enable graph physics simulation')
options.add_argument('-v', type=int, help='log level (0-2)', metavar='LOG_LEVEL')
+rpc = parser.add_argument_group('RPC options')
+rpc.add_argument('--rpc', help='connect via RPC', metavar='HOST:PORT')
+rpc.add_argument('--rpctls', type=bool, default=False, help='RPC connection over TLS')
+rpc.add_argument('--ganache', action='store_true', help='Preset: local Ganache')
+rpc.add_argument('--infura-mainnet', action='store_true', help='Preset: Infura Node service (Mainnet)')
+rpc.add_argument('--infura-rinkeby', action='store_true', help='Preset: Infura Node service (Rinkeby)')
+rpc.add_argument('--infura-kovan', action='store_true', help='Preset: Infura Node service (Kovan)')
+rpc.add_argument('--infura-ropsten', action='store_true', help='Preset: Infura Node service (Ropsten)')
+
# Get config values
try:
- db_dir = os.environ['DB_DIR']
+ mythril_dir = os.environ['MYTHRIL_DIR']
except KeyError:
- db_dir = None
+ mythril_dir = os.path.join(os.path.expanduser('~'), ".mythril")
try:
solc_binary = os.environ['SOLC']
except KeyError:
solc_binary = 'solc'
+
+# Initialize data directry and singature database
+
+if not os.path.exists(mythril_dir):
+ logging.info("Creating mythril data directory")
+
+ os.mkdir(mythril_dir)
+
+
+# If no function signature file exists, create it. Function signatures from Solidity source code are added automatically.
+
+signatures_file = os.path.join(mythril_dir, 'signatures.json')
+
+if not os.path.exists(signatures_file):
+ print("No signature database found. Creating empty database: " + signatures_file + "\n" \
+ "Consider replacing it with the pre-initialized database at " \
+ "https://raw.githubusercontent.com/ConsenSys/mythril/master/signatures.json")
+
+ sigs = {}
+
+ with open(signatures_file, 'a') as f:
+ json.dump({},f)
+
+else:
+ with open(signatures_file) as f:
+ try:
+ sigs = json.load(f)
+ except JSONDecodeError as e:
+ exitWithError("Invalid JSON in signatures file " + signatures_file + "\n" + str(e))
+
# Parse cmdline args
args = parser.parse_args()
-if not (args.search or args.init_db or args.hash or args.disassemble or args.graph or args.xrefs or args.fire_lasers or args.trace):
+if not (args.search or args.init_db or args.hash or args.disassemble or args.graph or args.xrefs or args.fire_lasers or args.storage or args.truffle):
parser.print_help()
sys.exit()
@@ -96,11 +138,50 @@ elif (args.hash):
print("0x" + utils.sha3(args.hash)[:4].hex())
sys.exit()
+
+if args.truffle:
+
+ try:
+ analyze_truffle_project()
+ except FileNotFoundError:
+ print("Build directory not found. Make sure that you start the analysis from the project root, and that 'truffle compile' has executed successfully.")
+
+ sys.exit()
+
+
+# Establish RPC/IPC connection if necessary
+
+if (args.address or len(args.solidity_file) or args.init_db):
+
+ if args.infura_mainnet:
+ eth = EthJsonRpc('mainnet.infura.io', 443, True)
+ elif args.infura_rinkeby:
+ eth = EthJsonRpc('rinkeby.infura.io', 443, True)
+ elif args.infura_kovan:
+ eth = EthJsonRpc('kovan.infura.io', 443, True)
+ elif args.infura_ropsten:
+ eth = EthJsonRpc('ropsten.infura.io', 443, True)
+ elif args.ganache:
+ eth = EthJsonRpc('localhost', 7545, False)
+ elif args.rpc:
+
+ try:
+ host, port = args.rpc.split(":")
+ except ValueError:
+ exitWithError("Invalid RPC argument, use HOST:PORT")
+
+ tls = args.rpctls
+
+ eth = EthJsonRpc(host, int(port), tls)
+ else:
+ eth = EthIpc()
+
+
# Database search ops
if args.search or args.init_db:
- contract_storage = get_persistent_storage(db_dir)
+ contract_storage = get_persistent_storage(mythril_dir)
if (args.search):
@@ -110,34 +191,16 @@ if args.search or args.init_db:
exitWithError("Syntax error in search expression.")
elif (args.init_db):
- contract_storage.initialize(args.rpchost, args.rpcport, args.rpctls, args.sync_all, args.ipc)
-
- sys.exit()
+ try:
+ contract_storage.initialize(eth, args.sync_all)
+ except FileNotFoundError as e:
+ print("Error syncing database over IPC: " + str(e))
+ except ConnectionError as e:
+ print("Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.")
-# Establish RPC/IPC connection if necessary
-if (args.address or len(args.solidity_file)):
- if args.ipc:
- try:
- eth = EthIpc()
+ sys.exit()
- except Exception as e:
- exitWithError("Error establishing IPC connection: " + str(e))
- else:
- try:
- if args.infura_mainnet:
- eth = EthJsonRpc('mainnet.infura.io', 443, True)
- elif args.infura_rinkeby:
- eth = EthJsonRpc('rinkeby.infura.io', 443, True)
- elif args.infura_kovan:
- eth = EthJsonRpc('kovan.infura.io', 443, True)
- elif args.infura_ropsten:
- eth = EthJsonRpc('ropsten.infura.io', 443, True)
- else:
- eth = EthJsonRpc(args.rpchost, args.rpcport, args.rpctls)
-
- except Exception as e:
- exitWithError("Error establishing RPC connection: " + str(e))
# Load / compile input contracts
@@ -146,7 +209,22 @@ contracts = []
if (args.code):
contracts.append(ETHContract(args.code, name="MAIN", address = util.get_indexed_address(0)))
elif (args.address):
- contracts.append(ETHContract(eth.eth_getCode(args.address), name=args.address, address = args.address))
+
+ if not re.match(r'0x[a-fA-F0-9]{40}', args.address):
+ exitWithError("Invalid contract address. Expected format is '0x...'.")
+
+ try:
+ code = eth.eth_getCode(args.address)
+
+ if (code == "0x"):
+ exitWithError("Received an empty response from eth_getCode. Check the contract address and verify that you are on the correct chain.")
+
+ except FileNotFoundError as e:
+ exitWithError("IPC error: " + str(e))
+ except ConnectionError as e:
+ exitWithError("Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.")
+
+ contracts.append(ETHContract(code, name=args.address, address = args.address))
elif (len(args.solidity_file)):
index = 0
@@ -154,12 +232,16 @@ elif (len(args.solidity_file)):
file = file.replace("~", str(Path.home())) # Expand user path
+ signatures.add_signatures_from_file(file, sigs)
+
+ logging.debug("Adding function signatures from source code:\n" + str(sigs))
+
try:
- name, bytecode = compile_solidity(solc_binary, file)
+ name, bytecode = compile_solidity(file, solc_binary)
except CompilerError as e:
exitWithError(e)
- # Max. 16 contracts supported!
+ # Max. 16 input files supported!
contract = ETHContract(bytecode, name = name, address = util.get_indexed_address(index))
index += 1
@@ -167,29 +249,59 @@ elif (len(args.solidity_file)):
contracts.append(contract)
logging.info(contract.name + " at " + contract.address)
+ # Save updated signature
+
+ with open(signatures_file, 'w') as f:
+ json.dump(sigs, f)
+
else:
exitWithError("No input bytecode. Please provide EVM code via -c BYTECODE, -a ADDRESS, or -i SOLIDITY_FILES")
# Commands
-if (args.disassemble):
+if args.storage:
+ if not args.address:
+ exitWithError("To read storage, provide the address of a deployed contract with the -a option.")
+ else:
+ position = 0
+ length = 1
+ array = 0
- easm_text = contracts[0].get_easm()
- sys.stdout.write(easm_text)
+ try:
+ params = (args.storage).split(",")
+ if len(params) >= 1 and len(params) <= 3:
+ position = int(params[0])
+ if len(params) >= 2 and len(params) <= 3:
+ length = int(params[1])
+ if len(params) == 3:
+ if re.match("array",params[2]):
+ array = 1
+ if len(params) >= 4:
+ exitWithError("Invalid number of parameters.")
+ except ValueError:
+ exitWithError("Invalid storage index. Please provide a numeric value.")
+
+ if array:
+ position_formated = str(position).zfill(64)
+ position = int(Web3.sha3(position_formated),16)
-elif (args.trace):
- if (args.data):
- trace = evm.trace(contracts[0].code, args.data)
+ try:
+ if length == 1:
+ print("{}: ".format(position) + eth.eth_getStorageAt(args.address, position));
+ else:
+ for i in range(position, position + length):
+ print("{}: ".format(hex(i)) + eth.eth_getStorageAt(args.address, i));
+ except FileNotFoundError as e:
+ exitWithError("IPC error: " + str(e))
+ except ConnectionError as e:
+ exitWithError("Could not connect to RPC server. Make sure that your node is running and that RPC parameters are set correctly.")
- else:
- trace = evm.trace(contracts[0].code)
- for i in trace:
- if (re.match(r'^PUSH.*', i['op'])):
- print(str(i['pc']) + " " + i['op'] + " " + i['pushvalue'] + ";\tSTACK: " + i['stack'])
- else:
- print(str(i['pc']) + " " + i['op'] + ";\tSTACK: " + i['stack'])
+elif (args.disassemble):
+
+ easm_text = contracts[0].get_easm()
+ sys.stdout.write(easm_text)
elif (args.xrefs):
@@ -197,38 +309,39 @@ elif (args.xrefs):
elif (args.graph) or (args.fire_lasers):
- # Convert to LASER SVM format
-
- modules = {}
-
- for contract in contracts:
- modules[contract.address] = contract.as_dict()
-
- if (args.dynld):
- loader = DynLoader(eth)
- _svm = svm.SVM(modules, dynamic_loader=loader)
- else:
- _svm = svm.SVM(modules)
-
if (args.graph):
- _svm.simplify_model = True
+ if (args.dynld):
+ states = StateSpace(contracts, dynloader=DynLoader(eth), max_depth=args.max_depth)
+ else:
+ states = StateSpace(contracts, max_depth=args.max_depth)
if args.enable_physics is not None:
- physics = True
+ physics = True
- html = generate_callgraph(_svm, contracts[0].address, args.enable_physics)
+ html = generate_graph(states, args.enable_physics)
try:
with open(args.graph, "w") as f:
f.write(html)
except Exception as e:
-
print("Error saving graph: " + str(e))
else:
- laserfree.fire(modules, contracts[0].address)
+ if (args.dynld):
+ states = StateSpace(contracts, dynloader=DynLoader(eth), max_depth=args.max_depth)
+ else:
+ states = StateSpace(contracts, max_depth=args.max_depth)
+
+ report = fire_lasers(states)
+
+ if (len(report.issues)):
+ print(report.as_text())
+
+ else:
+
+ print("The analysis was completed successfully. No issues were detected.")
else:
parser.print_help()
diff --git a/mythril/analysis/__init__.py b/mythril/analysis/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mythril/disassembler/callgraph.py b/mythril/analysis/callgraph.py
similarity index 65%
rename from mythril/disassembler/callgraph.py
rename to mythril/analysis/callgraph.py
index d166223a..19404eba 100644
--- a/mythril/disassembler/callgraph.py
+++ b/mythril/analysis/callgraph.py
@@ -78,8 +78,30 @@ graph_html = '''