diff --git a/crytic_compile/__main__.py b/crytic_compile/__main__.py index 6b55e784..0ce8240d 100644 --- a/crytic_compile/__main__.py +++ b/crytic_compile/__main__.py @@ -172,7 +172,7 @@ def _print_filenames(compilation: "CryticCompile") -> None: """ for compilation_id, compilation_unit in compilation.compilation_units.items(): print( - f"Compilation unit: {compilation_id} ({len(compilation_unit.contracts_names)} files, solc {compilation_unit.compiler_version.version})" + f"Compilation unit: {compilation_id} solc {compilation_unit.compiler_version.version})" ) for filename, contracts in compilation_unit.filename_to_contracts.items(): for contract in contracts: diff --git a/crytic_compile/compilation_unit.py b/crytic_compile/compilation_unit.py index 87622fbe..3198b739 100644 --- a/crytic_compile/compilation_unit.py +++ b/crytic_compile/compilation_unit.py @@ -1,23 +1,20 @@ """ Module handling the compilation unit """ -import re import uuid from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Dict, Set -from Crypto.Hash import keccak -from crytic_compile.utils.naming import Filename -from crytic_compile.utils.natspec import Natspec from crytic_compile.compiler.compiler import CompilerVersion +from crytic_compile.source_unit import SourceUnit +from crytic_compile.utils.naming import Filename # Cycle dependency if TYPE_CHECKING: from crytic_compile import CryticCompile - -# pylint: disable=too-many-instance-attributes,too-many-public-methods +# pylint: disable=too-many-instance-attributes class CompilationUnit: """CompilationUnit class""" @@ -28,32 +25,12 @@ def __init__(self, crytic_compile: "CryticCompile", unique_id: str): crytic_compile (CryticCompile): Associated CryticCompile object unique_id (str): Unique ID used to identify the compilation unit """ - # ASTS are indexed by absolute path - self._asts: Dict = {} - - # ABI, bytecode and srcmap are indexed by contract_name - self._abis: Dict = {} - self._runtime_bytecodes: Dict = {} - self._init_bytecodes: Dict = {} - self._hashes: Dict = {} - self._events: Dict = {} - self._srcmaps: Dict[str, List[str]] = {} - self._srcmaps_runtime: Dict[str, List[str]] = {} - - # set containing all the contract names - self._contracts_name: Set[str] = set() - # set containing all the contract name without the libraries - self._contracts_name_without_libraries: Optional[Set[str]] = None # mapping from filename to contract name self._filename_to_contracts: Dict[Filename, Set[str]] = defaultdict(set) - # Libraries used by the contract - # contract_name -> (library, pattern) - self._libraries: Dict[str, List[Tuple[str, str]]] = {} - - # Natspec - self._natspec: Dict[str, Natspec] = {} + # mapping from filename to source unit + self._source_units: Dict[Filename, SourceUnit] = {} # set containing all the filenames of this compilation unit self._filenames: Set[Filename] = set() @@ -68,7 +45,7 @@ def __init__(self, crytic_compile: "CryticCompile", unique_id: str): if unique_id == ".": unique_id = str(uuid.uuid4()) - crytic_compile.compilation_units[unique_id] = self + crytic_compile.compilation_units[unique_id] = self # type: ignore self._unique_id = unique_id @@ -90,21 +67,62 @@ def crytic_compile(self) -> "CryticCompile": """ return self._crytic_compile - ################################################################################### - ################################################################################### - # region Natspec - ################################################################################### - ################################################################################### + @property + def source_units(self) -> Dict[Filename, SourceUnit]: + """ + Return the dict of the source units + + Returns: + Dict[Filename, SourceUnit]: the source units + """ + return self._source_units + + def source_unit(self, filename: Filename) -> SourceUnit: + """ + Return the source unit associated to the filename. + The source unit must have been created by create_source_units + + Args: + filename: filename of the source unit + + Returns: + SourceUnit: the source unit + """ + return self._source_units[filename] @property - def natspec(self) -> Dict[str, Natspec]: - """Return the natspec of the contracts + def asts(self) -> Dict[str, Dict]: + """ + Return all the asts from the compilation unit Returns: - Dict[str, Natspec]: Contract name -> Natspec + Dict[str, Dict]: absolute path -> ast + """ + return { + source_unit.filename.absolute: source_unit.ast + for source_unit in self.source_units.values() + } + + def create_source_unit(self, filename: Filename) -> SourceUnit: """ - return self._natspec + Create the source unit associated with the filename + Add the relevant info in the compilation unit/crytic compile + If the source unit already exist, return it + Args: + filename (Filename): filename of the source unit + + Returns: + SourceUnit: the source unit + """ + if not filename in self._source_units: + source_unit = SourceUnit(self, filename) # type: ignore + self.filenames.add(filename) + self.crytic_compile.filenames.add(filename) + self._source_units[filename] = source_unit + return self._source_units[filename] + + # endregion ################################################################################### ################################################################################### # region Filenames @@ -113,7 +131,7 @@ def natspec(self) -> Dict[str, Natspec]: @property def filenames(self) -> Set[Filename]: - """Return the filenames used by the compilation units + """Return the filenames used by the compilation unit Returns: Set[Filename]: Filenames used by the compilation units @@ -176,553 +194,6 @@ def relative_filename_from_absolute_filename(self, absolute_filename: str) -> st raise ValueError("f{absolute_filename} does not exist in {d}") return d_file[absolute_filename] - # endregion - ################################################################################### - ################################################################################### - # region Contract Names - ################################################################################### - ################################################################################### - - @property - def contracts_names(self) -> Set[str]: - """Return the contracts names - - Returns: - Set[str]: List of the contracts names - """ - return self._contracts_name - - @contracts_names.setter - def contracts_names(self, names: Set[str]) -> None: - """Set the contract names - - Args: - names (Set[str]): New contracts names - """ - self._contracts_name = names - - @property - def contracts_names_without_libraries(self) -> Set[str]: - """Return the contracts names without the librairies - - Returns: - Set[str]: List of contracts - """ - if self._contracts_name_without_libraries is None: - libraries: List[str] = [] - for contract_name in self._contracts_name: - libraries += self.libraries_names(contract_name) - self._contracts_name_without_libraries = { - l for l in self._contracts_name if l not in set(libraries) - } - return self._contracts_name_without_libraries - - # endregion - ################################################################################### - ################################################################################### - # region ABI - ################################################################################### - ################################################################################### - - @property - def abis(self) -> Dict: - """Return the ABIs - - Returns: - Dict: ABIs (solc/vyper format) (contract name -> ABI) - """ - return self._abis - - def abi(self, name: str) -> Dict: - """Get the ABI from a contract - - Args: - name (str): Contract name - - Returns: - Dict: ABI (solc/vyper format) - """ - return self._abis.get(name, None) - - # endregion - ################################################################################### - ################################################################################### - # region AST - ################################################################################### - ################################################################################### - - @property - def asts(self) -> Dict: - """Return the ASTs - - Returns: - Dict: contract name -> AST (solc/vyper format) - """ - return self._asts - - @asts.setter - def asts(self, value: Dict) -> None: - """Set the ASTs - - Args: - value (Dict): New ASTs - """ - self._asts = value - - def ast(self, path: str) -> Union[Dict, None]: - """Return the ast of the file - - Args: - path (str): path to the file - - Returns: - Union[Dict, None]: Ast (solc/vyper format) - """ - if path not in self._asts: - try: - path = self.find_absolute_filename_from_used_filename(path) - except ValueError: - pass - return self._asts.get(path, None) - - # endregion - ################################################################################### - ################################################################################### - # region Bytecode - ################################################################################### - ################################################################################### - - @property - def bytecodes_runtime(self) -> Dict[str, str]: - """Return the runtime bytecodes - - Returns: - Dict[str, str]: contract => runtime bytecode - """ - return self._runtime_bytecodes - - @bytecodes_runtime.setter - def bytecodes_runtime(self, bytecodes: Dict[str, str]) -> None: - """Set the bytecodes runtime - - Args: - bytecodes (Dict[str, str]): New bytecodes runtime - """ - self._runtime_bytecodes = bytecodes - - @property - def bytecodes_init(self) -> Dict[str, str]: - """Return the init bytecodes - - Returns: - Dict[str, str]: contract => init bytecode - """ - return self._init_bytecodes - - @bytecodes_init.setter - def bytecodes_init(self, bytecodes: Dict[str, str]) -> None: - """Set the bytecodes init - - Args: - bytecodes (Dict[str, str]): New bytecodes init - """ - self._init_bytecodes = bytecodes - - def bytecode_runtime(self, name: str, libraries: Optional[Dict[str, str]] = None) -> str: - """Return the runtime bytecode of the contract. - If library is provided, patch the bytecode - - Args: - name (str): contract name - libraries (Optional[Dict[str, str]], optional): lib_name => address. Defaults to None. - - Returns: - str: runtime bytecode - """ - runtime = self._runtime_bytecodes.get(name, None) - return self._update_bytecode_with_libraries(runtime, libraries) - - def bytecode_init(self, name: str, libraries: Optional[Dict[str, str]] = None) -> str: - """Return the init bytecode of the contract. - If library is provided, patch the bytecode - - Args: - name (str): contract name - libraries (Optional[Dict[str, str]], optional): lib_name => address. Defaults to None. - - Returns: - str: init bytecode - """ - init = self._init_bytecodes.get(name, None) - return self._update_bytecode_with_libraries(init, libraries) - - # endregion - ################################################################################### - ################################################################################### - # region Source mapping - ################################################################################### - ################################################################################### - - @property - def srcmaps_init(self) -> Dict[str, List[str]]: - """Return the srcmaps init - - Returns: - Dict[str, List[str]]: Srcmaps init (solc/vyper format) - """ - return self._srcmaps - - @property - def srcmaps_runtime(self) -> Dict[str, List[str]]: - """Return the srcmaps runtime - - Returns: - Dict[str, List[str]]: Srcmaps runtime (solc/vyper format) - """ - return self._srcmaps_runtime - - def srcmap_init(self, name: str) -> List[str]: - """Return the srcmap init of a contract - - Args: - name (str): name of the contract - - Returns: - List[str]: Srcmap init (solc/vyper format) - """ - return self._srcmaps.get(name, []) - - def srcmap_runtime(self, name: str) -> List[str]: - """Return the srcmap runtime of a contract - - Args: - name (str): name of the contract - - Returns: - List[str]: Srcmap runtime (solc/vyper format) - """ - return self._srcmaps_runtime.get(name, []) - - # endregion - ################################################################################### - ################################################################################### - # region Libraries - ################################################################################### - ################################################################################### - - @property - def libraries(self) -> Dict[str, List[Tuple[str, str]]]: - """Return the libraries used - - Returns: - Dict[str, List[Tuple[str, str]]]: (contract_name -> [(library, pattern))]) - """ - return self._libraries - - def _convert_libraries_names(self, libraries: Dict[str, str]) -> Dict[str, str]: - """Convert the libraries names - The name in the argument can be the library name, or filename:library_name - The returned dict contains all the names possible with the different solc versions - - Args: - libraries (Dict[str, str]): lib_name => address - - Returns: - Dict[str, str]: lib_name => address - """ - new_names = {} - for (lib, addr) in libraries.items(): - # Prior solidity 0.5 - # libraries were on the format __filename:contract_name_____ - # From solidity 0.5, - # libraries are on the format __$keccak(filename:contract_name)[34]$__ - # https://solidity.readthedocs.io/en/v0.5.7/050-breaking-changes.html#command-line-and-json-interfaces - - lib_4 = "__" + lib + "_" * (38 - len(lib)) - - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(lib.encode("utf-8")) - lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" - - new_names[lib] = addr - new_names[lib_4] = addr - new_names[lib_5] = addr - - for lib_filename, contract_names in self._filename_to_contracts.items(): - for contract_name in contract_names: - if contract_name != lib: - continue - - lib_with_abs_filename = lib_filename.absolute + ":" + lib - lib_with_abs_filename = lib_with_abs_filename[0:36] - - lib_4 = "__" + lib_with_abs_filename + "_" * (38 - len(lib_with_abs_filename)) - new_names[lib_4] = addr - - lib_with_used_filename = lib_filename.used + ":" + lib - lib_with_used_filename = lib_with_used_filename[0:36] - - lib_4 = "__" + lib_with_used_filename + "_" * (38 - len(lib_with_used_filename)) - new_names[lib_4] = addr - - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(lib_with_abs_filename.encode("utf-8")) - lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" - new_names[lib_5] = addr - - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(lib_with_used_filename.encode("utf-8")) - lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" - new_names[lib_5] = addr - - return new_names - - def _library_name_lookup( - self, lib_name: str, original_contract: str - ) -> Optional[Tuple[str, str]]: - """Do a lookup on a library name to its name used in contracts - The library can be: - - the original contract name - - __X__ following Solidity 0.4 format - - __$..$__ following Solidity 0.5 format - - Args: - lib_name (str): library name - original_contract (str): original contract name - - Returns: - Optional[Tuple[str, str]]: contract_name, library_name - """ - - for filename, contract_names in self._filename_to_contracts.items(): - for name in contract_names: - if name == lib_name: - return name, name - - # Some platform use only the contract name - # Some use fimename:contract_name - name_with_absolute_filename = filename.absolute + ":" + name - name_with_absolute_filename = name_with_absolute_filename[0:36] - - name_with_used_filename = filename.used + ":" + name - name_with_used_filename = name_with_used_filename[0:36] - - # Solidity 0.4 - solidity_0_4 = "__" + name + "_" * (38 - len(name)) - if solidity_0_4 == lib_name: - return name, solidity_0_4 - - # Solidity 0.4 with filename - solidity_0_4_filename = ( - "__" - + name_with_absolute_filename - + "_" * (38 - len(name_with_absolute_filename)) - ) - if solidity_0_4_filename == lib_name: - return name, solidity_0_4_filename - - # Solidity 0.4 with filename - solidity_0_4_filename = ( - "__" + name_with_used_filename + "_" * (38 - len(name_with_used_filename)) - ) - if solidity_0_4_filename == lib_name: - return name, solidity_0_4_filename - - # Solidity 0.5 - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(name.encode("utf-8")) - v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" - - if v5_name == lib_name: - return name, v5_name - - # Solidity 0.5 with filename - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(name_with_absolute_filename.encode("utf-8")) - v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" - - if v5_name == lib_name: - return name, v5_name - - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(name_with_used_filename.encode("utf-8")) - v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" - - if v5_name == lib_name: - return name, v5_name - - # handle specific case of collision for Solidity <0.4 - # We can only detect that the second contract is meant to be the library - # if there is only two contracts in the codebase - if len(self._contracts_name) == 2: - return next( - ( - (c, "__" + c + "_" * (38 - len(c))) - for c in self._contracts_name - if c != original_contract - ), - None, - ) - - return None - - def libraries_names(self, name: str) -> List[str]: - """Return the names of the libraries used by the contract - - Args: - name (str): contract name - - Returns: - List[str]: libraries used - """ - - if name not in self._libraries: - init = re.findall(r"__.{36}__", self.bytecode_init(name)) - runtime = re.findall(r"__.{36}__", self.bytecode_runtime(name)) - libraires = [self._library_name_lookup(x, name) for x in set(init + runtime)] - self._libraries[name] = [lib for lib in libraires if lib] - return [name for (name, _) in self._libraries[name]] - - def libraries_names_and_patterns(self, name: str) -> List[Tuple[str, str]]: - """Return the names and the patterns of the libraries used by the contract - - Args: - name (str): contract name - - Returns: - List[Tuple[str, str]]: (lib_name, pattern) - """ - - if name not in self._libraries: - init = re.findall(r"__.{36}__", self.bytecode_init(name)) - runtime = re.findall(r"__.{36}__", self.bytecode_runtime(name)) - libraires = [self._library_name_lookup(x, name) for x in set(init + runtime)] - self._libraries[name] = [lib for lib in libraires if lib] - return self._libraries[name] - - def _update_bytecode_with_libraries( - self, bytecode: str, libraries: Union[None, Dict[str, str]] - ) -> str: - """Update the bytecode with the libraries address - - Args: - bytecode (str): bytecode to patch - libraries (Union[None, Dict[str, str]]): pattern => address - - Returns: - str: Patched bytecode - """ - if libraries: - libraries = self._convert_libraries_names(libraries) - for library_found in re.findall(r"__.{36}__", bytecode): - if library_found in libraries: - bytecode = re.sub( - re.escape(library_found), - f"{libraries[library_found]:0>40x}", - bytecode, - ) - return bytecode - - # endregion - ################################################################################### - ################################################################################### - # region Hashes - ################################################################################### - ################################################################################### - - def hashes(self, name: str) -> Dict[str, int]: - """Return the hashes of the functions - - Args: - name (str): contract name - - Returns: - Dict[str, int]: (function name => signature) - """ - if not name in self._hashes: - self._compute_hashes(name) - return self._hashes[name] - - def _compute_hashes(self, name: str) -> None: - """Compute the function hashes - - Args: - name (str): contract name - """ - self._hashes[name] = {} - for sig in self.abi(name): - if "type" in sig: - if sig["type"] == "function": - sig_name = sig["name"] - arguments = ",".join([x["type"] for x in sig["inputs"]]) - sig = f"{sig_name}({arguments})" - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(sig.encode("utf-8")) - self._hashes[name][sig] = int("0x" + sha3_result.hexdigest()[:8], 16) - - # endregion - ################################################################################### - ################################################################################### - # region Events - ################################################################################### - ################################################################################### - - def events_topics(self, name: str) -> Dict[str, Tuple[int, List[bool]]]: - """Return the topics of the contract's events - - Args: - name (str): contract name - - Returns: - Dict[str, Tuple[int, List[bool]]]: event signature => topic hash, [is_indexed for each parameter] - """ - if not name in self._events: - self._compute_topics_events(name) - return self._events[name] - - def _compute_topics_events(self, name: str) -> None: - """Compute the topics of the contract's events - - Args: - name (str): contract name - """ - self._events[name] = {} - for sig in self.abi(name): - if "type" in sig: - if sig["type"] == "event": - sig_name = sig["name"] - arguments = ",".join([x["type"] for x in sig["inputs"]]) - indexes = [x.get("indexed", False) for x in sig["inputs"]] - sig = f"{sig_name}({arguments})" - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(sig.encode("utf-8")) - - self._events[name][sig] = (int("0x" + sha3_result.hexdigest()[:8], 16), indexes) - - # endregion - ################################################################################### - ################################################################################### - # region Metadata - ################################################################################### - ################################################################################### - - def remove_metadata(self) -> None: - """Remove init bytecode - See - http://solidity.readthedocs.io/en/v0.4.24/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode - - Note we dont support recent Solidity version, see https://github.com/crytic/crytic-compile/issues/59 - """ - self._init_bytecodes = { - key: re.sub(r"a165627a7a72305820.{64}0029", r"", bytecode) - for (key, bytecode) in self._init_bytecodes.items() - } - - self._runtime_bytecodes = { - key: re.sub(r"a165627a7a72305820.{64}0029", r"", bytecode) - for (key, bytecode) in self._runtime_bytecodes.items() - } - # endregion ################################################################################### ################################################################################### @@ -747,3 +218,7 @@ def compiler_version(self, compiler: CompilerVersion) -> None: compiler (CompilerVersion): New compiler version """ self._compiler_version = compiler + + # endregion + ################################################################################### + ################################################################################### diff --git a/crytic_compile/crytic_compile.py b/crytic_compile/crytic_compile.py index bb5e3d21..0a21342b 100644 --- a/crytic_compile/crytic_compile.py +++ b/crytic_compile/crytic_compile.py @@ -65,7 +65,7 @@ class CryticCompile: Main class. """ - def __init__(self, target: Union[str, AbstractPlatform], **kwargs: str): + def __init__(self, target: Union[str, AbstractPlatform], **kwargs: str) -> None: """See https://github.com/crytic/crytic-compile/wiki/Configuration Target is usually a file or a project directory. It can be an AbstractPlatform for custom setup @@ -145,8 +145,9 @@ def is_in_multiple_compilation_unit(self, contract: str) -> bool: """ count = 0 for compilation_unit in self._compilation_units.values(): - if contract in compilation_unit.contracts_names: - count += 1 + for source_unit in compilation_unit.source_units.values(): + if contract in source_unit.contracts_names: + count += 1 return count >= 2 ################################################################################### @@ -550,7 +551,8 @@ def _compile(self, **kwargs: str) -> None: remove_metadata = kwargs.get("compile_remove_metadata", False) if remove_metadata: for compilation_unit in self._compilation_units.values(): - compilation_unit.remove_metadata() + for source_unit in compilation_unit.source_units.values(): + source_unit.remove_metadata() @staticmethod def _run_custom_build(custom_build: str) -> None: diff --git a/crytic_compile/platform/brownie.py b/crytic_compile/platform/brownie.py index b779299b..d7800e03 100755 --- a/crytic_compile/platform/brownie.py +++ b/crytic_compile/platform/brownie.py @@ -168,28 +168,28 @@ def _iterate_over_files( filename_txt, _relative_to_short, crytic_compile, working_dir=target ) - compilation_unit.asts[filename.absolute] = target_loaded["ast"] - compilation_unit.filenames.add(filename) - crytic_compile.filenames.add(filename) + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.ast = target_loaded["ast"] contract_name = target_loaded["contractName"] + compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] - compilation_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( - "0x", "" - ) - compilation_unit.bytecodes_runtime[contract_name] = target_loaded[ + + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = target_loaded["abi"] + source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace("0x", "") + source_unit.bytecodes_runtime[contract_name] = target_loaded[ "deployedBytecode" ].replace("0x", "") - compilation_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = target_loaded[ - "deployedSourceMap" - ].split(";") + source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") + source_unit.srcmaps_runtime[contract_name] = target_loaded["deployedSourceMap"].split( + ";" + ) userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec compilation_unit.compiler_version = CompilerVersion( compiler=compiler, version=version, optimized=optimized diff --git a/crytic_compile/platform/buidler.py b/crytic_compile/platform/buidler.py index d89447f7..3c26ea5f 100755 --- a/crytic_compile/platform/buidler.py +++ b/crytic_compile/platform/buidler.py @@ -113,6 +113,14 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: if "contracts" in targets_json: for original_filename, contracts_info in targets_json["contracts"].items(): + filename = convert_filename( + original_filename, + relative_to_short, + crytic_compile, + working_dir=buidler_working_dir, + ) + source_unit = compilation_unit.create_source_unit(filename) + for original_contract_name, info in contracts_info.items(): contract_name = extract_name(original_contract_name) @@ -122,33 +130,26 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ): original_filename = "c" + original_filename - contract_filename = convert_filename( - original_filename, - relative_to_short, - crytic_compile, - working_dir=buidler_working_dir, - ) - - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.abis[contract_name] = info["abi"] - compilation_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"][ + source_unit.abis[contract_name] = info["abi"] + source_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"][ "object" ] - compilation_unit.bytecodes_runtime[contract_name] = info["evm"][ + source_unit.bytecodes_runtime[contract_name] = info["evm"][ "deployedBytecode" ]["object"] - compilation_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ + source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["evm"][ + source_unit.srcmaps_runtime[contract_name] = info["evm"][ "deployedBytecode" ]["sourceMap"].split(";") userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec if "sources" in targets_json: for path, info in targets_json["sources"].items(): @@ -167,9 +168,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: path = convert_filename( path, relative_to_short, crytic_compile, working_dir=buidler_working_dir ) - compilation_unit.filenames.add(path) - crytic_compile.filenames.add(path) - compilation_unit.asts[path.absolute] = info["ast"] + source_unit = compilation_unit.create_source_unit(path) + source_unit.ast = info["ast"] @staticmethod def is_supported(target: str, **kwargs: str) -> bool: diff --git a/crytic_compile/platform/dapp.py b/crytic_compile/platform/dapp.py index 540e4f40..d71fba68 100755 --- a/crytic_compile/platform/dapp.py +++ b/crytic_compile/platform/dapp.py @@ -69,6 +69,11 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: version = re.findall(r"\d+\.\d+\.\d+", targets_json["version"])[0] for original_filename, contracts_info in targets_json["contracts"].items(): + + filename = convert_filename(original_filename, lambda x: x, crytic_compile) + + source_unit = compilation_unit.create_source_unit(filename) + for original_contract_name, info in contracts_info.items(): if "metadata" in info: metadata = json.loads(info["metadata"]) @@ -79,26 +84,24 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ): optimized |= metadata["settings"]["optimizer"]["enabled"] contract_name = extract_name(original_contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[original_filename].add(contract_name) + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.abis[contract_name] = info["abi"] - compilation_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"][ + source_unit.abis[contract_name] = info["abi"] + source_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"]["object"] + source_unit.bytecodes_runtime[contract_name] = info["evm"]["deployedBytecode"][ "object" ] - compilation_unit.bytecodes_runtime[contract_name] = info["evm"][ - "deployedBytecode" - ]["object"] - compilation_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ + source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["evm"]["bytecode"][ + source_unit.srcmaps_runtime[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec if version is None: metadata = json.loads(info["metadata"]) @@ -108,9 +111,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: path = convert_filename( path, _relative_to_short, crytic_compile, working_dir=self._target ) - compilation_unit.filenames.add(path) - crytic_compile.filenames.add(path) - compilation_unit.asts[path.absolute] = info["ast"] + source_unit = compilation_unit.create_source_unit(path) + source_unit.ast = info["ast"] compilation_unit.compiler_version = CompilerVersion( compiler="solc", version=version, optimized=optimized @@ -171,7 +173,7 @@ def _run_dapp(target: str) -> None: InvalidCompilation: If dapp failed to run """ # pylint: disable=import-outside-toplevel - from crytic_compile import InvalidCompilation + from crytic_compile.platform.exceptions import InvalidCompilation cmd = ["dapp", "build"] diff --git a/crytic_compile/platform/embark.py b/crytic_compile/platform/embark.py index 65cb39a6..8a763505 100755 --- a/crytic_compile/platform/embark.py +++ b/crytic_compile/platform/embark.py @@ -124,9 +124,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: filename = convert_filename( k, _relative_to_short, crytic_compile, working_dir=self._target ) - compilation_unit.asts[filename.absolute] = ast - compilation_unit.filenames.add(filename) - crytic_compile.filenames.add(filename) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast if not "contracts" in targets_loaded: LOGGER.error( @@ -138,35 +137,35 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: for original_contract_name, info in targets_loaded["contracts"].items(): contract_name = extract_name(original_contract_name) - contract_filename = convert_filename( + filename = convert_filename( extract_filename(original_contract_name), _relative_to_short, crytic_compile, working_dir=self._target, ) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) + source_unit = compilation_unit.create_source_unit(filename) + + compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit.contracts_names.add(contract_name) if "abi" in info: - compilation_unit.abis[contract_name] = info["abi"] + source_unit.abis[contract_name] = info["abi"] if "bin" in info: - compilation_unit.bytecodes_init[contract_name] = info["bin"].replace("0x", "") + source_unit.bytecodes_init[contract_name] = info["bin"].replace("0x", "") if "bin-runtime" in info: - compilation_unit.bytecodes_runtime[contract_name] = info["bin-runtime"].replace( + source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"].replace( "0x", "" ) if "srcmap" in info: - compilation_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") + source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") if "srcmap-runtime" in info: - compilation_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split( - ";" - ) + source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec @staticmethod def is_supported(target: str, **kwargs: str) -> bool: diff --git a/crytic_compile/platform/etherlime.py b/crytic_compile/platform/etherlime.py index 78f3deec..951d18db 100755 --- a/crytic_compile/platform/etherlime.py +++ b/crytic_compile/platform/etherlime.py @@ -130,28 +130,30 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: Any) -> None: filename_txt = target_loaded["ast"]["absolutePath"] filename = convert_filename(filename_txt, _relative_to_short, crytic_compile) - compilation_unit.asts[filename.absolute] = target_loaded["ast"] - compilation_unit.filenames.add(filename) - crytic_compile.filenames.add(filename) + + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.ast = target_loaded["ast"] contract_name = target_loaded["contractName"] + compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] - compilation_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = target_loaded["abi"] + source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( "0x", "" ) - compilation_unit.bytecodes_runtime[contract_name] = target_loaded[ + source_unit.bytecodes_runtime[contract_name] = target_loaded[ "deployedBytecode" ].replace("0x", "") - compilation_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = target_loaded[ + source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") + source_unit.srcmaps_runtime[contract_name] = target_loaded[ "deployedSourceMap" ].split(";") userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec compilation_unit.compiler_version = CompilerVersion( compiler=compiler, version=version, optimized=_is_optimized(compile_arguments) diff --git a/crytic_compile/platform/etherscan.py b/crytic_compile/platform/etherscan.py index a83c97d8..d090476e 100644 --- a/crytic_compile/platform/etherscan.py +++ b/crytic_compile/platform/etherscan.py @@ -76,13 +76,15 @@ def _handle_bytecode(crytic_compile: "CryticCompile", target: str, result_b: byt compilation_unit = CompilationUnit(crytic_compile, str(target)) - compilation_unit.contracts_names.add(contract_name) + source_unit = compilation_unit.create_source_unit(contract_filename) + + source_unit.contracts_names.add(contract_name) compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.abis[contract_name] = {} - compilation_unit.bytecodes_init[contract_name] = bytecode - compilation_unit.bytecodes_runtime[contract_name] = "" - compilation_unit.srcmaps_init[contract_name] = [] - compilation_unit.srcmaps_runtime[contract_name] = [] + source_unit.abis[contract_name] = {} + source_unit.bytecodes_init[contract_name] = bytecode + source_unit.bytecodes_runtime[contract_name] = "" + source_unit.srcmaps_init[contract_name] = [] + source_unit.srcmaps_runtime[contract_name] = [] compilation_unit.compiler_version = CompilerVersion( compiler="unknown", version="", optimized=False diff --git a/crytic_compile/platform/foundry.py b/crytic_compile/platform/foundry.py index 92f6bf8e..38f588d6 100755 --- a/crytic_compile/platform/foundry.py +++ b/crytic_compile/platform/foundry.py @@ -111,11 +111,11 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: if not "ast" in target_loaded: continue - filename = target_loaded["ast"]["absolutePath"] + filename_str = target_loaded["ast"]["absolutePath"] try: filename = convert_filename( - filename, lambda x: x, crytic_compile, working_dir=self._target + filename_str, lambda x: x, crytic_compile, working_dir=self._target ) except InvalidCompilation as i: txt = str(i) @@ -123,29 +123,29 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: # pylint: disable=raise-missing-from raise InvalidCompilation(txt) - compilation_unit.asts[filename.absolute] = target_loaded["ast"] - crytic_compile.filenames.add(filename) - compilation_unit.filenames.add(filename) + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.ast = target_loaded["ast"] contract_name = filename_txt.parts[-1] contract_name = contract_name[: -len(".json")] - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] - compilation_unit.bytecodes_init[contract_name] = target_loaded["bytecode"][ + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = target_loaded["abi"] + source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"][ + "object" + ].replace("0x", "") + source_unit.bytecodes_runtime[contract_name] = target_loaded["deployedBytecode"][ "object" ].replace("0x", "") - compilation_unit.bytecodes_runtime[contract_name] = target_loaded[ - "deployedBytecode" - ]["object"].replace("0x", "") - compilation_unit.srcmaps_init[contract_name] = ( + source_unit.srcmaps_init[contract_name] = ( target_loaded["bytecode"]["sourceMap"].split(";") if target_loaded["bytecode"].get("sourceMap") else [] ) - compilation_unit.srcmaps_runtime[contract_name] = ( + source_unit.srcmaps_runtime[contract_name] = ( target_loaded["deployedBytecode"]["sourceMap"].split(";") if target_loaded["deployedBytecode"].get("sourceMap") else [] diff --git a/crytic_compile/platform/hardhat.py b/crytic_compile/platform/hardhat.py index 982e8d93..aba547dd 100755 --- a/crytic_compile/platform/hardhat.py +++ b/crytic_compile/platform/hardhat.py @@ -14,11 +14,11 @@ from crytic_compile.platform.types import Type from crytic_compile.utils.naming import convert_filename, extract_name from crytic_compile.utils.natspec import Natspec -from .abstract_platform import AbstractPlatform +from crytic_compile.platform.abstract_platform import AbstractPlatform # Handle cycle -from .solc import relative_to_short -from ..compilation_unit import CompilationUnit +from crytic_compile.platform.solc import relative_to_short +from crytic_compile.compilation_unit import CompilationUnit if TYPE_CHECKING: from crytic_compile import CryticCompile @@ -123,38 +123,39 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: if "contracts" in targets_json: for original_filename, contracts_info in targets_json["contracts"].items(): + + filename = convert_filename( + original_filename, + relative_to_short, + crytic_compile, + working_dir=hardhat_working_dir, + ) + + source_unit = compilation_unit.create_source_unit(filename) + for original_contract_name, info in contracts_info.items(): contract_name = extract_name(original_contract_name) - contract_filename = convert_filename( - original_filename, - relative_to_short, - crytic_compile, - working_dir=hardhat_working_dir, - ) - - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add( - contract_name - ) + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.abis[contract_name] = info["abi"] - compilation_unit.bytecodes_init[contract_name] = info["evm"][ - "bytecode" - ]["object"] - compilation_unit.bytecodes_runtime[contract_name] = info["evm"][ + source_unit.abis[contract_name] = info["abi"] + source_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"][ + "object" + ] + source_unit.bytecodes_runtime[contract_name] = info["evm"][ "deployedBytecode" ]["object"] - compilation_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ + source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["evm"][ + source_unit.srcmaps_runtime[contract_name] = info["evm"][ "deployedBytecode" ]["sourceMap"].split(";") userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec if "sources" in targets_json: for path, info in targets_json["sources"].items(): @@ -172,9 +173,9 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: crytic_compile, working_dir=hardhat_working_dir, ) - crytic_compile.filenames.add(path) - compilation_unit.filenames.add(path) - compilation_unit.asts[path.absolute] = info["ast"] + + source_unit = compilation_unit.create_source_unit(path) + source_unit.ast = info["ast"] @staticmethod def is_supported(target: str, **kwargs: str) -> bool: diff --git a/crytic_compile/platform/solc.py b/crytic_compile/platform/solc.py index 14b53e3b..d6710d84 100644 --- a/crytic_compile/platform/solc.py +++ b/crytic_compile/platform/solc.py @@ -34,22 +34,22 @@ def _build_contract_data(compilation_unit: "CompilationUnit") -> Dict: contracts = {} - for filename, contract_names in compilation_unit.filename_to_contracts.items(): - for contract_name in contract_names: - abi = str(compilation_unit.abi(contract_name)) + for filename, source_unit in compilation_unit.source_units.items(): + for contract_name in source_unit.contracts_names: + abi = str(source_unit.abi(contract_name)) abi = abi.replace("'", '"') abi = abi.replace("True", "true") abi = abi.replace("False", "false") abi = abi.replace(" ", "") exported_name = combine_filename_name(filename.absolute, contract_name) contracts[exported_name] = { - "srcmap": ";".join(compilation_unit.srcmap_init(contract_name)), - "srcmap-runtime": ";".join(compilation_unit.srcmap_runtime(contract_name)), + "srcmap": ";".join(source_unit.srcmap_init(contract_name)), + "srcmap-runtime": ";".join(source_unit.srcmap_runtime(contract_name)), "abi": abi, - "bin": compilation_unit.bytecode_init(contract_name), - "bin-runtime": compilation_unit.bytecode_runtime(contract_name), - "userdoc": compilation_unit.natspec[contract_name].userdoc.export(), - "devdoc": compilation_unit.natspec[contract_name].devdoc.export(), + "bin": source_unit.bytecode_init(contract_name), + "bin-runtime": source_unit.bytecode_runtime(contract_name), + "userdoc": source_unit.natspec[contract_name].userdoc.export(), + "devdoc": source_unit.natspec[contract_name].devdoc.export(), } return contracts @@ -178,9 +178,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: path = convert_filename( path, relative_to_short, crytic_compile, working_dir=solc_working_dir ) - compilation_unit.filenames.add(path) - crytic_compile.filenames.add(path) - compilation_unit.asts[path.absolute] = info["AST"] + source_unit = compilation_unit.create_source_unit(path) + source_unit.ast = info["AST"] @staticmethod def is_supported(target: str, **kwargs: str) -> bool: @@ -309,32 +308,35 @@ def solc_handle_contracts( contract_name = extract_name(original_contract_name) # for solc < 0.4.10 we cant retrieve the filename from the ast if skip_filename: - contract_filename = convert_filename( + filename = convert_filename( target, relative_to_short, compilation_unit.crytic_compile, working_dir=solc_working_dir, ) else: - contract_filename = convert_filename( + filename = convert_filename( extract_filename(original_contract_name), relative_to_short, compilation_unit.crytic_compile, working_dir=solc_working_dir, ) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.abis[contract_name] = ( + + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit.abis[contract_name] = ( json.loads(info["abi"]) if not is_above_0_8 else info["abi"] ) - compilation_unit.bytecodes_init[contract_name] = info["bin"] - compilation_unit.bytecodes_runtime[contract_name] = info["bin-runtime"] - compilation_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") + source_unit.bytecodes_init[contract_name] = info["bin"] + source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"] + source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") userdoc = json.loads(info.get("userdoc", "{}")) if not is_above_0_8 else info["userdoc"] devdoc = json.loads(info.get("devdoc", "{}")) if not is_above_0_8 else info["devdoc"] natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec def _is_at_or_above_minor_version(compilation_unit: "CompilationUnit", version: int) -> bool: diff --git a/crytic_compile/platform/solc_standard_json.py b/crytic_compile/platform/solc_standard_json.py index 59e56324..d64f5b0a 100644 --- a/crytic_compile/platform/solc_standard_json.py +++ b/crytic_compile/platform/solc_standard_json.py @@ -266,36 +266,39 @@ def parse_standard_json_output( for contract_name, info in file_contracts.items(): # for solc < 0.4.10 we cant retrieve the filename from the ast if skip_filename: - contract_filename = convert_filename( + filename = convert_filename( file_path, relative_to_short, compilation_unit.crytic_compile, working_dir=solc_working_dir, ) else: - contract_filename = convert_filename( + filename = convert_filename( file_path, relative_to_short, compilation_unit.crytic_compile, working_dir=solc_working_dir, ) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.abis[contract_name] = info["abi"] + + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit.abis[contract_name] = info["abi"] userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec - compilation_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"]["object"] - compilation_unit.bytecodes_runtime[contract_name] = info["evm"]["deployedBytecode"][ + source_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"]["object"] + source_unit.bytecodes_runtime[contract_name] = info["evm"]["deployedBytecode"][ "object" ] - compilation_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ + source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["evm"]["deployedBytecode"][ + source_unit.srcmaps_runtime[contract_name] = info["evm"]["deployedBytecode"][ "sourceMap" ].split(";") @@ -315,10 +318,9 @@ def parse_standard_json_output( compilation_unit.crytic_compile, working_dir=solc_working_dir, ) - compilation_unit.crytic_compile.filenames.add(path) - compilation_unit.filenames.add(path) + source_unit = compilation_unit.create_source_unit(path) - compilation_unit.asts[path.absolute] = info.get("ast") + source_unit.ast = info.get("ast") # Inherits is_dependency/is_supported from Solc diff --git a/crytic_compile/platform/standard.py b/crytic_compile/platform/standard.py index 76d7447a..bc3f236c 100644 --- a/crytic_compile/platform/standard.py +++ b/crytic_compile/platform/standard.py @@ -3,6 +3,7 @@ """ import json import os +from collections import defaultdict from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Any @@ -10,7 +11,7 @@ from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform import Type as PlatformType from crytic_compile.platform.abstract_platform import AbstractPlatform -from crytic_compile.utils.naming import Filename +from crytic_compile.utils.naming import Filename, convert_filename # Cycle dependency from crytic_compile.utils.natspec import Natspec @@ -213,22 +214,23 @@ def generate_standard_export(crytic_compile: "CryticCompile") -> Dict: compilation_units = {} for key, compilation_unit in crytic_compile.compilation_units.items(): - contracts: Dict[str, Dict[str, Any]] = {} - for filename, contract_names in compilation_unit.filename_to_contracts.items(): - contracts[filename.relative] = {} - for contract_name in contract_names: - libraries = compilation_unit.libraries_names_and_patterns(contract_name) - contracts[filename.relative][contract_name] = { - "abi": compilation_unit.abi(contract_name), - "bin": compilation_unit.bytecode_init(contract_name), - "bin-runtime": compilation_unit.bytecode_runtime(contract_name), - "srcmap": ";".join(compilation_unit.srcmap_init(contract_name)), - "srcmap-runtime": ";".join(compilation_unit.srcmap_runtime(contract_name)), + source_unit_dict: Dict[str, Dict[str, Dict[str, Any]]] = {} + for filename, source_unit in compilation_unit.source_units.items(): + source_unit_dict[filename.relative] = defaultdict(dict) + source_unit_dict[filename.relative]["ast"] = source_unit.ast + for contract_name in source_unit.contracts_names: + libraries = source_unit.libraries_names_and_patterns(contract_name) + source_unit_dict[filename.relative]["contracts"][contract_name] = { + "abi": source_unit.abi(contract_name), + "bin": source_unit.bytecode_init(contract_name), + "bin-runtime": source_unit.bytecode_runtime(contract_name), + "srcmap": ";".join(source_unit.srcmap_init(contract_name)), + "srcmap-runtime": ";".join(source_unit.srcmap_runtime(contract_name)), "filenames": _convert_filename_to_dict(filename), "libraries": dict(libraries) if libraries else {}, "is_dependency": crytic_compile.is_dependency(filename.absolute), - "userdoc": compilation_unit.natspec[contract_name].userdoc.export(), - "devdoc": compilation_unit.natspec[contract_name].devdoc.export(), + "userdoc": source_unit.natspec[contract_name].userdoc.export(), + "devdoc": source_unit.natspec[contract_name].devdoc.export(), } # Create our root object to contain the contracts and other information. @@ -243,8 +245,7 @@ def generate_standard_export(crytic_compile: "CryticCompile") -> Dict: compilation_units[key] = { "compiler": compiler, - "asts": compilation_unit.asts, - "contracts": contracts, + "source_units": source_unit_dict, "filenames": [ _convert_filename_to_dict(filename) for filename in compilation_unit.filenames ], @@ -256,7 +257,7 @@ def generate_standard_export(crytic_compile: "CryticCompile") -> Dict: "working_dir": str(crytic_compile.working_dir), "type": int(crytic_compile.platform.platform_type_used), "unit_tests": crytic_compile.platform.guessed_tests(), - "crytic_version": "0.0.1", + "crytic_version": "0.0.2", } return output @@ -269,27 +270,27 @@ def _load_from_compile_legacy1(crytic_compile: "CryticCompile", loaded_json: Dic loaded_json (Dict): Json representation of the CryticCompile object """ compilation_unit = CompilationUnit(crytic_compile, "legacy") - compilation_unit.asts = loaded_json["asts"] compilation_unit.compiler_version = CompilerVersion( compiler=loaded_json["compiler"]["compiler"], version=loaded_json["compiler"]["version"], optimized=loaded_json["compiler"]["optimized"], ) for contract_name, contract in loaded_json["contracts"].items(): - compilation_unit.contracts_names.add(contract_name) filename = _convert_dict_to_filename(contract["filenames"]) compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit = compilation_unit.create_source_unit(filename) - compilation_unit.abis[contract_name] = contract["abi"] - compilation_unit.bytecodes_init[contract_name] = contract["bin"] - compilation_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] - compilation_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") - compilation_unit.libraries[contract_name] = contract["libraries"] + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = contract["abi"] + source_unit.bytecodes_init[contract_name] = contract["bin"] + source_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] + source_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") + source_unit.libraries[contract_name] = contract["libraries"] userdoc = contract.get("userdoc", {}) devdoc = contract.get("devdoc", {}) - compilation_unit.natspec[contract_name] = Natspec(userdoc, devdoc) + source_unit.natspec[contract_name] = Natspec(userdoc, devdoc) if contract["is_dependency"]: compilation_unit.crytic_compile.dependencies.add(filename.absolute) @@ -308,6 +309,12 @@ def _load_from_compile_legacy1(crytic_compile: "CryticCompile", loaded_json: Dic filename = _convert_dict_to_filename(contract["filenames"]) compilation_unit.filenames.add(filename) + for path, ast in loaded_json["asts"].items(): + # The following might create lookup issue? + filename = crytic_compile.filename_lookup(path) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast + def _load_from_compile_legacy2(crytic_compile: "CryticCompile", loaded_json: Dict) -> None: """Load from old (old) export @@ -325,7 +332,7 @@ def _load_from_compile_legacy2(crytic_compile: "CryticCompile", loaded_json: Dic optimized=compilation_unit_json["compiler"]["optimized"], ) for contract_name, contract in compilation_unit_json["contracts"].items(): - compilation_unit.contracts_names.add(contract_name) + filename = Filename( absolute=contract["filenames"]["absolute"], relative=contract["filenames"]["relative"], @@ -334,23 +341,24 @@ def _load_from_compile_legacy2(crytic_compile: "CryticCompile", loaded_json: Dic ) compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.abis[contract_name] = contract["abi"] - compilation_unit.bytecodes_init[contract_name] = contract["bin"] - compilation_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] - compilation_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") - compilation_unit.libraries[contract_name] = contract["libraries"] + source_unit = compilation_unit.create_source_unit(filename) + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = contract["abi"] + source_unit.bytecodes_init[contract_name] = contract["bin"] + source_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] + source_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") + source_unit.libraries[contract_name] = contract["libraries"] userdoc = contract.get("userdoc", {}) devdoc = contract.get("devdoc", {}) - compilation_unit.natspec[contract_name] = Natspec(userdoc, devdoc) + source_unit.natspec[contract_name] = Natspec(userdoc, devdoc) if contract["is_dependency"]: crytic_compile.dependencies.add(filename.absolute) crytic_compile.dependencies.add(filename.relative) crytic_compile.dependencies.add(filename.short) crytic_compile.dependencies.add(filename.used) - compilation_unit.asts = compilation_unit_json["asts"] if "filenames" in compilation_unit_json: compilation_unit.filenames = { @@ -358,14 +366,20 @@ def _load_from_compile_legacy2(crytic_compile: "CryticCompile", loaded_json: Dic for filename in compilation_unit_json["filenames"] } else: - # For legay code, we recover the filenames from the contracts list + # For legacy code, we recover the filenames from the contracts list # This is not perfect, as a filename might not be associated to any contract for contract_name, contract in compilation_unit_json["contracts"].items(): filename = _convert_dict_to_filename(contract["filenames"]) compilation_unit.filenames.add(filename) + for path, ast in loaded_json["asts"].items(): + # The following might create lookup issue? + filename = crytic_compile.filename_lookup(path) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast -def _load_from_compile_current(crytic_compile: "CryticCompile", loaded_json: Dict) -> None: + +def _load_from_compile_0_0_1(crytic_compile: "CryticCompile", loaded_json: Dict) -> None: for key, compilation_unit_json in loaded_json["compilation_units"].items(): compilation_unit = CompilationUnit(crytic_compile, key) compilation_unit.compiler_version = CompilerVersion( @@ -375,7 +389,7 @@ def _load_from_compile_current(crytic_compile: "CryticCompile", loaded_json: Dic ) for contracts_data in compilation_unit_json["contracts"].values(): for contract_name, contract in contracts_data.items(): - compilation_unit.contracts_names.add(contract_name) + filename = Filename( absolute=contract["filenames"]["absolute"], relative=contract["filenames"]["relative"], @@ -383,26 +397,81 @@ def _load_from_compile_current(crytic_compile: "CryticCompile", loaded_json: Dic used=contract["filenames"]["used"], ) compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = contract["abi"] + source_unit.bytecodes_init[contract_name] = contract["bin"] + source_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] + source_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") + source_unit.libraries[contract_name] = contract["libraries"] + + userdoc = contract.get("userdoc", {}) + devdoc = contract.get("devdoc", {}) + source_unit.natspec[contract_name] = Natspec(userdoc, devdoc) + + if contract["is_dependency"]: + crytic_compile.dependencies.add(filename.absolute) + crytic_compile.dependencies.add(filename.relative) + crytic_compile.dependencies.add(filename.short) + crytic_compile.dependencies.add(filename.used) + + compilation_unit.filenames = { + _convert_dict_to_filename(filename) + for filename in compilation_unit_json["filenames"] + } + + for path, ast in compilation_unit_json["asts"].items(): + # The following might create lookup issue? + filename = crytic_compile.filename_lookup(path) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast + - compilation_unit.abis[contract_name] = contract["abi"] - compilation_unit.bytecodes_init[contract_name] = contract["bin"] - compilation_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] - compilation_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split( - ";" +def _load_from_compile_current(crytic_compile: "CryticCompile", loaded_json: Dict) -> None: + for key, compilation_unit_json in loaded_json["compilation_units"].items(): + compilation_unit = CompilationUnit(crytic_compile, key) + compilation_unit.compiler_version = CompilerVersion( + compiler=compilation_unit_json["compiler"]["compiler"], + version=compilation_unit_json["compiler"]["version"], + optimized=compilation_unit_json["compiler"]["optimized"], + ) + for contracts_data in compilation_unit_json["contracts"].values(): + for contract_name, contract in contracts_data.items(): + + filename = Filename( + absolute=contract["filenames"]["absolute"], + relative=contract["filenames"]["relative"], + short=contract["filenames"]["short"], + used=contract["filenames"]["used"], ) - compilation_unit.libraries[contract_name] = contract["libraries"] + compilation_unit.filename_to_contracts[filename].add(contract_name) + + source_unit = compilation_unit.create_source_unit(filename) + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = contract["abi"] + source_unit.bytecodes_init[contract_name] = contract["bin"] + source_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] + source_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") + source_unit.libraries[contract_name] = contract["libraries"] userdoc = contract.get("userdoc", {}) devdoc = contract.get("devdoc", {}) - compilation_unit.natspec[contract_name] = Natspec(userdoc, devdoc) + source_unit.natspec[contract_name] = Natspec(userdoc, devdoc) if contract["is_dependency"]: crytic_compile.dependencies.add(filename.absolute) crytic_compile.dependencies.add(filename.relative) crytic_compile.dependencies.add(filename.short) crytic_compile.dependencies.add(filename.used) - compilation_unit.asts = compilation_unit_json["asts"] + + for path, ast in compilation_unit_json["asts"].items: + # The following might create lookup issue? + filename = convert_filename(path, lambda x: x, crytic_compile) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast + compilation_unit.filenames = { _convert_dict_to_filename(filename) for filename in compilation_unit_json["filenames"] @@ -428,6 +497,8 @@ def load_from_compile(crytic_compile: "CryticCompile", loaded_json: Dict) -> Tup elif "crytic_version" not in loaded_json: _load_from_compile_legacy2(crytic_compile, loaded_json) + elif loaded_json["crytic_version"] == "0.0.1": + _load_from_compile_0_0_1(crytic_compile, loaded_json) else: _load_from_compile_current(crytic_compile, loaded_json) diff --git a/crytic_compile/platform/truffle.py b/crytic_compile/platform/truffle.py index ce5f9bd6..33119b3d 100755 --- a/crytic_compile/platform/truffle.py +++ b/crytic_compile/platform/truffle.py @@ -54,17 +54,17 @@ def export_to_truffle(crytic_compile: "CryticCompile", **kwargs: str) -> List[st # Loop for each contract filename. results: List[Dict] = [] - for filename, contract_names in compilation_unit.filename_to_contracts.items(): - for contract_name in contract_names: + for source_unit in compilation_unit.source_units.values(): + for contract_name in source_unit.contracts_names: # Create the informational object to output for this contract output = { "contractName": contract_name, - "abi": compilation_unit.abi(contract_name), - "bytecode": "0x" + compilation_unit.bytecode_init(contract_name), - "deployedBytecode": "0x" + compilation_unit.bytecode_runtime(contract_name), - "ast": compilation_unit.ast(filename.absolute), - "userdoc": compilation_unit.natspec[contract_name].userdoc.export(), - "devdoc": compilation_unit.natspec[contract_name].devdoc.export(), + "abi": source_unit.abi(contract_name), + "bytecode": "0x" + source_unit.bytecode_init(contract_name), + "deployedBytecode": "0x" + source_unit.bytecode_runtime(contract_name), + "ast": source_unit.ast, + "userdoc": source_unit.natspec[contract_name].userdoc.export(), + "devdoc": source_unit.natspec[contract_name].devdoc.export(), } results.append(output) @@ -241,22 +241,23 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: # pylint: disable=raise-missing-from raise InvalidCompilation(txt) - compilation_unit.asts[filename.absolute] = target_loaded["ast"] - crytic_compile.filenames.add(filename) - compilation_unit.filenames.add(filename) + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.ast = target_loaded["ast"] + contract_name = target_loaded["contractName"] - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] - compilation_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = target_loaded["abi"] + source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( "0x", "" ) - compilation_unit.bytecodes_runtime[contract_name] = target_loaded[ + source_unit.bytecodes_runtime[contract_name] = target_loaded[ "deployedBytecode" ].replace("0x", "") - compilation_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = target_loaded[ + source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") + source_unit.srcmaps_runtime[contract_name] = target_loaded[ "deployedSourceMap" ].split(";") diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index c2ce8b6f..723b923d 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -56,36 +56,31 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: assert target in targets_json info = targets_json[target] - contract_filename = convert_filename(target, _relative_to_short, crytic_compile) + filename = convert_filename(target, _relative_to_short, crytic_compile) contract_name = Path(target).parts[-1] - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.abis[contract_name] = info["abi"] - compilation_unit.bytecodes_init[contract_name] = info["bytecode"].replace("0x", "") - compilation_unit.bytecodes_runtime[contract_name] = info["bytecode_runtime"].replace( - "0x", "" - ) + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit.abis[contract_name] = info["abi"] + source_unit.bytecodes_init[contract_name] = info["bytecode"].replace("0x", "") + source_unit.bytecodes_runtime[contract_name] = info["bytecode_runtime"].replace("0x", "") # Vyper does not provide the source mapping for the init bytecode - compilation_unit.srcmaps_init[contract_name] = [] + source_unit.srcmaps_init[contract_name] = [] # info["source_map"]["pc_pos_map"] contains the source mapping in a simpler format # However pc_pos_map_compressed" seems to follow solc's format, so for convenience # We store the same # TODO: create SourceMapping class, so that srcmaps_runtime would store an class # That will give more flexebility to different compilers - compilation_unit.srcmaps_runtime[contract_name] = info["source_map"][ - "pc_pos_map_compressed" - ] - - crytic_compile.filenames.add(contract_filename) - compilation_unit.filenames.add(contract_filename) + source_unit.srcmaps_runtime[contract_name] = info["source_map"]["pc_pos_map_compressed"] # Natspec not yet handled for vyper - compilation_unit.natspec[contract_name] = Natspec({}, {}) + source_unit.natspec[contract_name] = Natspec({}, {}) ast = _get_vyper_ast(target, vyper) - compilation_unit.asts[contract_filename.absolute] = ast + source_unit.ast = ast def is_dependency(self, _path: str) -> bool: """Check if the path is a dependency (not supported for vyper) diff --git a/crytic_compile/platform/waffle.py b/crytic_compile/platform/waffle.py index d6d1dee4..547b1ce7 100755 --- a/crytic_compile/platform/waffle.py +++ b/crytic_compile/platform/waffle.py @@ -189,31 +189,30 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ) contract_name = contract[1] + source_unit = compilation_unit.create_source_unit(filename) - compilation_unit.asts[filename.absolute] = target_all["sources"][contract[0]]["AST"] + source_unit.ast = target_all["sources"][contract[0]]["AST"] crytic_compile.filenames.add(filename) compilation_unit.filenames.add(filename) compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = target_loaded["abi"] userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec - compilation_unit.bytecodes_init[contract_name] = target_loaded["evm"]["bytecode"][ + source_unit.bytecodes_init[contract_name] = target_loaded["evm"]["bytecode"]["object"] + source_unit.srcmaps_init[contract_name] = target_loaded["evm"]["bytecode"][ + "sourceMap" + ].split(";") + source_unit.bytecodes_runtime[contract_name] = target_loaded["evm"]["deployedBytecode"][ "object" ] - compilation_unit.srcmaps_init[contract_name] = target_loaded["evm"]["bytecode"][ + source_unit.srcmaps_runtime[contract_name] = target_loaded["evm"]["deployedBytecode"][ "sourceMap" ].split(";") - compilation_unit.bytecodes_runtime[contract_name] = target_loaded["evm"][ - "deployedBytecode" - ]["object"] - compilation_unit.srcmaps_runtime[contract_name] = target_loaded["evm"][ - "deployedBytecode" - ]["sourceMap"].split(";") compilation_unit.compiler_version = CompilerVersion( compiler=compiler, version=version, optimized=optimized diff --git a/crytic_compile/source_unit.py b/crytic_compile/source_unit.py new file mode 100644 index 00000000..6aa8a905 --- /dev/null +++ b/crytic_compile/source_unit.py @@ -0,0 +1,568 @@ +""" +Module handling the source unit +""" +import re +from typing import Dict, List, Optional, Union, Tuple, Set, TYPE_CHECKING + +from Crypto.Hash import keccak + +from crytic_compile.utils.naming import Filename +from crytic_compile.utils.natspec import Natspec + +if TYPE_CHECKING: + from crytic_compile.compilation_unit import CompilationUnit + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class SourceUnit: + """SourceUnit class""" + + def __init__(self, compilation_unit: "CompilationUnit", filename: Filename): + + self.filename = filename + self.compilation_unit: "CompilationUnit" = compilation_unit + + # ABI, bytecode and srcmap are indexed by contract_name + self._abis: Dict = {} + self._runtime_bytecodes: Dict = {} + self._init_bytecodes: Dict = {} + self._hashes: Dict = {} + self._events: Dict = {} + self._srcmaps: Dict[str, List[str]] = {} + self._srcmaps_runtime: Dict[str, List[str]] = {} + self.ast: Dict = {} + + # Natspec + self._natspec: Dict[str, Natspec] = {} + + # Libraries used by the contract + # contract_name -> (library, pattern) + self._libraries: Dict[str, List[Tuple[str, str]]] = {} + + # set containing all the contract names + self._contracts_name: Set[str] = set() + + # set containing all the contract name without the libraries + self._contracts_name_without_libraries: Optional[Set[str]] = None + + # region ABI + ################################################################################### + ################################################################################### + + @property + def abis(self) -> Dict: + """Return the ABIs + + Returns: + Dict: ABIs (solc/vyper format) (contract name -> ABI) + """ + return self._abis + + def abi(self, name: str) -> Dict: + """Get the ABI from a contract + + Args: + name (str): Contract name + + Returns: + Dict: ABI (solc/vyper format) + """ + return self._abis.get(name, None) + + # endregion + ################################################################################### + ################################################################################### + # region Bytecode + ################################################################################### + ################################################################################### + + @property + def bytecodes_runtime(self) -> Dict[str, str]: + """Return the runtime bytecodes + + Returns: + Dict[str, str]: contract => runtime bytecode + """ + return self._runtime_bytecodes + + @bytecodes_runtime.setter + def bytecodes_runtime(self, bytecodes: Dict[str, str]) -> None: + """Set the bytecodes runtime + + Args: + bytecodes (Dict[str, str]): New bytecodes runtime + """ + self._runtime_bytecodes = bytecodes + + @property + def bytecodes_init(self) -> Dict[str, str]: + """Return the init bytecodes + + Returns: + Dict[str, str]: contract => init bytecode + """ + return self._init_bytecodes + + @bytecodes_init.setter + def bytecodes_init(self, bytecodes: Dict[str, str]) -> None: + """Set the bytecodes init + + Args: + bytecodes (Dict[str, str]): New bytecodes init + """ + self._init_bytecodes = bytecodes + + def bytecode_runtime(self, name: str, libraries: Optional[Dict[str, str]] = None) -> str: + """Return the runtime bytecode of the contract. + If library is provided, patch the bytecode + + Args: + name (str): contract name + libraries (Optional[Dict[str, str]], optional): lib_name => address. Defaults to None. + + Returns: + str: runtime bytecode + """ + runtime = self._runtime_bytecodes.get(name, None) + return self._update_bytecode_with_libraries(runtime, libraries) + + def bytecode_init(self, name: str, libraries: Optional[Dict[str, str]] = None) -> str: + """Return the init bytecode of the contract. + If library is provided, patch the bytecode + + Args: + name (str): contract name + libraries (Optional[Dict[str, str]], optional): lib_name => address. Defaults to None. + + Returns: + str: init bytecode + """ + init = self._init_bytecodes.get(name, None) + return self._update_bytecode_with_libraries(init, libraries) + + # endregion + ################################################################################### + ################################################################################### + # region Source mapping + ################################################################################### + ################################################################################### + + @property + def srcmaps_init(self) -> Dict[str, List[str]]: + """Return the srcmaps init + + Returns: + Dict[str, List[str]]: Srcmaps init (solc/vyper format) + """ + return self._srcmaps + + @property + def srcmaps_runtime(self) -> Dict[str, List[str]]: + """Return the srcmaps runtime + + Returns: + Dict[str, List[str]]: Srcmaps runtime (solc/vyper format) + """ + return self._srcmaps_runtime + + def srcmap_init(self, name: str) -> List[str]: + """Return the srcmap init of a contract + + Args: + name (str): name of the contract + + Returns: + List[str]: Srcmap init (solc/vyper format) + """ + return self._srcmaps.get(name, []) + + def srcmap_runtime(self, name: str) -> List[str]: + """Return the srcmap runtime of a contract + + Args: + name (str): name of the contract + + Returns: + List[str]: Srcmap runtime (solc/vyper format) + """ + return self._srcmaps_runtime.get(name, []) + + # endregion + ################################################################################### + ################################################################################### + # region Libraries + ################################################################################### + ################################################################################### + + @property + def libraries(self) -> Dict[str, List[Tuple[str, str]]]: + """Return the libraries used + + Returns: + Dict[str, List[Tuple[str, str]]]: (contract_name -> [(library, pattern))]) + """ + return self._libraries + + def _convert_libraries_names(self, libraries: Dict[str, str]) -> Dict[str, str]: + """Convert the libraries names + The name in the argument can be the library name, or filename:library_name + The returned dict contains all the names possible with the different solc versions + + Args: + libraries (Dict[str, str]): lib_name => address + + Returns: + Dict[str, str]: lib_name => address + """ + new_names = {} + for (lib, addr) in libraries.items(): + # Prior solidity 0.5 + # libraries were on the format __filename:contract_name_____ + # From solidity 0.5, + # libraries are on the format __$keccak(filename:contract_name)[34]$__ + # https://solidity.readthedocs.io/en/v0.5.7/050-breaking-changes.html#command-line-and-json-interfaces + + lib_4 = "__" + lib + "_" * (38 - len(lib)) + + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(lib.encode("utf-8")) + lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" + + new_names[lib] = addr + new_names[lib_4] = addr + new_names[lib_5] = addr + + for lib_filename, contract_names in self.compilation_unit.filename_to_contracts.items(): + for contract_name in contract_names: + if contract_name != lib: + continue + + lib_with_abs_filename = lib_filename.absolute + ":" + lib + lib_with_abs_filename = lib_with_abs_filename[0:36] + + lib_4 = "__" + lib_with_abs_filename + "_" * (38 - len(lib_with_abs_filename)) + new_names[lib_4] = addr + + lib_with_used_filename = lib_filename.used + ":" + lib + lib_with_used_filename = lib_with_used_filename[0:36] + + lib_4 = "__" + lib_with_used_filename + "_" * (38 - len(lib_with_used_filename)) + new_names[lib_4] = addr + + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(lib_with_abs_filename.encode("utf-8")) + lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" + new_names[lib_5] = addr + + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(lib_with_used_filename.encode("utf-8")) + lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" + new_names[lib_5] = addr + + return new_names + + def _library_name_lookup( + self, lib_name: str, original_contract: str + ) -> Optional[Tuple[str, str]]: + """Do a lookup on a library name to its name used in contracts + The library can be: + - the original contract name + - __X__ following Solidity 0.4 format + - __$..$__ following Solidity 0.5 format + + Args: + lib_name (str): library name + original_contract (str): original contract name + + Returns: + Optional[Tuple[str, str]]: contract_name, library_name + """ + + for filename, contract_names in self.compilation_unit.filename_to_contracts.items(): + for name in contract_names: + if name == lib_name: + return name, name + + # Some platform use only the contract name + # Some use fimename:contract_name + name_with_absolute_filename = filename.absolute + ":" + name + name_with_absolute_filename = name_with_absolute_filename[0:36] + + name_with_used_filename = filename.used + ":" + name + name_with_used_filename = name_with_used_filename[0:36] + + # Solidity 0.4 + solidity_0_4 = "__" + name + "_" * (38 - len(name)) + if solidity_0_4 == lib_name: + return name, solidity_0_4 + + # Solidity 0.4 with filename + solidity_0_4_filename = ( + "__" + + name_with_absolute_filename + + "_" * (38 - len(name_with_absolute_filename)) + ) + if solidity_0_4_filename == lib_name: + return name, solidity_0_4_filename + + # Solidity 0.4 with filename + solidity_0_4_filename = ( + "__" + name_with_used_filename + "_" * (38 - len(name_with_used_filename)) + ) + if solidity_0_4_filename == lib_name: + return name, solidity_0_4_filename + + # Solidity 0.5 + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(name.encode("utf-8")) + v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" + + if v5_name == lib_name: + return name, v5_name + + # Solidity 0.5 with filename + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(name_with_absolute_filename.encode("utf-8")) + v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" + + if v5_name == lib_name: + return name, v5_name + + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(name_with_used_filename.encode("utf-8")) + v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" + + if v5_name == lib_name: + return name, v5_name + + # handle specific case of collision for Solidity <0.4 + # We can only detect that the second contract is meant to be the library + # if there is only two contracts in the codebase + if len(self._contracts_name) == 2: + return next( + ( + (c, "__" + c + "_" * (38 - len(c))) + for c in self._contracts_name + if c != original_contract + ), + None, + ) + + return None + + def libraries_names(self, name: str) -> List[str]: + """Return the names of the libraries used by the contract + + Args: + name (str): contract name + + Returns: + List[str]: libraries used + """ + + if name not in self._libraries: + init = re.findall(r"__.{36}__", self.bytecode_init(name)) + runtime = re.findall(r"__.{36}__", self.bytecode_runtime(name)) + libraires = [self._library_name_lookup(x, name) for x in set(init + runtime)] + self._libraries[name] = [lib for lib in libraires if lib] + return [name for (name, _) in self._libraries[name]] + + def libraries_names_and_patterns(self, name: str) -> List[Tuple[str, str]]: + """Return the names and the patterns of the libraries used by the contract + + Args: + name (str): contract name + + Returns: + List[Tuple[str, str]]: (lib_name, pattern) + """ + + if name not in self._libraries: + init = re.findall(r"__.{36}__", self.bytecode_init(name)) + runtime = re.findall(r"__.{36}__", self.bytecode_runtime(name)) + libraires = [self._library_name_lookup(x, name) for x in set(init + runtime)] + self._libraries[name] = [lib for lib in libraires if lib] + return self._libraries[name] + + def _update_bytecode_with_libraries( + self, bytecode: str, libraries: Union[None, Dict[str, str]] + ) -> str: + """Update the bytecode with the libraries address + + Args: + bytecode (str): bytecode to patch + libraries (Union[None, Dict[str, str]]): pattern => address + + Returns: + str: Patched bytecode + """ + if libraries: + libraries = self._convert_libraries_names(libraries) + for library_found in re.findall(r"__.{36}__", bytecode): + if library_found in libraries: + bytecode = re.sub( + re.escape(library_found), + f"{libraries[library_found]:0>40x}", + bytecode, + ) + return bytecode + + # endregion + ################################################################################### + ################################################################################### + # region Natspec + ################################################################################### + ################################################################################### + + @property + def natspec(self) -> Dict[str, Natspec]: + """Return the natspec of the contracts + + Returns: + Dict[str, Natspec]: Contract name -> Natspec + """ + return self._natspec + + # endregion + ################################################################################### + ################################################################################### + # region Contract Names + ################################################################################### + ################################################################################### + + @property + def contracts_names(self) -> Set[str]: + """Return the contracts names + + Returns: + Set[str]: List of the contracts names + """ + return self._contracts_name + + @contracts_names.setter + def contracts_names(self, names: Set[str]) -> None: + """Set the contract names + + Args: + names (Set[str]): New contracts names + """ + self._contracts_name = names + + @property + def contracts_names_without_libraries(self) -> Set[str]: + """Return the contracts names without the librairies + + Returns: + Set[str]: List of contracts + """ + if self._contracts_name_without_libraries is None: + libraries: List[str] = [] + for contract_name in self._contracts_name: + libraries += self.libraries_names(contract_name) + self._contracts_name_without_libraries = { + l for l in self._contracts_name if l not in set(libraries) + } + return self._contracts_name_without_libraries + + # endregion + ################################################################################### + ################################################################################### + # region Hashes + ################################################################################### + ################################################################################### + + def hashes(self, name: str) -> Dict[str, int]: + """Return the hashes of the functions + + Args: + name (str): contract name + + Returns: + Dict[str, int]: (function name => signature) + """ + if not name in self._hashes: + self._compute_hashes(name) + return self._hashes[name] + + def _compute_hashes(self, name: str) -> None: + """Compute the function hashes + + Args: + name (str): contract name + """ + self._hashes[name] = {} + for sig in self.abi(name): + if "type" in sig: + if sig["type"] == "function": + sig_name = sig["name"] + arguments = ",".join([x["type"] for x in sig["inputs"]]) + sig = f"{sig_name}({arguments})" + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(sig.encode("utf-8")) + self._hashes[name][sig] = int("0x" + sha3_result.hexdigest()[:8], 16) + + # endregion + ################################################################################### + ################################################################################### + # region Events + ################################################################################### + ################################################################################### + + def events_topics(self, name: str) -> Dict[str, Tuple[int, List[bool]]]: + """Return the topics of the contract's events + + Args: + name (str): contract name + + Returns: + Dict[str, Tuple[int, List[bool]]]: event signature => topic hash, [is_indexed for each parameter] + """ + if not name in self._events: + self._compute_topics_events(name) + return self._events[name] + + def _compute_topics_events(self, name: str) -> None: + """Compute the topics of the contract's events + + Args: + name (str): contract name + """ + self._events[name] = {} + for sig in self.abi(name): + if "type" in sig: + if sig["type"] == "event": + sig_name = sig["name"] + arguments = ",".join([x["type"] for x in sig["inputs"]]) + indexes = [x.get("indexed", False) for x in sig["inputs"]] + sig = f"{sig_name}({arguments})" + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(sig.encode("utf-8")) + + self._events[name][sig] = (int("0x" + sha3_result.hexdigest()[:8], 16), indexes) + + # endregion + ################################################################################### + ################################################################################### + # region Metadata + ################################################################################### + ################################################################################### + + def remove_metadata(self) -> None: + """Remove init bytecode + See + http://solidity.readthedocs.io/en/v0.4.24/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode + + Note we dont support recent Solidity version, see https://github.com/crytic/crytic-compile/issues/59 + """ + self._init_bytecodes = { + key: re.sub(r"a165627a7a72305820.{64}0029", r"", bytecode) + for (key, bytecode) in self._init_bytecodes.items() + } + + self._runtime_bytecodes = { + key: re.sub(r"a165627a7a72305820.{64}0029", r"", bytecode) + for (key, bytecode) in self._runtime_bytecodes.items() + } + + # endregion + ################################################################################### + ################################################################################### diff --git a/crytic_compile/utils/zip.py b/crytic_compile/utils/zip.py index f8a25f4e..ab5e7b78 100644 --- a/crytic_compile/utils/zip.py +++ b/crytic_compile/utils/zip.py @@ -38,7 +38,7 @@ def load_from_zip(target: str) -> List["CryticCompile"]: List[CryticCompile]: List of loaded projects """ # pylint: disable=import-outside-toplevel - from crytic_compile.crytic_compile import CryticCompile + from crytic_compile import CryticCompile compilations = [] with ZipFile(target, "r") as file_desc: