diff --git a/crytic_compile/compilation_unit.py b/crytic_compile/compilation_unit.py index 4e3550ee..f53b2670 100644 --- a/crytic_compile/compilation_unit.py +++ b/crytic_compile/compilation_unit.py @@ -1,96 +1,74 @@ -""" -Module handling the compilation unit -""" import uuid +from typing import List from collections import defaultdict from typing import TYPE_CHECKING, Dict, Set, Optional from crytic_compile.compiler.compiler import CompilerVersion -from crytic_compile.source_unit import SourceUnit +from crytic_compile.contract import Contract from crytic_compile.utils.naming import Filename # Cycle dependency if TYPE_CHECKING: from crytic_compile import CryticCompile + from crytic_compile.source_unit import SourceUnit # pylint: disable=too-many-instance-attributes class CompilationUnit: - """CompilationUnit class""" - + """The CompilationUnit class represents a "call" to the compiler. + + Attributes + ---------- + crytic_compile: CryticCompile + A pointer to the associated CryticCompile object + source_units: Dict[Filename, SourceUnit] + The mapping of a Filename object to the associated SourceUnit + compiler_version: CompilerVersion + A pointer to the CompilerVersion object + unique_id: str + The unique identifier provided to this CompilationUnit + """ + def __init__(self, crytic_compile: "CryticCompile", unique_id: str): - """Init the object - - Args: - crytic_compile (CryticCompile): Associated CryticCompile object - unique_id (str): Unique ID used to identify the compilation unit - """ - - # mapping from filename to contract name - self._filename_to_contracts: Dict[Filename, Set[str]] = defaultdict(set) - - # 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() - - # mapping from absolute/relative/used to filename - self._filenames_lookup: Optional[Dict[str, Filename]] = None - - # compiler.compiler - self._compiler_version: CompilerVersion = CompilerVersion( + """Initialize the CompilationUnit object""" + + self._crytic_compile: "CryticCompile" = crytic_compile + self.source_units: Dict[Filename, SourceUnit] = {} + # TODO: Fix compiler_version public / private capabilities + self.compiler_version: CompilerVersion = CompilerVersion( compiler="N/A", version="N/A", optimized=False ) - - self._crytic_compile: "CryticCompile" = crytic_compile - + # Libraries used by the contract + # contract_name -> (library, pattern) + self._libraries: Dict[str, List[Tuple[str, str]]] = {} + if unique_id == ".": unique_id = str(uuid.uuid4()) - - crytic_compile.compilation_units[unique_id] = self # type: ignore - self._unique_id = unique_id - @property - def unique_id(self) -> str: - """Return the compilation unit ID - - Returns: - str: Compilation unit unique ID - """ - return self._unique_id + # TODO: Make the addition of a `CompilationUnit` happen in each `compile()` function instead of `__init__` + crytic_compile.compilation_units[unique_id] = self # type: ignore + # region Getters + ################################################################################### + ################################################################################### + @property def crytic_compile(self) -> "CryticCompile": - """Return the CryticCompile object + """Return the CryticCompile object associated with this CompilationUnit Returns: - CryticCompile: Associated CryticCompile object + CryticCompile: Pointer to the CryticCompile object """ return self._crytic_compile - + @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 + def unique_id(self) -> str: + """Return the compilation unit ID Returns: - SourceUnit: the source unit + str: Compilation unit unique ID """ - return self._source_units[filename] + return self._unique_id @property def asts(self) -> Dict[str, Dict]: @@ -104,28 +82,11 @@ def asts(self) -> Dict[str, Dict]: source_unit.filename.absolute: source_unit.ast for source_unit in self.source_units.values() } - - def create_source_unit(self, filename: Filename) -> SourceUnit: - """ - 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._source_units[filename] = source_unit - return self._source_units[filename] - + # endregion ################################################################################### ################################################################################### + # region Filenames ################################################################################### ################################################################################### @@ -137,63 +98,19 @@ def filenames(self) -> Set[Filename]: Returns: Set[Filename]: Filenames used by the compilation units """ - return self._filenames - - @filenames.setter - def filenames(self, all_filenames: Set[Filename]) -> None: - """Set the filenames - - Args: - all_filenames (Set[Filename]): new filenames - """ - self._filenames = all_filenames + return set(self.source_units.keys()) - @property - def filename_to_contracts(self) -> Dict[Filename, Set[str]]: + def filename_to_contracts(self) -> Dict[Filename, List[ContractUnit]]: """Return a dict mapping the filename to a list of contract declared Returns: Dict[Filename, List[str]]: Filename -> List[contract_name] """ - return self._filename_to_contracts - - def find_absolute_filename_from_used_filename(self, used_filename: str) -> str: - """Return the absolute filename based on the used one - - Args: - used_filename (str): Used filename - - Raises: - ValueError: If the filename is not found - - Returns: - str: Absolute filename - """ - # Note: we could memoize this function if the third party end up using it heavily - # If used_filename is already an absolute pathn no need to lookup - if used_filename in self._crytic_compile.filenames: - return used_filename - d_file = {f.used: f.absolute for f in self._filenames} - if used_filename not in d_file: - raise ValueError("f{filename} does not exist in {d}") - return d_file[used_filename] - - def relative_filename_from_absolute_filename(self, absolute_filename: str) -> str: - """Return the relative file based on the absolute name - - Args: - absolute_filename (str): Absolute filename - - Raises: - ValueError: If the filename is not found - - Returns: - str: Absolute filename - """ - d_file = {f.absolute: f.relative for f in self._filenames} - if absolute_filename not in d_file: - raise ValueError("f{absolute_filename} does not exist in {d}") - return d_file[absolute_filename] + filename_to_contracts: Dict[Filename, List[ContractUnit]] = {} + for filename, source_unit in self.source_units.items(): + filename_to_contracts[filename] = source_unit.contracts.values() + + return filename_to_contracts def filename_lookup(self, filename: str) -> Filename: """Return a crytic_compile.naming.Filename from a any filename @@ -228,28 +145,169 @@ def filename_lookup(self, filename: str) -> Filename: # endregion ################################################################################### ################################################################################### - # region Compiler information + + # region Libraries ################################################################################### ################################################################################### @property - def compiler_version(self) -> "CompilerVersion": - """Return the compiler info + def libraries(self) -> Dict[str, List[Tuple[str, str]]]: + """Return the libraries used Returns: - CompilerVersion: compiler info + Dict[str, List[Tuple[str, str]]]: (contract_name -> [(library, placeholder))]) """ - return self._compiler_version + return self._libraries + + def _library_placeholders_legacy(self, library_name: str, filename: Filename) -> Optional[Dict[str, str]]: + """Return a list of all possible placeholders for a given library name for Solidity version 0.4.x and below. There are a total of three possibilities: + library name, absolute path of the library, or the path used during compilation. - @compiler_version.setter - def compiler_version(self, compiler: CompilerVersion) -> None: - """Set the compiler version + Args: + library_name (str): The name of the library + filename (Filename): A Filename object that holds the absolute / used filepaths for the given library + + Returns: + Optional[Dict[str, str]]: Returns a dictionary of all possible placeholders. Returns an empty dictionary if library_name is empty or None + """ + + + # Guard clause + if library_name == "" or library_name is None: + return None + + # Get absolute and used source paths + absolute_srcpath = filename.absolute + ":" + library_name + used_srcpath = filename.used + ":" + library_name + if len(absolute_srcpath) > 36: + absolute_srcpath = absolute_srcpath[:36] + if len(used_srcpath) > 36: + used_srcpath = used_srcpath[:36] + + retVal = {} + # The basic placeholder is __LibraryName____ + retVal["basicPlaceholder"] = "__" + library_name + "_" * (38 - len(library_name)) + + # The absolute srcpath placeholder is __absolutePath:LibraryName__ + retVal["absoluteSrcPathPlaceholder"] = ( + "__" + + absolute_srcpath + + "_" * (38 - len(absolute_srcpath)) + ) + + # The used srcpath placeholder is __usedPath:LibraryName__ + retVal["usedSrcPathPlaceholder"] = ( + "__" + used_srcpath + "_" * (38 - len(used_srcpath)) + ) + + return retVal + + def _library_placeholders_latest(self, library_name: str, filename: Filename) -> Optional[Dict[str, str]]: + """Return a list of all possible placeholders for a given library name for Solidity version 0.5.x and above. There are a total of three possibilities: + keccak hash of the library name, keccak hash of the absolute path of the library, or the keccak hash of the path used during compilation. Args: - compiler (CompilerVersion): New compiler version + library_name (str): The name of the library + filename (Filename): A Filename object that holds the absolute / used filepaths for the given library + + Returns: + Dict[str, str]: Returns a dictionary of all possible placeholders. Returns None if library_name is empty or None """ - self._compiler_version = compiler + + # Guard clause + if library_name == "" or library_name is None: + return None + + # Get absolute and used source paths + absolute_srcpath = filename.absolute + ":" + library_name + used_srcpath = filename.used + ":" + library_name + + retVal = {} + # The basic placeholder is __keccak256(LibraryName)__ + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(library_name.encode("utf-8")) + retVal["basicPlaceholder"] = "__$" + sha3_result.hexdigest()[:34] + "$__" + + # The absolute srcpath placeholder is __keccak256(absolutePath:LibraryName)__ + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(absolute_srcpath.encode("utf-8")) + retVal["absoluteSrcPathPlaceholder"] = "__$" + sha3_result.hexdigest()[:34] + "$__" + + # The used srcpath placeholder is __keccak256(usedPath:LibraryName)__ + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(used_srcpath.encode("utf-8")) + retVal["usedSrcPathPlaceholder"] = "__$" + sha3_result.hexdigest()[:34] + "$__" + + return retVal + + def _library_placeholder_lookup( + self, placeholder: str, original_contract: str + ) -> Optional[str]: + """Identify the library that is associated with a given placeholder + + Args: + placeholder (str): placeholder + original_contract (str): original contract name where the placeholder was derived from + + Returns: + Optional[str]: library name associated with a given placeholder + """ + + compiler_version = self.compilation_unit.compiler_version + + # Guard clause to ignore library lookups when the compiler is not `solc` or if semantic version is not set + if compiler_version.compiler != "solc" or compiler_version.compiler_version is None: + return None + + for filename, contract_names in self.compilation_unit.filename_to_contracts().items(): + for contract_name in contract_names: + # Call `latest` if solidity version is 0.5.x and above or `legacy` if version is 0.4.x and below + # `placeholders` is the list of possible placeholders associated with a given contract_name + if compiler_version.version.major == 0 and compiler_version.version.minor > 4: + placeholders = self._library_placeholders_latest(contract_name, filename) + elif compiler_version.version.major == 0 and compiler_version.version.minor <= 4: + placeholders = self._library_placeholders_legacy(contract_name, filename) + else: + placeholders = None + + if placeholders and placeholder in placeholders.values(): + return contract_name + + # Handle edge case for Solidity version < 0.4 as a last-ditch effort + if compiler_version.version.major == 0 and compiler_version.version.minor < 4: + 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_and_placeholders(self, contract_name: str) -> List[Tuple[str, str]]: + """Return the library names and their associated placeholders in a given contract. Also sets self._libraries for the given contract_name + + Args: + name (str): contract name + + Returns: + List[Tuple[str, str]]: (library_name, pattern) + """ + if contract_name in self._libraries: + return self._libraries[contract_name] + + init = re.findall(r"__.{36}__", self._init_bytecodes[contract_name]) + runtime = re.findall(r"__.{36}__", self._runtime_bytecodes(contract_name)) + for placeholder in set(init + runtime): + library_name = self._library_placeholder_lookup(placeholder, contract_name) + if library_name: + self._libraries[contract_name].append((library_name, placeholder)) + return self._libraries[contract_name] # endregion ################################################################################### ################################################################################### + diff --git a/crytic_compile/contract.py b/crytic_compile/contract.py new file mode 100644 index 00000000..3f071a0e --- /dev/null +++ b/crytic_compile/contract.py @@ -0,0 +1,279 @@ +""" +Module handling the source unit +""" +import re +from typing import Dict, List, Optional, Union, Tuple, Set, TYPE_CHECKING +import cbor2 + +from Crypto.Hash import keccak + +from crytic_compile.utils.natspec import Natspec +if TYPE_CHECKING: + from crytic_compile.source_unit import SourceUnit +from crytic_compile.utils.naming import combine_filename_name + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class Contract: + """The Contract class represents a single compiled contract within a source unit + + Attributes + ---------- + source_unit: SourceUnit + A pointer to the associated SourceUnit + contract_name: str + The contract's name + abi: Dict + The application binary interface (ABI) of the contract + init_bytecode: str + The initialization bytecode for the contract + runtime_bytecode: str + The runtime bytecode of the contract + srcmap_init: str + The initialization source mapping of the contract + srcmap_runtime: str + The runtime source mapping of the contract + natspec: Natspec + The NatSpec for the contract + function_hashes: Dict + The contract's function signatures and their associated hashes + events: Dict + The contract's event signatures and their associated hashes + placeholder_set: Set[str] + The set of library placeholders identified in the contract + """ + + def __init__(self, source_unit: "SourceUnit", contract_name: str, abi: Dict, init_bytecode: str, runtime_bytecode: str, srcmap_init: str, srcmap_runtime: str, natspec: Natspec): + """Initialize the Contract class""" + + self._source_unit: SourceUnit = source_unit + self._contract_name: str = contract_name + self._abi: Dict = abi + self._init_bytecode: str = init_bytecode + self._runtime_bytecode: str = runtime_bytecode + self._srcmap_init: str = srcmap_init + self._srcmap_runtime: str = srcmap_runtime + self._natspec: Natspec = natspec + self._function_hashes: Dict = self._compute_function_hashes() + self._events: Dict = self._compute_topics_events() + self._placeholder_set: Set[str] = self._compute_placeholder_set() + # TODO: Maybe introduce metadata in a future PR + + # region Getters + ################################################################################### + ################################################################################### + + @property + def source_unit(self) -> SourceUnit: + """Return the SourceUnit associated with this Contract object + + Returns: + SourceUnit: Pointer to the associated SourceUnit + """ + return self._source_unit + + @property + def contract_name(self) -> str: + """Return the name of the contract + + Returns: + str: Contract name + """ + return self._contract_name + + @property + def abi(self) -> Dict: + """Return the ABI of the contract + + Returns: + Dict: ABI + """ + return self._abi + + @property + def init_bytecode(self) -> SourceUnit: + """Return the init bytecode of the contract + + Returns: + str: Init bytecode + """ + return self._init_bytecode + + @property + def runtime_bytecode(self) -> SourceUnit: + """Return the runtime bytecode of the contract + + Returns: + str: Runtime bytecode + """ + return self._runtime_bytecode + + @property + def srcmap_init(self) -> str: + """Return the init source mapping of the contract + + Returns: + str: The initialization source mapping + """ + return self._srcmap_init + + @property + def srcmap_runtime(self) -> str: + """Return the runtime source mapping of the contract + + Returns: + str: The runtime source mapping + """ + return self._srcmap_runtime + + @property + def natspec(self) -> Natspec: + """Returns the Natspec associated with the contract + + Returns: + Natspec: Natspec of contract + """ + return self._natspec + + @property + def function_hashes(self) -> Dict[str, int]: + """Return a mapping of function signatures to keccak hashes within a contract + + Returns: + Dict[str, int]: Mapping of function signature to keccak hash + """ + return self._function_hashes + + @property + def events(self) -> Dict[str, Tuple[int, List[bool]]]: + """Return a mapping of event signatures to keccak hashes within the contract + + Returns: + Dict[str, Tuple[int, List[bool]]]: Mapping of event signature to keccak hash in addition to which input parameters are indexed + """ + return self._events + + @property + def placeholder_set(self) -> Set(str): + """Returns any library placeholders found in the contract + + Returns: + Set(str): Set of library placeholders + """ + return self._placeholder_set + + + # endregion + ################################################################################### + ################################################################################### + + # region Internal functions + ################################################################################### + ################################################################################### + + def _compute_placeholder_set(self) -> Set[str]: + """Returns all library placeholders within the init bytecode of the contract. + + If there are different placeholders within the runtime bytecode of a contract, which is true for a compilation platform like Brownie, + then this function will not find those placeholders. + + Returns: + Set[str]: This is the list of placeholders identified in the init bytecode of the contract + """ + + # Use regex to find __PLACEHOLDER__ strings + init = re.findall(r"__(\$[0-9a-zA-Z]*\$|\w*)__", self.init_bytecode) + return set(init) + + def _compute_function_hashes(self) -> Dict[str, int]: + """Compute the function hashes + + Returns: + Dict[str, int]: Returns a dictionary mapping the function signature to the keccak hash as a 256-bit integer + """ + + function_hashes: Dict[str, int] = {} + + # Iterate through each key in the ABI + for function in self._abi: + function_type = function.get("type", "N/A") + # If the object describes a function + if function_type == "function": + # Grab the name + try: + function_name = function["name"] + except KeyError: + raise KeyError + + # Create a comma-delimited string containing all the input arguments + try: + function_args = ",".join([input["type"] for input in function["inputs"]]) + except KeyError: + raise KeyError + + # Format and hash the function signature + sig = f"{function_name}({function_args})" + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(sig.encode("utf-8")) + + # Update mapping + function_hashes[sig] = int("0x" + sha3_result.hexdigest()[:8], 16) + + return function_hashes + + def _compute_topics_events(self) -> Dict[str, Tuple[int, List[bool]]]: + """Computes each event's signature, keccak hash, and which parameters are indexed + + Returns: + Dict[str, Tuple[int, List[bool]]]: Returns a mapping from event signature to a tuple where the integer is the 256-bit keccak + hash and the list tells you which parameters are indexed + """ + events: Dict[str, Tuple[int, List[bool]]] = {} + + # Iterate through each key in the ABI + for event in self._abi: + event_type = event.get("type", "N/A") + # If the object describes an event + if event_type == "event": + # Grab the name + try: + event_name = event["name"] + except KeyError: + raise KeyError + + # Create a comma-delimited string containing all the input arguments + try: + event_args = ",".join([input["type"] for input in event["inputs"]]) + except KeyError: + raise KeyError + + # Figure out which input arguments are indexed + indexed = [input.get("indexed", False) for input in event["inputs"]] + + # Format and hash the event signature + sig = f"{event_name}({event_args})" + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(sig.encode("utf-8")) + + # Update mapping + events[sig] = (int("0x" + sha3_result.hexdigest()[:8], 16), indexed) + + return events + + # endregion + ################################################################################### + ################################################################################### + + # region Metadata + ################################################################################### + ################################################################################### + + # TODO: Metadata parsing is broken. Needs to be fixed in a separate PR + def metadata_of(self, name: str) -> Dict[str, Union[str, bool]]: + return None + + def remove_metadata(self) -> None: + return None + + # endregion + ################################################################################### + ################################################################################### \ No newline at end of file diff --git a/crytic_compile/platform/all_export.py b/crytic_compile/platform/all_export.py index aeab9882..aa5f82bc 100644 --- a/crytic_compile/platform/all_export.py +++ b/crytic_compile/platform/all_export.py @@ -4,12 +4,10 @@ from crytic_compile.platform.archive import export_to_archive from crytic_compile.platform.solc import export_to_solc from crytic_compile.platform.standard import export_to_standard -from crytic_compile.platform.truffle import export_to_truffle PLATFORMS_EXPORT = { "standard": export_to_standard, "crytic-compile": export_to_standard, "solc": export_to_solc, - "truffle": export_to_truffle, "archive": export_to_archive, } diff --git a/crytic_compile/platform/brownie.py b/crytic_compile/platform/brownie.py index 745df321..4a956213 100755 --- a/crytic_compile/platform/brownie.py +++ b/crytic_compile/platform/brownie.py @@ -15,6 +15,8 @@ from crytic_compile.platform.exceptions import InvalidCompilation from crytic_compile.platform.types import Type from crytic_compile.utils.naming import Filename, convert_filename +from crytic_compile.source_unit import SourceUnit +from crytic_compile.contract import Contract # Cycle dependency from crytic_compile.utils.natspec import Natspec @@ -102,6 +104,12 @@ def is_supported(target: str, **kwargs: str) -> bool: return False # < 1.1.0: brownie-config.json # >= 1.1.0: brownie-config.yaml + + # If there is both foundry and hardhat, foundry takes priority + # TODO: See if we want to prioritize foundry over brownie + if os.path.isfile(os.path.join(target, "foundry.toml")): + return False + return ( os.path.isfile(os.path.join(target, "brownie-config.json")) or os.path.isfile(os.path.join(target, "brownie-config.yaml")) @@ -144,7 +152,8 @@ def _iterate_over_files( version = None compilation_unit = CompilationUnit(crytic_compile, str(target)) - + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit + for original_filename in filenames: with open(original_filename, encoding="utf8") as f_file: target_loaded: Dict = json.load(f_file) @@ -171,30 +180,23 @@ def _iterate_over_files( filename: Filename = convert_filename( filename_txt, _relative_to_short, crytic_compile, working_dir=target ) + ast = target_loaded["ast"] + source_unit = SourceUnit(compilation_unit, filename, ast) + compilation_unit.source_units[filename] = source_unit - 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) - - 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", "") - source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") - source_unit.srcmaps_runtime[contract_name] = target_loaded["deployedSourceMap"].split( - ";" - ) - + abi = target_loaded["abi"] + init_bytecode = target_loaded["bytecode"].replace("0x", "") + runtime_bytecode = target_loaded["deployedBytecode"].replace("0x", "") + srcmap_init = target_loaded["sourceMap"] + srcmap_runtime = target_loaded["deployedSourceMap"] userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - source_unit.natspec[contract_name] = natspec - + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract + + # TODO: What is going on here 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 577cc08f..d9e223d8 100755 --- a/crytic_compile/platform/buidler.py +++ b/crytic_compile/platform/buidler.py @@ -15,6 +15,8 @@ from crytic_compile.utils.naming import convert_filename, extract_name from crytic_compile.utils.natspec import Natspec from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.contract import Contract +from crytic_compile.source_unit import SourceUnit from .abstract_platform import AbstractPlatform # Handle cycle @@ -97,6 +99,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: raise InvalidCompilation(txt) compilation_unit = CompilationUnit(crytic_compile, str(target_solc_file)) + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit (compiler, version_from_config, optimized) = _get_version_from_config(Path(cache_directory)) @@ -107,69 +110,66 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: skip_filename = compilation_unit.compiler_version.version in [ f"0.4.{x}" for x in range(0, 10) ] - + with open(target_solc_file, encoding="utf8") as file_desc: targets_json = json.load(file_desc) - - if "contracts" in targets_json: - for original_filename, contracts_info in targets_json["contracts"].items(): + + if "sources" not in targets_json or "contracts" not in targets_json: + LOGGER.error( + "Some error" + ) + raise InvalidCompilation( + f"Incorrect json file generated" + ) + + for path, info in targets_json["sources"].items(): + if path.startswith("ontracts/") and not skip_directory_name_fix: + path = "c" + path + + if skip_filename: filename = convert_filename( - original_filename, + self._target, 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) - - if ( - original_filename.startswith("ontracts/") - and not skip_directory_name_fix - ): - original_filename = "c" + original_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["evm"]["bytecode"][ - "object" - ] - source_unit.bytecodes_runtime[contract_name] = info["evm"][ - "deployedBytecode" - ]["object"] - source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ - "sourceMap" - ].split(";") - source_unit.srcmaps_runtime[contract_name] = info["evm"][ - "deployedBytecode" - ]["sourceMap"].split(";") - userdoc = info.get("userdoc", {}) - devdoc = info.get("devdoc", {}) - natspec = Natspec(userdoc, devdoc) - source_unit.natspec[contract_name] = natspec - - if "sources" in targets_json: - for path, info in targets_json["sources"].items(): - - if path.startswith("ontracts/") and not skip_directory_name_fix: - path = "c" + path - - if skip_filename: - path = convert_filename( - self._target, - relative_to_short, - crytic_compile, - working_dir=buidler_working_dir, - ) - else: - path = convert_filename( - path, relative_to_short, crytic_compile, working_dir=buidler_working_dir - ) - source_unit = compilation_unit.create_source_unit(path) - source_unit.ast = info["ast"] + else: + filename = convert_filename( + path, relative_to_short, crytic_compile, working_dir=buidler_working_dir + ) + + ast = info["ast"] + source_unit = SourceUnit(compilation_unit, filename, ast) + compilation_unit.source_units[filename] = source_unit + + 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.source_units[filename] + + for original_contract_name, info in contracts_info.items(): + if ( + original_filename.startswith("ontracts/") + and not skip_directory_name_fix + ): + original_filename = "c" + original_filename + + contract_name = extract_name(original_contract_name) + abi = info["abi"] + init_bytecode = info["evm"]["bytecode"]["object"].replace("0x", "") + runtime_bytecode = info["evm"]["deployedBytecode"]["object"].replace("0x", "") + srcmap_init = info["evm"]["bytecode"][ "sourceMap"] + srcmap_runtime = info["evm"]["deployedBytecode"]["sourceMap"] + userdoc = info.get("userdoc", {}) + devdoc = info.get("devdoc", {}) + natspec = Natspec(userdoc, devdoc) + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract + def clean(self, **kwargs: str) -> None: # TODO: call "buldler clean"? diff --git a/crytic_compile/platform/dapp.py b/crytic_compile/platform/dapp.py index 8c3aee7d..20b7ed28 100755 --- a/crytic_compile/platform/dapp.py +++ b/crytic_compile/platform/dapp.py @@ -15,6 +15,8 @@ from typing import TYPE_CHECKING, List, Optional from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.contract import Contract +from crytic_compile.source_unit import SourceUnit from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.types import Type @@ -57,6 +59,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: _run_dapp(self._target) compilation_unit = CompilationUnit(crytic_compile, str(self._target)) + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit compilation_unit.compiler_version = _get_version(self._target) @@ -68,14 +71,21 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: version: Optional[str] = None if "version" in targets_json: version = re.findall(r"\d+\.\d+\.\d+", targets_json["version"])[0] - + + for path, info in targets_json["sources"].items(): + filename = convert_filename( + path, _relative_to_short, crytic_compile, working_dir=self._target + ) + ast = info["ast"] + source_unit = SourceUnit(compilation_unit, filename, ast) + compilation_unit.source_units[filename] = source_unit + for original_filename, contracts_info in targets_json["contracts"].items(): - filename = convert_filename( original_filename, lambda x: x, crytic_compile, self._target ) - source_unit = compilation_unit.create_source_unit(filename) + source_unit = compilation_unit.source_units[filename] for original_contract_name, info in contracts_info.items(): if "metadata" in info: @@ -86,36 +96,24 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: and "enabled" in metadata["settings"]["optimizer"] ): optimized |= metadata["settings"]["optimizer"]["enabled"] + contract_name = extract_name(original_contract_name) - 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["evm"]["bytecode"]["object"] - source_unit.bytecodes_runtime[contract_name] = info["evm"]["deployedBytecode"][ - "object" - ] - source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ - "sourceMap" - ].split(";") - source_unit.srcmaps_runtime[contract_name] = info["evm"]["bytecode"][ - "sourceMap" - ].split(";") + abi = info["abi"] + init_bytecode = info["evm"]["bytecode"]["object"].replace("0x", "") + runtime_bytecode = info["evm"]["deployedBytecode"]["object"].replace("0x", "") + srcmap_init = info["evm"]["bytecode"][ "sourceMap"] + srcmap_runtime = info["evm"]["deployedBytecode"]["sourceMap"] # TODO: Potential bug here userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - source_unit.natspec[contract_name] = natspec - + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract + if version is None: metadata = json.loads(info["metadata"]) version = re.findall(r"\d+\.\d+\.\d+", metadata["compiler"]["version"])[0] - for path, info in targets_json["sources"].items(): - path = convert_filename( - path, _relative_to_short, crytic_compile, working_dir=self._target - ) - source_unit = compilation_unit.create_source_unit(path) - source_unit.ast = info["ast"] + compilation_unit.compiler_version = CompilerVersion( compiler="solc", version=version, optimized=optimized diff --git a/crytic_compile/platform/embark.py b/crytic_compile/platform/embark.py index f81c20d6..2f2bd34a 100755 --- a/crytic_compile/platform/embark.py +++ b/crytic_compile/platform/embark.py @@ -11,6 +11,8 @@ from typing import TYPE_CHECKING, List from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.contract import Contract +from crytic_compile.source_unit import SourceUnit from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.exceptions import InvalidCompilation @@ -115,6 +117,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: "(npm install -g embark)? Is embark-contract-info installed? (npm install -g embark)." ) compilation_unit = CompilationUnit(crytic_compile, str(self._target)) + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit compilation_unit.compiler_version = _get_version(self._target) @@ -124,10 +127,10 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: filename = convert_filename( k, _relative_to_short, crytic_compile, working_dir=self._target ) - source_unit = compilation_unit.create_source_unit(filename) - source_unit.ast = ast + source_unit = SourceUnit(compilation_unit, filename, ast) + compilation_unit.source_units[filename] = source_unit - if not "contracts" in targets_loaded: + if "sources" not in targets_loaded or "contracts" not in targets_loaded: LOGGER.error( "Incorrect json file generated. Are you using %s >= 1.1.0?", plugin_name ) @@ -136,36 +139,25 @@ 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) filename = convert_filename( extract_filename(original_contract_name), _relative_to_short, crytic_compile, working_dir=self._target, ) - - 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: - source_unit.abis[contract_name] = info["abi"] - if "bin" in info: - source_unit.bytecodes_init[contract_name] = info["bin"].replace("0x", "") - if "bin-runtime" in info: - source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"].replace( - "0x", "" - ) - if "srcmap" in info: - source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") - if "srcmap-runtime" in info: - source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") - + source_unit = compilation_unit.source_units[filename] + + contract_name = extract_name(original_contract_name) + abi = info["abi"] + init_bytecode = info["bin"].replace("0x", "") + runtime_bytecode = info["bin-runtime"].replace("0x", "") + srcmap_init = info["srcmap"] + srcmap_runtime = info["srcmap-runtime"] userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - source_unit.natspec[contract_name] = natspec + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract def clean(self, **_kwargs: str) -> None: """Clean compilation artifacts diff --git a/crytic_compile/platform/etherlime.py b/crytic_compile/platform/etherlime.py index 7504c15e..ce1e873e 100755 --- a/crytic_compile/platform/etherlime.py +++ b/crytic_compile/platform/etherlime.py @@ -13,6 +13,8 @@ from typing import TYPE_CHECKING, List, Optional, Any from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.contract import Contract +from crytic_compile.source_unit import SourceUnit from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.exceptions import InvalidCompilation @@ -113,6 +115,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: Any) -> None: compiler = "solc-js" compilation_unit = CompilationUnit(crytic_compile, str(self._target)) + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit for file in filenames: with open(file, encoding="utf8") as file_desc: @@ -130,30 +133,21 @@ 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) - - source_unit = compilation_unit.create_source_unit(filename) - - source_unit.ast = target_loaded["ast"] + ast = target_loaded["ast"] + source_unit = SourceUnit(compilation_unit, filename, ast) + compilation_unit.source_units[filename] = source_unit + contract_name = target_loaded["contractName"] - - compilation_unit.filename_to_contracts[filename].add(contract_name) - 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", "") - source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") - source_unit.srcmaps_runtime[contract_name] = target_loaded[ - "deployedSourceMap" - ].split(";") - + abi = target_loaded["abi"] + init_bytecode = target_loaded["bytecode"].replace("0x", "") + runtime_bytecode = target_loaded["deployedBytecode"].replace("0x", "") + srcmap_init = target_loaded["sourceMap"] + srcmap_runtime = target_loaded["deployedSourceMap"] userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - source_unit.natspec[contract_name] = natspec + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract 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 d58d678e..e555d0d3 100644 --- a/crytic_compile/platform/etherscan.py +++ b/crytic_compile/platform/etherscan.py @@ -78,8 +78,8 @@ def _handle_bytecode(crytic_compile: "CryticCompile", target: str, result_b: byt 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) + #source_unit.contracts_names.add(contract_name) + #compilation_unit.filename_to_contracts[contract_filename].add(contract_name) source_unit.abis[contract_name] = {} source_unit.bytecodes_init[contract_name] = bytecode source_unit.bytecodes_runtime[contract_name] = "" @@ -350,7 +350,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ] compilation_unit = CompilationUnit(crytic_compile, contract_name) - + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit + compilation_unit.compiler_version = CompilerVersion( compiler=kwargs.get("solc", "solc"), version=compiler_version, diff --git a/crytic_compile/platform/hardhat.py b/crytic_compile/platform/hardhat.py index 2efc9336..3083c1cc 100755 --- a/crytic_compile/platform/hardhat.py +++ b/crytic_compile/platform/hardhat.py @@ -20,10 +20,13 @@ # Handle cycle from crytic_compile.platform.solc import relative_to_short from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.contract import Contract +from crytic_compile.source_unit import SourceUnit if TYPE_CHECKING: from crytic_compile import CryticCompile + LOGGER = logging.getLogger("CryticCompile") # pylint: disable=too-many-locals @@ -59,81 +62,90 @@ def hardhat_like_parsing( # The file here should always ends .json, but just in case use ife uniq_id = file if ".json" not in file else file[0:-5] compilation_unit = CompilationUnit(crytic_compile, uniq_id) - + crytic_compile.compilation_units[uniq_id] = compilation_unit + with open(build_info, encoding="utf8") as file_desc: loaded_json = json.load(file_desc) targets_json = loaded_json["output"] - version_from_config = loaded_json["solcVersion"] # TODO supper vyper + version = loaded_json.get["solcVersion"] # TODO supper vyper input_json = loaded_json["input"] compiler = "solc" if input_json["language"] == "Solidity" else "vyper" optimized = input_json["settings"]["optimizer"]["enabled"] compilation_unit.compiler_version = CompilerVersion( - compiler=compiler, version=version_from_config, optimized=optimized + compiler=compiler, version=version, optimized=optimized ) + #compiler_version = compilation_unit.compiler_version.version + + #skip_filename = False + #if compiler_version.major == 0 and compiler_version.minor == 4 and compiler_version.patch >= 0 and compiler_version.patch < 10: + # skip_filename = True + skip_filename = compilation_unit.compiler_version.version in [ f"0.4.{x}" for x in range(0, 10) ] - if "contracts" in targets_json: - for original_filename, contracts_info in targets_json["contracts"].items(): - + if "sources" not in targets_json or "contracts" not in targets_json: + LOGGER.error( + "Malformed compilation JSON output" + ) + raise InvalidCompilation( + f"Malformed compilation JSON output" + ) + + for path, info in targets_json["sources"].items(): + if skip_filename: filename = convert_filename( - original_filename, + target, relative_to_short, crytic_compile, working_dir=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) - - 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["evm"]["bytecode"][ - "object" - ] - source_unit.bytecodes_runtime[contract_name] = info["evm"][ - "deployedBytecode" - ]["object"] - source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ - "sourceMap" - ].split(";") - source_unit.srcmaps_runtime[contract_name] = info["evm"][ - "deployedBytecode" - ]["sourceMap"].split(";") - userdoc = info.get("userdoc", {}) - devdoc = info.get("devdoc", {}) - natspec = Natspec(userdoc, devdoc) - source_unit.natspec[contract_name] = natspec - - if "sources" in targets_json: - for path, info in targets_json["sources"].items(): - if skip_filename: - path = convert_filename( - target, - relative_to_short, - crytic_compile, - working_dir=working_dir, - ) - else: - path = convert_filename( - path, - relative_to_short, - crytic_compile, - working_dir=working_dir, - ) - - source_unit = compilation_unit.create_source_unit(path) - source_unit.ast = info["ast"] - + else: + filename = convert_filename( + path, + relative_to_short, + crytic_compile, + working_dir=working_dir, + ) + ast = info["ast"] + source_unit = SourceUnit(compilation_unit, filename, ast) + compilation_unit.source_units[filename] = source_unit + + for original_filename, contracts_info in targets_json["contracts"].items(): + + filename = convert_filename( + original_filename, + relative_to_short, + crytic_compile, + working_dir=working_dir, + ) + + source_unit = compilation_unit.source_units[filename] + for original_contract_name, info in contracts_info.items(): + contract_name = extract_name(original_contract_name) + + abi = info["abi"] + init_bytecode = info["evm"]["bytecode"][ + "object" + ].replace("0x", "") + runtime_bytecode = info["evm"][ + "deployedBytecode" + ]["object"].replace("0x", "") + srcmap_init = info["evm"]["bytecode"][ + "sourceMap" + ] + srcmap_runtime = info["evm"][ + "deployedBytecode" + ]["sourceMap"] + userdoc = info.get("userdoc", {}) + devdoc = info.get("devdoc", {}) + natspec = Natspec(userdoc, devdoc) + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract class Hardhat(AbstractPlatform): """ diff --git a/crytic_compile/platform/solc.py b/crytic_compile/platform/solc.py index 3ecdc1c4..b0622b3f 100644 --- a/crytic_compile/platform/solc.py +++ b/crytic_compile/platform/solc.py @@ -11,6 +11,8 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Union, Any from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.contract import Contract +from crytic_compile.source_unit import SourceUnit from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.exceptions import InvalidCompilation @@ -35,21 +37,21 @@ def _build_contract_data(compilation_unit: "CompilationUnit") -> Dict: contracts = {} 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)) + for contract_name, contract in source_unit.contracts.items(): + abi = str(contract.abi()) 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(source_unit.srcmap_init(contract_name)), - "srcmap-runtime": ";".join(source_unit.srcmap_runtime(contract_name)), + "srcmap": ";".join(contract.init_srcmap), + "srcmap-runtime": ";".join(contract.runtime_srcmap), "abi": abi, - "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(), + "bin": contract.init_bytecode, + "bin-runtime": contract.runtime_bytecode, + "userdoc": contract.natspec.userdoc.export(), + "devdoc": contract.natspec.devdoc.export(), } return contracts @@ -71,6 +73,7 @@ def export_to_solc_from_compilation_unit( contracts = _build_contract_data(compilation_unit) # Create additional informational objects. + # TODO: Fix without the use of the `asts` function sources = {filename: {"AST": ast} for (filename, ast) in compilation_unit.asts.items()} source_list = [x.absolute for x in compilation_unit.filenames] @@ -150,6 +153,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: solc_working_dir = kwargs.get("solc_working_dir", None) force_legacy_json = kwargs.get("solc_force_legacy_json", False) compilation_unit = CompilationUnit(crytic_compile, str(self._target)) + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit targets_json = _get_targets_json(compilation_unit, self._target, **kwargs) @@ -160,26 +164,29 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: skip_filename = compilation_unit.compiler_version.version in [ f"0.4.{x}" for x in range(0, 10) ] - - solc_handle_contracts( - targets_json, skip_filename, compilation_unit, self._target, solc_working_dir - ) - + if "sources" in targets_json: for path, info in targets_json["sources"].items(): if skip_filename: - path = convert_filename( + filename = convert_filename( self._target, relative_to_short, crytic_compile, working_dir=solc_working_dir, ) else: - path = convert_filename( + filename = convert_filename( path, relative_to_short, crytic_compile, working_dir=solc_working_dir ) - source_unit = compilation_unit.create_source_unit(path) - source_unit.ast = info["AST"] + ast = info["AST"] + source_unit = SourceUnit(compilation_unit, filename, ast) + compilation_unit.source_units[filename] = source_unit + + solc_handle_contracts( + targets_json, skip_filename, compilation_unit, self._target, solc_working_dir + ) + + def clean(self, **_kwargs: str) -> None: """Clean compilation artifacts @@ -309,43 +316,37 @@ def solc_handle_contracts( solc_working_dir (Optional[str]): Working directory for running solc """ is_above_0_8 = _is_at_or_above_minor_version(compilation_unit, 8) - + # for solc < 0.4.10 we cant retrieve the filename from the ast + + if skip_filename: + filename = convert_filename( + target, + relative_to_short, + compilation_unit.crytic_compile, + working_dir=solc_working_dir, + ) + else: + filename = convert_filename( + extract_filename(original_contract_name), + relative_to_short, + compilation_unit.crytic_compile, + working_dir=solc_working_dir, + ) + source_unit = compilation_unit.source_units[filename] + if "contracts" in targets_json: - for original_contract_name, info in targets_json["contracts"].items(): contract_name = extract_name(original_contract_name) - # for solc < 0.4.10 we cant retrieve the filename from the ast - if skip_filename: - filename = convert_filename( - target, - relative_to_short, - compilation_unit.crytic_compile, - working_dir=solc_working_dir, - ) - else: - filename = convert_filename( - extract_filename(original_contract_name), - relative_to_short, - compilation_unit.crytic_compile, - working_dir=solc_working_dir, - ) - - 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"] - ) - 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"] + abi = info["abi"] + init_bytecode = info["bin"].replace("0x", "") + runtime_bytecode = info["bin-runtime"].replace("0x", "") + srcmap_init = info["srcmap"] + srcmap_runtime = info["srcmap-runtime"] + userdoc = info.get("userdoc", {}) + devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - source_unit.natspec[contract_name] = natspec - + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract def _is_at_or_above_minor_version(compilation_unit: "CompilationUnit", version: int) -> bool: """Checks if the solc version is at or above(=newer) a given minor (0.x.0) version @@ -428,13 +429,14 @@ def _build_options(compiler_version: CompilerVersion, force_legacy_json: bool) - old_04_versions = [f"0.4.{x}" for x in range(0, 12)] # compact-format was removed from solc 0.8.10 explicit_compact_format = ( - [f"0.4.{x}" for x in range(13, 27)] + [f"0.4.{x}" for x in range(12, 27)] + [f"0.5.{x}" for x in range(0, 18)] + [f"0.6.{x}" for x in range(0, 13)] + [f"0.7.{x}" for x in range(0, 7)] + [f"0.8.{x}" for x in range(0, 10)] ) assert compiler_version.version + if compiler_version.version in old_04_versions or compiler_version.version.startswith("0.3"): return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc" if force_legacy_json: diff --git a/crytic_compile/platform/solc_standard_json.py b/crytic_compile/platform/solc_standard_json.py index ab500458..3f62da59 100644 --- a/crytic_compile/platform/solc_standard_json.py +++ b/crytic_compile/platform/solc_standard_json.py @@ -10,6 +10,8 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Union, Any from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.source_unit import SourceUnit +from crytic_compile.contract import Contract from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform.exceptions import InvalidCompilation from crytic_compile.platform.solc import Solc, get_version, is_optimized, relative_to_short @@ -260,68 +262,63 @@ def parse_standard_json_output( """ skip_filename = compilation_unit.compiler_version.version in [f"0.4.{x}" for x in range(0, 10)] + if "sources" not in targets_json or "contracts" not in targets_json: + LOGGER.error( + "Some error" + ) + raise InvalidCompilation( + f"Incorrect json file generated" + ) - if "contracts" in targets_json: - for file_path, file_contracts in targets_json["contracts"].items(): - for contract_name, info in file_contracts.items(): - # for solc < 0.4.10 we cant retrieve the filename from the ast - if skip_filename: - filename = convert_filename( - file_path, - relative_to_short, - compilation_unit.crytic_compile, - working_dir=solc_working_dir, - ) - else: - filename = convert_filename( - file_path, - relative_to_short, - compilation_unit.crytic_compile, - working_dir=solc_working_dir, - ) - - 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) - source_unit.natspec[contract_name] = natspec - - source_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"]["object"] - source_unit.bytecodes_runtime[contract_name] = info["evm"]["deployedBytecode"][ - "object" - ] - source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ - "sourceMap" - ].split(";") - source_unit.srcmaps_runtime[contract_name] = info["evm"]["deployedBytecode"][ - "sourceMap" - ].split(";") - - if "sources" in targets_json: - for path, info in targets_json["sources"].items(): - if skip_filename: - path = convert_filename( - path, - relative_to_short, - compilation_unit.crytic_compile, - working_dir=solc_working_dir, - ) - else: - path = convert_filename( - path, - relative_to_short, - compilation_unit.crytic_compile, - working_dir=solc_working_dir, - ) - source_unit = compilation_unit.create_source_unit(path) - - source_unit.ast = info.get("ast") - + for path, info in targets_json["sources"].items(): + if skip_filename: + filename = convert_filename( + path, + relative_to_short, + compilation_unit.crytic_compile, + working_dir=solc_working_dir, + ) + else: + filename = convert_filename( + path, + relative_to_short, + compilation_unit.crytic_compile, + working_dir=solc_working_dir, + ) + ast = info["ast"] + source_unit = SourceUnit(compilation_unit, filename, ast) + compilation_unit.source_units[filename] = source_unit + + for file_path, file_contracts in targets_json["contracts"].items(): + # TODO: There is a bug here. Regardless of `skip_filename`, `file_path` is used + # for solc < 0.4.10 we cant retrieve the filename from the ast + if skip_filename: + filename = convert_filename( + file_path, + relative_to_short, + compilation_unit.crytic_compile, + working_dir=solc_working_dir, + ) + else: + filename = convert_filename( + file_path, + relative_to_short, + compilation_unit.crytic_compile, + working_dir=solc_working_dir, + ) + source_unit = compilation_unit.source_units[filename] + for contract_name, info in file_contracts.items(): + abi = info["abi"] + init_bytecode = info["evm"]["bytecode"]["object"].replace("0x", "") + runtime_bytecode = info["evm"]["deployedBytecode"]["object"].replace("0x", "") + srcmap_init = info["evm"]["bytecode"]["sourceMap"] + srcmap_runtime = info["evm"]["deployedBytecode"]["sourceMap"] + userdoc = info.get("userdoc", {}) + devdoc = info.get("devdoc", {}) + natspec = Natspec(userdoc, devdoc) + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract + # Inherits is_dependency/is_supported from Solc class SolcStandardJson(Solc): @@ -404,6 +401,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: Any) -> None: solc_working_dir: Optional[str] = kwargs.get("solc_working_dir", None) compilation_unit = CompilationUnit(crytic_compile, "standard_json") + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit compilation_unit.compiler_version = CompilerVersion( compiler="solc", diff --git a/crytic_compile/platform/standard.py b/crytic_compile/platform/standard.py index 19669ff5..da440ec6 100644 --- a/crytic_compile/platform/standard.py +++ b/crytic_compile/platform/standard.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Any from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.source_unit import SourceUnit from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform import Type as PlatformType from crytic_compile.platform.abstract_platform import AbstractPlatform @@ -226,19 +227,19 @@ def generate_standard_export(crytic_compile: "CryticCompile") -> Dict: 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: + for contract_name, contract in source_unit.contracts.items(): 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)), + "abi": contract.abi, + "bin": contract.init_bytecode, + "bin-runtime": contract.runtime_bytecode, + "srcmap": ";".join(contract.init_srcmap), + "srcmap-runtime": ";".join(contract.runtime_srcmap), "filenames": _convert_filename_to_dict(filename), "libraries": dict(libraries) if libraries else {}, "is_dependency": crytic_compile.is_dependency(filename.absolute), - "userdoc": source_unit.natspec[contract_name].userdoc.export(), - "devdoc": source_unit.natspec[contract_name].devdoc.export(), + "userdoc": contract.natspec.userdoc.export(), + "devdoc": contract.natspec.devdoc.export(), } # Create our root object to contain the contracts and other information. @@ -285,8 +286,10 @@ def _load_from_compile_legacy1(crytic_compile: "CryticCompile", loaded_json: Dic ) for contract_name, contract in loaded_json["contracts"].items(): filename = _convert_dict_to_filename(contract["filenames"]) - compilation_unit.filename_to_contracts[filename].add(contract_name) - source_unit = compilation_unit.create_source_unit(filename) + ast = loaded_json["asts"][filename.relative] + source_unit = SourceUnit(compilation_unit, filename, ast) + #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"] diff --git a/crytic_compile/platform/truffle.py b/crytic_compile/platform/truffle.py index 202b2ae4..8c67c5ad 100755 --- a/crytic_compile/platform/truffle.py +++ b/crytic_compile/platform/truffle.py @@ -14,6 +14,8 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Tuple from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.contract import Contract +from crytic_compile.source_unit import SourceUnit from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform import solc from crytic_compile.platform.abstract_platform import AbstractPlatform @@ -28,55 +30,6 @@ LOGGER = logging.getLogger("CryticCompile") - -def export_to_truffle(crytic_compile: "CryticCompile", **kwargs: str) -> List[str]: - """Export to the truffle format - - Args: - crytic_compile (CryticCompile): CryticCompile object to export - **kwargs: optional arguments. Used: "export_dir" - - Raises: - InvalidCompilation: If there are more than 1 compilation unit - - Returns: - List[str]: Singleton with the generated directory - """ - # Get our export directory, if it's set, we create the path. - export_dir = kwargs.get("export_dir", "crytic-export") - if export_dir and not os.path.exists(export_dir): - os.makedirs(export_dir) - - compilation_units = list(crytic_compile.compilation_units.values()) - if len(compilation_units) != 1: - raise InvalidCompilation("Truffle export require 1 compilation unit") - compilation_unit = compilation_units[0] - - # Loop for each contract filename. - results: List[Dict] = [] - 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": 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) - - # If we have an export directory, export it. - - path = os.path.join(export_dir, contract_name + ".json") - with open(path, "w", encoding="utf8") as file_desc: - json.dump(output, file_desc) - - return [export_dir] - - class Truffle(AbstractPlatform): """ Truffle platform @@ -200,6 +153,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: version = None compiler = None compilation_unit = CompilationUnit(crytic_compile, str(self._target)) + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit for filename_txt in filenames: with open(filename_txt, encoding="utf8") as file_desc: @@ -217,10 +171,6 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: except json.decoder.JSONDecodeError: pass - userdoc = target_loaded.get("userdoc", {}) - devdoc = target_loaded.get("devdoc", {}) - natspec = Natspec(userdoc, devdoc) - if not "ast" in target_loaded: continue @@ -241,26 +191,28 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: # pylint: disable=raise-missing-from raise InvalidCompilation(txt) - source_unit = compilation_unit.create_source_unit(filename) - - source_unit.ast = target_loaded["ast"] - + ast = target_loaded["ast"] + source_unit = SourceUnit(compilation_unit, filename, ast) + compilation_unit.source_units[filename] = source_unit + contract_name = target_loaded["contractName"] - source_unit.natspec[contract_name] = natspec - compilation_unit.filename_to_contracts[filename].add(contract_name) - 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( + abi = target_loaded["abi"] + init_bytecode = target_loaded["bytecode"].replace( "0x", "" ) - source_unit.bytecodes_runtime[contract_name] = target_loaded[ + runtime_bytecode = target_loaded[ "deployedBytecode" ].replace("0x", "") - source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") - source_unit.srcmaps_runtime[contract_name] = target_loaded[ + srcmap_init = target_loaded["sourceMap"] + srcmap_runtime = target_loaded[ "deployedSourceMap" - ].split(";") - + ] + userdoc = target_loaded.get("userdoc", {}) + devdoc = target_loaded.get("devdoc", {}) + natspec = Natspec(userdoc, devdoc) + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract + if compiler is None: compiler = target_loaded.get("compiler", {}).get("name", None) if version is None: diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index 5b985aef..60dc4402 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -9,6 +9,8 @@ from typing import TYPE_CHECKING, Dict, List, Optional from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.contract import Contract +from crytic_compile.source_unit import SourceUnit from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.exceptions import InvalidCompilation @@ -48,6 +50,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: assert "version" in targets_json compilation_unit = CompilationUnit(crytic_compile, str(target)) + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit compilation_unit.compiler_version = CompilerVersion( compiler="vyper", version=targets_json["version"], optimized=False @@ -57,30 +60,25 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: info = targets_json[target] filename = convert_filename(target, _relative_to_short, crytic_compile) - + ast = _get_vyper_ast(target, vyper) + source_unit = SourceUnit(compilation_unit, filename, ast) contract_name = Path(target).parts[-1] - - 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", "") + + abi = info["abi"] + init_bytecode = info["bytecode"].replace("0x", "") + runtime_bytecode = info["bytecode_runtime"].replace("0x", "") # Vyper does not provide the source mapping for the init bytecode - source_unit.srcmaps_init[contract_name] = [] + srcmap_init = "" # 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 - source_unit.srcmaps_runtime[contract_name] = info["source_map"]["pc_pos_map_compressed"] - + srcmap_runtime = info["source_map"]["pc_pos_map_compressed"] # Natspec not yet handled for vyper - source_unit.natspec[contract_name] = Natspec({}, {}) - - ast = _get_vyper_ast(target, vyper) - source_unit.ast = ast + natspec = Natspec({}, {}) + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract def clean(self, **_kwargs: str) -> None: """Clean compilation artifacts diff --git a/crytic_compile/platform/waffle.py b/crytic_compile/platform/waffle.py index 280d5d31..8d3f5d95 100755 --- a/crytic_compile/platform/waffle.py +++ b/crytic_compile/platform/waffle.py @@ -13,6 +13,8 @@ from typing import TYPE_CHECKING, Dict, List, Optional from crytic_compile.compilation_unit import CompilationUnit +from crytic_compile.contract import Contract +from crytic_compile.source_unit import SourceUnit from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.exceptions import InvalidCompilation @@ -180,7 +182,12 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: optimized = None compilation_unit = CompilationUnit(crytic_compile, str(target)) - + crytic_compile.compilation_units[compilation_unit.unique_id] = compilation_unit + + compilation_unit.compiler_version = CompilerVersion( + compiler=compiler, version=version, optimized=optimized + ) + for contract in target_all["contracts"]: target_loaded = target_all["contracts"][contract] contract = contract.split(":") @@ -188,34 +195,27 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: contract[0], _relative_to_short, crytic_compile, working_dir=target ) - contract_name = contract[1] - source_unit = compilation_unit.create_source_unit(filename) - - source_unit.ast = target_all["sources"][contract[0]]["AST"] - compilation_unit.filenames.add(filename) - compilation_unit.filename_to_contracts[filename].add(contract_name) - source_unit.contracts_names.add(contract_name) - source_unit.abis[contract_name] = target_loaded["abi"] + ast = target_all["sources"][contract[0]]["AST"] + source_unit = SourceUnit(compilation_unit, filename, ast) + + contract_name = contract[1] + abi = target_loaded["abi"] + init_bytecode = target_loaded["evm"]["bytecode"]["object"].replace("0x", "") + runtime_bytecode = target_loaded["evm"]["deployedBytecode"][ + "object" + ].replace("0x", "") + srcmap_init = target_loaded["evm"]["bytecode"][ + "sourceMap" + ] + srcmap_runtime = target_loaded["evm"]["deployedBytecode"][ + "sourceMap" + ] userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - source_unit.natspec[contract_name] = natspec - - 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" - ] - source_unit.srcmaps_runtime[contract_name] = target_loaded["evm"]["deployedBytecode"][ - "sourceMap" - ].split(";") - - compilation_unit.compiler_version = CompilerVersion( - compiler=compiler, version=version, optimized=optimized - ) + contract = Contract(source_unit, contract_name, abi, init_bytecode, runtime_bytecode, srcmap_init, srcmap_runtime, natspec) + source_unit.contracts[contract_name] = contract def clean(self, **_kwargs: str) -> None: """Clean compilation artifacts diff --git a/crytic_compile/source_unit.py b/crytic_compile/source_unit.py index d2552dff..08c38058 100644 --- a/crytic_compile/source_unit.py +++ b/crytic_compile/source_unit.py @@ -12,600 +12,64 @@ if TYPE_CHECKING: from crytic_compile.compilation_unit import CompilationUnit + from crytic_compile.contract import Contract + # 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 - ################################################################################### - ################################################################################### - + """The SourceUnit class represents a set of contracts within a single file + + Attributes + ---------- + compilation_unit: CompilationUnit + A pointer to the associated CompilationUnit + filename: Filename + The Filename object associated with this SourceUnit + ast: Dict + The abstract syntax tree (AST) of the SourceUnit + contracts: Dict[str, Contract] + The mapping of contract name to the Contract + """ + + def __init__(self, compilation_unit: "CompilationUnit", filename: Filename, ast: Dict): + """Initialize the SourceUnit class""" + + self._compilation_unit: "CompilationUnit" = compilation_unit + self._filename: Filename = filename + self._ast: Dict = ast + self.contracts: Dict[str, Contract] = {} + + # region Getters + ################################################################################### + ################################################################################### + @property - def natspec(self) -> Dict[str, Natspec]: - """Return the natspec of the contracts + def compilation_unit(self) -> CompilationUnit: + """Return the CompilationUnit associated with this SourceUnit Returns: - Dict[str, Natspec]: Contract name -> Natspec + CompilationUnit: Pointer to the associated CompilationUnit """ - return self._natspec - - # endregion - ################################################################################### - ################################################################################### - # region Contract Names - ################################################################################### - ################################################################################### + return self._compilation_unit @property - def contracts_names(self) -> Set[str]: - """Return the contracts names + def filename(self) -> Filename: + """Return the Filename associated with this SourceUnit Returns: - Set[str]: List of the contracts names + Filename: Filename object """ - 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 - + return self._filename + @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 + def ast(self) -> Dict: + """Return the AST associated with this SourceUnit Returns: - Dict[str, int]: (function name => signature) + Dict: AST """ - 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 metadata_of(self, name: str) -> Dict[str, Union[str, bool]]: - """Return the parsed metadata of a contract by name - - Args: - name (str): contract name - - Raises: - ValueError: If no contract/library with that name exists - - Returns: - Dict[str, Union[str, bool]]: fielname => value - """ - # the metadata is at the end of the runtime(!) bytecode - try: - bytecode = self._runtime_bytecodes[name] - print("runtime bytecode", bytecode) - except: - raise ValueError( # pylint: disable=raise-missing-from - f"contract {name} does not exist" - ) - - # the last two bytes contain the length of the preceding metadata. - metadata_length = int(f"0x{bytecode[-4:]}", base=16) - # extract the metadata - metadata = bytecode[-(metadata_length * 2 + 4) :] - metadata_decoded = cbor2.loads(bytearray.fromhex(metadata)) - - for k, v in metadata_decoded.items(): - if len(v) == 1: - metadata_decoded[k] = bool(v) - elif k == "solc": - metadata_decoded[k] = ".".join([str(d) for d in v]) - else: - # there might be nested items or other unforeseen errors - try: - metadata_decoded[k] = v.hex() - except: # pylint: disable=bare-except - pass - - return metadata_decoded - - 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 - """ - # the metadata is at the end of the runtime(!) bytecode of each contract - for (key, bytecode) in self._runtime_bytecodes.items(): - if not bytecode or bytecode == "0x": - continue - # the last two bytes contain the length of the preceding metadata. - metadata_length = int(f"0x{bytecode[-4:]}", base=16) - # store the metadata here so we can remove it from the init bytecode later on - metadata = bytecode[-(metadata_length * 2 + 4) :] - # remove the metadata from the runtime bytecode, '+ 4' for the two length-indication bytes at the end - self._runtime_bytecodes[key] = bytecode[0 : -(metadata_length * 2 + 4)] - # remove the metadata from the init bytecode - self._init_bytecodes[key] = self._init_bytecodes[key].replace(metadata, "") - + return self._ast + # endregion ################################################################################### ###################################################################################