diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..bf470b12 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +--- +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 7d3059db..a04bb8cc 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8722a81f..4c92657c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "windows-2022"] - type: ["brownie", "buidler", "dapp", "embark", "etherlime", "hardhat", "solc", "truffle", "waffle", "foundry"] + type: ["brownie", "buidler", "dapp", "embark", "etherlime", "hardhat", "solc", "truffle", "waffle", "foundry", "standard"] exclude: # Currently broken, tries to pull git:// which is blocked by GH - type: embark @@ -39,17 +39,17 @@ jobs: id: node shell: bash run: | - if [ ${{ matrix.type }} = etherlime ]; then - echo '::set-output name=version::10.17.0' + if [ "${{ matrix.type }}" = "etherlime" ]; then + echo 'version=10.17.0' >> "$GITHUB_OUTPUT" else - echo '::set-output name=version::lts/*' + echo 'version=lts/*' >> "$GITHUB_OUTPUT" fi - name: Set up Node - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ steps.node.outputs.version }} - name: Set up Python 3.8 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: 3.8 - name: Install dependencies @@ -59,16 +59,17 @@ jobs: pip install . - name: Set up nix if: matrix.type == 'dapp' - uses: cachix/install-nix-action@v16 + uses: cachix/install-nix-action@v18 - name: Set up cachix if: matrix.type == 'dapp' - uses: cachix/cachix-action@v10 + uses: cachix/cachix-action@v12 with: name: dapp - name: Run Tests env: TEST_TYPE: ${{ matrix.type }} GITHUB_ETHERSCAN: ${{ secrets.GITHUB_ETHERSCAN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | bash "scripts/ci_test_${TEST_TYPE}.sh" diff --git a/.github/workflows/darglint.yml b/.github/workflows/darglint.yml index 36a2fbed..11fb4145 100644 --- a/.github/workflows/darglint.yml +++ b/.github/workflows/darglint.yml @@ -13,9 +13,9 @@ jobs: tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.8 - name: Run Tests diff --git a/.github/workflows/etherscan.yml b/.github/workflows/etherscan.yml index a65b5754..c4db3ae0 100644 --- a/.github/workflows/etherscan.yml +++ b/.github/workflows/etherscan.yml @@ -27,7 +27,7 @@ jobs: echo 'C:\msys64\mingw64\bin' >> "$GITHUB_PATH" echo 'C:\msys64\usr\bin' >> "$GITHUB_PATH" - name: Set up Python 3.8 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: 3.8 - name: Install dependencies diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 2fc65a16..89e64263 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.8 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 32ee3bd2..71b350a9 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.8 diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index f4028291..0571c3bd 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python 3.8 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.8 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 00000000..f0e9e0a5 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,39 @@ +--- +name: Pytest + +defaults: + run: + # To load bashrc + shell: bash -ieo pipefail {0} + +on: + push: + branches: + - main + - dev + pull_request: + branches: [main, dev] + schedule: + # run CI every day even if no PRs/merges occur + - cron: '0 12 * * *' + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + # Used by ci_test.sh + - name: Install dependencies + run: | + python setup.py install + pip install pytest + pip install solc-select + - name: Run Tests + run: | + pytest tests/test_metadata.py diff --git a/.gitignore b/.gitignore index 837adab6..6407522a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ +*.nix +*.pyc .idea/ -crytic_compile.egg-info/ __pycache__/ -*.pyc +artifacts build/ -dist/ \ No newline at end of file +cache +crytic_compile.egg-info/ +dist/ +node_modules +package-lock.json +result diff --git a/crytic_compile/__main__.py b/crytic_compile/__main__.py index 6b55e784..dd77944e 100644 --- a/crytic_compile/__main__.py +++ b/crytic_compile/__main__.py @@ -13,6 +13,7 @@ from crytic_compile.crytic_compile import compile_all, get_platforms from crytic_compile.cryticparser import DEFAULTS_FLAG_IN_CONFIG, cryticparser from crytic_compile.platform import InvalidCompilation +from crytic_compile.platform.all_export import PLATFORMS_EXPORT from crytic_compile.utils.zip import ZIP_TYPES_ACCEPTED, save_to_zip if TYPE_CHECKING: @@ -50,8 +51,8 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--export-format", - help="""Export json with non crytic-compile format -(default None. Accepted: standard, solc, truffle)""", + help=f"""Export json with non crytic-compile format + (default None. Accepted: ({", ".join(list(PLATFORMS_EXPORT))})""", action="store", dest="export_format", default=None, @@ -172,7 +173,7 @@ def _print_filenames(compilation: "CryticCompile") -> None: """ for compilation_id, compilation_unit in compilation.compilation_units.items(): print( - f"Compilation unit: {compilation_id} ({len(compilation_unit.contracts_names)} files, solc {compilation_unit.compiler_version.version})" + f"Compilation unit: {compilation_id} solc {compilation_unit.compiler_version.version})" ) for filename, contracts in compilation_unit.filename_to_contracts.items(): for contract in contracts: diff --git a/crytic_compile/compilation_unit.py b/crytic_compile/compilation_unit.py index 87622fbe..4e3550ee 100644 --- a/crytic_compile/compilation_unit.py +++ b/crytic_compile/compilation_unit.py @@ -1,23 +1,19 @@ """ Module handling the compilation unit """ -import re import uuid from collections import defaultdict -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union +from typing import TYPE_CHECKING, Dict, Set, Optional -from Crypto.Hash import keccak - -from crytic_compile.utils.naming import Filename -from crytic_compile.utils.natspec import Natspec from crytic_compile.compiler.compiler import CompilerVersion +from crytic_compile.source_unit import SourceUnit +from crytic_compile.utils.naming import Filename # Cycle dependency if TYPE_CHECKING: from crytic_compile import CryticCompile - -# pylint: disable=too-many-instance-attributes,too-many-public-methods +# pylint: disable=too-many-instance-attributes class CompilationUnit: """CompilationUnit class""" @@ -28,36 +24,19 @@ def __init__(self, crytic_compile: "CryticCompile", unique_id: str): crytic_compile (CryticCompile): Associated CryticCompile object unique_id (str): Unique ID used to identify the compilation unit """ - # ASTS are indexed by absolute path - self._asts: Dict = {} - - # ABI, bytecode and srcmap are indexed by contract_name - self._abis: Dict = {} - self._runtime_bytecodes: Dict = {} - self._init_bytecodes: Dict = {} - self._hashes: Dict = {} - self._events: Dict = {} - self._srcmaps: Dict[str, List[str]] = {} - self._srcmaps_runtime: Dict[str, List[str]] = {} - - # set containing all the contract names - self._contracts_name: Set[str] = set() - # set containing all the contract name without the libraries - self._contracts_name_without_libraries: Optional[Set[str]] = None # mapping from filename to contract name self._filename_to_contracts: Dict[Filename, Set[str]] = defaultdict(set) - # Libraries used by the contract - # contract_name -> (library, pattern) - self._libraries: Dict[str, List[Tuple[str, str]]] = {} - - # Natspec - self._natspec: Dict[str, Natspec] = {} + # mapping from filename to source unit + self._source_units: Dict[Filename, SourceUnit] = {} # set containing all the filenames of this compilation unit self._filenames: Set[Filename] = set() + # mapping from absolute/relative/used to filename + self._filenames_lookup: Optional[Dict[str, Filename]] = None + # compiler.compiler self._compiler_version: CompilerVersion = CompilerVersion( compiler="N/A", version="N/A", optimized=False @@ -68,7 +47,7 @@ def __init__(self, crytic_compile: "CryticCompile", unique_id: str): if unique_id == ".": unique_id = str(uuid.uuid4()) - crytic_compile.compilation_units[unique_id] = self + crytic_compile.compilation_units[unique_id] = self # type: ignore self._unique_id = unique_id @@ -90,21 +69,61 @@ def crytic_compile(self) -> "CryticCompile": """ return self._crytic_compile - ################################################################################### - ################################################################################### - # region Natspec - ################################################################################### - ################################################################################### + @property + def source_units(self) -> Dict[Filename, SourceUnit]: + """ + Return the dict of the source units + + Returns: + Dict[Filename, SourceUnit]: the source units + """ + return self._source_units + + def source_unit(self, filename: Filename) -> SourceUnit: + """ + Return the source unit associated to the filename. + The source unit must have been created by create_source_units + + Args: + filename: filename of the source unit + + Returns: + SourceUnit: the source unit + """ + return self._source_units[filename] @property - def natspec(self) -> Dict[str, Natspec]: - """Return the natspec of the contracts + def asts(self) -> Dict[str, Dict]: + """ + Return all the asts from the compilation unit + + Returns: + Dict[str, Dict]: absolute path -> ast + """ + return { + source_unit.filename.absolute: source_unit.ast + for source_unit in self.source_units.values() + } + + def create_source_unit(self, filename: Filename) -> SourceUnit: + """ + 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: - Dict[str, Natspec]: Contract name -> Natspec + SourceUnit: the source unit """ - return self._natspec + 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 @@ -113,7 +132,7 @@ def natspec(self) -> Dict[str, Natspec]: @property def filenames(self) -> Set[Filename]: - """Return the filenames used by the compilation units + """Return the filenames used by the compilation unit Returns: Set[Filename]: Filenames used by the compilation units @@ -176,552 +195,35 @@ def relative_filename_from_absolute_filename(self, absolute_filename: str) -> st raise ValueError("f{absolute_filename} does not exist in {d}") return d_file[absolute_filename] - # endregion - ################################################################################### - ################################################################################### - # region Contract Names - ################################################################################### - ################################################################################### - - @property - def contracts_names(self) -> Set[str]: - """Return the contracts names - - Returns: - Set[str]: List of the contracts names - """ - return self._contracts_name - - @contracts_names.setter - def contracts_names(self, names: Set[str]) -> None: - """Set the contract names - - Args: - names (Set[str]): New contracts names - """ - self._contracts_name = names - - @property - def contracts_names_without_libraries(self) -> Set[str]: - """Return the contracts names without the librairies - - Returns: - Set[str]: List of contracts - """ - if self._contracts_name_without_libraries is None: - libraries: List[str] = [] - for contract_name in self._contracts_name: - libraries += self.libraries_names(contract_name) - self._contracts_name_without_libraries = { - l for l in self._contracts_name if l not in set(libraries) - } - return self._contracts_name_without_libraries - - # endregion - ################################################################################### - ################################################################################### - # region ABI - ################################################################################### - ################################################################################### - - @property - def abis(self) -> Dict: - """Return the ABIs - - Returns: - Dict: ABIs (solc/vyper format) (contract name -> ABI) - """ - return self._abis - - def abi(self, name: str) -> Dict: - """Get the ABI from a contract - - Args: - name (str): Contract name - - Returns: - Dict: ABI (solc/vyper format) - """ - return self._abis.get(name, None) - - # endregion - ################################################################################### - ################################################################################### - # region AST - ################################################################################### - ################################################################################### - - @property - def asts(self) -> Dict: - """Return the ASTs - - Returns: - Dict: contract name -> AST (solc/vyper format) - """ - return self._asts - - @asts.setter - def asts(self, value: Dict) -> None: - """Set the ASTs - - Args: - value (Dict): New ASTs - """ - self._asts = value - - def ast(self, path: str) -> Union[Dict, None]: - """Return the ast of the file - - Args: - path (str): path to the file - - Returns: - Union[Dict, None]: Ast (solc/vyper format) - """ - if path not in self._asts: - try: - path = self.find_absolute_filename_from_used_filename(path) - except ValueError: - pass - return self._asts.get(path, None) - - # endregion - ################################################################################### - ################################################################################### - # region Bytecode - ################################################################################### - ################################################################################### - - @property - def bytecodes_runtime(self) -> Dict[str, str]: - """Return the runtime bytecodes - - Returns: - Dict[str, str]: contract => runtime bytecode - """ - return self._runtime_bytecodes - - @bytecodes_runtime.setter - def bytecodes_runtime(self, bytecodes: Dict[str, str]) -> None: - """Set the bytecodes runtime - - Args: - bytecodes (Dict[str, str]): New bytecodes runtime - """ - self._runtime_bytecodes = bytecodes - - @property - def bytecodes_init(self) -> Dict[str, str]: - """Return the init bytecodes - - Returns: - Dict[str, str]: contract => init bytecode - """ - return self._init_bytecodes - - @bytecodes_init.setter - def bytecodes_init(self, bytecodes: Dict[str, str]) -> None: - """Set the bytecodes init - - Args: - bytecodes (Dict[str, str]): New bytecodes init - """ - self._init_bytecodes = bytecodes - - def bytecode_runtime(self, name: str, libraries: Optional[Dict[str, str]] = None) -> str: - """Return the runtime bytecode of the contract. - If library is provided, patch the bytecode - - Args: - name (str): contract name - libraries (Optional[Dict[str, str]], optional): lib_name => address. Defaults to None. - - Returns: - str: runtime bytecode - """ - runtime = self._runtime_bytecodes.get(name, None) - return self._update_bytecode_with_libraries(runtime, libraries) - - def bytecode_init(self, name: str, libraries: Optional[Dict[str, str]] = None) -> str: - """Return the init bytecode of the contract. - If library is provided, patch the bytecode - - Args: - name (str): contract name - libraries (Optional[Dict[str, str]], optional): lib_name => address. Defaults to None. - - Returns: - str: init bytecode - """ - init = self._init_bytecodes.get(name, None) - return self._update_bytecode_with_libraries(init, libraries) - - # endregion - ################################################################################### - ################################################################################### - # region Source mapping - ################################################################################### - ################################################################################### - - @property - def srcmaps_init(self) -> Dict[str, List[str]]: - """Return the srcmaps init - - Returns: - Dict[str, List[str]]: Srcmaps init (solc/vyper format) - """ - return self._srcmaps - - @property - def srcmaps_runtime(self) -> Dict[str, List[str]]: - """Return the srcmaps runtime - - Returns: - Dict[str, List[str]]: Srcmaps runtime (solc/vyper format) - """ - return self._srcmaps_runtime - - def srcmap_init(self, name: str) -> List[str]: - """Return the srcmap init of a contract + def filename_lookup(self, filename: str) -> Filename: + """Return a crytic_compile.naming.Filename from a any filename 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 - ################################################################################### - ################################################################################### + filename (str): filename (used/absolute/relative) - @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 + Raises: + ValueError: If the filename is not in the project Returns: - Dict[str, str]: lib_name => address + Filename: Associated Filename object """ - new_names = {} - for (lib, addr) in libraries.items(): - # Prior solidity 0.5 - # libraries were on the format __filename:contract_name_____ - # From solidity 0.5, - # libraries are on the format __$keccak(filename:contract_name)[34]$__ - # https://solidity.readthedocs.io/en/v0.5.7/050-breaking-changes.html#command-line-and-json-interfaces - - lib_4 = "__" + lib + "_" * (38 - len(lib)) - - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(lib.encode("utf-8")) - lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" - - new_names[lib] = addr - new_names[lib_4] = addr - new_names[lib_5] = addr - - for lib_filename, contract_names in self._filename_to_contracts.items(): - for contract_name in contract_names: - if contract_name != lib: - continue - - lib_with_abs_filename = lib_filename.absolute + ":" + lib - lib_with_abs_filename = lib_with_abs_filename[0:36] - - lib_4 = "__" + lib_with_abs_filename + "_" * (38 - len(lib_with_abs_filename)) - new_names[lib_4] = addr - - lib_with_used_filename = lib_filename.used + ":" + lib - lib_with_used_filename = lib_with_used_filename[0:36] - - lib_4 = "__" + lib_with_used_filename + "_" * (38 - len(lib_with_used_filename)) - new_names[lib_4] = addr - - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(lib_with_abs_filename.encode("utf-8")) - lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" - new_names[lib_5] = addr - - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(lib_with_used_filename.encode("utf-8")) - lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" - new_names[lib_5] = addr - - return new_names - - def _library_name_lookup( - self, lib_name: str, original_contract: str - ) -> Optional[Tuple[str, str]]: - """Do a lookup on a library name to its name used in contracts - The library can be: - - the original contract name - - __X__ following Solidity 0.4 format - - __$..$__ following Solidity 0.5 format - - Args: - lib_name (str): library name - original_contract (str): original contract name + # pylint: disable=import-outside-toplevel + from crytic_compile.platform.truffle import Truffle - Returns: - Optional[Tuple[str, str]]: contract_name, library_name - """ + if isinstance(self.crytic_compile.platform, Truffle) and filename.startswith("project:/"): + filename = filename[len("project:/") :] - for filename, contract_names in self._filename_to_contracts.items(): - for name in contract_names: - if name == lib_name: - return name, name - - # Some platform use only the contract name - # Some use fimename:contract_name - name_with_absolute_filename = filename.absolute + ":" + name - name_with_absolute_filename = name_with_absolute_filename[0:36] - - name_with_used_filename = filename.used + ":" + name - name_with_used_filename = name_with_used_filename[0:36] - - # Solidity 0.4 - solidity_0_4 = "__" + name + "_" * (38 - len(name)) - if solidity_0_4 == lib_name: - return name, solidity_0_4 - - # Solidity 0.4 with filename - solidity_0_4_filename = ( - "__" - + name_with_absolute_filename - + "_" * (38 - len(name_with_absolute_filename)) - ) - if solidity_0_4_filename == lib_name: - return name, solidity_0_4_filename - - # Solidity 0.4 with filename - solidity_0_4_filename = ( - "__" + name_with_used_filename + "_" * (38 - len(name_with_used_filename)) - ) - if solidity_0_4_filename == lib_name: - return name, solidity_0_4_filename - - # Solidity 0.5 - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(name.encode("utf-8")) - v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" - - if v5_name == lib_name: - return name, v5_name - - # Solidity 0.5 with filename - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(name_with_absolute_filename.encode("utf-8")) - v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" - - if v5_name == lib_name: - return name, v5_name - - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(name_with_used_filename.encode("utf-8")) - v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" - - if v5_name == lib_name: - return name, v5_name - - # handle specific case of collision for Solidity <0.4 - # We can only detect that the second contract is meant to be the library - # if there is only two contracts in the codebase - if len(self._contracts_name) == 2: - return next( - ( - (c, "__" + c + "_" * (38 - len(c))) - for c in self._contracts_name - if c != original_contract - ), - None, + if self._filenames_lookup is None: + self._filenames_lookup = {} + for file in self._filenames: + self._filenames_lookup[file.absolute] = file + self._filenames_lookup[file.relative] = file + self._filenames_lookup[file.used] = file + if filename not in self._filenames_lookup: + raise ValueError( + f"{filename} does not exist in {[f.absolute for f in self._filenames_lookup.values()]}" ) - - return None - - def libraries_names(self, name: str) -> List[str]: - """Return the names of the libraries used by the contract - - Args: - name (str): contract name - - Returns: - List[str]: libraries used - """ - - if name not in self._libraries: - init = re.findall(r"__.{36}__", self.bytecode_init(name)) - runtime = re.findall(r"__.{36}__", self.bytecode_runtime(name)) - libraires = [self._library_name_lookup(x, name) for x in set(init + runtime)] - self._libraries[name] = [lib for lib in libraires if lib] - return [name for (name, _) in self._libraries[name]] - - def libraries_names_and_patterns(self, name: str) -> List[Tuple[str, str]]: - """Return the names and the patterns of the libraries used by the contract - - Args: - name (str): contract name - - Returns: - List[Tuple[str, str]]: (lib_name, pattern) - """ - - if name not in self._libraries: - init = re.findall(r"__.{36}__", self.bytecode_init(name)) - runtime = re.findall(r"__.{36}__", self.bytecode_runtime(name)) - libraires = [self._library_name_lookup(x, name) for x in set(init + runtime)] - self._libraries[name] = [lib for lib in libraires if lib] - return self._libraries[name] - - def _update_bytecode_with_libraries( - self, bytecode: str, libraries: Union[None, Dict[str, str]] - ) -> str: - """Update the bytecode with the libraries address - - Args: - bytecode (str): bytecode to patch - libraries (Union[None, Dict[str, str]]): pattern => address - - Returns: - str: Patched bytecode - """ - if libraries: - libraries = self._convert_libraries_names(libraries) - for library_found in re.findall(r"__.{36}__", bytecode): - if library_found in libraries: - bytecode = re.sub( - re.escape(library_found), - f"{libraries[library_found]:0>40x}", - bytecode, - ) - return bytecode - - # endregion - ################################################################################### - ################################################################################### - # region Hashes - ################################################################################### - ################################################################################### - - def hashes(self, name: str) -> Dict[str, int]: - """Return the hashes of the functions - - Args: - name (str): contract name - - Returns: - Dict[str, int]: (function name => signature) - """ - if not name in self._hashes: - self._compute_hashes(name) - return self._hashes[name] - - def _compute_hashes(self, name: str) -> None: - """Compute the function hashes - - Args: - name (str): contract name - """ - self._hashes[name] = {} - for sig in self.abi(name): - if "type" in sig: - if sig["type"] == "function": - sig_name = sig["name"] - arguments = ",".join([x["type"] for x in sig["inputs"]]) - sig = f"{sig_name}({arguments})" - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(sig.encode("utf-8")) - self._hashes[name][sig] = int("0x" + sha3_result.hexdigest()[:8], 16) - - # endregion - ################################################################################### - ################################################################################### - # region Events - ################################################################################### - ################################################################################### - - def events_topics(self, name: str) -> Dict[str, Tuple[int, List[bool]]]: - """Return the topics of the contract's events - - Args: - name (str): contract name - - Returns: - Dict[str, Tuple[int, List[bool]]]: event signature => topic hash, [is_indexed for each parameter] - """ - if not name in self._events: - self._compute_topics_events(name) - return self._events[name] - - def _compute_topics_events(self, name: str) -> None: - """Compute the topics of the contract's events - - Args: - name (str): contract name - """ - self._events[name] = {} - for sig in self.abi(name): - if "type" in sig: - if sig["type"] == "event": - sig_name = sig["name"] - arguments = ",".join([x["type"] for x in sig["inputs"]]) - indexes = [x.get("indexed", False) for x in sig["inputs"]] - sig = f"{sig_name}({arguments})" - sha3_result = keccak.new(digest_bits=256) - sha3_result.update(sig.encode("utf-8")) - - self._events[name][sig] = (int("0x" + sha3_result.hexdigest()[:8], 16), indexes) - - # endregion - ################################################################################### - ################################################################################### - # region Metadata - ################################################################################### - ################################################################################### - - def remove_metadata(self) -> None: - """Remove init bytecode - See - http://solidity.readthedocs.io/en/v0.4.24/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode - - Note we dont support recent Solidity version, see https://github.com/crytic/crytic-compile/issues/59 - """ - self._init_bytecodes = { - key: re.sub(r"a165627a7a72305820.{64}0029", r"", bytecode) - for (key, bytecode) in self._init_bytecodes.items() - } - - self._runtime_bytecodes = { - key: re.sub(r"a165627a7a72305820.{64}0029", r"", bytecode) - for (key, bytecode) in self._runtime_bytecodes.items() - } + return self._filenames_lookup[filename] # endregion ################################################################################### @@ -747,3 +249,7 @@ def compiler_version(self, compiler: CompilerVersion) -> None: compiler (CompilerVersion): New compiler version """ self._compiler_version = compiler + + # endregion + ################################################################################### + ################################################################################### diff --git a/crytic_compile/crytic_compile.py b/crytic_compile/crytic_compile.py index bb5e3d21..8fb63a0e 100644 --- a/crytic_compile/crytic_compile.py +++ b/crytic_compile/crytic_compile.py @@ -19,7 +19,6 @@ from crytic_compile.platform.all_export import PLATFORMS_EXPORT from crytic_compile.platform.solc import Solc from crytic_compile.platform.standard import export_to_standard -from crytic_compile.platform.truffle import Truffle from crytic_compile.utils.naming import Filename from crytic_compile.utils.npm import get_package_name from crytic_compile.utils.zip import load_from_zip @@ -65,7 +64,7 @@ class CryticCompile: Main class. """ - def __init__(self, target: Union[str, AbstractPlatform], **kwargs: str): + def __init__(self, target: Union[str, AbstractPlatform], **kwargs: str) -> None: """See https://github.com/crytic/crytic-compile/wiki/Configuration Target is usually a file or a project directory. It can be an AbstractPlatform for custom setup @@ -78,12 +77,6 @@ def __init__(self, target: Union[str, AbstractPlatform], **kwargs: str): # dependencies is needed for platform conversion self._dependencies: Set = set() - # set containing all the filenames - self._filenames: Set[Filename] = set() - - # mapping from absolute/relative/used to filename - self._filenames_lookup: Optional[Dict[str, Filename]] = None - self._src_content: Dict = {} # Mapping each file to @@ -145,33 +138,28 @@ def is_in_multiple_compilation_unit(self, contract: str) -> bool: """ count = 0 for compilation_unit in self._compilation_units.values(): - if contract in compilation_unit.contracts_names: - count += 1 + for source_unit in compilation_unit.source_units.values(): + if contract in source_unit.contracts_names: + count += 1 return count >= 2 ################################################################################### ################################################################################### - # region Filenames + # region Utils ################################################################################### ################################################################################### - @property def filenames(self) -> Set[Filename]: - """All the project filenames - - Returns: - Set[Filename]: Project's filenames """ - return self._filenames + Return the set of all the filenames used - @filenames.setter - def filenames(self, all_filenames: Set[Filename]) -> None: - """Set the filenames - - Args: - all_filenames (Set[Filename]): New filenames + Returns: + Set[Filename]: list of filenames """ - self._filenames = all_filenames + filenames: Set[Filename] = set() + for compile_unit in self._compilation_units.values(): + filenames = filenames.union(compile_unit.filenames) + return filenames def filename_lookup(self, filename: str) -> Filename: """Return a crytic_compile.naming.Filename from a any filename @@ -185,21 +173,13 @@ def filename_lookup(self, filename: str) -> Filename: Returns: Filename: Associated Filename object """ + for compile_unit in self.compilation_units.values(): + try: + return compile_unit.filename_lookup(filename) + except ValueError: + pass - if isinstance(self.platform, Truffle) and filename.startswith("project:/"): - filename = filename[len("project:/") :] - - if self._filenames_lookup is None: - self._filenames_lookup = {} - for file in self._filenames: - self._filenames_lookup[file.absolute] = file - self._filenames_lookup[file.relative] = file - self._filenames_lookup[file.used] = file - if filename not in self._filenames_lookup: - raise ValueError( - f"{filename} does not exist in {[f.absolute for f in self._filenames_lookup.values()]}" - ) - return self._filenames_lookup[filename] + raise ValueError(f"{filename} does not exist") @property def dependencies(self) -> Set[str]: @@ -545,12 +525,15 @@ def _compile(self, **kwargs: str) -> None: self._run_custom_build(custom_build) else: + if not kwargs.get("skip_clean", False) and not kwargs.get("ignore_compile", False): + self._platform.clean(**kwargs) self._platform.compile(self, **kwargs) remove_metadata = kwargs.get("compile_remove_metadata", False) if remove_metadata: for compilation_unit in self._compilation_units.values(): - compilation_unit.remove_metadata() + for source_unit in compilation_unit.source_units.values(): + source_unit.remove_metadata() @staticmethod def _run_custom_build(custom_build: str) -> None: @@ -564,8 +547,8 @@ def _run_custom_build(custom_build: str) -> None: with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process: stdout_bytes, stderr_bytes = process.communicate() stdout, stderr = ( - stdout_bytes.decode(), - stderr_bytes.decode(), + stdout_bytes.decode(errors="backslashreplace"), + stderr_bytes.decode(errors="backslashreplace"), ) # convert bytestrings to unicode strings LOGGER.info(stdout) diff --git a/crytic_compile/cryticparser/cryticparser.py b/crytic_compile/cryticparser/cryticparser.py index b0126e20..e8dd9d63 100755 --- a/crytic_compile/cryticparser/cryticparser.py +++ b/crytic_compile/cryticparser/cryticparser.py @@ -16,11 +16,11 @@ def init(parser: ArgumentParser) -> None: parser (ArgumentParser): argparser where the cli flags are added """ - group_solc = parser.add_argument_group("Compile options") + group_compile = parser.add_argument_group("Compile options") platforms = get_platforms() - group_solc.add_argument( + group_compile.add_argument( "--compile-force-framework", help="Force the compile to a given framework " f"({','.join([x.NAME.lower() for x in platforms])})", @@ -28,21 +28,21 @@ def init(parser: ArgumentParser) -> None: default=DEFAULTS_FLAG_IN_CONFIG["compile_force_framework"], ) - group_solc.add_argument( + group_compile.add_argument( "--compile-remove-metadata", help="Remove the metadata from the bytecodes", action="store_true", default=DEFAULTS_FLAG_IN_CONFIG["compile_remove_metadata"], ) - group_solc.add_argument( + group_compile.add_argument( "--compile-custom-build", help="Replace platform specific build command", action="store", default=DEFAULTS_FLAG_IN_CONFIG["compile_custom_build"], ) - group_solc.add_argument( + group_compile.add_argument( "--ignore-compile", help="Do not run compile of any platform", action="store_true", @@ -50,9 +50,18 @@ def init(parser: ArgumentParser) -> None: default=DEFAULTS_FLAG_IN_CONFIG["ignore_compile"], ) + group_compile.add_argument( + "--skip-clean", + help="Do not attempt to clean before compiling with a platform", + action="store_true", + dest="skip_clean", + default=DEFAULTS_FLAG_IN_CONFIG["skip_clean"], + ) + _init_solc(parser) _init_truffle(parser) _init_embark(parser) + _init_brownie(parser) _init_dapp(parser) _init_etherlime(parser) _init_etherscan(parser) @@ -60,6 +69,7 @@ def init(parser: ArgumentParser) -> None: _init_npx(parser) _init_buidler(parser) _init_hardhat(parser) + _init_foundry(parser) def _init_solc(parser: ArgumentParser) -> None: @@ -230,8 +240,8 @@ def _init_brownie(parser: ArgumentParser) -> None: Args: parser (ArgumentParser): argparser where the cli flags are added """ - group_embark = parser.add_argument_group("Brownie options") - group_embark.add_argument( + group_brownie = parser.add_argument_group("Brownie options") + group_brownie.add_argument( "--brownie-ignore-compile", help="Do not run brownie compile", action="store_true", @@ -431,7 +441,7 @@ def _init_hardhat(parser: ArgumentParser) -> None: Args: parser (ArgumentParser): argparser where the cli flags are added """ - group_hardhat = parser.add_argument_group("hardhat options") + group_hardhat = parser.add_argument_group("Hardhat options") group_hardhat.add_argument( "--hardhat-ignore-compile", help="Do not run hardhat compile", @@ -463,8 +473,8 @@ def _init_foundry(parser: ArgumentParser) -> None: Args: parser (ArgumentParser): argparser where the cli flags are added """ - group_hardhat = parser.add_argument_group("foundry options") - group_hardhat.add_argument( + group_foundry = parser.add_argument_group("Foundry options") + group_foundry.add_argument( "--foundry-ignore-compile", help="Do not run foundry compile", action="store_true", @@ -472,7 +482,7 @@ def _init_foundry(parser: ArgumentParser) -> None: default=DEFAULTS_FLAG_IN_CONFIG["foundry_ignore_compile"], ) - group_hardhat.add_argument( + group_foundry.add_argument( "--foundry-out-directory", help="Use an alternative out directory (default: out)", action="store", diff --git a/crytic_compile/cryticparser/defaults.py b/crytic_compile/cryticparser/defaults.py index 5395fb7b..106fe661 100755 --- a/crytic_compile/cryticparser/defaults.py +++ b/crytic_compile/cryticparser/defaults.py @@ -36,6 +36,7 @@ "waffle_config_file": None, "npx_disable": False, "ignore_compile": False, + "skip_clean": False, "buidler_ignore_compile": False, "buidler_cache_directory": "cache", "buidler_skip_directory_name_fix": False, diff --git a/crytic_compile/platform/abstract_platform.py b/crytic_compile/platform/abstract_platform.py index 62cdddd0..c266520a 100644 --- a/crytic_compile/platform/abstract_platform.py +++ b/crytic_compile/platform/abstract_platform.py @@ -119,6 +119,15 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: """ return + @abc.abstractmethod + def clean(self, **kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **kwargs: optional arguments. + """ + return + @staticmethod @abc.abstractmethod def is_supported(target: str, **kwargs: str) -> bool: diff --git a/crytic_compile/platform/archive.py b/crytic_compile/platform/archive.py index c4f8d1ef..e7fc417d 100644 --- a/crytic_compile/platform/archive.py +++ b/crytic_compile/platform/archive.py @@ -71,7 +71,7 @@ def compile(self, crytic_compile: "CryticCompile", **_kwargs: str) -> None: """Run the compilation Args: - crytic_compile (CryticCompile): asscoiated CryticCompile object + crytic_compile (CryticCompile): associated CryticCompile object **_kwargs: unused """ # pylint: disable=import-outside-toplevel @@ -97,6 +97,9 @@ def compile(self, crytic_compile: "CryticCompile", **_kwargs: str) -> None: crytic_compile.src_content = loaded_json["source_content"] + def clean(self, **_kwargs: str) -> None: + pass + @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """Check if the target is an archive diff --git a/crytic_compile/platform/brownie.py b/crytic_compile/platform/brownie.py index b779299b..745df321 100755 --- a/crytic_compile/platform/brownie.py +++ b/crytic_compile/platform/brownie.py @@ -63,8 +63,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ) as process: stdout_bytes, stderr_bytes = process.communicate() stdout, stderr = ( - stdout_bytes.decode(), - stderr_bytes.decode(), + stdout_bytes.decode(errors="backslashreplace"), + stderr_bytes.decode(errors="backslashreplace"), ) # convert bytestrings to unicode strings LOGGER.info(stdout) @@ -82,6 +82,10 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: _iterate_over_files(crytic_compile, Path(self._target), filenames) + def clean(self, **_kwargs: str) -> None: + # brownie does not offer a way to clean a project + pass + @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """Check if the target is a brownie project @@ -168,28 +172,28 @@ def _iterate_over_files( filename_txt, _relative_to_short, crytic_compile, working_dir=target ) - compilation_unit.asts[filename.absolute] = target_loaded["ast"] - compilation_unit.filenames.add(filename) - crytic_compile.filenames.add(filename) + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.ast = target_loaded["ast"] contract_name = target_loaded["contractName"] + compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] - compilation_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( - "0x", "" - ) - compilation_unit.bytecodes_runtime[contract_name] = target_loaded[ + + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = target_loaded["abi"] + source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace("0x", "") + source_unit.bytecodes_runtime[contract_name] = target_loaded[ "deployedBytecode" ].replace("0x", "") - compilation_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = target_loaded[ - "deployedSourceMap" - ].split(";") + source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") + source_unit.srcmaps_runtime[contract_name] = target_loaded["deployedSourceMap"].split( + ";" + ) userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec compilation_unit.compiler_version = CompilerVersion( compiler=compiler, version=version, optimized=optimized diff --git a/crytic_compile/platform/buidler.py b/crytic_compile/platform/buidler.py index d89447f7..577cc08f 100755 --- a/crytic_compile/platform/buidler.py +++ b/crytic_compile/platform/buidler.py @@ -80,8 +80,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: stdout_bytes, stderr_bytes = process.communicate() stdout, stderr = ( - stdout_bytes.decode(), - stderr_bytes.decode(), + stdout_bytes.decode(errors="backslashreplace"), + stderr_bytes.decode(errors="backslashreplace"), ) # convert bytestrings to unicode strings LOGGER.info(stdout) @@ -113,6 +113,14 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: if "contracts" in targets_json: for original_filename, contracts_info in targets_json["contracts"].items(): + filename = convert_filename( + original_filename, + relative_to_short, + crytic_compile, + working_dir=buidler_working_dir, + ) + source_unit = compilation_unit.create_source_unit(filename) + for original_contract_name, info in contracts_info.items(): contract_name = extract_name(original_contract_name) @@ -122,33 +130,26 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ): original_filename = "c" + original_filename - contract_filename = convert_filename( - original_filename, - relative_to_short, - crytic_compile, - working_dir=buidler_working_dir, - ) - - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.abis[contract_name] = info["abi"] - compilation_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"][ + source_unit.abis[contract_name] = info["abi"] + source_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"][ "object" ] - compilation_unit.bytecodes_runtime[contract_name] = info["evm"][ + source_unit.bytecodes_runtime[contract_name] = info["evm"][ "deployedBytecode" ]["object"] - compilation_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ + source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["evm"][ + source_unit.srcmaps_runtime[contract_name] = info["evm"][ "deployedBytecode" ]["sourceMap"].split(";") userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec if "sources" in targets_json: for path, info in targets_json["sources"].items(): @@ -167,9 +168,12 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: path = convert_filename( path, relative_to_short, crytic_compile, working_dir=buidler_working_dir ) - compilation_unit.filenames.add(path) - crytic_compile.filenames.add(path) - compilation_unit.asts[path.absolute] = info["ast"] + source_unit = compilation_unit.create_source_unit(path) + source_unit.ast = info["ast"] + + def clean(self, **kwargs: str) -> None: + # TODO: call "buldler clean"? + pass @staticmethod def is_supported(target: str, **kwargs: str) -> bool: diff --git a/crytic_compile/platform/dapp.py b/crytic_compile/platform/dapp.py index 540e4f40..558f7499 100755 --- a/crytic_compile/platform/dapp.py +++ b/crytic_compile/platform/dapp.py @@ -19,6 +19,7 @@ from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.types import Type from crytic_compile.utils.naming import convert_filename, extract_name +from crytic_compile.utils.subprocess import run # Handle cycle from crytic_compile.utils.natspec import Natspec @@ -69,6 +70,11 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: version = re.findall(r"\d+\.\d+\.\d+", targets_json["version"])[0] for original_filename, contracts_info in targets_json["contracts"].items(): + + filename = convert_filename(original_filename, lambda x: x, crytic_compile) + + source_unit = compilation_unit.create_source_unit(filename) + for original_contract_name, info in contracts_info.items(): if "metadata" in info: metadata = json.loads(info["metadata"]) @@ -79,26 +85,24 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ): optimized |= metadata["settings"]["optimizer"]["enabled"] contract_name = extract_name(original_contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[original_filename].add(contract_name) + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.abis[contract_name] = info["abi"] - compilation_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"][ + source_unit.abis[contract_name] = info["abi"] + source_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"]["object"] + source_unit.bytecodes_runtime[contract_name] = info["evm"]["deployedBytecode"][ "object" ] - compilation_unit.bytecodes_runtime[contract_name] = info["evm"][ - "deployedBytecode" - ]["object"] - compilation_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ + source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["evm"]["bytecode"][ + source_unit.srcmaps_runtime[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec if version is None: metadata = json.loads(info["metadata"]) @@ -108,14 +112,28 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: path = convert_filename( path, _relative_to_short, crytic_compile, working_dir=self._target ) - compilation_unit.filenames.add(path) - crytic_compile.filenames.add(path) - compilation_unit.asts[path.absolute] = info["ast"] + source_unit = compilation_unit.create_source_unit(path) + source_unit.ast = info["ast"] compilation_unit.compiler_version = CompilerVersion( compiler="solc", version=version, optimized=optimized ) + def clean(self, **kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **kwargs: optional arguments. + """ + + dapp_ignore_compile = kwargs.get("dapp_ignore_compile", False) or kwargs.get( + "ignore_compile", False + ) + if dapp_ignore_compile: + return + + run(["dapp", "clean"], cwd=self._target) + @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """Check if the target is a dapp project @@ -171,7 +189,7 @@ def _run_dapp(target: str) -> None: InvalidCompilation: If dapp failed to run """ # pylint: disable=import-outside-toplevel - from crytic_compile import InvalidCompilation + from crytic_compile.platform.exceptions import InvalidCompilation cmd = ["dapp", "build"] diff --git a/crytic_compile/platform/embark.py b/crytic_compile/platform/embark.py index 65cb39a6..f81c20d6 100755 --- a/crytic_compile/platform/embark.py +++ b/crytic_compile/platform/embark.py @@ -104,10 +104,10 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: # pylint: disable=raise-missing-from raise InvalidCompilation(error) stdout, stderr = process.communicate() - LOGGER.info("%s\n", stdout.decode()) + LOGGER.info("%s\n", stdout.decode(errors="backslashreplace")) if stderr: # Embark might return information to stderr, but compile without issue - LOGGER.error("%s", stderr.decode()) + LOGGER.error("%s", stderr.decode(errors="backslashreplace")) infile = os.path.join(self._target, "crytic-export", "contracts-embark.json") if not os.path.isfile(infile): raise InvalidCompilation( @@ -124,9 +124,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: filename = convert_filename( k, _relative_to_short, crytic_compile, working_dir=self._target ) - compilation_unit.asts[filename.absolute] = ast - compilation_unit.filenames.add(filename) - crytic_compile.filenames.add(filename) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast if not "contracts" in targets_loaded: LOGGER.error( @@ -138,35 +137,43 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: for original_contract_name, info in targets_loaded["contracts"].items(): contract_name = extract_name(original_contract_name) - contract_filename = convert_filename( + filename = convert_filename( extract_filename(original_contract_name), _relative_to_short, crytic_compile, working_dir=self._target, ) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) + source_unit = compilation_unit.create_source_unit(filename) + + compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit.contracts_names.add(contract_name) if "abi" in info: - compilation_unit.abis[contract_name] = info["abi"] + source_unit.abis[contract_name] = info["abi"] if "bin" in info: - compilation_unit.bytecodes_init[contract_name] = info["bin"].replace("0x", "") + source_unit.bytecodes_init[contract_name] = info["bin"].replace("0x", "") if "bin-runtime" in info: - compilation_unit.bytecodes_runtime[contract_name] = info["bin-runtime"].replace( + source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"].replace( "0x", "" ) if "srcmap" in info: - compilation_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") + source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") if "srcmap-runtime" in info: - compilation_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split( - ";" - ) + source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec + + def clean(self, **_kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **_kwargs: unused. + """ + return @staticmethod def is_supported(target: str, **kwargs: str) -> bool: diff --git a/crytic_compile/platform/etherlime.py b/crytic_compile/platform/etherlime.py index 78f3deec..7504c15e 100755 --- a/crytic_compile/platform/etherlime.py +++ b/crytic_compile/platform/etherlime.py @@ -57,8 +57,8 @@ def _run_etherlime(target: str, npx_disable: bool, compile_arguments: Optional[s ) as process: stdout_bytes, stderr_bytes = process.communicate() stdout, stderr = ( - stdout_bytes.decode(), - stderr_bytes.decode(), + stdout_bytes.decode(errors="backslashreplace"), + stderr_bytes.decode(errors="backslashreplace"), ) # convert bytestrings to unicode strings LOGGER.info(stdout) @@ -130,33 +130,39 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: Any) -> None: filename_txt = target_loaded["ast"]["absolutePath"] filename = convert_filename(filename_txt, _relative_to_short, crytic_compile) - compilation_unit.asts[filename.absolute] = target_loaded["ast"] - compilation_unit.filenames.add(filename) - crytic_compile.filenames.add(filename) + + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.ast = target_loaded["ast"] contract_name = target_loaded["contractName"] + compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] - compilation_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = target_loaded["abi"] + source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( "0x", "" ) - compilation_unit.bytecodes_runtime[contract_name] = target_loaded[ + source_unit.bytecodes_runtime[contract_name] = target_loaded[ "deployedBytecode" ].replace("0x", "") - compilation_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = target_loaded[ + source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") + source_unit.srcmaps_runtime[contract_name] = target_loaded[ "deployedSourceMap" ].split(";") userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec compilation_unit.compiler_version = CompilerVersion( compiler=compiler, version=version, optimized=_is_optimized(compile_arguments) ) + def clean(self, **_kwargs: str) -> None: + # TODO: research if there's a way to clean artifacts + pass + @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """Check if the target is an etherlime project diff --git a/crytic_compile/platform/etherscan.py b/crytic_compile/platform/etherscan.py index a83c97d8..d58d678e 100644 --- a/crytic_compile/platform/etherscan.py +++ b/crytic_compile/platform/etherscan.py @@ -76,13 +76,15 @@ def _handle_bytecode(crytic_compile: "CryticCompile", target: str, result_b: byt compilation_unit = CompilationUnit(crytic_compile, str(target)) - compilation_unit.contracts_names.add(contract_name) + source_unit = compilation_unit.create_source_unit(contract_filename) + + source_unit.contracts_names.add(contract_name) compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.abis[contract_name] = {} - compilation_unit.bytecodes_init[contract_name] = bytecode - compilation_unit.bytecodes_runtime[contract_name] = "" - compilation_unit.srcmaps_init[contract_name] = [] - compilation_unit.srcmaps_runtime[contract_name] = [] + source_unit.abis[contract_name] = {} + source_unit.bytecodes_init[contract_name] = bytecode + source_unit.bytecodes_runtime[contract_name] = "" + source_unit.srcmaps_init[contract_name] = [] + source_unit.srcmaps_runtime[contract_name] = [] compilation_unit.compiler_version = CompilerVersion( compiler="unknown", version="", optimized=False @@ -150,7 +152,9 @@ def _handle_multiple_files( filtered_paths: List[str] = [] for filename, source_code in source_codes.items(): path_filename = PurePosixPath(filename) - if "contracts" in path_filename.parts and not filename.startswith("@"): + # https://etherscan.io/address/0x19bb64b80cbf61e61965b0e5c2560cc7364c6546#code has an import of erc721a/contracts/ERC721A.sol + # if the full path is lost then won't compile + if "contracts" == path_filename.parts[0] and not filename.startswith("@"): path_filename = PurePosixPath( *path_filename.parts[path_filename.parts.index("contracts") :] ) @@ -357,6 +361,9 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: solc_standard_json.standalone_compile(filenames, compilation_unit, working_dir=working_dir) + def clean(self, **_kwargs: str) -> None: + pass + @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """Check if the target is a etherscan project diff --git a/crytic_compile/platform/foundry.py b/crytic_compile/platform/foundry.py index 92f6bf8e..1aabb340 100755 --- a/crytic_compile/platform/foundry.py +++ b/crytic_compile/platform/foundry.py @@ -1,21 +1,17 @@ """ Truffle platform """ -import json import logging import os import shutil import subprocess from pathlib import Path -from typing import TYPE_CHECKING, List, Tuple, Optional +from typing import TYPE_CHECKING, List -from crytic_compile.compilation_unit import CompilationUnit -from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform.abstract_platform import AbstractPlatform -from crytic_compile.platform.exceptions import InvalidCompilation from crytic_compile.platform.types import Type -from crytic_compile.utils.naming import convert_filename -from crytic_compile.utils.natspec import Natspec +from crytic_compile.platform.hardhat import hardhat_like_parsing +from crytic_compile.utils.subprocess import run # Handle cycle if TYPE_CHECKING: @@ -41,8 +37,6 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: crytic_compile (CryticCompile): CryticCompile object to populate **kwargs: optional arguments. Used: "foundry_ignore_compile", "foundry_out_directory" - Raises: - InvalidCompilation: If foundry failed to run """ ignore_compile = kwargs.get("foundry_ignore_compile", False) or kwargs.get( @@ -60,14 +54,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: cmd = [ "forge", "build", - "--extra-output", - "abi", - "--extra-output", - "userdoc", - "--extra-output", - "devdoc", - "--extra-output", - "evm.methodIdentifiers", + "--build-info", "--force", ] @@ -86,77 +73,38 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: stdout_bytes, stderr_bytes = process.communicate() stdout, stderr = ( - stdout_bytes.decode(), - stderr_bytes.decode(), + stdout_bytes.decode(errors="backslashreplace"), + stderr_bytes.decode(errors="backslashreplace"), ) # convert bytestrings to unicode strings LOGGER.info(stdout) if stderr: LOGGER.error(stderr) - filenames = Path(self._target, out_directory).rglob("*.json") - - # foundry only support solc for now - compiler = "solc" - compilation_unit = CompilationUnit(crytic_compile, str(self._target)) - - for filename_txt in filenames: - with open(filename_txt, encoding="utf8") as file_desc: - target_loaded = json.load(file_desc) - - userdoc = target_loaded.get("userdoc", {}) - devdoc = target_loaded.get("devdoc", {}) - natspec = Natspec(userdoc, devdoc) - - if not "ast" in target_loaded: - continue - - filename = target_loaded["ast"]["absolutePath"] - - try: - filename = convert_filename( - filename, lambda x: x, crytic_compile, working_dir=self._target - ) - except InvalidCompilation as i: - txt = str(i) - txt += "\nSomething went wrong, please open an issue in https://github.com/crytic/crytic-compile" - # pylint: disable=raise-missing-from - raise InvalidCompilation(txt) - - compilation_unit.asts[filename.absolute] = target_loaded["ast"] - crytic_compile.filenames.add(filename) - compilation_unit.filenames.add(filename) - - contract_name = filename_txt.parts[-1] - contract_name = contract_name[: -len(".json")] - - compilation_unit.natspec[contract_name] = natspec - compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] - compilation_unit.bytecodes_init[contract_name] = target_loaded["bytecode"][ - "object" - ].replace("0x", "") - compilation_unit.bytecodes_runtime[contract_name] = target_loaded[ - "deployedBytecode" - ]["object"].replace("0x", "") - compilation_unit.srcmaps_init[contract_name] = ( - target_loaded["bytecode"]["sourceMap"].split(";") - if target_loaded["bytecode"].get("sourceMap") - else [] - ) - compilation_unit.srcmaps_runtime[contract_name] = ( - target_loaded["deployedBytecode"]["sourceMap"].split(";") - if target_loaded["deployedBytecode"].get("sourceMap") - else [] - ) - - version, optimized, runs = _get_config_info(self._target) - - compilation_unit.compiler_version = CompilerVersion( - compiler=compiler, version=version, optimized=optimized, optimize_runs=runs + build_directory = Path( + self._target, + out_directory, + "build-info", ) + hardhat_like_parsing(crytic_compile, self._target, build_directory, self._target) + + def clean(self, **kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **kwargs: optional arguments. + """ + + ignore_compile = kwargs.get("foundry_ignore_compile", False) or kwargs.get( + "ignore_compile", False + ) + + if ignore_compile: + return + + run(["forge", "clean"], cwd=self._target) + @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """Check if the target is a foundry project @@ -197,65 +145,3 @@ def _guessed_tests(self) -> List[str]: List[str]: The guessed unit tests commands """ return ["forge test"] - - -def _get_config_info(target: str) -> Tuple[str, Optional[bool], Optional[int]]: - """get the compiler version from solidity-files-cache.json - - Args: - target (str): path to the project directory - - Returns: - (str, str, str): compiler version, optimized, runs - - Raises: - InvalidCompilation: If cache/solidity-files-cache.json cannot be parsed - """ - config = Path(target, "cache", "solidity-files-cache.json") - if not config.exists(): - raise InvalidCompilation( - "Could not find the cache/solidity-files-cache.json file." - + "If you are using 'cache = true' in foundry's config file, please remove it." - + " Otherwise please open an issue in https://github.com/crytic/crytic-compile" - ) - with open(config, "r", encoding="utf8") as config_f: - config_dict = json.load(config_f) - - version: Optional[str] = None - optimizer: Optional[bool] = None - runs: Optional[int] = None - - if "files" in config_dict: - items = list(config_dict["files"].values()) - # On the form - # { .. - # "artifacts": { - # "CONTRACT_NAME": { - # "0.8.X+commit...": "filename"} - # - if len(items) >= 1: - item = items[0] - if "artifacts" in item: - items_artifact = list(item["artifacts"].values()) - if len(items_artifact) >= 1: - item_version = items_artifact[0] - version = list(item_version.keys())[0] - assert version - plus_position = version.find("+") - if plus_position > 0: - version = version[:plus_position] - if ( - "solcConfig" in item - and "settings" in item["solcConfig"] - and "optimizer" in item["solcConfig"]["settings"] - ): - optimizer = item["solcConfig"]["settings"]["optimizer"]["enabled"] - runs = item["solcConfig"]["settings"]["optimizer"].get("runs", None) - - if version is None: - raise InvalidCompilation( - "Something went wrong with cache/solidity-files-cache.json parsing" - + ". Please open an issue in https://github.com/crytic/crytic-compile" - ) - - return version, optimizer, runs diff --git a/crytic_compile/platform/hardhat.py b/crytic_compile/platform/hardhat.py index 03a75639..406c8c86 100755 --- a/crytic_compile/platform/hardhat.py +++ b/crytic_compile/platform/hardhat.py @@ -7,24 +7,133 @@ import shutil import subprocess from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union from crytic_compile.compiler.compiler import CompilerVersion from crytic_compile.platform.exceptions import InvalidCompilation from crytic_compile.platform.types import Type from crytic_compile.utils.naming import convert_filename, extract_name from crytic_compile.utils.natspec import Natspec -from .abstract_platform import AbstractPlatform +from crytic_compile.utils.subprocess import run +from crytic_compile.platform.abstract_platform import AbstractPlatform # Handle cycle -from .solc import relative_to_short -from ..compilation_unit import CompilationUnit +from crytic_compile.platform.solc import relative_to_short +from crytic_compile.compilation_unit import CompilationUnit if TYPE_CHECKING: from crytic_compile import CryticCompile LOGGER = logging.getLogger("CryticCompile") +# pylint: disable=too-many-locals +def hardhat_like_parsing( + crytic_compile: "CryticCompile", target: str, build_directory: Path, working_dir: str +) -> None: + """ + This function parse the output generated by hardhat. + It can be re-used by any platform that follows the same schema (ex:foudnry) + + + Args: + crytic_compile: CryticCompile object + target: target + build_directory: build directory + working_dir: working directory + + Raises: + InvalidCompilation: If hardhat failed to run + + """ + files = sorted( + os.listdir(build_directory), key=lambda x: os.path.getmtime(Path(build_directory, x)) + ) + files = [str(f) for f in files if str(f).endswith(".json")] + if not files: + txt = f"`compile` failed. Can you run it?\n{build_directory} is empty" + raise InvalidCompilation(txt) + + for file in files: + build_info = Path(build_directory, file) + + # 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) + + 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 + 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 + ) + + 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(): + + filename = convert_filename( + original_filename, + 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"] + class Hardhat(AbstractPlatform): """ @@ -35,7 +144,6 @@ class Hardhat(AbstractPlatform): PROJECT_URL = "https://github.com/nomiclabs/hardhat" TYPE = Type.HARDHAT - # pylint: disable=too-many-locals,too-many-statements def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: """Run the compilation @@ -44,17 +152,9 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: **kwargs: optional arguments. Used: "hardhat_ignore", "hardhat_ignore_compile", "ignore_compile", "hardhat_artifacts_directory","hardhat_working_dir","npx_disable" - Raises: - InvalidCompilation: If hardhat failed to run """ - hardhat_ignore_compile = kwargs.get("hardhat_ignore_compile", False) or kwargs.get( - "ignore_compile", False - ) - - base_cmd = ["hardhat"] - if not kwargs.get("npx_disable", False): - base_cmd = ["npx"] + base_cmd + hardhat_ignore_compile, base_cmd = self._settings(kwargs) detected_paths = self._get_hardhat_paths(base_cmd, kwargs) @@ -64,7 +164,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: "build-info", ) - hardhat_working_dir = Path(self._target, detected_paths["root"]) + hardhat_working_dir = str(Path(self._target, detected_paths["root"])) if not hardhat_ignore_compile: cmd = base_cmd + ["compile", "--force"] @@ -84,101 +184,30 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: stdout_bytes, stderr_bytes = process.communicate() stdout, stderr = ( - stdout_bytes.decode(), - stderr_bytes.decode(), + stdout_bytes.decode(errors="backslashreplace"), + stderr_bytes.decode(errors="backslashreplace"), ) # convert bytestrings to unicode strings LOGGER.info(stdout) if stderr: LOGGER.error(stderr) - files = sorted( - os.listdir(build_directory), key=lambda x: os.path.getmtime(Path(build_directory, x)) - ) - files = [f for f in files if f.endswith(".json")] - if not files: - txt = f"`hardhat compile` failed. Can you run it?\n{build_directory} is empty" - raise InvalidCompilation(txt) - - for file in files: - build_info = Path(build_directory, file) - - # 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) - - 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 - 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 - ) - - 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(): - for original_contract_name, info in contracts_info.items(): - contract_name = extract_name(original_contract_name) - - contract_filename = convert_filename( - original_filename, - relative_to_short, - crytic_compile, - working_dir=hardhat_working_dir, - ) - - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add( - contract_name - ) - - compilation_unit.abis[contract_name] = info["abi"] - compilation_unit.bytecodes_init[contract_name] = info["evm"][ - "bytecode" - ]["object"] - compilation_unit.bytecodes_runtime[contract_name] = info["evm"][ - "deployedBytecode" - ]["object"] - compilation_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ - "sourceMap" - ].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["evm"][ - "deployedBytecode" - ]["sourceMap"].split(";") - userdoc = info.get("userdoc", {}) - devdoc = info.get("devdoc", {}) - natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec - - if "sources" in targets_json: - for path, info in targets_json["sources"].items(): - if skip_filename: - path = convert_filename( - self._target, - relative_to_short, - crytic_compile, - working_dir=hardhat_working_dir, - ) - else: - path = convert_filename( - path, - relative_to_short, - crytic_compile, - working_dir=hardhat_working_dir, - ) - crytic_compile.filenames.add(path) - compilation_unit.filenames.add(path) - compilation_unit.asts[path.absolute] = info["ast"] + hardhat_like_parsing(crytic_compile, self._target, build_directory, hardhat_working_dir) + + def clean(self, **kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **kwargs: optional arguments. + """ + + hardhat_ignore_compile, base_cmd = self._settings(kwargs) + + if hardhat_ignore_compile: + return + + for clean_cmd in [["clean"], ["clean", "--global"]]: + run(base_cmd + clean_cmd, cwd=self._target) @staticmethod def is_supported(target: str, **kwargs: str) -> bool: @@ -194,6 +223,11 @@ def is_supported(target: str, **kwargs: str) -> bool: hardhat_ignore = kwargs.get("hardhat_ignore", False) if hardhat_ignore: return False + + # If there is both foundry and hardhat, foundry takes priority + if os.path.isfile(os.path.join(target, "foundry.toml")): + return False + return os.path.isfile(os.path.join(target, "hardhat.config.js")) | os.path.isfile( os.path.join(target, "hardhat.config.ts") ) @@ -221,6 +255,18 @@ def _guessed_tests(self) -> List[str]: """ return ["hardhat test"] + @staticmethod + def _settings(args: Dict[str, Any]) -> Tuple[bool, List[str]]: + hardhat_ignore_compile = args.get("hardhat_ignore_compile", False) or args.get( + "ignore_compile", False + ) + + base_cmd = ["hardhat"] + if not args.get("npx_disable", False): + base_cmd = ["npx"] + base_cmd + + return hardhat_ignore_compile, base_cmd + def _get_hardhat_paths( self, base_cmd: List[str], args: Dict[str, str] ) -> Dict[str, Union[Path, str]]: @@ -285,7 +331,7 @@ def _run_hardhat_console(self, base_cmd: List[str], command: str) -> Optional[st stdout_bytes, stderr_bytes = process.communicate(command.encode("utf-8")) stdout, stderr = ( stdout_bytes.decode(), - stderr_bytes.decode(), + stderr_bytes.decode(errors="backslashreplace"), ) if stderr: diff --git a/crytic_compile/platform/solc.py b/crytic_compile/platform/solc.py index 14b53e3b..3ecdc1c4 100644 --- a/crytic_compile/platform/solc.py +++ b/crytic_compile/platform/solc.py @@ -34,22 +34,22 @@ def _build_contract_data(compilation_unit: "CompilationUnit") -> Dict: contracts = {} - for filename, contract_names in compilation_unit.filename_to_contracts.items(): - for contract_name in contract_names: - abi = str(compilation_unit.abi(contract_name)) + for filename, source_unit in compilation_unit.source_units.items(): + for contract_name in source_unit.contracts_names: + abi = str(source_unit.abi(contract_name)) abi = abi.replace("'", '"') abi = abi.replace("True", "true") abi = abi.replace("False", "false") abi = abi.replace(" ", "") exported_name = combine_filename_name(filename.absolute, contract_name) contracts[exported_name] = { - "srcmap": ";".join(compilation_unit.srcmap_init(contract_name)), - "srcmap-runtime": ";".join(compilation_unit.srcmap_runtime(contract_name)), + "srcmap": ";".join(source_unit.srcmap_init(contract_name)), + "srcmap-runtime": ";".join(source_unit.srcmap_runtime(contract_name)), "abi": abi, - "bin": compilation_unit.bytecode_init(contract_name), - "bin-runtime": compilation_unit.bytecode_runtime(contract_name), - "userdoc": compilation_unit.natspec[contract_name].userdoc.export(), - "devdoc": compilation_unit.natspec[contract_name].devdoc.export(), + "bin": source_unit.bytecode_init(contract_name), + "bin-runtime": source_unit.bytecode_runtime(contract_name), + "userdoc": source_unit.natspec[contract_name].userdoc.export(), + "devdoc": source_unit.natspec[contract_name].devdoc.export(), } return contracts @@ -178,9 +178,16 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: path = convert_filename( path, relative_to_short, crytic_compile, working_dir=solc_working_dir ) - compilation_unit.filenames.add(path) - crytic_compile.filenames.add(path) - compilation_unit.asts[path.absolute] = info["AST"] + source_unit = compilation_unit.create_source_unit(path) + source_unit.ast = info["AST"] + + def clean(self, **_kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **_kwargs: unused. + """ + return @staticmethod def is_supported(target: str, **kwargs: str) -> bool: @@ -309,32 +316,35 @@ def solc_handle_contracts( contract_name = extract_name(original_contract_name) # for solc < 0.4.10 we cant retrieve the filename from the ast if skip_filename: - contract_filename = convert_filename( + filename = convert_filename( target, relative_to_short, compilation_unit.crytic_compile, working_dir=solc_working_dir, ) else: - contract_filename = convert_filename( + filename = convert_filename( extract_filename(original_contract_name), relative_to_short, compilation_unit.crytic_compile, working_dir=solc_working_dir, ) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.abis[contract_name] = ( + + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit.abis[contract_name] = ( json.loads(info["abi"]) if not is_above_0_8 else info["abi"] ) - compilation_unit.bytecodes_init[contract_name] = info["bin"] - compilation_unit.bytecodes_runtime[contract_name] = info["bin-runtime"] - compilation_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") + source_unit.bytecodes_init[contract_name] = info["bin"] + source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"] + source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") userdoc = json.loads(info.get("userdoc", "{}")) if not is_above_0_8 else info["userdoc"] devdoc = json.loads(info.get("devdoc", "{}")) if not is_above_0_8 else info["devdoc"] natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec def _is_at_or_above_minor_version(compilation_unit: "CompilationUnit", version: int) -> bool: @@ -374,11 +384,16 @@ def get_version(solc: str, env: Optional[Dict[str, str]]) -> str: env=env, executable=shutil.which(cmd[0]), ) as process: - stdout_bytes, _ = process.communicate() - stdout = stdout_bytes.decode() # convert bytestrings to unicode strings + stdout_bytes, stderr_bytes = process.communicate() + stdout, stderr = ( + stdout_bytes.decode(errors="backslashreplace"), + stderr_bytes.decode(errors="backslashreplace"), + ) # convert bytestrings to unicode strings version = re.findall(r"\d+\.\d+\.\d+", stdout) if len(version) == 0: - raise InvalidCompilation(f"Solidity version not found: {stdout}") + raise InvalidCompilation( + f"\nSolidity version not found:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}" + ) return version[0] except OSError as error: # pylint: disable=raise-missing-from @@ -582,6 +597,7 @@ def _run_solcs_path( targets_json = None if isinstance(solcs_path, dict): guessed_solcs = _guess_solc(filename, working_dir) + compilation_errors = [] for guessed_solc in guessed_solcs: if not guessed_solc in solcs_path: continue @@ -622,12 +638,14 @@ def _run_solcs_path( working_dir=working_dir, force_legacy_json=force_legacy_json, ) - except InvalidCompilation: - pass + break + except InvalidCompilation as ic: + compilation_errors.append(solc_bin + ": " + ic.args[0]) if not targets_json: raise InvalidCompilation( - "Invalid solc compilation, none of the solc versions provided worked" + "Invalid solc compilation, none of the solc versions provided worked:\n" + + "\n".join(compilation_errors) ) return targets_json @@ -670,6 +688,7 @@ def _run_solcs_env( env = dict(os.environ) if env is None else env targets_json = None guessed_solcs = _guess_solc(filename, working_dir) + compilation_errors = [] for guessed_solc in guessed_solcs: if solcs_env and not guessed_solc in solcs_env: continue @@ -686,6 +705,7 @@ def _run_solcs_env( working_dir=working_dir, force_legacy_json=force_legacy_json, ) + break except InvalidCompilation: pass @@ -706,12 +726,14 @@ def _run_solcs_env( working_dir=working_dir, force_legacy_json=force_legacy_json, ) - except InvalidCompilation: - pass + break + except InvalidCompilation as ic: + compilation_errors.append(version_env + ": " + ic.args[0]) if not targets_json: raise InvalidCompilation( - "Invalid solc compilation, none of the solc versions provided worked" + "Invalid solc compilation, none of the solc versions provided worked:\n" + + "\n".join(compilation_errors) ) return targets_json diff --git a/crytic_compile/platform/solc_standard_json.py b/crytic_compile/platform/solc_standard_json.py index 59e56324..ab500458 100644 --- a/crytic_compile/platform/solc_standard_json.py +++ b/crytic_compile/platform/solc_standard_json.py @@ -157,7 +157,7 @@ def run_solc_standard_json( stdout_b, stderr_b = process.communicate(json.dumps(solc_input).encode("utf-8")) stdout, stderr = ( stdout_b.decode(), - stderr_b.decode(), + stderr_b.decode(errors="backslashreplace"), ) # convert bytestrings to unicode strings solc_json_output = json.loads(stdout) @@ -266,36 +266,39 @@ def parse_standard_json_output( for contract_name, info in file_contracts.items(): # for solc < 0.4.10 we cant retrieve the filename from the ast if skip_filename: - contract_filename = convert_filename( + filename = convert_filename( file_path, relative_to_short, compilation_unit.crytic_compile, working_dir=solc_working_dir, ) else: - contract_filename = convert_filename( + filename = convert_filename( file_path, relative_to_short, compilation_unit.crytic_compile, working_dir=solc_working_dir, ) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.abis[contract_name] = info["abi"] + + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit.abis[contract_name] = info["abi"] userdoc = info.get("userdoc", {}) devdoc = info.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec - compilation_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"]["object"] - compilation_unit.bytecodes_runtime[contract_name] = info["evm"]["deployedBytecode"][ + source_unit.bytecodes_init[contract_name] = info["evm"]["bytecode"]["object"] + source_unit.bytecodes_runtime[contract_name] = info["evm"]["deployedBytecode"][ "object" ] - compilation_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ + source_unit.srcmaps_init[contract_name] = info["evm"]["bytecode"][ "sourceMap" ].split(";") - compilation_unit.srcmaps_runtime[contract_name] = info["evm"]["deployedBytecode"][ + source_unit.srcmaps_runtime[contract_name] = info["evm"]["deployedBytecode"][ "sourceMap" ].split(";") @@ -315,10 +318,9 @@ def parse_standard_json_output( compilation_unit.crytic_compile, working_dir=solc_working_dir, ) - compilation_unit.crytic_compile.filenames.add(path) - compilation_unit.filenames.add(path) + source_unit = compilation_unit.create_source_unit(path) - compilation_unit.asts[path.absolute] = info.get("ast") + source_unit.ast = info.get("ast") # Inherits is_dependency/is_supported from Solc diff --git a/crytic_compile/platform/standard.py b/crytic_compile/platform/standard.py index 76d7447a..19669ff5 100644 --- a/crytic_compile/platform/standard.py +++ b/crytic_compile/platform/standard.py @@ -3,6 +3,7 @@ """ import json import os +from collections import defaultdict from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Any @@ -93,6 +94,14 @@ def compile(self, crytic_compile: "CryticCompile", **_kwargs: str) -> None: self._underlying_platform = platform self._unit_tests = unit_tests + def clean(self, **_kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **_kwargs: unused. + """ + return + @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """Check if the target has the standard crytic-compile format @@ -213,22 +222,23 @@ def generate_standard_export(crytic_compile: "CryticCompile") -> Dict: compilation_units = {} for key, compilation_unit in crytic_compile.compilation_units.items(): - contracts: Dict[str, Dict[str, Any]] = {} - for filename, contract_names in compilation_unit.filename_to_contracts.items(): - contracts[filename.relative] = {} - for contract_name in contract_names: - libraries = compilation_unit.libraries_names_and_patterns(contract_name) - contracts[filename.relative][contract_name] = { - "abi": compilation_unit.abi(contract_name), - "bin": compilation_unit.bytecode_init(contract_name), - "bin-runtime": compilation_unit.bytecode_runtime(contract_name), - "srcmap": ";".join(compilation_unit.srcmap_init(contract_name)), - "srcmap-runtime": ";".join(compilation_unit.srcmap_runtime(contract_name)), + source_unit_dict: Dict[str, Dict[str, Dict[str, Any]]] = {} + for filename, source_unit in compilation_unit.source_units.items(): + source_unit_dict[filename.relative] = defaultdict(dict) + source_unit_dict[filename.relative]["ast"] = source_unit.ast + for contract_name in source_unit.contracts_names: + libraries = source_unit.libraries_names_and_patterns(contract_name) + source_unit_dict[filename.relative]["contracts"][contract_name] = { + "abi": source_unit.abi(contract_name), + "bin": source_unit.bytecode_init(contract_name), + "bin-runtime": source_unit.bytecode_runtime(contract_name), + "srcmap": ";".join(source_unit.srcmap_init(contract_name)), + "srcmap-runtime": ";".join(source_unit.srcmap_runtime(contract_name)), "filenames": _convert_filename_to_dict(filename), "libraries": dict(libraries) if libraries else {}, "is_dependency": crytic_compile.is_dependency(filename.absolute), - "userdoc": compilation_unit.natspec[contract_name].userdoc.export(), - "devdoc": compilation_unit.natspec[contract_name].devdoc.export(), + "userdoc": source_unit.natspec[contract_name].userdoc.export(), + "devdoc": source_unit.natspec[contract_name].devdoc.export(), } # Create our root object to contain the contracts and other information. @@ -243,8 +253,7 @@ def generate_standard_export(crytic_compile: "CryticCompile") -> Dict: compilation_units[key] = { "compiler": compiler, - "asts": compilation_unit.asts, - "contracts": contracts, + "source_units": source_unit_dict, "filenames": [ _convert_filename_to_dict(filename) for filename in compilation_unit.filenames ], @@ -256,7 +265,7 @@ def generate_standard_export(crytic_compile: "CryticCompile") -> Dict: "working_dir": str(crytic_compile.working_dir), "type": int(crytic_compile.platform.platform_type_used), "unit_tests": crytic_compile.platform.guessed_tests(), - "crytic_version": "0.0.1", + "crytic_version": "0.0.2", } return output @@ -269,27 +278,27 @@ def _load_from_compile_legacy1(crytic_compile: "CryticCompile", loaded_json: Dic loaded_json (Dict): Json representation of the CryticCompile object """ compilation_unit = CompilationUnit(crytic_compile, "legacy") - compilation_unit.asts = loaded_json["asts"] compilation_unit.compiler_version = CompilerVersion( compiler=loaded_json["compiler"]["compiler"], version=loaded_json["compiler"]["version"], optimized=loaded_json["compiler"]["optimized"], ) for contract_name, contract in loaded_json["contracts"].items(): - compilation_unit.contracts_names.add(contract_name) filename = _convert_dict_to_filename(contract["filenames"]) compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit = compilation_unit.create_source_unit(filename) - compilation_unit.abis[contract_name] = contract["abi"] - compilation_unit.bytecodes_init[contract_name] = contract["bin"] - compilation_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] - compilation_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") - compilation_unit.libraries[contract_name] = contract["libraries"] + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = contract["abi"] + source_unit.bytecodes_init[contract_name] = contract["bin"] + source_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] + source_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") + source_unit.libraries[contract_name] = contract["libraries"] userdoc = contract.get("userdoc", {}) devdoc = contract.get("devdoc", {}) - compilation_unit.natspec[contract_name] = Natspec(userdoc, devdoc) + source_unit.natspec[contract_name] = Natspec(userdoc, devdoc) if contract["is_dependency"]: compilation_unit.crytic_compile.dependencies.add(filename.absolute) @@ -308,6 +317,12 @@ def _load_from_compile_legacy1(crytic_compile: "CryticCompile", loaded_json: Dic filename = _convert_dict_to_filename(contract["filenames"]) compilation_unit.filenames.add(filename) + for path, ast in loaded_json["asts"].items(): + # The following might create lookup issue? + filename = crytic_compile.filename_lookup(path) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast + def _load_from_compile_legacy2(crytic_compile: "CryticCompile", loaded_json: Dict) -> None: """Load from old (old) export @@ -325,7 +340,7 @@ def _load_from_compile_legacy2(crytic_compile: "CryticCompile", loaded_json: Dic optimized=compilation_unit_json["compiler"]["optimized"], ) for contract_name, contract in compilation_unit_json["contracts"].items(): - compilation_unit.contracts_names.add(contract_name) + filename = Filename( absolute=contract["filenames"]["absolute"], relative=contract["filenames"]["relative"], @@ -334,23 +349,24 @@ def _load_from_compile_legacy2(crytic_compile: "CryticCompile", loaded_json: Dic ) compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.abis[contract_name] = contract["abi"] - compilation_unit.bytecodes_init[contract_name] = contract["bin"] - compilation_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] - compilation_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") - compilation_unit.libraries[contract_name] = contract["libraries"] + source_unit = compilation_unit.create_source_unit(filename) + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = contract["abi"] + source_unit.bytecodes_init[contract_name] = contract["bin"] + source_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] + source_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") + source_unit.libraries[contract_name] = contract["libraries"] userdoc = contract.get("userdoc", {}) devdoc = contract.get("devdoc", {}) - compilation_unit.natspec[contract_name] = Natspec(userdoc, devdoc) + source_unit.natspec[contract_name] = Natspec(userdoc, devdoc) if contract["is_dependency"]: crytic_compile.dependencies.add(filename.absolute) crytic_compile.dependencies.add(filename.relative) crytic_compile.dependencies.add(filename.short) crytic_compile.dependencies.add(filename.used) - compilation_unit.asts = compilation_unit_json["asts"] if "filenames" in compilation_unit_json: compilation_unit.filenames = { @@ -358,14 +374,20 @@ def _load_from_compile_legacy2(crytic_compile: "CryticCompile", loaded_json: Dic for filename in compilation_unit_json["filenames"] } else: - # For legay code, we recover the filenames from the contracts list + # For legacy code, we recover the filenames from the contracts list # This is not perfect, as a filename might not be associated to any contract for contract_name, contract in compilation_unit_json["contracts"].items(): filename = _convert_dict_to_filename(contract["filenames"]) compilation_unit.filenames.add(filename) + for path, ast in loaded_json["asts"].items(): + # The following might create lookup issue? + filename = crytic_compile.filename_lookup(path) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast -def _load_from_compile_current(crytic_compile: "CryticCompile", loaded_json: Dict) -> None: + +def _load_from_compile_0_0_1(crytic_compile: "CryticCompile", loaded_json: Dict) -> None: for key, compilation_unit_json in loaded_json["compilation_units"].items(): compilation_unit = CompilationUnit(crytic_compile, key) compilation_unit.compiler_version = CompilerVersion( @@ -375,7 +397,7 @@ def _load_from_compile_current(crytic_compile: "CryticCompile", loaded_json: Dic ) for contracts_data in compilation_unit_json["contracts"].values(): for contract_name, contract in contracts_data.items(): - compilation_unit.contracts_names.add(contract_name) + filename = Filename( absolute=contract["filenames"]["absolute"], relative=contract["filenames"]["relative"], @@ -383,30 +405,76 @@ def _load_from_compile_current(crytic_compile: "CryticCompile", loaded_json: Dic used=contract["filenames"]["used"], ) compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = contract["abi"] + source_unit.bytecodes_init[contract_name] = contract["bin"] + source_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] + source_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") + source_unit.libraries[contract_name] = contract["libraries"] + + userdoc = contract.get("userdoc", {}) + devdoc = contract.get("devdoc", {}) + source_unit.natspec[contract_name] = Natspec(userdoc, devdoc) + + if contract["is_dependency"]: + crytic_compile.dependencies.add(filename.absolute) + crytic_compile.dependencies.add(filename.relative) + crytic_compile.dependencies.add(filename.short) + crytic_compile.dependencies.add(filename.used) + + compilation_unit.filenames = { + _convert_dict_to_filename(filename) for filename in compilation_unit_json["filenames"] + } + + for path, ast in compilation_unit_json["asts"].items(): + # The following might create lookup issue? + filename = crytic_compile.filename_lookup(path) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast - compilation_unit.abis[contract_name] = contract["abi"] - compilation_unit.bytecodes_init[contract_name] = contract["bin"] - compilation_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] - compilation_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split( - ";" - ) - compilation_unit.libraries[contract_name] = contract["libraries"] + +def _load_from_compile_current(crytic_compile: "CryticCompile", loaded_json: Dict) -> None: + for key, compilation_unit_json in loaded_json["compilation_units"].items(): + compilation_unit = CompilationUnit(crytic_compile, key) + compilation_unit.compiler_version = CompilerVersion( + compiler=compilation_unit_json["compiler"]["compiler"], + version=compilation_unit_json["compiler"]["version"], + optimized=compilation_unit_json["compiler"]["optimized"], + ) + + compilation_unit.filenames = { + _convert_dict_to_filename(filename) for filename in compilation_unit_json["filenames"] + } + + for filename_str, source_unit_data in compilation_unit_json["source_units"].items(): + filename = compilation_unit.filename_lookup(filename_str) + source_unit = compilation_unit.create_source_unit(filename) + + for contract_name, contract in source_unit_data.get("contracts", {}).items(): + compilation_unit.filename_to_contracts[filename].add(contract_name) + + source_unit = compilation_unit.create_source_unit(filename) + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = contract["abi"] + source_unit.bytecodes_init[contract_name] = contract["bin"] + source_unit.bytecodes_runtime[contract_name] = contract["bin-runtime"] + source_unit.srcmaps_init[contract_name] = contract["srcmap"].split(";") + source_unit.srcmaps_runtime[contract_name] = contract["srcmap-runtime"].split(";") + source_unit.libraries[contract_name] = contract["libraries"] userdoc = contract.get("userdoc", {}) devdoc = contract.get("devdoc", {}) - compilation_unit.natspec[contract_name] = Natspec(userdoc, devdoc) + source_unit.natspec[contract_name] = Natspec(userdoc, devdoc) if contract["is_dependency"]: crytic_compile.dependencies.add(filename.absolute) crytic_compile.dependencies.add(filename.relative) crytic_compile.dependencies.add(filename.short) crytic_compile.dependencies.add(filename.used) - compilation_unit.asts = compilation_unit_json["asts"] - compilation_unit.filenames = { - _convert_dict_to_filename(filename) - for filename in compilation_unit_json["filenames"] - } + + source_unit.ast = source_unit_data["ast"] def load_from_compile(crytic_compile: "CryticCompile", loaded_json: Dict) -> Tuple[int, List[str]]: @@ -421,20 +489,17 @@ def load_from_compile(crytic_compile: "CryticCompile", loaded_json: Dict) -> Tup Tuple[int, List[str]]: (underlying platform types, guessed unit tests) """ crytic_compile.package_name = loaded_json.get("package", None) - if "compilation_units" not in loaded_json: _load_from_compile_legacy1(crytic_compile, loaded_json) elif "crytic_version" not in loaded_json: _load_from_compile_legacy2(crytic_compile, loaded_json) + elif loaded_json["crytic_version"] == "0.0.1": + _load_from_compile_0_0_1(crytic_compile, loaded_json) else: _load_from_compile_current(crytic_compile, loaded_json) - # Set our filenames - for compilation_unit in crytic_compile.compilation_units.values(): - crytic_compile.filenames |= set(compilation_unit.filenames) - crytic_compile.working_dir = loaded_json["working_dir"] return loaded_json["type"], loaded_json.get("unit_tests", []) diff --git a/crytic_compile/platform/truffle.py b/crytic_compile/platform/truffle.py index ce5f9bd6..202b2ae4 100755 --- a/crytic_compile/platform/truffle.py +++ b/crytic_compile/platform/truffle.py @@ -54,17 +54,17 @@ def export_to_truffle(crytic_compile: "CryticCompile", **kwargs: str) -> List[st # Loop for each contract filename. results: List[Dict] = [] - for filename, contract_names in compilation_unit.filename_to_contracts.items(): - for contract_name in contract_names: + for source_unit in compilation_unit.source_units.values(): + for contract_name in source_unit.contracts_names: # Create the informational object to output for this contract output = { "contractName": contract_name, - "abi": compilation_unit.abi(contract_name), - "bytecode": "0x" + compilation_unit.bytecode_init(contract_name), - "deployedBytecode": "0x" + compilation_unit.bytecode_runtime(contract_name), - "ast": compilation_unit.ast(filename.absolute), - "userdoc": compilation_unit.natspec[contract_name].userdoc.export(), - "devdoc": compilation_unit.natspec[contract_name].devdoc.export(), + "abi": source_unit.abi(contract_name), + "bytecode": "0x" + source_unit.bytecode_init(contract_name), + "deployedBytecode": "0x" + source_unit.bytecode_runtime(contract_name), + "ast": source_unit.ast, + "userdoc": source_unit.natspec[contract_name].userdoc.export(), + "devdoc": source_unit.natspec[contract_name].devdoc.export(), } results.append(output) @@ -176,8 +176,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: stdout_bytes, stderr_bytes = process.communicate() stdout, stderr = ( - stdout_bytes.decode(), - stderr_bytes.decode(), + stdout_bytes.decode(errors="backslashreplace"), + stderr_bytes.decode(errors="backslashreplace"), ) # convert bytestrings to unicode strings if truffle_overwrite_config: @@ -241,22 +241,23 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: # pylint: disable=raise-missing-from raise InvalidCompilation(txt) - compilation_unit.asts[filename.absolute] = target_loaded["ast"] - crytic_compile.filenames.add(filename) - compilation_unit.filenames.add(filename) + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.ast = target_loaded["ast"] + contract_name = target_loaded["contractName"] - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] - compilation_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = target_loaded["abi"] + source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace( "0x", "" ) - compilation_unit.bytecodes_runtime[contract_name] = target_loaded[ + source_unit.bytecodes_runtime[contract_name] = target_loaded[ "deployedBytecode" ].replace("0x", "") - compilation_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") - compilation_unit.srcmaps_runtime[contract_name] = target_loaded[ + source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") + source_unit.srcmaps_runtime[contract_name] = target_loaded[ "deployedSourceMap" ].split(";") @@ -278,6 +279,14 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: compiler=compiler, version=version, optimized=optimized ) + def clean(self, **_kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **_kwargs: unused. + """ + return + @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """Check if the target is a truffle project diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index c2ce8b6f..5b985aef 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -56,36 +56,39 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: assert target in targets_json info = targets_json[target] - contract_filename = convert_filename(target, _relative_to_short, crytic_compile) + filename = convert_filename(target, _relative_to_short, crytic_compile) contract_name = Path(target).parts[-1] - compilation_unit.contracts_names.add(contract_name) - compilation_unit.filename_to_contracts[contract_filename].add(contract_name) - compilation_unit.abis[contract_name] = info["abi"] - compilation_unit.bytecodes_init[contract_name] = info["bytecode"].replace("0x", "") - compilation_unit.bytecodes_runtime[contract_name] = info["bytecode_runtime"].replace( - "0x", "" - ) + source_unit = compilation_unit.create_source_unit(filename) + + source_unit.contracts_names.add(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) + source_unit.abis[contract_name] = info["abi"] + source_unit.bytecodes_init[contract_name] = info["bytecode"].replace("0x", "") + source_unit.bytecodes_runtime[contract_name] = info["bytecode_runtime"].replace("0x", "") # Vyper does not provide the source mapping for the init bytecode - compilation_unit.srcmaps_init[contract_name] = [] + source_unit.srcmaps_init[contract_name] = [] # info["source_map"]["pc_pos_map"] contains the source mapping in a simpler format # However pc_pos_map_compressed" seems to follow solc's format, so for convenience # We store the same # TODO: create SourceMapping class, so that srcmaps_runtime would store an class # That will give more flexebility to different compilers - compilation_unit.srcmaps_runtime[contract_name] = info["source_map"][ - "pc_pos_map_compressed" - ] - - crytic_compile.filenames.add(contract_filename) - compilation_unit.filenames.add(contract_filename) + source_unit.srcmaps_runtime[contract_name] = info["source_map"]["pc_pos_map_compressed"] # Natspec not yet handled for vyper - compilation_unit.natspec[contract_name] = Natspec({}, {}) + source_unit.natspec[contract_name] = Natspec({}, {}) ast = _get_vyper_ast(target, vyper) - compilation_unit.asts[contract_filename.absolute] = ast + source_unit.ast = ast + + def clean(self, **_kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **_kwargs: unused. + """ + return def is_dependency(self, _path: str) -> bool: """Check if the path is a dependency (not supported for vyper) diff --git a/crytic_compile/platform/waffle.py b/crytic_compile/platform/waffle.py index d6d1dee4..280d5d31 100755 --- a/crytic_compile/platform/waffle.py +++ b/crytic_compile/platform/waffle.py @@ -160,9 +160,9 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ) as process: stdout, stderr = process.communicate() if stdout: - LOGGER.info(stdout.decode()) + LOGGER.info(stdout.decode(errors="backslashreplace")) if stderr: - LOGGER.error(stderr.decode()) + LOGGER.error(stderr.decode(errors="backslashreplace")) except OSError as error: # pylint: disable=raise-missing-from raise InvalidCompilation(error) @@ -189,36 +189,42 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ) contract_name = contract[1] + source_unit = compilation_unit.create_source_unit(filename) - compilation_unit.asts[filename.absolute] = target_all["sources"][contract[0]]["AST"] - crytic_compile.filenames.add(filename) + source_unit.ast = target_all["sources"][contract[0]]["AST"] compilation_unit.filenames.add(filename) compilation_unit.filename_to_contracts[filename].add(contract_name) - compilation_unit.contracts_names.add(contract_name) - compilation_unit.abis[contract_name] = target_loaded["abi"] + source_unit.contracts_names.add(contract_name) + source_unit.abis[contract_name] = target_loaded["abi"] userdoc = target_loaded.get("userdoc", {}) devdoc = target_loaded.get("devdoc", {}) natspec = Natspec(userdoc, devdoc) - compilation_unit.natspec[contract_name] = natspec + source_unit.natspec[contract_name] = natspec - compilation_unit.bytecodes_init[contract_name] = target_loaded["evm"]["bytecode"][ + source_unit.bytecodes_init[contract_name] = target_loaded["evm"]["bytecode"]["object"] + source_unit.srcmaps_init[contract_name] = target_loaded["evm"]["bytecode"][ + "sourceMap" + ].split(";") + source_unit.bytecodes_runtime[contract_name] = target_loaded["evm"]["deployedBytecode"][ "object" ] - compilation_unit.srcmaps_init[contract_name] = target_loaded["evm"]["bytecode"][ + source_unit.srcmaps_runtime[contract_name] = target_loaded["evm"]["deployedBytecode"][ "sourceMap" ].split(";") - compilation_unit.bytecodes_runtime[contract_name] = target_loaded["evm"][ - "deployedBytecode" - ]["object"] - compilation_unit.srcmaps_runtime[contract_name] = target_loaded["evm"][ - "deployedBytecode" - ]["sourceMap"].split(";") compilation_unit.compiler_version = CompilerVersion( compiler=compiler, version=version, optimized=optimized ) + def clean(self, **_kwargs: str) -> None: + """Clean compilation artifacts + + Args: + **_kwargs: unused. + """ + return + @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """Check if the target is a waffle project diff --git a/crytic_compile/source_unit.py b/crytic_compile/source_unit.py new file mode 100644 index 00000000..d2552dff --- /dev/null +++ b/crytic_compile/source_unit.py @@ -0,0 +1,611 @@ +""" +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.naming import Filename +from crytic_compile.utils.natspec import Natspec + +if TYPE_CHECKING: + from crytic_compile.compilation_unit import CompilationUnit + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class SourceUnit: + """SourceUnit class""" + + def __init__(self, compilation_unit: "CompilationUnit", filename: Filename): + + self.filename = filename + self.compilation_unit: "CompilationUnit" = compilation_unit + + # ABI, bytecode and srcmap are indexed by contract_name + self._abis: Dict = {} + self._runtime_bytecodes: Dict = {} + self._init_bytecodes: Dict = {} + self._hashes: Dict = {} + self._events: Dict = {} + self._srcmaps: Dict[str, List[str]] = {} + self._srcmaps_runtime: Dict[str, List[str]] = {} + self.ast: Dict = {} + + # Natspec + self._natspec: Dict[str, Natspec] = {} + + # Libraries used by the contract + # contract_name -> (library, pattern) + self._libraries: Dict[str, List[Tuple[str, str]]] = {} + + # set containing all the contract names + self._contracts_name: Set[str] = set() + + # set containing all the contract name without the libraries + self._contracts_name_without_libraries: Optional[Set[str]] = None + + # region ABI + ################################################################################### + ################################################################################### + + @property + def abis(self) -> Dict: + """Return the ABIs + + Returns: + Dict: ABIs (solc/vyper format) (contract name -> ABI) + """ + return self._abis + + def abi(self, name: str) -> Dict: + """Get the ABI from a contract + + Args: + name (str): Contract name + + Returns: + Dict: ABI (solc/vyper format) + """ + return self._abis.get(name, None) + + # endregion + ################################################################################### + ################################################################################### + # region Bytecode + ################################################################################### + ################################################################################### + + @property + def bytecodes_runtime(self) -> Dict[str, str]: + """Return the runtime bytecodes + + Returns: + Dict[str, str]: contract => runtime bytecode + """ + return self._runtime_bytecodes + + @bytecodes_runtime.setter + def bytecodes_runtime(self, bytecodes: Dict[str, str]) -> None: + """Set the bytecodes runtime + + Args: + bytecodes (Dict[str, str]): New bytecodes runtime + """ + self._runtime_bytecodes = bytecodes + + @property + def bytecodes_init(self) -> Dict[str, str]: + """Return the init bytecodes + + Returns: + Dict[str, str]: contract => init bytecode + """ + return self._init_bytecodes + + @bytecodes_init.setter + def bytecodes_init(self, bytecodes: Dict[str, str]) -> None: + """Set the bytecodes init + + Args: + bytecodes (Dict[str, str]): New bytecodes init + """ + self._init_bytecodes = bytecodes + + def bytecode_runtime(self, name: str, libraries: Optional[Dict[str, str]] = None) -> str: + """Return the runtime bytecode of the contract. + If library is provided, patch the bytecode + + Args: + name (str): contract name + libraries (Optional[Dict[str, str]], optional): lib_name => address. Defaults to None. + + Returns: + str: runtime bytecode + """ + runtime = self._runtime_bytecodes.get(name, None) + return self._update_bytecode_with_libraries(runtime, libraries) + + def bytecode_init(self, name: str, libraries: Optional[Dict[str, str]] = None) -> str: + """Return the init bytecode of the contract. + If library is provided, patch the bytecode + + Args: + name (str): contract name + libraries (Optional[Dict[str, str]], optional): lib_name => address. Defaults to None. + + Returns: + str: init bytecode + """ + init = self._init_bytecodes.get(name, None) + return self._update_bytecode_with_libraries(init, libraries) + + # endregion + ################################################################################### + ################################################################################### + # region Source mapping + ################################################################################### + ################################################################################### + + @property + def srcmaps_init(self) -> Dict[str, List[str]]: + """Return the srcmaps init + + Returns: + Dict[str, List[str]]: Srcmaps init (solc/vyper format) + """ + return self._srcmaps + + @property + def srcmaps_runtime(self) -> Dict[str, List[str]]: + """Return the srcmaps runtime + + Returns: + Dict[str, List[str]]: Srcmaps runtime (solc/vyper format) + """ + return self._srcmaps_runtime + + def srcmap_init(self, name: str) -> List[str]: + """Return the srcmap init of a contract + + Args: + name (str): name of the contract + + Returns: + List[str]: Srcmap init (solc/vyper format) + """ + return self._srcmaps.get(name, []) + + def srcmap_runtime(self, name: str) -> List[str]: + """Return the srcmap runtime of a contract + + Args: + name (str): name of the contract + + Returns: + List[str]: Srcmap runtime (solc/vyper format) + """ + return self._srcmaps_runtime.get(name, []) + + # endregion + ################################################################################### + ################################################################################### + # region Libraries + ################################################################################### + ################################################################################### + + @property + def libraries(self) -> Dict[str, List[Tuple[str, str]]]: + """Return the libraries used + + Returns: + Dict[str, List[Tuple[str, str]]]: (contract_name -> [(library, pattern))]) + """ + return self._libraries + + def _convert_libraries_names(self, libraries: Dict[str, str]) -> Dict[str, str]: + """Convert the libraries names + The name in the argument can be the library name, or filename:library_name + The returned dict contains all the names possible with the different solc versions + + Args: + libraries (Dict[str, str]): lib_name => address + + Returns: + Dict[str, str]: lib_name => address + """ + new_names = {} + for (lib, addr) in libraries.items(): + # Prior solidity 0.5 + # libraries were on the format __filename:contract_name_____ + # From solidity 0.5, + # libraries are on the format __$keccak(filename:contract_name)[34]$__ + # https://solidity.readthedocs.io/en/v0.5.7/050-breaking-changes.html#command-line-and-json-interfaces + + lib_4 = "__" + lib + "_" * (38 - len(lib)) + + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(lib.encode("utf-8")) + lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" + + new_names[lib] = addr + new_names[lib_4] = addr + new_names[lib_5] = addr + + for lib_filename, contract_names in self.compilation_unit.filename_to_contracts.items(): + for contract_name in contract_names: + if contract_name != lib: + continue + + lib_with_abs_filename = lib_filename.absolute + ":" + lib + lib_with_abs_filename = lib_with_abs_filename[0:36] + + lib_4 = "__" + lib_with_abs_filename + "_" * (38 - len(lib_with_abs_filename)) + new_names[lib_4] = addr + + lib_with_used_filename = lib_filename.used + ":" + lib + lib_with_used_filename = lib_with_used_filename[0:36] + + lib_4 = "__" + lib_with_used_filename + "_" * (38 - len(lib_with_used_filename)) + new_names[lib_4] = addr + + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(lib_with_abs_filename.encode("utf-8")) + lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" + new_names[lib_5] = addr + + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(lib_with_used_filename.encode("utf-8")) + lib_5 = "__$" + sha3_result.hexdigest()[:34] + "$__" + new_names[lib_5] = addr + + return new_names + + def _library_name_lookup( + self, lib_name: str, original_contract: str + ) -> Optional[Tuple[str, str]]: + """Do a lookup on a library name to its name used in contracts + The library can be: + - the original contract name + - __X__ following Solidity 0.4 format + - __$..$__ following Solidity 0.5 format + + Args: + lib_name (str): library name + original_contract (str): original contract name + + Returns: + Optional[Tuple[str, str]]: contract_name, library_name + """ + + for filename, contract_names in self.compilation_unit.filename_to_contracts.items(): + for name in contract_names: + if name == lib_name: + return name, name + + # Some platform use only the contract name + # Some use fimename:contract_name + name_with_absolute_filename = filename.absolute + ":" + name + name_with_absolute_filename = name_with_absolute_filename[0:36] + + name_with_used_filename = filename.used + ":" + name + name_with_used_filename = name_with_used_filename[0:36] + + # Solidity 0.4 + solidity_0_4 = "__" + name + "_" * (38 - len(name)) + if solidity_0_4 == lib_name: + return name, solidity_0_4 + + # Solidity 0.4 with filename + solidity_0_4_filename = ( + "__" + + name_with_absolute_filename + + "_" * (38 - len(name_with_absolute_filename)) + ) + if solidity_0_4_filename == lib_name: + return name, solidity_0_4_filename + + # Solidity 0.4 with filename + solidity_0_4_filename = ( + "__" + name_with_used_filename + "_" * (38 - len(name_with_used_filename)) + ) + if solidity_0_4_filename == lib_name: + return name, solidity_0_4_filename + + # Solidity 0.5 + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(name.encode("utf-8")) + v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" + + if v5_name == lib_name: + return name, v5_name + + # Solidity 0.5 with filename + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(name_with_absolute_filename.encode("utf-8")) + v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" + + if v5_name == lib_name: + return name, v5_name + + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(name_with_used_filename.encode("utf-8")) + v5_name = "__$" + sha3_result.hexdigest()[:34] + "$__" + + if v5_name == lib_name: + return name, v5_name + + # handle specific case of collision for Solidity <0.4 + # We can only detect that the second contract is meant to be the library + # if there is only two contracts in the codebase + if len(self._contracts_name) == 2: + return next( + ( + (c, "__" + c + "_" * (38 - len(c))) + for c in self._contracts_name + if c != original_contract + ), + None, + ) + + return None + + def libraries_names(self, name: str) -> List[str]: + """Return the names of the libraries used by the contract + + Args: + name (str): contract name + + Returns: + List[str]: libraries used + """ + + if name not in self._libraries: + init = re.findall(r"__.{36}__", self.bytecode_init(name)) + runtime = re.findall(r"__.{36}__", self.bytecode_runtime(name)) + libraires = [self._library_name_lookup(x, name) for x in set(init + runtime)] + self._libraries[name] = [lib for lib in libraires if lib] + return [name for (name, _) in self._libraries[name]] + + def libraries_names_and_patterns(self, name: str) -> List[Tuple[str, str]]: + """Return the names and the patterns of the libraries used by the contract + + Args: + name (str): contract name + + Returns: + List[Tuple[str, str]]: (lib_name, pattern) + """ + + if name not in self._libraries: + init = re.findall(r"__.{36}__", self.bytecode_init(name)) + runtime = re.findall(r"__.{36}__", self.bytecode_runtime(name)) + libraires = [self._library_name_lookup(x, name) for x in set(init + runtime)] + self._libraries[name] = [lib for lib in libraires if lib] + return self._libraries[name] + + def _update_bytecode_with_libraries( + self, bytecode: str, libraries: Union[None, Dict[str, str]] + ) -> str: + """Update the bytecode with the libraries address + + Args: + bytecode (str): bytecode to patch + libraries (Union[None, Dict[str, str]]): pattern => address + + Returns: + str: Patched bytecode + """ + if libraries: + libraries = self._convert_libraries_names(libraries) + for library_found in re.findall(r"__.{36}__", bytecode): + if library_found in libraries: + bytecode = re.sub( + re.escape(library_found), + f"{libraries[library_found]:0>40x}", + bytecode, + ) + return bytecode + + # endregion + ################################################################################### + ################################################################################### + # region Natspec + ################################################################################### + ################################################################################### + + @property + def natspec(self) -> Dict[str, Natspec]: + """Return the natspec of the contracts + + Returns: + Dict[str, Natspec]: Contract name -> Natspec + """ + return self._natspec + + # endregion + ################################################################################### + ################################################################################### + # region Contract Names + ################################################################################### + ################################################################################### + + @property + def contracts_names(self) -> Set[str]: + """Return the contracts names + + Returns: + Set[str]: List of the contracts names + """ + return self._contracts_name + + @contracts_names.setter + def contracts_names(self, names: Set[str]) -> None: + """Set the contract names + + Args: + names (Set[str]): New contracts names + """ + self._contracts_name = names + + @property + def contracts_names_without_libraries(self) -> Set[str]: + """Return the contracts names without the librairies + + Returns: + Set[str]: List of contracts + """ + if self._contracts_name_without_libraries is None: + libraries: List[str] = [] + for contract_name in self._contracts_name: + libraries += self.libraries_names(contract_name) + self._contracts_name_without_libraries = { + l for l in self._contracts_name if l not in set(libraries) + } + return self._contracts_name_without_libraries + + # endregion + ################################################################################### + ################################################################################### + # region Hashes + ################################################################################### + ################################################################################### + + def hashes(self, name: str) -> Dict[str, int]: + """Return the hashes of the functions + + Args: + name (str): contract name + + Returns: + Dict[str, int]: (function name => signature) + """ + if not name in self._hashes: + self._compute_hashes(name) + return self._hashes[name] + + def _compute_hashes(self, name: str) -> None: + """Compute the function hashes + + Args: + name (str): contract name + """ + self._hashes[name] = {} + for sig in self.abi(name): + if "type" in sig: + if sig["type"] == "function": + sig_name = sig["name"] + arguments = ",".join([x["type"] for x in sig["inputs"]]) + sig = f"{sig_name}({arguments})" + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(sig.encode("utf-8")) + self._hashes[name][sig] = int("0x" + sha3_result.hexdigest()[:8], 16) + + # endregion + ################################################################################### + ################################################################################### + # region Events + ################################################################################### + ################################################################################### + + def events_topics(self, name: str) -> Dict[str, Tuple[int, List[bool]]]: + """Return the topics of the contract's events + + Args: + name (str): contract name + + Returns: + Dict[str, Tuple[int, List[bool]]]: event signature => topic hash, [is_indexed for each parameter] + """ + if not name in self._events: + self._compute_topics_events(name) + return self._events[name] + + def _compute_topics_events(self, name: str) -> None: + """Compute the topics of the contract's events + + Args: + name (str): contract name + """ + self._events[name] = {} + for sig in self.abi(name): + if "type" in sig: + if sig["type"] == "event": + sig_name = sig["name"] + arguments = ",".join([x["type"] for x in sig["inputs"]]) + indexes = [x.get("indexed", False) for x in sig["inputs"]] + sig = f"{sig_name}({arguments})" + sha3_result = keccak.new(digest_bits=256) + sha3_result.update(sig.encode("utf-8")) + + self._events[name][sig] = (int("0x" + sha3_result.hexdigest()[:8], 16), indexes) + + # endregion + ################################################################################### + ################################################################################### + # region Metadata + ################################################################################### + ################################################################################### + + def 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, "") + + # endregion + ################################################################################### + ################################################################################### diff --git a/crytic_compile/utils/naming.py b/crytic_compile/utils/naming.py index d5ded767..8f5a65fd 100644 --- a/crytic_compile/utils/naming.py +++ b/crytic_compile/utils/naming.py @@ -59,6 +59,48 @@ def combine_filename_name(filename: str, name: str) -> str: return filename + ":" + name +def _verify_filename_existence(filename: Path, cwd: Path) -> Path: + """ + Check if the filename exist. If it does not, try multiple heuristics to find the right filename: + - Look for contracts/FILENAME + - Look for node_modules/FILENAME + - Look for node_modules/FILENAME in all the parents directories + + + Args: + filename (Path): filename to check + cwd (Path): directory + + Raises: + InvalidCompilation: if the filename is not found + + Returns: + Path: the filename + """ + + if filename.exists(): + return filename + + if cwd.joinpath(Path("contracts"), filename).exists(): + filename = cwd.joinpath("contracts", filename) + elif cwd.joinpath(filename).exists(): + filename = cwd.joinpath(filename) + # how node.js loads dependencies from node_modules: + # https://nodejs.org/api/modules.html#loading-from-node_modules-folders + elif cwd.joinpath(Path("node_modules"), filename).exists(): + filename = cwd.joinpath("node_modules", filename) + else: + for parent in cwd.parents: + if parent.joinpath(Path("node_modules"), filename).exists(): + filename = parent.joinpath(Path("node_modules"), filename) + break + + if not filename.exists(): + raise InvalidCompilation(f"Unknown file: {filename}") + + return filename + + # pylint: disable=too-many-branches def convert_filename( used_filename: Union[str, Path], @@ -75,9 +117,6 @@ def convert_filename( crytic_compile (CryticCompile): Associated CryticCompile object working_dir (Optional[Union[str, Path]], optional): Working directory. Defaults to None. - Raises: - InvalidCompilation: [description] - Returns: Filename: Filename converted """ @@ -91,9 +130,9 @@ def convert_filename( else: filename = Path(filename_txt) + # cwd points to the directory to be used if working_dir is None: cwd = Path.cwd() - working_dir = cwd else: working_dir = Path(working_dir) if working_dir.is_absolute(): @@ -106,16 +145,10 @@ def convert_filename( filename = filename.relative_to(Path(crytic_compile.package_name)) except ValueError: pass - if not filename.exists(): - if cwd.joinpath(Path("node_modules"), filename).exists(): - filename = cwd.joinpath("node_modules", filename) - elif cwd.joinpath(Path("contracts"), filename).exists(): - filename = cwd.joinpath("contracts", filename) - elif working_dir.joinpath(filename).exists(): - filename = working_dir.joinpath(filename) - else: - raise InvalidCompilation(f"Unknown file: {filename}") - elif not filename.is_absolute(): + + filename = _verify_filename_existence(filename, cwd) + + if not filename.is_absolute(): filename = cwd.joinpath(filename) absolute = filename @@ -123,10 +156,10 @@ def convert_filename( # Build the short path try: - if working_dir.is_absolute(): - short = absolute.relative_to(working_dir) + if cwd.is_absolute(): + short = absolute.relative_to(cwd) else: - short = relative.relative_to(working_dir) + short = relative.relative_to(cwd) except ValueError: short = relative except RuntimeError: diff --git a/crytic_compile/utils/subprocess.py b/crytic_compile/utils/subprocess.py new file mode 100644 index 00000000..dd2dd0bc --- /dev/null +++ b/crytic_compile/utils/subprocess.py @@ -0,0 +1,72 @@ +""" +Process execution helpers. +""" +import logging +import os +from pathlib import Path +import shutil +import subprocess +from typing import Any, Dict, List, Optional, Union + +LOGGER = logging.getLogger("CryticCompile") + + +def run( + cmd: List[str], + cwd: Optional[Union[str, os.PathLike]] = None, + extra_env: Optional[Dict[str, str]] = None, + **kwargs: Any, +) -> Optional[subprocess.CompletedProcess]: + """ + Execute a command in a cross-platform compatible way. + + Args: + cmd (List[str]): Command to run + cwd (PathLike): Working directory to run the command in + extra_env (Dict[str, str]): extra environment variables to define for the execution + **kwargs: optional arguments passed to `subprocess.run` + + Returns: + CompletedProcess: If the execution succeeded + None: if there was a problem executing + """ + subprocess_cwd = Path(os.getcwd() if cwd is None else cwd).resolve() + subprocess_env = None if extra_env is None else dict(os.environ, **extra_env) + subprocess_exe = shutil.which(cmd[0]) + + if subprocess_exe is None: + LOGGER.error("Cannot execute `%s`, is it installed and in PATH?", cmd[0]) + return None + + LOGGER.info( + "'%s' running (wd: %s)", + " ".join(cmd), + subprocess_cwd, + ) + + try: + return subprocess.run( + cmd, + executable=subprocess_exe, + cwd=subprocess_cwd, + env=subprocess_env, + check=True, + capture_output=True, + **kwargs, + ) + except FileNotFoundError: + LOGGER.error("Could not execute `%s`, is it installed and in PATH?", cmd[0]) + except subprocess.CalledProcessError as e: + LOGGER.error("'%s' returned non-zero exit code %d", cmd[0], e.returncode) + stdout, stderr = ( + e.stdout.decode(errors="backslashreplace").strip(), + e.stderr.decode(errors="backslashreplace").strip(), + ) + if stdout: + LOGGER.error("\nstdout: ".join(stdout.split("\n"))) + if stderr: + LOGGER.error("\nstderr: ".join(stderr.split("\n"))) + except OSError: + LOGGER.error("OS error executing:", exc_info=True) + + return None diff --git a/crytic_compile/utils/zip.py b/crytic_compile/utils/zip.py index f8a25f4e..ab5e7b78 100644 --- a/crytic_compile/utils/zip.py +++ b/crytic_compile/utils/zip.py @@ -38,7 +38,7 @@ def load_from_zip(target: str) -> List["CryticCompile"]: List[CryticCompile]: List of loaded projects """ # pylint: disable=import-outside-toplevel - from crytic_compile.crytic_compile import CryticCompile + from crytic_compile import CryticCompile compilations = [] with ZipFile(target, "r") as file_desc: diff --git a/scripts/ci_test_etherscan.sh b/scripts/ci_test_etherscan.sh index b53475c9..4103972f 100755 --- a/scripts/ci_test_etherscan.sh +++ b/scripts/ci_test_etherscan.sh @@ -7,27 +7,44 @@ cd "$DIR" || exit 255 solc-select use 0.4.25 --always-install +delay_no_key () { + # Perform a small sleep when API key is not available (e.g. on PR CI from external contributor) + if [ "$GITHUB_ETHERSCAN" = "" ]; then + sleep 5s + fi +} + +echo "::group::Etherscan mainnet" crytic-compile 0x7F37f78cBD74481E593F9C737776F7113d76B315 --compile-remove-metadata --etherscan-apikey "$GITHUB_ETHERSCAN" if [ $? -ne 0 ] then - echo "Etherscan test failed" + echo "Etherscan mainnet test failed" exit 255 fi +echo "::endgroup::" +delay_no_key + +echo "::group::Etherscan rinkeby" crytic-compile rinkeby:0xFe05820C5A92D9bc906D4A46F662dbeba794d3b7 --compile-remove-metadata --etherscan-apikey "$GITHUB_ETHERSCAN" if [ $? -ne 0 ] then - echo "Etherscan test failed" + echo "Etherscan rinkeby test failed" exit 255 fi +echo "::endgroup::" + +delay_no_key # From crytic/slither#1154 +echo "::group::Etherscan #3" crytic-compile 0xcfc1E0968CA08aEe88CbF664D4A1f8B881d90f37 --compile-remove-metadata --etherscan-apikey "$GITHUB_ETHERSCAN" if [ $? -ne 0 ] then - echo "Etherscan test failed" + echo "Etherscan #3 test failed" exit 255 fi +echo "::endgroup::" diff --git a/scripts/ci_test_hardhat.sh b/scripts/ci_test_hardhat.sh index 5e04dbb1..28f74b80 100755 --- a/scripts/ci_test_hardhat.sh +++ b/scripts/ci_test_hardhat.sh @@ -1,14 +1,12 @@ #!/usr/bin/env bash -### Test hardhat integration +echo "Testing hardhat integration of $(realpath "$(which crytic-compile)")" cd tests/hardhat || exit 255 npm install -crytic-compile . -if [ $? -ne 0 ] -then - echo "hardhat test failed" - exit 255 +if ! crytic-compile . +then echo "Monorepo test failed" && exit 255 +else echo "Monorepo test passed" && exit 0 fi diff --git a/scripts/ci_test_monorepo.sh b/scripts/ci_test_monorepo.sh new file mode 100755 index 00000000..10ca98cd --- /dev/null +++ b/scripts/ci_test_monorepo.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +echo "Testing monorepo integration of $(realpath "$(which crytic-compile)")" + +cd tests/monorepo || exit 255 + +npm install + +echo "Testing from the root of a monorepo" +if ! crytic-compile ./contracts +then echo "Monorepo test failed" && exit 255 +fi + +cd contracts || exit 255 + +echo "Testing from within a subdir of a monorepo" +if ! crytic-compile . +then echo "Monorepo test failed" && exit 255 +fi + +echo "Monorepo test passed" && exit 0 diff --git a/scripts/ci_test_standard.sh b/scripts/ci_test_standard.sh new file mode 100755 index 00000000..281087f4 --- /dev/null +++ b/scripts/ci_test_standard.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +DIR=$(mktemp -d) + +cp tests/contract_with_toplevel.sol "$DIR" +cp tests/toplevel.sol "$DIR" +cd "$DIR" || exit 255 + +solc-select use 0.8.0 --always-install + +crytic-compile contract_with_toplevel.sol --export-format archive + +if [ $? -ne 0 ] +then + echo "Standard test failed" + exit 255 +fi + +crytic-compile crytic-export/contract_with_toplevel.sol_export_archive.json + +if [ $? -ne 0 ] +then + echo "Standard test failed" + exit 255 +fi + + +crytic-compile contract_with_toplevel.sol --export-zip test.zip + +if [ $? -ne 0 ] +then + echo "Standard test failed" + exit 255 +fi + +crytic-compile test.zip + +if [ $? -ne 0 ] +then + echo "Standard test failed" + exit 255 +fi diff --git a/setup.py b/setup.py index a92a4cfd..d75c1a27 100644 --- a/setup.py +++ b/setup.py @@ -11,10 +11,10 @@ description="Util to facilitate smart contracts compilation.", url="https://github.com/crytic/crytic-compile", author="Trail of Bits", - version="0.2.4", + version="0.2.5", packages=find_packages(), python_requires=">=3.8", - install_requires=["pycryptodome>=3.4.6"], + install_requires=["pycryptodome>=3.4.6", "cbor2"], license="AGPL-3.0", long_description=long_description, package_data={"crytic_compile": ["py.typed"]}, diff --git a/tests/contract_with_toplevel.sol b/tests/contract_with_toplevel.sol new file mode 100644 index 00000000..0e1155d5 --- /dev/null +++ b/tests/contract_with_toplevel.sol @@ -0,0 +1,7 @@ +import "./toplevel.sol"; + +contract C{ + function f() external{ + + } +} diff --git a/tests/monorepo/contracts/contracts/Greeter.sol b/tests/monorepo/contracts/contracts/Greeter.sol new file mode 100644 index 00000000..8f67bdf6 --- /dev/null +++ b/tests/monorepo/contracts/contracts/Greeter.sol @@ -0,0 +1,23 @@ +//SPDX-License-Identifier: Unlicense +pragma solidity ^0.7.0; + +import "hardhat/console.sol"; + + +contract Greeter { + string greeting; + + constructor(string memory _greeting) { + console.log("Deploying a Greeter with greeting:", _greeting); + greeting = _greeting; + } + + function greet() public view returns (string memory) { + return greeting; + } + + function setGreeting(string memory _greeting) public { + console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); + greeting = _greeting; + } +} diff --git a/tests/monorepo/contracts/hardhat.config.js b/tests/monorepo/contracts/hardhat.config.js new file mode 100644 index 00000000..c51cfb93 --- /dev/null +++ b/tests/monorepo/contracts/hardhat.config.js @@ -0,0 +1,22 @@ +require("@nomiclabs/hardhat-waffle"); + +// This is a sample Hardhat task. To learn how to create your own go to +// https://hardhat.org/guides/create-task.html +task("accounts", "Prints the list of accounts", async () => { + const accounts = await ethers.getSigners(); + + for (const account of accounts) { + console.log(account.address); + } +}); + +// You need to export an object to set up your config +// Go to https://hardhat.org/config/ to learn more + +/** + * @type import('hardhat/config').HardhatUserConfig + */ +module.exports = { + solidity: "0.7.3", +}; + diff --git a/tests/monorepo/contracts/package.json b/tests/monorepo/contracts/package.json new file mode 100644 index 00000000..074828dd --- /dev/null +++ b/tests/monorepo/contracts/package.json @@ -0,0 +1,11 @@ +{ + "name": "hardhat-project", + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.0.0", + "@nomiclabs/hardhat-waffle": "^2.0.0", + "chai": "^4.2.0", + "ethereum-waffle": "^3.2.0", + "ethers": "^5.0.19", + "hardhat": "^2.0.2" + } +} diff --git a/tests/monorepo/package.json b/tests/monorepo/package.json new file mode 100644 index 00000000..f9a5282f --- /dev/null +++ b/tests/monorepo/package.json @@ -0,0 +1,7 @@ +{ + "name": "monorepo", + "workspaces": [ + "contracts" + ], + "private": true +} diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 00000000..460be929 --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,36 @@ +""" +Test file +""" +from crytic_compile import CryticCompile +from crytic_compile.source_unit import SourceUnit + + +DAI_BYTECODE = """608060405234801561001057600080fd5b506040516120d33803806120d38339818101604052602081101561003357600080fd5b810190808051906020019092919050505060016000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550604051808061208160529139605201905060405180910390206040518060400160405280600e81526020017f44616920537461626c65636f696e000000000000000000000000000000000000815250805190602001206040518060400160405280600181526020017f3100000000000000000000000000000000000000000000000000000000000000815250805190602001208330604051602001808681526020018581526020018481526020018381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001955050505050506040516020818303038152906040528051906020012060058190555050611ee0806101a16000396000f3fe608060405234801561001057600080fd5b50600436106101425760003560e01c80637ecebe00116100b8578063a9059cbb1161007c578063a9059cbb146106b4578063b753a98c1461071a578063bb35783b14610768578063bf353dbb146107d6578063dd62ed3e1461082e578063f2d5d56b146108a657610142565b80637ecebe00146104a15780638fcbaf0c146104f957806395d89b411461059f5780639c52a7f1146106225780639dc29fac1461066657610142565b8063313ce5671161010a578063313ce567146102f25780633644e5151461031657806340c10f191461033457806354fd4d501461038257806365fae35e1461040557806370a082311461044957610142565b806306fdde0314610147578063095ea7b3146101ca57806318160ddd1461023057806323b872dd1461024e57806330adf81f146102d4575b600080fd5b61014f6108f4565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561018f578082015181840152602081019050610174565b50505050905090810190601f1680156101bc5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b610216600480360360408110156101e057600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061092d565b604051808215151515815260200191505060405180910390f35b610238610a1f565b6040518082815260200191505060405180910390f35b6102ba6004803603606081101561026457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610a25565b604051808215151515815260200191505060405180910390f35b6102dc610f3a565b6040518082815260200191505060405180910390f35b6102fa610f61565b604051808260ff1660ff16815260200191505060405180910390f35b61031e610f66565b6040518082815260200191505060405180910390f35b6103806004803603604081101561034a57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610f6c565b005b61038a611128565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156103ca5780820151818401526020810190506103af565b50505050905090810190601f1680156103f75780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6104476004803603602081101561041b57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050611161565b005b61048b6004803603602081101561045f57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919050505061128f565b6040518082815260200191505060405180910390f35b6104e3600480360360208110156104b757600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506112a7565b6040518082815260200191505060405180910390f35b61059d600480360361010081101561051057600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919080359060200190929190803515159060200190929190803560ff16906020019092919080359060200190929190803590602001909291905050506112bf565b005b6105a76117fa565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156105e75780820151818401526020810190506105cc565b50505050905090810190601f1680156106145780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6106646004803603602081101561063857600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050611833565b005b6106b26004803603604081101561067c57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050611961565b005b610700600480360360408110156106ca57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050611df4565b604051808215151515815260200191505060405180910390f35b6107666004803603604081101561073057600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050611e09565b005b6107d46004803603606081101561077e57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050611e19565b005b610818600480360360208110156107ec57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050611e2a565b6040518082815260200191505060405180910390f35b6108906004803603604081101561084457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050611e42565b6040518082815260200191505060405180910390f35b6108f2600480360360408110156108bc57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050611e67565b005b6040518060400160405280600e81526020017f44616920537461626c65636f696e00000000000000000000000000000000000081525081565b600081600360003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040518082815260200191505060405180910390a36001905092915050565b60015481565b600081600260008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015610adc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260188152602001807f4461692f696e73756666696369656e742d62616c616e6365000000000000000081525060200191505060405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1614158015610bb457507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b15610db25781600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015610cab576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601a8152602001807f4461692f696e73756666696369656e742d616c6c6f77616e636500000000000081525060200191505060405180910390fd5b610d31600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205483611e77565b600360008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b610dfb600260008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205483611e77565b600260008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610e87600260008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205483611e91565b600260008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b7fea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb60001b81565b601281565b60055481565b60016000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414611020576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4461692f6e6f742d617574686f72697a6564000000000000000000000000000081525060200191505060405180910390fd5b611069600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205482611e91565b600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506110b860015482611e91565b6001819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a35050565b6040518060400160405280600181526020017f310000000000000000000000000000000000000000000000000000000000000081525081565b60016000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414611215576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4461692f6e6f742d617574686f72697a6564000000000000000000000000000081525060200191505060405180910390fd5b60016000808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505961012081016040526020815260e0602082015260e0600060408301376024356004353360003560e01c60e01b61012085a45050565b60026020528060005260406000206000915090505481565b60046020528060005260406000206000915090505481565b60006005547fea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb60001b8a8a8a8a8a604051602001808781526020018673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020018481526020018381526020018215151515815260200196505050505050506040516020818303038152906040528051906020012060405160200180807f190100000000000000000000000000000000000000000000000000000000000081525060020183815260200182815260200192505050604051602081830303815290604052805190602001209050600073ffffffffffffffffffffffffffffffffffffffff168973ffffffffffffffffffffffffffffffffffffffff16141561148c576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260158152602001807f4461692f696e76616c69642d616464726573732d30000000000000000000000081525060200191505060405180910390fd5b60018185858560405160008152602001604052604051808581526020018460ff1660ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa1580156114e9573d6000803e3d6000fd5b5050506020604051035173ffffffffffffffffffffffffffffffffffffffff168973ffffffffffffffffffffffffffffffffffffffff1614611593576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4461692f696e76616c69642d7065726d6974000000000000000000000000000081525060200191505060405180910390fd5b60008614806115a25750854211155b611614576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4461692f7065726d69742d65787069726564000000000000000000000000000081525060200191505060405180910390fd5b600460008a73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008154809291906001019190505587146116d6576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260118152602001807f4461692f696e76616c69642d6e6f6e636500000000000000000000000000000081525060200191505060405180910390fd5b6000856116e4576000611706565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff5b905080600360008c73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508873ffffffffffffffffffffffffffffffffffffffff168a73ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040518082815260200191505060405180910390a350505050505050505050565b6040518060400160405280600381526020017f444149000000000000000000000000000000000000000000000000000000000081525081565b60016000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054146118e7576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4461692f6e6f742d617574686f72697a6564000000000000000000000000000081525060200191505060405180910390fd5b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505961012081016040526020815260e0602082015260e0600060408301376024356004353360003560e01c60e01b61012085a45050565b80600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015611a16576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260188152602001807f4461692f696e73756666696369656e742d62616c616e6365000000000000000081525060200191505060405180910390fd5b3373ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614158015611aee57507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414155b15611cec5780600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015611be5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601a8152602001807f4461692f696e73756666696369656e742d616c6c6f77616e636500000000000081525060200191505060405180910390fd5b611c6b600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205482611e77565b600360008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b611d35600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205482611e77565b600260008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550611d8460015482611e77565b600181905550600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a35050565b6000611e01338484610a25565b905092915050565b611e14338383610a25565b505050565b611e24838383610a25565b50505050565b60006020528060005260406000206000915090505481565b6003602052816000526040600020602052806000526040600020600091509150505481565b611e72823383610a25565b505050565b6000828284039150811115611e8b57600080fd5b92915050565b6000828284019150811015611ea557600080fd5b9291505056fe454950373132446f6d61696e28737472696e67206e616d652c737472696e672076657273696f6e2c75696e7432353620636861696e49642c6164647265737320766572696679696e67436f6e747261637429""" + + +def test_metadata() -> None: + """ + Test the metadata + + Returns: + + """ + crytic_compile_instance = CryticCompile( + "0x6B175474E89094C44Da98b954EedeAC495271d0F", # solc 0.5.12 + ) + assert len(crytic_compile_instance.compilation_units) == 1 + unit = list(crytic_compile_instance.compilation_units.values())[0] + + assert len(unit.source_units) == 1 + + source_unit: SourceUnit + source_unit = list(unit.source_units.values())[0] + + with_metadata = source_unit.bytecode_init("Dai") + assert source_unit.metadata_of("Dai") == { + "bzzr1": "92df983266c28b6fb4c7c776b695725fd63d55b8cd5d5618b69fb544ce801d85", + "solc": "0.5.12", + } + source_unit.remove_metadata() + assert with_metadata != source_unit.bytecode_init("Dai") + assert source_unit.bytecode_init("Dai") == DAI_BYTECODE diff --git a/tests/toplevel.sol b/tests/toplevel.sol new file mode 100644 index 00000000..2c82f464 --- /dev/null +++ b/tests/toplevel.sol @@ -0,0 +1,3 @@ +function g(){ + +} \ No newline at end of file