Skip to content

Commit

Permalink
Add support to foundry (#1744)
Browse files Browse the repository at this point in the history
* Add foundry command support

* Add foundry support
  • Loading branch information
norhh authored Feb 20, 2023
1 parent 34aa49c commit f7e5deb
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 33 deletions.
71 changes: 49 additions & 22 deletions docs/source/tutorial.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
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
******************************************

We consider a contract simple if it does not have any imports, like the following contract:
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
Expand Down Expand Up @@ -51,13 +61,14 @@ We consider a contract simple if it does not have any imports, like the followin
}
We can execute such a contract by directly using the following command:
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 <file_path>
This execution can give the following output:
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
Expand Down Expand Up @@ -112,10 +123,8 @@ This execution can give the following output:
Caller: [SOMEGUY], function: assert3(uint256), txdata: 0x546455b50000000000000000000000000000000000000000000000000000000000000017, value: 0x0
We can observe that the function ``assert5(uint256)`` should have an assertion failure
with the assertion ``assert(input_x > 10)`` which is missing from our output. This can be attributed to
Mythril's default configuration of running three transactions. We can increase the transaction count to 4
using the ``-t <tx_count>``.
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
Expand Down Expand Up @@ -204,6 +213,8 @@ This gives the following execution output:
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``
Expand Down Expand Up @@ -407,7 +418,10 @@ which can be increased for better results. The default execution-timeout and sol
Executing Mythril on Contracts with Imports
********************************************************

Consider the following contract:
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
Expand Down Expand Up @@ -483,42 +497,54 @@ We encounter the following error:
1 | import "@openzeppelin/contracts/token/PRC20/PRC20.sol";
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is because Mythril uses Solidity to compile the program. Solidity does not have access to the import locations.
This import information has to be explicitly provided to Solidity through Mythril.
We can do this by providing the remapping information to Mythril as follows:
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/"]
}
Here we are mapping the import ``@openzeppelin/contracts/token/PRC20/`` to the path which contains ``PRC20.sol``, which this example
assumes as ``node_modules/PRC20``. This instructs the compiler to search for anything with the prefix ``@openzeppelin/contracts/token/PRC20/` `
in the path ``node_modules/PRC20`` in our file system. We feed this file to Mythril using ``--solc-json`` argument, which
relays it to the solc compiler.
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}
This can effectively execute the file since the Solidity compiler can locate `PRC20.sol`. For more information on remappings, you can
refer to `Solc docs <https://docs.soliditylang.org/en/v0.8.14/using-the-compiler.html#base-path-and-import-remapping>`_.
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 <https://docs.soliditylang.org/en/v0.8.14/using-the-compiler.html#base-path-and-import-remapping>`_.

********************************************************
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:


You can use Mythril to search certain transaction sequences. You can use ``--transaction-sequences`` argument to direct the search.
An example usage is ``[[func_hash1,func_hash2], [func_hash2,func_hash3],[]]`` where the first transaction is constrained with ``func_hash1`` and ``func_hash2``, the second tx is constrained with ``func_hash2`` and ``func_hash3`` and the final transaction is unconstrained on any function.
Use ``-1`` as a proxy for the hash of ``fallback()`` function and ``-2`` as a proxy for the hash of ``receive()`` function.

Consider the following contract

.. code-block:: solidity
pragma solidity 0.5.0;
pragma solidity ^0.5.0;
contract Rubixi {
Expand Down Expand Up @@ -671,6 +697,7 @@ Consider the following contract
}
}
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.

Expand Down
30 changes: 27 additions & 3 deletions mythril/interfaces/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

ANALYZE_LIST = ("analyze", "a")
DISASSEMBLE_LIST = ("disassemble", "d")
FOUNDRY_LIST = ("foundry", "f")

CONCOLIC_LIST = ("concolic", "c")
SAFE_FUNCTIONS_COMMAND = "safe-functions"
Expand All @@ -53,6 +54,7 @@
COMMAND_LIST = (
ANALYZE_LIST
+ DISASSEMBLE_LIST
+ FOUNDRY_LIST
+ CONCOLIC_LIST
+ (
READ_STORAGE_COMNAND,
Expand Down Expand Up @@ -308,7 +310,19 @@ def main() -> None:
)
create_concolic_parser(concolic_parser)

subparsers.add_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,
)

list_detectors_parser = subparsers.add_parser(
LIST_DETECTORS_COMMAND,
parents=[output_parser],
help="Lists available detection modules",
Expand All @@ -328,10 +342,11 @@ def main() -> None:
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
Expand Down Expand Up @@ -586,6 +601,12 @@ def create_analyzer_parser(analyzer_parser: ArgumentParser):
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
Expand Down Expand Up @@ -698,6 +719,9 @@ def load_code(disassembler: MythrilDisassembler, args: Namespace):
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(
args.__dict__.get("outform", "text"),
Expand Down Expand Up @@ -782,7 +806,7 @@ def execute_command(
except CriticalError as e:
exit_with_error("text", "Analysis error encountered: " + format(e))

elif args.command in ANALYZE_LIST:
elif args.command in ANALYZE_LIST + FOUNDRY_LIST:
analyzer = MythrilAnalyzer(
strategy=strategy, disassembler=disassembler, address=address, cmd_args=args
)
Expand Down
1 change: 1 addition & 0 deletions mythril/laser/ethereum/call.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def get_call_parameters(
"""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
Expand Down
88 changes: 84 additions & 4 deletions mythril/mythril/mythril_disassembler.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import json
import logging
import os
from pathlib import Path
import re
import shutil
import solc
import subprocess
import sys
import os
import warnings

from eth_utils import int_to_big_endian
from semantic_version import Version, NpmSpec
from typing import List, Tuple, Optional
from typing import List, Tuple, Optional, TYPE_CHECKING

from mythril.support.support_utils import sha3, zpad
from mythril.ethereum import util
Expand All @@ -16,10 +22,20 @@
from mythril.support.support_args import args
from mythril.ethereum.evmcontract import EVMContract
from mythril.ethereum.interface.rpc.exceptions import ConnectionError
from mythril.solidity.soliditycontract import SolidityContract, get_contracts_from_file
from mythril.solidity.soliditycontract import (
SolidityContract,
get_contracts_from_file,
get_contracts_from_foundry,
)
from mythril.support.support_args import args

from eth_utils import int_to_big_endian

def format_Warning(message, category, filename, lineno, line=""):
return "{}: {}\n\n".format(str(filename), str(message))


warnings.formatwarning = format_Warning


log = logging.getLogger(__name__)

Expand Down Expand Up @@ -152,6 +168,70 @@ def load_from_address(self, address: str) -> Tuple[str, EVMContract]:
)
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")

files = os.listdir(build_dir)
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)

uniq_id = file if ".json" not in file else file[0:-5]

with open(build_info, encoding="utf8") as file_desc:
loaded_json = json.load(file_desc)

targets_json = loaded_json["output"]

version_from_config = loaded_json["solcVersion"]
input_json = loaded_json["input"]
compiler = "solc" if input_json["language"] == "Solidity" else "vyper"
optimizer = input_json["settings"]["optimizer"]["enabled"]

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 load_from_solidity(
self, solidity_files: List[str]
) -> Tuple[str, List[SolidityContract]]:
Expand Down
Loading

0 comments on commit f7e5deb

Please sign in to comment.