From 05b29f0e4371c03d7b597eae80a56f95c86db197 Mon Sep 17 00:00:00 2001 From: Josselin Date: Thu, 31 Mar 2022 10:59:56 +0200 Subject: [PATCH] Add foundry support Fix 230 Related https://github.com/crytic/slither/issues/1007 --- .github/workflows/ci.yml | 4 +- crytic_compile/cryticparser/cryticparser.py | 24 ++ crytic_compile/cryticparser/defaults.py | 2 + crytic_compile/platform/all_platforms.py | 1 + crytic_compile/platform/foundry.py | 252 ++++++++++++++++++ crytic_compile/platform/types.py | 1 + ...vis_test_brownie.sh => ci_test_brownie.sh} | 0 ...vis_test_buidler.sh => ci_test_buidler.sh} | 0 .../{travis_test_dapp.sh => ci_test_dapp.sh} | 0 ...ravis_test_embark.sh => ci_test_embark.sh} | 0 ...test_etherlime.sh => ci_test_etherlime.sh} | 0 ...test_etherscan.sh => ci_test_etherscan.sh} | 0 scripts/ci_test_foundry.sh | 19 ++ ...vis_test_hardhat.sh => ci_test_hardhat.sh} | 0 .../{travis_test_solc.sh => ci_test_solc.sh} | 0 ...vis_test_truffle.sh => ci_test_truffle.sh} | 0 ...ravis_test_waffle.sh => ci_test_waffle.sh} | 0 17 files changed, 301 insertions(+), 2 deletions(-) create mode 100755 crytic_compile/platform/foundry.py rename scripts/{travis_test_brownie.sh => ci_test_brownie.sh} (100%) rename scripts/{travis_test_buidler.sh => ci_test_buidler.sh} (100%) rename scripts/{travis_test_dapp.sh => ci_test_dapp.sh} (100%) rename scripts/{travis_test_embark.sh => ci_test_embark.sh} (100%) rename scripts/{travis_test_etherlime.sh => ci_test_etherlime.sh} (100%) rename scripts/{travis_test_etherscan.sh => ci_test_etherscan.sh} (100%) create mode 100755 scripts/ci_test_foundry.sh rename scripts/{travis_test_hardhat.sh => ci_test_hardhat.sh} (100%) rename scripts/{travis_test_solc.sh => ci_test_solc.sh} (100%) rename scripts/{travis_test_truffle.sh => ci_test_truffle.sh} (100%) rename scripts/{travis_test_waffle.sh => ci_test_waffle.sh} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d69fa02..0cdf0267 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - type: ["solc", "truffle", "embark", "etherlime", "brownie", "waffle", "buidler", "hardhat"] + type: ["solc", "truffle", "embark", "etherlime", "brownie", "waffle", "buidler", "hardhat", "foundry"] steps: - uses: actions/checkout@v1 - name: Set up Python 3.6 @@ -32,4 +32,4 @@ jobs: TEST_TYPE: ${{ matrix.type }} GITHUB_ETHERSCAN: ${{ secrets.GITHUB_ETHERSCAN }} run: | - bash "scripts/travis_test_${TEST_TYPE}.sh" + bash "scripts/ci_test_${TEST_TYPE}.sh" diff --git a/crytic_compile/cryticparser/cryticparser.py b/crytic_compile/cryticparser/cryticparser.py index 19df89e7..fa93e330 100755 --- a/crytic_compile/cryticparser/cryticparser.py +++ b/crytic_compile/cryticparser/cryticparser.py @@ -439,3 +439,27 @@ def _init_hardhat(parser: ArgumentParser) -> None: dest="hardhat_artifacts_directory", default=DEFAULTS_FLAG_IN_CONFIG["hardhat_artifacts_directory"], ) + + +def _init_foundry(parser: ArgumentParser) -> None: + """Init foundry arguments + + Args: + parser (ArgumentParser): argparser where the cli flags are added + """ + group_hardhat = parser.add_argument_group("foundry options") + group_hardhat.add_argument( + "--foundry-ignore-compile", + help="Do not run foundry compile", + action="store_true", + dest="foundry_ignore_compile", + default=DEFAULTS_FLAG_IN_CONFIG["foundry_ignore_compile"], + ) + + group_hardhat.add_argument( + "--foundry-out-directory", + help="Use an alternative out directory (default: out)", + action="store", + dest="foundry_out_directory", + default=DEFAULTS_FLAG_IN_CONFIG["foundry_out_directory"], + ) diff --git a/crytic_compile/cryticparser/defaults.py b/crytic_compile/cryticparser/defaults.py index b8f99677..62c2ace2 100755 --- a/crytic_compile/cryticparser/defaults.py +++ b/crytic_compile/cryticparser/defaults.py @@ -42,5 +42,7 @@ "hardhat_ignore_compile": False, "hardhat_cache_directory": "cache", "hardhat_artifacts_directory": "artifacts", + "foundry_ignore_compile": False, + "foundry_out_directory": "out", "export_dir": "crytic-export", } diff --git a/crytic_compile/platform/all_platforms.py b/crytic_compile/platform/all_platforms.py index 38cb0933..624629e2 100644 --- a/crytic_compile/platform/all_platforms.py +++ b/crytic_compile/platform/all_platforms.py @@ -16,3 +16,4 @@ from .truffle import Truffle from .vyper import Vyper from .waffle import Waffle +from .foundry import Foundry diff --git a/crytic_compile/platform/foundry.py b/crytic_compile/platform/foundry.py new file mode 100755 index 00000000..be58aa6f --- /dev/null +++ b/crytic_compile/platform/foundry.py @@ -0,0 +1,252 @@ +""" +Truffle platform +""" +import json +import logging +import os +import subprocess +from pathlib import Path +from typing import TYPE_CHECKING, List, Tuple, Optional + +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 + +# Handle cycle +if TYPE_CHECKING: + from crytic_compile import CryticCompile + +LOGGER = logging.getLogger("CryticCompile") + + +class Foundry(AbstractPlatform): + """ + Foundry platform + """ + + NAME = "Foundry" + PROJECT_URL = "https://github.com/gakonst/foundry" + TYPE = Type.FOUNDRY + + # pylint: disable=too-many-locals,too-many-statements,too-many-branches + def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: + """Compile + + Args: + 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( + "ignore_compile", False + ) + + out_directory = kwargs.get("foundry_out_directory", "out") + + if ignore_compile: + LOGGER.info( + "--ignore-compile used, if something goes wrong, consider removing the ignore compile flag" + ) + + if not ignore_compile: + cmd = [ + "forge", + "build", + "--extra-output", + "abi", + "--extra-output", + "userdoc", + "--extra-output", + "devdoc", + "--extra-output", + "evm.methodIdentifiers", + "--force", + ] + + LOGGER.info( + "'%s' running", + " ".join(cmd), + ) + + with subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self._target + ) as process: + + stdout_bytes, stderr_bytes = process.communicate() + stdout, stderr = ( + stdout_bytes.decode(), + stderr_bytes.decode(), + ) # 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(";") + compilation_unit.srcmaps_runtime[contract_name] = target_loaded["deployedBytecode"][ + "sourceMap" + ].split(";") + + version, optimized, runs = _get_config_info(self._target) + + compilation_unit.compiler_version = CompilerVersion( + compiler=compiler, version=version, optimized=optimized, optimize_runs=runs + ) + + @staticmethod + def is_supported(target: str, **kwargs: str) -> bool: + """Check if the target is a foundry project + + Args: + target (str): path to the target + **kwargs: optional arguments. Used: "foundry_ignore" + + Returns: + bool: True if the target is a foundry project + """ + if kwargs.get("foundry_ignore", False): + return False + + return os.path.isfile(os.path.join(target, "foundry.toml")) + + # pylint: disable=no-self-use + def is_dependency(self, path: str) -> bool: + """Check if the path is a dependency + + Args: + path (str): path to the target + + Returns: + bool: True if the target is a dependency + """ + if path in self._cached_dependencies: + return self._cached_dependencies[path] + ret = "lib" in Path(path).parts + self._cached_dependencies[path] = ret + return ret + + # pylint: disable=no-self-use + def _guessed_tests(self) -> List[str]: + """Guess the potential unit tests commands + + Returns: + 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/types.py b/crytic_compile/platform/types.py index dcd210e2..d02b1e05 100644 --- a/crytic_compile/platform/types.py +++ b/crytic_compile/platform/types.py @@ -23,6 +23,7 @@ class Type(IntEnum): SOLC_STANDARD_JSON = 10 BUILDER = 11 HARDHAT = 11 + FOUNDRY = 12 STANDARD = 100 ARCHIVE = 101 diff --git a/scripts/travis_test_brownie.sh b/scripts/ci_test_brownie.sh similarity index 100% rename from scripts/travis_test_brownie.sh rename to scripts/ci_test_brownie.sh diff --git a/scripts/travis_test_buidler.sh b/scripts/ci_test_buidler.sh similarity index 100% rename from scripts/travis_test_buidler.sh rename to scripts/ci_test_buidler.sh diff --git a/scripts/travis_test_dapp.sh b/scripts/ci_test_dapp.sh similarity index 100% rename from scripts/travis_test_dapp.sh rename to scripts/ci_test_dapp.sh diff --git a/scripts/travis_test_embark.sh b/scripts/ci_test_embark.sh similarity index 100% rename from scripts/travis_test_embark.sh rename to scripts/ci_test_embark.sh diff --git a/scripts/travis_test_etherlime.sh b/scripts/ci_test_etherlime.sh similarity index 100% rename from scripts/travis_test_etherlime.sh rename to scripts/ci_test_etherlime.sh diff --git a/scripts/travis_test_etherscan.sh b/scripts/ci_test_etherscan.sh similarity index 100% rename from scripts/travis_test_etherscan.sh rename to scripts/ci_test_etherscan.sh diff --git a/scripts/ci_test_foundry.sh b/scripts/ci_test_foundry.sh new file mode 100755 index 00000000..dba77f79 --- /dev/null +++ b/scripts/ci_test_foundry.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +### Test foundry integration + + +cd /tmp || exit 255 + +curl -L https://foundry.paradigm.xyz | bash +source ~/.bash_profile +foundryup + +forge init + +crytic-compile . +if [ $? -ne 0 ] +then + echo "hardhat test failed" + exit 255 +fi diff --git a/scripts/travis_test_hardhat.sh b/scripts/ci_test_hardhat.sh similarity index 100% rename from scripts/travis_test_hardhat.sh rename to scripts/ci_test_hardhat.sh diff --git a/scripts/travis_test_solc.sh b/scripts/ci_test_solc.sh similarity index 100% rename from scripts/travis_test_solc.sh rename to scripts/ci_test_solc.sh diff --git a/scripts/travis_test_truffle.sh b/scripts/ci_test_truffle.sh similarity index 100% rename from scripts/travis_test_truffle.sh rename to scripts/ci_test_truffle.sh diff --git a/scripts/travis_test_waffle.sh b/scripts/ci_test_waffle.sh similarity index 100% rename from scripts/travis_test_waffle.sh rename to scripts/ci_test_waffle.sh