From a6a33a8c42659ce7b01ad6dd57c117a5e7ce3759 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 8 Aug 2023 15:45:30 -0500 Subject: [PATCH 01/10] feat: compile vyper 0.3.7 via standard json input --- crytic_compile/crytic_compile.py | 41 ++--- crytic_compile/platform/all_platforms.py | 2 +- crytic_compile/platform/vyper.py | 196 ++++++++++------------- 3 files changed, 107 insertions(+), 132 deletions(-) diff --git a/crytic_compile/crytic_compile.py b/crytic_compile/crytic_compile.py index 7b81bbaa..98becc40 100644 --- a/crytic_compile/crytic_compile.py +++ b/crytic_compile/crytic_compile.py @@ -15,7 +15,9 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union from crytic_compile.compilation_unit import CompilationUnit -from crytic_compile.platform import all_platforms, solc_standard_json +from crytic_compile.platform import all_platforms +from crytic_compile.platform.solc_standard_json import SolcStandardJson +from crytic_compile.platform.vyper import VyperStandardJson from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.all_export import PLATFORMS_EXPORT from crytic_compile.platform.solc import Solc @@ -628,11 +630,7 @@ def compile_all(target: str, **kwargs: str) -> List[CryticCompile]: """ use_solc_standard_json = kwargs.get("solc_standard_json", False) - # Attempt to perform glob expansion of target/filename - globbed_targets = glob.glob(target, recursive=True) - # Check if the target refers to a valid target already. - # If it does not, we assume it's a glob pattern. compilations: List[CryticCompile] = [] if os.path.isfile(target) or is_supported(target): if target.endswith(".zip"): @@ -644,28 +642,33 @@ def compile_all(target: str, **kwargs: str) -> List[CryticCompile]: compilations = load_from_zip(tmp.name) else: compilations.append(CryticCompile(target, **kwargs)) - elif os.path.isdir(target) or len(globbed_targets) > 0: - # We create a new glob to find solidity files at this path (in case this is a directory) - filenames = glob.glob(os.path.join(target, "*.sol")) - if not filenames: - filenames = glob.glob(os.path.join(target, "*.vy")) - if not filenames: - filenames = globbed_targets - + elif os.path.isdir(target): + solidity_filenames = glob.glob(os.path.join(target, "*.sol")) + vyper_filenames = glob.glob(os.path.join(target, "*.vy")) # Determine if we're using --standard-solc option to # aggregate many files into a single compilation. if use_solc_standard_json: # If we're using standard solc, then we generated our # input to create a single compilation with all files - standard_json = solc_standard_json.SolcStandardJson() - for filename in filenames: - standard_json.add_source_file(filename) - compilations.append(CryticCompile(standard_json, **kwargs)) + solc_standard_json = SolcStandardJson() + solc_standard_json.add_source_files(solidity_filenames) + compilations.append(CryticCompile(solc_standard_json, **kwargs)) else: # We compile each file and add it to our compilations. - for filename in filenames: + for filename in solidity_filenames: compilations.append(CryticCompile(filename, **kwargs)) + + if vyper_filenames: + vyper_standard_json = VyperStandardJson() + vyper_standard_json.add_source_files(vyper_filenames) + compilations.append(CryticCompile(vyper_standard_json, **kwargs)) else: - raise ValueError(f"{str(target)} is not a file or directory.") + raise NotImplementedError() + # TODO split glob into language + # # Attempt to perform glob expansion of target/filename + # globbed_targets = glob.glob(target, recursive=True) + # print(globbed_targets) + + # raise ValueError(f"{str(target)} is not a file or directory.") return compilations diff --git a/crytic_compile/platform/all_platforms.py b/crytic_compile/platform/all_platforms.py index 624629e2..5e2fe1ba 100644 --- a/crytic_compile/platform/all_platforms.py +++ b/crytic_compile/platform/all_platforms.py @@ -14,6 +14,6 @@ from .solc_standard_json import SolcStandardJson from .standard import Standard from .truffle import Truffle -from .vyper import Vyper +from .vyper import VyperStandardJson from .waffle import Waffle from .foundry import Foundry diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index 49133fd9..ad8783ee 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -15,6 +15,7 @@ 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.subprocess import run # Handle cycle from crytic_compile.utils.natspec import Natspec @@ -25,7 +26,7 @@ LOGGER = logging.getLogger("CryticCompile") -class Vyper(AbstractPlatform): +class VyperStandardJson(AbstractPlatform): """ Vyper platform """ @@ -33,6 +34,28 @@ class Vyper(AbstractPlatform): NAME = "vyper" PROJECT_URL = "https://github.com/vyperlang/vyper" TYPE = Type.VYPER + standard_json_input: Dict = { + "language": "Vyper", + "sources": {}, + "settings": { + "outputSelection": { + "*": { + "*": [ + "abi", + "devdoc", + "userdoc", + "evm.bytecode", + "evm.deployedBytecode", + "evm.deployedBytecode.sourceMap", + ], + "": ["ast"], + } + } + }, + } + + def __init__(self, target: Optional[Path] = None, **_kwargs: str): + super().__init__(target, **_kwargs) def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: """Compile the target @@ -44,46 +67,61 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: """ target = self._target + # If the target was a directory `add_source_file` should have been called + # by `compile_all`. Otherwise, we should have a single file target. + if self._target is not None and os.path.isfile(self._target): + self.add_source_files([target]) - vyper = kwargs.get("vyper", "vyper") + vyper_bin = kwargs.get("vyper", "vyper") + output_file = Path("crytic-export/standard_input.json") + output_file.parent.mkdir(exist_ok=True, parents=True) + with open(output_file, "w") as f: + f.write(json.dumps(self.standard_json_input)) - targets_json = _run_vyper(target, vyper) + compilation_artifacts = _run_vyper_standard_json(output_file.as_posix(), vyper_bin) - assert "version" in targets_json compilation_unit = CompilationUnit(crytic_compile, str(target)) + compiler_version = compilation_artifacts["compiler"].split("-")[1] + assert compiler_version == "0.3.7" compilation_unit.compiler_version = CompilerVersion( - compiler="vyper", version=targets_json["version"], optimized=False + compiler="vyper", version=compiler_version, optimized=False ) - assert target in targets_json - - info = targets_json[target] - filename = convert_filename(target, _relative_to_short, crytic_compile) - - contract_name = Path(target).parts[-1] - - source_unit = compilation_unit.create_source_unit(filename) - - source_unit.add_contract_name(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 - 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 - source_unit.srcmaps_runtime[contract_name] = info["source_map"]["pc_pos_map_compressed"] - - # Natspec not yet handled for vyper - source_unit.natspec[contract_name] = Natspec({}, {}) - - ast = _get_vyper_ast(target, vyper) - source_unit.ast = ast + for source_file, contract_info in compilation_artifacts["contracts"].items(): + filename = convert_filename(source_file, _relative_to_short, crytic_compile) + source_unit = compilation_unit.create_source_unit(filename) + for contract_name, contract_metadata in contract_info.items(): + source_unit.add_contract_name(contract_name) + compilation_unit.filename_to_contracts[filename].add(contract_name) + + source_unit.abis[contract_name] = contract_metadata["abi"] + source_unit.bytecodes_init[contract_name] = contract_metadata["evm"]["bytecode"][ + "object" + ].replace("0x", "") + # Vyper does not provide the source mapping for the init bytecode + source_unit.srcmaps_init[contract_name] = [] + source_unit.srcmaps_runtime[contract_name] = contract_metadata["evm"][ + "deployedBytecode" + ]["sourceMap"] + source_unit.bytecodes_runtime[contract_name] = contract_metadata["evm"][ + "deployedBytecode" + ]["object"].replace("0x", "") + source_unit.natspec[contract_name] = Natspec( + contract_metadata["userdoc"], contract_metadata["devdoc"] + ) + + for source_file, ast in compilation_artifacts["sources"].items(): + filename = convert_filename(source_file, _relative_to_short, crytic_compile) + source_unit = compilation_unit.create_source_unit(filename) + source_unit.ast = ast + + def add_source_files(self, file_paths: List[str]) -> None: + for file_path in file_paths: + with open(file_path, "r") as f: + self.standard_json_input["sources"][file_path] = { + "content": f.read(), + } def clean(self, **_kwargs: str) -> None: """Clean compilation artifacts @@ -129,13 +167,16 @@ def _guessed_tests(self) -> List[str]: return [] -def _run_vyper( - filename: str, vyper: str, env: Optional[Dict] = None, working_dir: Optional[str] = None +def _run_vyper_standard_json( + standard_input_path: str, + vyper: str, + env: Optional[Dict] = None, + working_dir: Optional[str] = None, ) -> Dict: - """Run vyper + """Run vyper and write compilation output to a file Args: - filename (str): vyper file + standard_input_path (str): path to the standard input json file vyper (str): vyper binary env (Optional[Dict], optional): Environment variables. Defaults to None. working_dir (Optional[str], optional): Working directory. Defaults to None. @@ -146,81 +187,12 @@ def _run_vyper( Returns: Dict: Vyper json compilation artifact """ - if not os.path.isfile(filename): - raise InvalidCompilation(f"{filename} does not exist (are you in the correct directory?)") - - cmd = [vyper, filename, "-f", "combined_json"] - - additional_kwargs: Dict = {"cwd": working_dir} if working_dir else {} - stderr = "" - LOGGER.info( - "'%s' running", - " ".join(cmd), - ) - try: - with subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env, - executable=shutil.which(cmd[0]), - **additional_kwargs, - ) as process: - stdout, stderr = process.communicate() - res = stdout.split(b"\n") - res = res[-2] - return json.loads(res) - except OSError as error: - # pylint: disable=raise-missing-from - raise InvalidCompilation(error) - except json.decoder.JSONDecodeError: - # pylint: disable=raise-missing-from - raise InvalidCompilation(f"Invalid vyper compilation\n{stderr}") - - -def _get_vyper_ast( - filename: str, vyper: str, env: Optional[Dict] = None, working_dir: Optional[str] = None -) -> Dict: - """Get ast from vyper - - Args: - filename (str): vyper file - vyper (str): vyper binary - env (Dict, optional): Environment variables. Defaults to None. - working_dir (str, optional): Working directory. Defaults to None. - - Raises: - InvalidCompilation: If vyper failed to run - - Returns: - Dict: [description] - """ - if not os.path.isfile(filename): - raise InvalidCompilation(f"{filename} does not exist (are you in the correct directory?)") - - cmd = [vyper, filename, "-f", "ast"] - - additional_kwargs: Dict = {"cwd": working_dir} if working_dir else {} - stderr = "" - try: - with subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env, - executable=shutil.which(cmd[0]), - **additional_kwargs, - ) as process: - stdout, stderr = process.communicate() - res = stdout.split(b"\n") - res = res[-2] - return json.loads(res) - except json.decoder.JSONDecodeError: - # pylint: disable=raise-missing-from - raise InvalidCompilation(f"Invalid vyper compilation\n{stderr}") - except Exception as exception: - # pylint: disable=raise-missing-from - raise InvalidCompilation(exception) + cmd = [vyper, standard_input_path, "--standard-json", "-o", "crytic-export/artifacts.json"] + success = run(cmd, cwd=working_dir, extra_env=env) + if success is None: + raise InvalidCompilation("Vyper compilation failed") + with open("crytic-export/artifacts.json", "r") as f: + return json.load(f) def _relative_to_short(relative: Path) -> Path: From a7c32c00b116a2fbf965f91907d56a6ec8f57959 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Tue, 29 Aug 2023 16:01:56 -0500 Subject: [PATCH 02/10] split srcmap --- crytic_compile/platform/vyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index ad8783ee..83542625 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -103,7 +103,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: source_unit.srcmaps_init[contract_name] = [] source_unit.srcmaps_runtime[contract_name] = contract_metadata["evm"][ "deployedBytecode" - ]["sourceMap"] + ]["sourceMap"].split(";") source_unit.bytecodes_runtime[contract_name] = contract_metadata["evm"][ "deployedBytecode" ]["object"].replace("0x", "") From 1150349d640452e6b9ba6d71c1e9ddaa2ba9c63b Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 31 Aug 2023 09:57:51 -0500 Subject: [PATCH 03/10] use tempfiles --- crytic_compile/platform/vyper.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index 83542625..fb02b1ff 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -4,8 +4,7 @@ import json import logging import os -import shutil -import subprocess +import tempfile from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional @@ -73,13 +72,15 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: self.add_source_files([target]) vyper_bin = kwargs.get("vyper", "vyper") - output_file = Path("crytic-export/standard_input.json") - output_file.parent.mkdir(exist_ok=True, parents=True) - with open(output_file, "w") as f: - f.write(json.dumps(self.standard_json_input)) - - compilation_artifacts = _run_vyper_standard_json(output_file.as_posix(), vyper_bin) - + compilation_artifacts = None + with tempfile.NamedTemporaryFile(mode="a+") as f: + json.dump(self.standard_json_input, f) + f.seek(0) + compilation_artifacts = _run_vyper_standard_json(f.name, vyper_bin) + + if "errors" in compilation_artifacts: + # TODO format errors + raise InvalidCompilation(compilation_artifacts["errors"]) compilation_unit = CompilationUnit(crytic_compile, str(target)) compiler_version = compilation_artifacts["compiler"].split("-")[1] @@ -187,12 +188,13 @@ def _run_vyper_standard_json( Returns: Dict: Vyper json compilation artifact """ - cmd = [vyper, standard_input_path, "--standard-json", "-o", "crytic-export/artifacts.json"] - success = run(cmd, cwd=working_dir, extra_env=env) - if success is None: - raise InvalidCompilation("Vyper compilation failed") - with open("crytic-export/artifacts.json", "r") as f: - return json.load(f) + with tempfile.NamedTemporaryFile(mode="a+") as f: + cmd = [vyper, standard_input_path, "--standard-json", "-o", f.name] + success = run(cmd, cwd=working_dir, extra_env=env) + if success is None: + raise InvalidCompilation("Vyper compilation failed") + f.seek(0) + return json.loads(f.read()) def _relative_to_short(relative: Path) -> Path: From 852d59a040e4cb570c49105ff161d37d804f8019 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 31 Aug 2023 11:28:58 -0500 Subject: [PATCH 04/10] fix bug causing standard json to be shared across classes instances --- crytic_compile/platform/vyper.py | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index fb02b1ff..bb69a587 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -33,28 +33,28 @@ class VyperStandardJson(AbstractPlatform): NAME = "vyper" PROJECT_URL = "https://github.com/vyperlang/vyper" TYPE = Type.VYPER - standard_json_input: Dict = { - "language": "Vyper", - "sources": {}, - "settings": { - "outputSelection": { - "*": { - "*": [ - "abi", - "devdoc", - "userdoc", - "evm.bytecode", - "evm.deployedBytecode", - "evm.deployedBytecode.sourceMap", - ], - "": ["ast"], - } - } - }, - } def __init__(self, target: Optional[Path] = None, **_kwargs: str): super().__init__(target, **_kwargs) + self.standard_json_input = { + "language": "Vyper", + "sources": {}, + "settings": { + "outputSelection": { + "*": { + "*": [ + "abi", + "devdoc", + "userdoc", + "evm.bytecode", + "evm.deployedBytecode", + "evm.deployedBytecode.sourceMap", + ], + "": ["ast"], + } + } + }, + } def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: """Compile the target @@ -75,7 +75,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: compilation_artifacts = None with tempfile.NamedTemporaryFile(mode="a+") as f: json.dump(self.standard_json_input, f) - f.seek(0) + f.flush() compilation_artifacts = _run_vyper_standard_json(f.name, vyper_bin) if "errors" in compilation_artifacts: From 13968446d404e59840814eb9a6ea42975929b915 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Thu, 31 Aug 2023 17:28:11 -0500 Subject: [PATCH 05/10] use stdin/stdout --- crytic_compile/platform/vyper.py | 46 +++++++++++++++++++------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index bb69a587..4479c533 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -4,7 +4,8 @@ import json import logging import os -import tempfile +import subprocess +import shutil from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional @@ -72,15 +73,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: self.add_source_files([target]) vyper_bin = kwargs.get("vyper", "vyper") - compilation_artifacts = None - with tempfile.NamedTemporaryFile(mode="a+") as f: - json.dump(self.standard_json_input, f) - f.flush() - compilation_artifacts = _run_vyper_standard_json(f.name, vyper_bin) - if "errors" in compilation_artifacts: - # TODO format errors - raise InvalidCompilation(compilation_artifacts["errors"]) + compilation_artifacts = _run_vyper_standard_json(self.standard_json_input, vyper_bin) compilation_unit = CompilationUnit(crytic_compile, str(target)) compiler_version = compilation_artifacts["compiler"].split("-")[1] @@ -169,7 +163,7 @@ def _guessed_tests(self) -> List[str]: def _run_vyper_standard_json( - standard_input_path: str, + standard_json_input: Dict, vyper: str, env: Optional[Dict] = None, working_dir: Optional[str] = None, @@ -177,7 +171,7 @@ def _run_vyper_standard_json( """Run vyper and write compilation output to a file Args: - standard_input_path (str): path to the standard input json file + standard_json_input (Dict): Dict containing the vyper standard json input vyper (str): vyper binary env (Optional[Dict], optional): Environment variables. Defaults to None. working_dir (Optional[str], optional): Working directory. Defaults to None. @@ -188,13 +182,29 @@ def _run_vyper_standard_json( Returns: Dict: Vyper json compilation artifact """ - with tempfile.NamedTemporaryFile(mode="a+") as f: - cmd = [vyper, standard_input_path, "--standard-json", "-o", f.name] - success = run(cmd, cwd=working_dir, extra_env=env) - if success is None: - raise InvalidCompilation("Vyper compilation failed") - f.seek(0) - return json.loads(f.read()) + cmd = [vyper, "--standard-json"] + + with subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=env, + executable=shutil.which(cmd[0]), + ) as process: + + stdout_b, stderr_b = process.communicate(json.dumps(standard_json_input).encode("utf-8")) + stdout, stderr = ( + stdout_b.decode(), + stderr_b.decode(errors="backslashreplace"), + ) # convert bytestrings to unicode strings + + vyper_standard_output = json.loads(stdout) + if "errors" in vyper_standard_output: + # TODO format errors + raise InvalidCompilation(vyper_standard_output["errors"]) + + return vyper_standard_output def _relative_to_short(relative: Path) -> Path: From 2ac11ee30f38a3df868274f67fb8dd94aff1d874 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Thu, 5 Oct 2023 12:02:44 +0200 Subject: [PATCH 06/10] - Fix bug (missing add_source_files) - Fix linters --- crytic_compile/crytic_compile.py | 2 +- crytic_compile/platform/solc_standard_json.py | 9 ++++++++ crytic_compile/platform/vyper.py | 23 +++++++++++-------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/crytic_compile/crytic_compile.py b/crytic_compile/crytic_compile.py index 98becc40..b7b55a7f 100644 --- a/crytic_compile/crytic_compile.py +++ b/crytic_compile/crytic_compile.py @@ -623,7 +623,7 @@ def compile_all(target: str, **kwargs: str) -> List[CryticCompile]: **kwargs: optional arguments. Used: "solc_standard_json" Raises: - ValueError: If the target could not be compiled + NotImplementedError: If the target could not be compiled Returns: List[CryticCompile]: Returns a list of CryticCompile instances for all compilations which occurred. diff --git a/crytic_compile/platform/solc_standard_json.py b/crytic_compile/platform/solc_standard_json.py index d7006f9a..beadf442 100644 --- a/crytic_compile/platform/solc_standard_json.py +++ b/crytic_compile/platform/solc_standard_json.py @@ -405,6 +405,15 @@ def add_source_file(self, file_path: str) -> None: """ add_source_file(self._json, file_path) + def add_source_files(self, files_path: List[str]) -> None: + """Append files + + Args: + files_path (List[str]): files to append + """ + for file_path in files_path: + add_source_file(self._json, file_path) + def add_remapping(self, remapping: str) -> None: """Append our remappings diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index 4479c533..4721faa6 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -4,8 +4,8 @@ import json import logging import os -import subprocess import shutil +import subprocess from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional @@ -15,7 +15,6 @@ 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.subprocess import run # Handle cycle from crytic_compile.utils.natspec import Natspec @@ -112,8 +111,18 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: source_unit.ast = ast def add_source_files(self, file_paths: List[str]) -> None: + """ + Append files + + Args: + file_paths (List[str]): files to append + + Returns: + + """ + for file_path in file_paths: - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf8") as f: self.standard_json_input["sources"][file_path] = { "content": f.read(), } @@ -163,10 +172,7 @@ def _guessed_tests(self) -> List[str]: def _run_vyper_standard_json( - standard_json_input: Dict, - vyper: str, - env: Optional[Dict] = None, - working_dir: Optional[str] = None, + standard_json_input: Dict, vyper: str, env: Optional[Dict] = None ) -> Dict: """Run vyper and write compilation output to a file @@ -174,7 +180,6 @@ def _run_vyper_standard_json( standard_json_input (Dict): Dict containing the vyper standard json input vyper (str): vyper binary env (Optional[Dict], optional): Environment variables. Defaults to None. - working_dir (Optional[str], optional): Working directory. Defaults to None. Raises: InvalidCompilation: If vyper failed to run @@ -194,7 +199,7 @@ def _run_vyper_standard_json( ) as process: stdout_b, stderr_b = process.communicate(json.dumps(standard_json_input).encode("utf-8")) - stdout, stderr = ( + stdout, _stderr = ( stdout_b.decode(), stderr_b.decode(errors="backslashreplace"), ) # convert bytestrings to unicode strings From 8fdc90ec9db224a040ee4400a98c5a95d79929cd Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Thu, 5 Oct 2023 13:34:41 +0200 Subject: [PATCH 07/10] Fix mypy --- crytic_compile/platform/vyper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index 4721faa6..bcf4725b 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -35,7 +35,7 @@ class VyperStandardJson(AbstractPlatform): TYPE = Type.VYPER def __init__(self, target: Optional[Path] = None, **_kwargs: str): - super().__init__(target, **_kwargs) + super().__init__(str(target), **_kwargs) self.standard_json_input = { "language": "Vyper", "sources": {}, @@ -123,7 +123,7 @@ def add_source_files(self, file_paths: List[str]) -> None: for file_path in file_paths: with open(file_path, "r", encoding="utf8") as f: - self.standard_json_input["sources"][file_path] = { + self.standard_json_input["sources"][file_path] = { # type: ignore "content": f.read(), } From 938913a36e8631f70c1a046c7231c748ca6f76b1 Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Thu, 12 Oct 2023 10:39:32 +0200 Subject: [PATCH 08/10] Add vyper ci test --- .github/workflows/ci.yml | 2 +- scripts/ci_test_vyper.sh | 14 +++ tests/vyper/auction.vy | 178 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100755 scripts/ci_test_vyper.sh create mode 100644 tests/vyper/auction.vy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index caf91f58..153e6919 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "windows-2022"] - type: ["brownie", "buidler", "dapp", "embark", "hardhat", "solc", "truffle", "waffle", "foundry", "standard"] + type: ["brownie", "buidler", "dapp", "embark", "hardhat", "solc", "truffle", "waffle", "foundry", "standard", "vyper"] exclude: # Currently broken, tries to pull git:// which is blocked by GH - type: embark diff --git a/scripts/ci_test_vyper.sh b/scripts/ci_test_vyper.sh new file mode 100755 index 00000000..6f81efc7 --- /dev/null +++ b/scripts/ci_test_vyper.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +### Test vyper integration + +pip install vyper + +echo "Testing vyper integration of $(realpath "$(which crytic-compile)")" + +cd tests/vyper || exit 255 + +if ! crytic-compile auction.vy +then echo "vyper test failed" && exit 255 +else echo "vyper test passed" && exit 0 +fi diff --git a/tests/vyper/auction.vy b/tests/vyper/auction.vy new file mode 100644 index 00000000..1486d2d8 --- /dev/null +++ b/tests/vyper/auction.vy @@ -0,0 +1,178 @@ +# Taken from https://github.com/vyperlang/vyper/blob/9136169468f317a53b4e7448389aa315f90b95ba/examples/auctions/blind_auction.vy +# Blind Auction. Adapted to Vyper from [Solidity by Example](https://github.com/ethereum/solidity/blob/develop/docs/solidity-by-example.rst#blind-auction-1) + +struct Bid: + blindedBid: bytes32 + deposit: uint256 + +# Note: because Vyper does not allow for dynamic arrays, we have limited the +# number of bids that can be placed by one address to 128 in this example +MAX_BIDS: constant(int128) = 128 + +# Event for logging that auction has ended +event AuctionEnded: + highestBidder: address + highestBid: uint256 + +# Auction parameters +beneficiary: public(address) +biddingEnd: public(uint256) +revealEnd: public(uint256) + +# Set to true at the end of auction, disallowing any new bids +ended: public(bool) + +# Final auction state +highestBid: public(uint256) +highestBidder: public(address) + +# State of the bids +bids: HashMap[address, Bid[128]] +bidCounts: HashMap[address, int128] + +# Allowed withdrawals of previous bids +pendingReturns: HashMap[address, uint256] + + +# Create a blinded auction with `_biddingTime` seconds bidding time and +# `_revealTime` seconds reveal time on behalf of the beneficiary address +# `_beneficiary`. +@external +def __init__(_beneficiary: address, _biddingTime: uint256, _revealTime: uint256): + self.beneficiary = _beneficiary + self.biddingEnd = block.timestamp + _biddingTime + self.revealEnd = self.biddingEnd + _revealTime + + +# Place a blinded bid with: +# +# _blindedBid = keccak256(concat( +# convert(value, bytes32), +# convert(fake, bytes32), +# secret) +# ) +# +# The sent ether is only refunded if the bid is correctly revealed in the +# revealing phase. The bid is valid if the ether sent together with the bid is +# at least "value" and "fake" is not true. Setting "fake" to true and sending +# not the exact amount are ways to hide the real bid but still make the +# required deposit. The same address can place multiple bids. +@external +@payable +def bid(_blindedBid: bytes32): + # Check if bidding period is still open + assert block.timestamp < self.biddingEnd + + # Check that payer hasn't already placed maximum number of bids + numBids: int128 = self.bidCounts[msg.sender] + assert numBids < MAX_BIDS + + # Add bid to mapping of all bids + self.bids[msg.sender][numBids] = Bid({ + blindedBid: _blindedBid, + deposit: msg.value + }) + self.bidCounts[msg.sender] += 1 + + +# Returns a boolean value, `True` if bid placed successfully, `False` otherwise. +@internal +def placeBid(bidder: address, _value: uint256) -> bool: + # If bid is less than highest bid, bid fails + if (_value <= self.highestBid): + return False + + # Refund the previously highest bidder + if (self.highestBidder != empty(address)): + self.pendingReturns[self.highestBidder] += self.highestBid + + # Place bid successfully and update auction state + self.highestBid = _value + self.highestBidder = bidder + + return True + + +# Reveal your blinded bids. You will get a refund for all correctly blinded +# invalid bids and for all bids except for the totally highest. +@external +def reveal(_numBids: int128, _values: uint256[128], _fakes: bool[128], _secrets: bytes32[128]): + # Check that bidding period is over + assert block.timestamp > self.biddingEnd + + # Check that reveal end has not passed + assert block.timestamp < self.revealEnd + + # Check that number of bids being revealed matches log for sender + assert _numBids == self.bidCounts[msg.sender] + + # Calculate refund for sender + refund: uint256 = 0 + for i in range(MAX_BIDS): + # Note that loop may break sooner than 128 iterations if i >= _numBids + if (i >= _numBids): + break + + # Get bid to check + bidToCheck: Bid = (self.bids[msg.sender])[i] + + # Check against encoded packet + value: uint256 = _values[i] + fake: bool = _fakes[i] + secret: bytes32 = _secrets[i] + blindedBid: bytes32 = keccak256(concat( + convert(value, bytes32), + convert(fake, bytes32), + secret + )) + + # Bid was not actually revealed + # Do not refund deposit + assert blindedBid == bidToCheck.blindedBid + + # Add deposit to refund if bid was indeed revealed + refund += bidToCheck.deposit + if (not fake and bidToCheck.deposit >= value): + if (self.placeBid(msg.sender, value)): + refund -= value + + # Make it impossible for the sender to re-claim the same deposit + zeroBytes32: bytes32 = empty(bytes32) + bidToCheck.blindedBid = zeroBytes32 + + # Send refund if non-zero + if (refund != 0): + send(msg.sender, refund) + + +# Withdraw a bid that was overbid. +@external +def withdraw(): + # Check that there is an allowed pending return. + pendingAmount: uint256 = self.pendingReturns[msg.sender] + if (pendingAmount > 0): + # If so, set pending returns to zero to prevent recipient from calling + # this function again as part of the receiving call before `transfer` + # returns (see the remark above about conditions -> effects -> + # interaction). + self.pendingReturns[msg.sender] = 0 + + # Then send return + send(msg.sender, pendingAmount) + + +# End the auction and send the highest bid to the beneficiary. +@external +def auctionEnd(): + # Check that reveal end has passed + assert block.timestamp > self.revealEnd + + # Check that auction has not already been marked as ended + assert not self.ended + + # Log auction ending and set flag + log AuctionEnded(self.highestBidder, self.highestBid) + self.ended = True + + # Transfer funds to beneficiary + send(self.beneficiary, self.highestBid) \ No newline at end of file From 8b0bc8f0ccd8e9ef409d017262af9d5f07a0139d Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Thu, 12 Oct 2023 10:44:30 +0200 Subject: [PATCH 09/10] Allow for more vyper versions CI improvements --- crytic_compile/platform/vyper.py | 3 ++- scripts/ci_test_vyper.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index bcf4725b..c705620c 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -77,7 +77,8 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: compilation_unit = CompilationUnit(crytic_compile, str(target)) compiler_version = compilation_artifacts["compiler"].split("-")[1] - assert compiler_version == "0.3.7" + if compiler_version != "0.3.7": + logging.info("Vyper != 0.3.7 support is a best effort and might fail") compilation_unit.compiler_version = CompilerVersion( compiler="vyper", version=compiler_version, optimized=False ) diff --git a/scripts/ci_test_vyper.sh b/scripts/ci_test_vyper.sh index 6f81efc7..9be8fa31 100755 --- a/scripts/ci_test_vyper.sh +++ b/scripts/ci_test_vyper.sh @@ -8,7 +8,7 @@ echo "Testing vyper integration of $(realpath "$(which crytic-compile)")" cd tests/vyper || exit 255 -if ! crytic-compile auction.vy +if ! crytic-compile auction.vy --export-formats standard then echo "vyper test failed" && exit 255 else echo "vyper test passed" && exit 0 fi From bc69c9e664b7cd12d8b13123722a476eddee5e1a Mon Sep 17 00:00:00 2001 From: Feist Josselin Date: Thu, 12 Oct 2023 10:45:12 +0200 Subject: [PATCH 10/10] fix logging --- crytic_compile/platform/vyper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crytic_compile/platform/vyper.py b/crytic_compile/platform/vyper.py index c705620c..4d6fc13f 100644 --- a/crytic_compile/platform/vyper.py +++ b/crytic_compile/platform/vyper.py @@ -78,7 +78,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: compiler_version = compilation_artifacts["compiler"].split("-")[1] if compiler_version != "0.3.7": - logging.info("Vyper != 0.3.7 support is a best effort and might fail") + LOGGER.info("Vyper != 0.3.7 support is a best effort and might fail") compilation_unit.compiler_version = CompilerVersion( compiler="vyper", version=compiler_version, optimized=False )