diff --git a/.circleci/config.yml b/.circleci/config.yml index 42dd2538..46a67785 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,9 @@ defaults: &defaults docker: + # TODO: Try to replace this with `mythril/mythx-ci` image. That image is + # used for new integration testing, and it is build on top of the previous + # `mythril/dev_test_environment:0.0.43`, though a bit newer versions, thus + # there is a chance that it breaks some small things. - image: mythril/dev_test_environment:0.0.43 version: 2 @@ -79,6 +83,39 @@ jobs: command: | curl -I -X POST -H -d "https://circleci.com/api/v1/project/${ORGANIZATION}/${WEBHOOK_PROJECT}/tree/master?circle-token=${CIRCLE_TOKEN}" | head -n 1 | cut -d$' ' -f2 + integration_tests: + docker: + - image: mythril/mythx-ci + working_directory: /home + steps: + - checkout: + path: /home/mythril-classic + - run: + name: Builds `mythril-classic` + command: cd mythril-classic && python3 setup.py install + - run: + name: Installs other MythX components + command: | + ./install-mythx-components.sh pythx edelweiss harvey-cli \ + harvey-tyro maestro maru maru-tyro mythril-api \ + mythril-tyro tyro + - run: + background: true + name: Launches MythX platform + command: ./launch-mythx.sh + - run: + name: Waits for MythX to spin-up + command: sleep 15s + - run: + name: Quick Edelweiss test + command: /home/run-edelweiss-test.sh CircleCI/latest.quick.csv 5 + + # TODO: Temporary disabled + # - run: + # name: Full Edelweiss test + # environment: + # MYTHX_API_FULL_MODE: true + # command: /home/run-edelweiss-test.sh CircleCI/latest.full.csv pypi_release: <<: *defaults @@ -129,6 +166,16 @@ workflows: filters: tags: only: /.*/ + - integration_tests: + filters: + branches: + only: + - develop + - master + tags: + only: /v[0-9]+(\.[0-9]+)*/ + requires: + - test - pypi_release: filters: branches: @@ -136,13 +183,13 @@ workflows: tags: only: /v[0-9]+(\.[0-9]+)*/ requires: - - test + - integration_tests - dockerhub_dev_release: filters: branches: only: develop - # requires: - # - test + requires: + - integration_tests - dockerhub_release: filters: branches: @@ -150,4 +197,4 @@ workflows: tags: only: /v[0-9]+(\.[0-9]+)*/ requires: - - test + - integration_tests diff --git a/mythril/analysis/modules/integer.py b/mythril/analysis/modules/integer.py index 0c6582a0..43f57c96 100644 --- a/mythril/analysis/modules/integer.py +++ b/mythril/analysis/modules/integer.py @@ -31,6 +31,8 @@ import logging log = logging.getLogger(__name__) +DISABLE_EFFECT_CHECK = True + class OverUnderflowAnnotation: """ Symbol Annotation used if a BitVector can overflow""" @@ -287,9 +289,15 @@ class IntegerOverflowUnderflowModule(DetectionModule): continue try: + # This check can be disabled if the contraints are to difficult for z3 to solve + # within any reasonable time. + if DISABLE_EFFECT_CHECK: + constraints = ostate.mstate.constraints + [annotation.constraint] + else: + constraints = state.mstate.constraints + [annotation.constraint] transaction_sequence = solver.get_transaction_sequence( - state, state.mstate.constraints + [annotation.constraint] + state, constraints ) except UnsatError: continue diff --git a/mythril/ethereum/util.py b/mythril/ethereum/util.py index ac80863b..2b6c7771 100644 --- a/mythril/ethereum/util.py +++ b/mythril/ethereum/util.py @@ -33,7 +33,7 @@ def get_solc_json(file, solc_binary="solc", solc_args=None): :return: """ - cmd = [solc_binary, "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime"] + cmd = [solc_binary, "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,ast"] if solc_args: cmd.extend(solc_args.split()) diff --git a/mythril/laser/ethereum/transaction/transaction_models.py b/mythril/laser/ethereum/transaction/transaction_models.py index 9ad5b76e..3c5aabf4 100644 --- a/mythril/laser/ethereum/transaction/transaction_models.py +++ b/mythril/laser/ethereum/transaction/transaction_models.py @@ -180,11 +180,12 @@ class ContractCreationTransaction(BaseTransaction): :param revert: """ if ( - not all([isinstance(element, int) for element in return_data]) + return_data is None + or not all([isinstance(element, int) for element in return_data]) or len(return_data) == 0 ): self.return_data = None - raise TransactionEndSignal(global_state) + raise TransactionEndSignal(global_state, revert=revert) contract_code = bytes.hex(array.array("B", return_data).tostring()) diff --git a/mythril/mythril.py b/mythril/mythril.py deleted file mode 100644 index 86aa6c2f..00000000 --- a/mythril/mythril.py +++ /dev/null @@ -1,719 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""mythril.py: Bug hunting on the Ethereum blockchain - - http://www.github.com/b-mueller/mythril -""" - -import codecs -import logging -import os -import platform -import re -import traceback -from pathlib import Path -from shutil import copyfile -from configparser import ConfigParser - -import solc -from ethereum import utils -from solc.exceptions import SolcError - -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.solidity.soliditycontract import SolidityContract, get_contracts_from_file -from mythril.support import signatures -from mythril.support.source_support import Source -from mythril.support.loader import DynLoader -from mythril.exceptions import CompilerError, NoContractFoundError, CriticalError -from mythril.analysis.symbolic import SymExecWrapper -from mythril.analysis.callgraph import generate_graph -from mythril.analysis.traceexplore import get_serializable_statespace -from mythril.analysis.security import fire_lasers, retrieve_callback_issues -from mythril.analysis.report import Report -from mythril.support.truffle import analyze_truffle_project -from mythril.ethereum.interface.leveldb.client import EthLevelDB -from mythril.laser.smt import SolverStatistics -from mythril.support.start_time import StartTime - - -log = logging.getLogger(__name__) - - -class Mythril(object): - """Mythril main interface class. - - 1. create mythril object - 2. set rpc or leveldb interface if needed - 3. load contracts (from solidity, bytecode, address) - 4. fire_lasers - - .. code-block:: python - - mythril = Mythril() - mythril.set_api_rpc_infura() - - # (optional) other API adapters - mythril.set_api_rpc(args) - mythril.set_api_rpc_localhost() - mythril.set_api_leveldb(path) - - # (optional) other func - mythril.analyze_truffle_project(args) - mythril.search_db(args) - - # load contract - mythril.load_from_bytecode(bytecode) - mythril.load_from_address(address) - mythril.load_from_solidity(solidity_file) - - # analyze - print(mythril.fire_lasers(args).as_text()) - - # (optional) graph - for contract in mythril.contracts: - # prints html or save it to file - print(mythril.graph_html(args)) - - # (optional) other funcs - mythril.dump_statespaces(args) - mythril.disassemble(contract) - mythril.get_state_variable_from_storage(args) - """ - - def __init__( - self, - solv=None, - solc_args=None, - dynld=False, - enable_online_lookup=False, - onchain_storage_access=True, - ): - - self.solv = solv - self.solc_args = solc_args - self.dynld = dynld - self.onchain_storage_access = onchain_storage_access - self.enable_online_lookup = enable_online_lookup - - self.mythril_dir = self._init_mythril_dir() - - # tries mythril_dir/signatures.db by default (provide path= arg to make this configurable) - self.sigs = signatures.SignatureDB( - enable_online_lookup=self.enable_online_lookup - ) - - self.solc_binary = self._init_solc_binary(solv) - self.config_path = os.path.join(self.mythril_dir, "config.ini") - self.leveldb_dir = self._init_config() - - self.eth = None # ethereum API client - self.eth_db = None # ethereum LevelDB client - - self.contracts = [] # loaded contracts - - @staticmethod - def _init_mythril_dir(): - 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 / "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. - - Default LevelDB path is specified based on OS - dynamic loading is set to infura by default in the file - Returns: leveldb directory - """ - - system = platform.system().lower() - leveldb_fallback_dir = os.path.expanduser("~") - if system.startswith("darwin"): - leveldb_fallback_dir = os.path.join( - leveldb_fallback_dir, "Library", "Ethereum" - ) - elif system.startswith("windows"): - leveldb_fallback_dir = os.path.join( - leveldb_fallback_dir, "AppData", "Roaming", "Ethereum" - ) - else: - leveldb_fallback_dir = os.path.join(leveldb_fallback_dir, ".ethereum") - leveldb_fallback_dir = os.path.join(leveldb_fallback_dir, "geth", "chaindata") - - 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 - config.read(self.config_path, "utf-8") - if "defaults" not in config.sections(): - self._add_default_options(config) - - if not config.has_option("defaults", "leveldb_dir"): - self._add_leveldb_option(config, leveldb_fallback_dir) - - if not config.has_option("defaults", "dynamic_loading"): - self._add_dynamic_loading_option(config) - - with codecs.open(self.config_path, "w", "utf-8") as fp: - config.write(fp) - - leveldb_dir = config.get( - "defaults", "leveldb_dir", fallback=leveldb_fallback_dir - ) - return os.path.expanduser(leveldb_dir) - - @staticmethod - def _add_default_options(config): - config.add_section("defaults") - - @staticmethod - def _add_leveldb_option(config, leveldb_fallback_dir): - config.set("defaults", "#Default chaindata locations:") - config.set("defaults", "#– Mac: ~/Library/Ethereum/geth/chaindata") - config.set("defaults", "#– Linux: ~/.ethereum/geth/chaindata") - config.set( - "defaults", - "#– Windows: %USERPROFILE%\\AppData\\Roaming\\Ethereum\\geth\\chaindata", - ) - config.set("defaults", "leveldb_dir", leveldb_fallback_dir) - - @staticmethod - def _add_dynamic_loading_option(config): - 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 analyze_truffle_project(self, *args, **kwargs): - """ - - :param args: - :param kwargs: - :return: - """ - return analyze_truffle_project( - self.sigs, *args, **kwargs - ) # just passthru by passing signatures for now - - @staticmethod - def _init_solc_binary(version): - """Figure out solc binary and version. - - Only proper versions are supported. No nightlies, commits etc (such as available in remix). - """ - - if not version: - return os.environ.get("SOLC") or "solc" - - # tried converting input to semver, seemed not necessary so just slicing for now - main_version = solc.main.get_solc_version_string() - main_version_number = re.match(r"\d+.\d+.\d+", main_version) - if main_version is None: - raise CriticalError( - "Could not extract solc version from string {}".format(main_version) - ) - 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: - log.info("Given version is already installed") - else: - try: - solc.install_solc("v" + version) - solc_binary = util.solc_exists(version) - if not solc_binary: - raise SolcError() - except SolcError: - raise CriticalError( - "There was an error when trying to install the specified solc version" - ) - - log.info("Setting the compiler to %s", solc_binary) - - return solc_binary - - def set_api_leveldb(self, leveldb): - """ - - :param leveldb: - :return: - """ - self.eth_db = EthLevelDB(leveldb) - self.eth = self.eth_db - return self.eth - - def set_api_rpc_infura(self): - """Set the RPC mode to INFURA on mainnet.""" - self.eth = EthJsonRpc("mainnet.infura.io", 443, True) - log.info("Using INFURA for RPC queries") - - def set_api_rpc(self, rpc=None, rpctls=False): - """ - - :param rpc: - :param rpctls: - """ - if rpc == "ganache": - rpcconfig = ("localhost", 8545, False) - else: - m = re.match(r"infura-(.*)", rpc) - if m and m.group(1) in ["mainnet", "rinkeby", "kovan", "ropsten"]: - rpcconfig = (m.group(1) + ".infura.io", 443, True) - 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: - self.eth = EthJsonRpc(rpcconfig[0], int(rpcconfig[1]), rpcconfig[2]) - log.info("Using RPC settings: %s" % str(rpcconfig)) - else: - raise CriticalError("Invalid RPC settings, check help for details.") - - def set_api_rpc_localhost(self): - """Set the RPC mode to a local instance.""" - self.eth = EthJsonRpc("localhost", 8545) - log.info("Using default RPC settings: http://localhost:8545") - - def set_api_from_config_path(self): - """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" - if dynamic_loading == "infura": - self.set_api_rpc_infura() - elif dynamic_loading == "localhost": - self.set_api_rpc_localhost() - else: - self.set_api_rpc(dynamic_loading) - - def search_db(self, search): - """ - - :param search: - """ - - def search_callback(_, address, balance): - """ - - :param _: - :param address: - :param balance: - """ - print("Address: " + address + ", balance: " + str(balance)) - - try: - self.eth_db.search(search, search_callback) - - except SyntaxError: - raise CriticalError("Syntax error in search expression.") - - def contract_hash_to_address(self, hash): - """ - - :param hash: - """ - if not re.match(r"0x[a-fA-F0-9]{64}", hash): - raise CriticalError("Invalid address hash. Expected format is '0x...'.") - - print(self.eth_db.contract_hash_to_address(hash)) - - def load_from_bytecode(self, code, bin_runtime=False, address=None): - """ - - :param code: - :param bin_runtime: - :param address: - :return: - """ - if address is None: - address = util.get_indexed_address(0) - if bin_runtime: - self.contracts.append( - EVMContract( - code=code, - name="MAIN", - enable_online_lookup=self.enable_online_lookup, - ) - ) - else: - self.contracts.append( - EVMContract( - creation_code=code, - name="MAIN", - enable_online_lookup=self.enable_online_lookup, - ) - ) - return address, self.contracts[-1] # return address and contract object - - def load_from_address(self, address): - """ - - :param address: - :return: - """ - if not re.match(r"0x[a-fA-F0-9]{40}", address): - raise CriticalError("Invalid contract address. Expected format is '0x...'.") - - 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)) - else: - 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, - enable_online_lookup=self.enable_online_lookup, - ) - ) - return address, self.contracts[-1] # return address and contract object - - def load_from_solidity(self, solidity_files): - """ - - :param solidity_files: - :return: - """ - 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) - - try: - # import signatures from solidity source - self.sigs.import_solidity_file( - file, solc_binary=self.solc_binary, solc_args=self.solc_args - ) - if contract_name is not None: - contract = SolidityContract( - input_file=file, - name=contract_name, - solc_args=self.solc_args, - solc_binary=self.solc_binary, - ) - self.contracts.append(contract) - contracts.append(contract) - else: - for contract in get_contracts_from_file( - input_file=file, - solc_args=self.solc_args, - solc_binary=self.solc_binary, - ): - self.contracts.append(contract) - contracts.append(contract) - - except FileNotFoundError: - raise CriticalError("Input file not found: " + file) - except CompilerError as e: - raise CriticalError(e) - except NoContractFoundError: - log.error( - "The file " + file + " does not contain a compilable contract." - ) - - return address, contracts - - def dump_statespace( - self, - strategy, - contract, - address=None, - max_depth=None, - execution_timeout=None, - create_timeout=None, - enable_iprof=False, - ): - """ - - :param strategy: - :param contract: - :param address: - :param max_depth: - :param execution_timeout: - :param create_timeout: - :return: - """ - sym = SymExecWrapper( - contract, - address, - strategy, - dynloader=DynLoader( - self.eth, - storage_loading=self.onchain_storage_access, - contract_loading=self.dynld, - ), - max_depth=max_depth, - execution_timeout=execution_timeout, - create_timeout=create_timeout, - enable_iprof=enable_iprof, - ) - - return get_serializable_statespace(sym) - - def graph_html( - self, - strategy, - contract, - address, - max_depth=None, - enable_physics=False, - phrackify=False, - execution_timeout=None, - create_timeout=None, - enable_iprof=False, - ): - """ - - :param strategy: - :param contract: - :param address: - :param max_depth: - :param enable_physics: - :param phrackify: - :param execution_timeout: - :param create_timeout: - :return: - """ - sym = SymExecWrapper( - contract, - address, - strategy, - dynloader=DynLoader( - self.eth, - storage_loading=self.onchain_storage_access, - contract_loading=self.dynld, - ), - max_depth=max_depth, - execution_timeout=execution_timeout, - create_timeout=create_timeout, - enable_iprof=enable_iprof, - ) - return generate_graph(sym, physics=enable_physics, phrackify=phrackify) - - def fire_lasers( - self, - strategy, - contracts=None, - address=None, - modules=None, - verbose_report=False, - max_depth=None, - execution_timeout=None, - create_timeout=None, - transaction_count=None, - enable_iprof=False, - ): - """ - - :param strategy: - :param contracts: - :param address: - :param modules: - :param verbose_report: - :param max_depth: - :param execution_timeout: - :param create_timeout: - :param transaction_count: - :return: - """ - all_issues = [] - SolverStatistics().enabled = True - exceptions = [] - for contract in contracts or self.contracts: - StartTime() # Reinitialize start time for new contracts - try: - sym = SymExecWrapper( - contract, - address, - strategy, - dynloader=DynLoader( - self.eth, - storage_loading=self.onchain_storage_access, - contract_loading=self.dynld, - ), - max_depth=max_depth, - execution_timeout=execution_timeout, - create_timeout=create_timeout, - transaction_count=transaction_count, - modules=modules, - compulsory_statespace=False, - enable_iprof=enable_iprof, - ) - issues = fire_lasers(sym, modules) - 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()))) - - # Finally, output the results - report = Report(verbose_report, self.contracts, exceptions=exceptions) - for issue in all_issues: - report.append_issue(issue) - - return report - - def get_state_variable_from_storage(self, address, params=None): - """ - - :param address: - :param params: - :return: - """ - if params is None: - params = [] - (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 = utils.zpad(utils.int_to_big_endian(position), 32) - for i in range(2, len(params)): - key = bytes(params[i], "utf8") - key_formatted = utils.rzpad(key, 32) - mappings.append( - int.from_bytes( - utils.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 = utils.zpad( - utils.int_to_big_endian(position), 32 - ) - position = int.from_bytes( - utils.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) - - @staticmethod - def disassemble(contract): - """ - - :param contract: - :return: - """ - return contract.get_easm() - - @staticmethod - def hash_for_function_signature(sig): - """ - - :param sig: - :return: - """ - return "0x%s" % utils.sha3(sig)[:4].hex() diff --git a/mythril/solidity/soliditycontract.py b/mythril/solidity/soliditycontract.py index 6f83ead7..f8594701 100644 --- a/mythril/solidity/soliditycontract.py +++ b/mythril/solidity/soliditycontract.py @@ -1,5 +1,7 @@ """This module contains representation classes for Solidity files, contracts and source mappings.""" +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 @@ -20,9 +22,16 @@ class SourceMapping: class SolidityFile: """Representation of a file containing Solidity code.""" - def __init__(self, filename, data): + 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: @@ -69,7 +78,12 @@ class SolidityContract(EVMContract): for filename in data["sourceList"]: with open(filename, "r", encoding="utf-8") as file: code = file.read() - self.solidity_files.append(SolidityFile(filename, code)) + full_contract_src_maps = self.get_full_contract_src_maps( + data["sources"][filename]["AST"] + ) + self.solidity_files.append( + SolidityFile(filename, code, full_contract_src_maps) + ) has_contract = False @@ -117,6 +131,19 @@ class SolidityContract(EVMContract): super().__init__(code, creation_code, name=name) + @staticmethod + def get_full_contract_src_maps(ast: Dict) -> 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() + for child in ast["children"]: + if "contractKind" in child["attributes"]: + source_maps.add(child["src"]) + return source_maps + def get_source_info(self, address, constructor=False): """ @@ -140,6 +167,26 @@ class SolidityContract(EVMContract): 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 + """ + # Handle internal compiler files + if file_index == -1: + return True + # Handle the common code src map for the entire code. + if ( + "{}:{}:{}".format(offset, length, file_index) + in self.solidity_files[file_index].full_contract_src_maps + ): + return True + + return False + def _get_solc_mappings(self, srcmap, constructor=False): """ @@ -161,7 +208,8 @@ class SolidityContract(EVMContract): if len(mapping) > 2 and len(mapping[2]) > 0: idx = int(mapping[2]) - if idx == -1: + + if self._is_autogenerated_code(offset, length, idx): lineno = None else: lineno = ( diff --git a/requirements.txt b/requirements.txt index c22d72d0..11cdeff3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ configparser>=3.5.0 coverage py_ecc==1.4.2 eth_abi==1.3.0 -eth-account>=0.1.0a2 +eth-account>=0.1.0a2,<=0.3.0 ethereum>=2.3.2 ethereum-input-decoder>=0.2.2 eth-hash>=0.1.0 diff --git a/setup.py b/setup.py index 67d7f39a..4605b5d3 100755 --- a/setup.py +++ b/setup.py @@ -81,7 +81,7 @@ setup( "plyvel", "eth_abi==1.3.0", "eth-utils>=1.0.1", - "eth-account>=0.1.0a2", + "eth-account>=0.1.0a2,<=0.3.0", "eth-hash>=0.1.0", "eth-keyfile>=0.5.1", "eth-keys>=0.2.0b3",