crytic_compile.platform.solc

Solc platform

  1"""
  2Solc platform
  3"""
  4import json
  5import logging
  6import os
  7import re
  8import shutil
  9import subprocess
 10from pathlib import Path
 11from typing import TYPE_CHECKING, Dict, List, Optional, Union, Any
 12
 13from crytic_compile.compilation_unit import CompilationUnit
 14from crytic_compile.compiler.compiler import CompilerVersion
 15from crytic_compile.platform.abstract_platform import AbstractPlatform
 16from crytic_compile.platform.exceptions import InvalidCompilation
 17from crytic_compile.platform.types import Type
 18from crytic_compile.utils.naming import (
 19    combine_filename_name,
 20    convert_filename,
 21    extract_filename,
 22    extract_name,
 23)
 24
 25# Cycle dependency
 26from crytic_compile.utils.natspec import Natspec
 27
 28if TYPE_CHECKING:
 29    from crytic_compile import CryticCompile
 30
 31LOGGER = logging.getLogger("CryticCompile")
 32
 33
 34def _build_contract_data(compilation_unit: "CompilationUnit") -> Dict:
 35    contracts = {}
 36
 37    libraries_to_update = compilation_unit.crytic_compile.libraries
 38
 39    for filename, source_unit in compilation_unit.source_units.items():
 40        for contract_name in source_unit.contracts_names:
 41            libraries = source_unit.libraries_names_and_patterns(contract_name)
 42            abi = str(source_unit.abi(contract_name))
 43            abi = abi.replace("'", '"')
 44            abi = abi.replace("True", "true")
 45            abi = abi.replace("False", "false")
 46            exported_name = combine_filename_name(filename.absolute, contract_name)
 47            contracts[exported_name] = {
 48                "srcmap": ";".join(source_unit.srcmap_init(contract_name)),
 49                "srcmap-runtime": ";".join(source_unit.srcmap_runtime(contract_name)),
 50                "abi": abi,
 51                "bin": source_unit.bytecode_init(contract_name, libraries_to_update),
 52                "bin-runtime": source_unit.bytecode_runtime(contract_name, libraries_to_update),
 53                "userdoc": source_unit.natspec[contract_name].userdoc.export(),
 54                "devdoc": source_unit.natspec[contract_name].devdoc.export(),
 55                "libraries": dict(libraries) if libraries else {},
 56            }
 57    return contracts
 58
 59
 60def export_to_solc_from_compilation_unit(
 61    compilation_unit: "CompilationUnit", key: str, export_dir: str
 62) -> Optional[str]:
 63    """Export the compilation unit to the standard solc output format.
 64    The exported file will be $key.json
 65
 66    Args:
 67        compilation_unit (CompilationUnit): Compilation unit to export
 68        key (str): Filename Id
 69        export_dir (str): Export directory
 70
 71    Returns:
 72        Optional[str]: path to the file generated
 73    """
 74    contracts = _build_contract_data(compilation_unit)
 75
 76    # Create additional informational objects.
 77    sources = {filename: {"AST": ast} for (filename, ast) in compilation_unit.asts.items()}
 78    source_list = [x.absolute for x in compilation_unit.filenames]
 79
 80    # Create our root object to contain the contracts and other information.
 81    output = {"sources": sources, "sourceList": source_list, "contracts": contracts}
 82
 83    # If we have an export directory specified, we output the JSON.
 84    if export_dir:
 85        if not os.path.exists(export_dir):
 86            os.makedirs(export_dir)
 87        path = os.path.join(export_dir, f"{key}.json")
 88
 89        with open(path, "w", encoding="utf8") as file_desc:
 90            json.dump(output, file_desc)
 91        return path
 92    return None
 93
 94
 95def export_to_solc(crytic_compile: "CryticCompile", **kwargs: str) -> List[str]:
 96    """Export all the compilation units to the standard solc output format.
 97    The files generated will be either
 98    - combined_solc.json, if there is one compilation unit (echidna legacy)
 99    - $key.json, where $key is the compilation unit identifiant
100
101    Args:
102        crytic_compile (CryticCompile): CryticCompile object to export
103        **kwargs: optional arguments. Used: "export_dir"
104
105    Returns:
106        List[str]: List of filenames generated
107    """
108    # Obtain objects to represent each contract
109    export_dir = kwargs.get("export_dir", "crytic-export")
110
111    if len(crytic_compile.compilation_units) == 1:
112        compilation_unit = list(crytic_compile.compilation_units.values())[0]
113        path = export_to_solc_from_compilation_unit(compilation_unit, "combined_solc", export_dir)
114        if path:
115            return [path]
116        return []
117
118    paths = []
119    for key, compilation_unit in crytic_compile.compilation_units.items():
120        path = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir)
121        if path:
122            paths.append(path)
123    return paths
124
125
126class Solc(AbstractPlatform):
127    """
128    Solc platform
129    """
130
131    NAME = "solc"
132    PROJECT_URL = "https://github.com/ethereum/solidity"
133    TYPE = Type.SOLC
134
135    def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
136        """Run the compilation
137
138        Args:
139            crytic_compile (CryticCompile): Associated CryticCompile object
140            **kwargs: optional arguments. Used: "solc_working_dir", "solc_force_legacy_json"
141
142        Raises:
143            InvalidCompilation: If solc failed to run
144        """
145
146        solc_working_dir = kwargs.get("solc_working_dir", None)
147        force_legacy_json = kwargs.get("solc_force_legacy_json", False)
148        compilation_unit = CompilationUnit(crytic_compile, str(self._target))
149
150        targets_json = _get_targets_json(compilation_unit, self._target, **kwargs)
151
152        # there have been a couple of changes in solc starting from 0.8.x,
153        if force_legacy_json and _is_at_or_above_minor_version(compilation_unit, 8):
154            raise InvalidCompilation("legacy JSON not supported from 0.8.x onwards")
155
156        skip_filename = compilation_unit.compiler_version.version in [
157            f"0.4.{x}" for x in range(0, 10)
158        ]
159
160        if "sources" in targets_json:
161            for path, info in targets_json["sources"].items():
162                if skip_filename:
163                    path = convert_filename(
164                        self._target,
165                        relative_to_short,
166                        crytic_compile,
167                        working_dir=solc_working_dir,
168                    )
169                else:
170                    path = convert_filename(
171                        path, relative_to_short, crytic_compile, working_dir=solc_working_dir
172                    )
173                source_unit = compilation_unit.create_source_unit(path)
174                source_unit.ast = info["AST"]
175
176        solc_handle_contracts(
177            targets_json, skip_filename, compilation_unit, self._target, solc_working_dir
178        )
179
180    def clean(self, **_kwargs: str) -> None:
181        """Clean compilation artifacts
182
183        Args:
184            **_kwargs: unused.
185        """
186        return
187
188    @staticmethod
189    def is_supported(target: str, **kwargs: str) -> bool:
190        """Check if the target is a Solidity file
191
192        Args:
193            target (str): path to the target
194            **kwargs: optional arguments. Not used
195
196        Returns:
197            bool: True if the target is a Solidity file
198        """
199        return os.path.isfile(target) and target.endswith(".sol")
200
201    def is_dependency(self, _path: str) -> bool:
202        """Check if the path is a dependency (always false for direct solc)
203
204        Args:
205            _path (str): path to the target
206
207        Returns:
208            bool: True if the target is a dependency
209        """
210        return False
211
212    def _guessed_tests(self) -> List[str]:
213        """Guess the potential unit tests commands (always empty for direct solc)
214
215        Returns:
216            List[str]: The guessed unit tests commands
217        """
218        return []
219
220
221def _get_targets_json(compilation_unit: "CompilationUnit", target: str, **kwargs: Any) -> Dict:
222    """Run the compilation, population the compilation info, and returns the json compilation artifacts
223
224    Args:
225        compilation_unit (CompilationUnit): Compilation unit
226        target (str): path to the solidity file
227        **kwargs: optional arguments. Used: "solc", "solc_disable_warnings", "solc_args", "solc_remaps",
228            "solc_solcs_bin", "solc_solcs_select", "solc_working_dir", "solc_force_legacy_json"
229
230    Returns:
231        Dict: Json of the compilation artifacts
232    """
233    solc: str = kwargs.get("solc", "solc")
234    solc_disable_warnings: bool = kwargs.get("solc_disable_warnings", False)
235    solc_arguments: str = kwargs.get("solc_args", "")
236    solc_remaps: Optional[Union[str, List[str]]] = kwargs.get("solc_remaps", None)
237    # From config file, solcs is a dict (version -> path)
238    # From command line, solc is a list
239    # The guessing of version only works from config file
240    # This is to prevent too complex command line
241    solcs_path_: Optional[Union[str, Dict, List[str]]] = kwargs.get("solc_solcs_bin")
242    solcs_path: Optional[Union[Dict, List[str]]] = None
243    if solcs_path_:
244        if isinstance(solcs_path_, str):
245            solcs_path = solcs_path_.split(",")
246        else:
247            solcs_path = solcs_path_
248    # solcs_env is always a list. It matches solc-select list
249    solcs_env = kwargs.get("solc_solcs_select")
250    solc_working_dir = kwargs.get("solc_working_dir", None)
251    force_legacy_json = kwargs.get("solc_force_legacy_json", False)
252
253    if solcs_path:
254        return _run_solcs_path(
255            compilation_unit,
256            target,
257            solcs_path,
258            solc_disable_warnings,
259            solc_arguments,
260            solc_remaps=solc_remaps,
261            working_dir=solc_working_dir,
262            force_legacy_json=force_legacy_json,
263        )
264
265    if solcs_env:
266        solcs_env_list = solcs_env.split(",")
267        return _run_solcs_env(
268            compilation_unit,
269            target,
270            solc,
271            solc_disable_warnings,
272            solc_arguments,
273            solcs_env=solcs_env_list,
274            solc_remaps=solc_remaps,
275            working_dir=solc_working_dir,
276            force_legacy_json=force_legacy_json,
277        )
278
279    return _run_solc(
280        compilation_unit,
281        target,
282        solc,
283        solc_disable_warnings,
284        solc_arguments,
285        solc_remaps=solc_remaps,
286        working_dir=solc_working_dir,
287        force_legacy_json=force_legacy_json,
288    )
289
290
291def solc_handle_contracts(
292    targets_json: Dict,
293    skip_filename: bool,
294    compilation_unit: "CompilationUnit",
295    target: str,
296    solc_working_dir: Optional[str],
297) -> None:
298    """Populate the compilation unit from the compilation json artifacts
299
300    Args:
301        targets_json (Dict): Compilation artifacts
302        skip_filename (bool): If true, skip the filename (for solc <0.4.10)
303        compilation_unit (CompilationUnit): Associated compilation unit
304        target (str): Path to the target
305        solc_working_dir (Optional[str]): Working directory for running solc
306    """
307    is_above_0_8 = _is_at_or_above_minor_version(compilation_unit, 8)
308
309    if "contracts" in targets_json:
310
311        for original_contract_name, info in targets_json["contracts"].items():
312            contract_name = extract_name(original_contract_name)
313            # for solc < 0.4.10 we cant retrieve the filename from the ast
314            if skip_filename:
315                filename = convert_filename(
316                    target,
317                    relative_to_short,
318                    compilation_unit.crytic_compile,
319                    working_dir=solc_working_dir,
320                )
321            else:
322                filename = convert_filename(
323                    extract_filename(original_contract_name),
324                    relative_to_short,
325                    compilation_unit.crytic_compile,
326                    working_dir=solc_working_dir,
327                )
328
329            source_unit = compilation_unit.create_source_unit(filename)
330
331            source_unit.add_contract_name(contract_name)
332            compilation_unit.filename_to_contracts[filename].add(contract_name)
333            source_unit.abis[contract_name] = (
334                json.loads(info["abi"]) if not is_above_0_8 else info["abi"]
335            )
336            source_unit.bytecodes_init[contract_name] = info["bin"]
337            source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"]
338            source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";")
339            source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";")
340            userdoc = json.loads(info.get("userdoc", "{}")) if not is_above_0_8 else info["userdoc"]
341            devdoc = json.loads(info.get("devdoc", "{}")) if not is_above_0_8 else info["devdoc"]
342            natspec = Natspec(userdoc, devdoc)
343            source_unit.natspec[contract_name] = natspec
344
345
346def _is_at_or_above_minor_version(compilation_unit: "CompilationUnit", version: int) -> bool:
347    """Checks if the solc version is at or above(=newer) a given minor (0.x.0) version
348
349    Args:
350        compilation_unit (CompilationUnit): Associated compilation unit
351        version (int): version to check
352
353    Returns:
354        bool: True if the compilation unit version is above or equal to the provided version
355    """
356    assert compilation_unit.compiler_version.version
357    return int(compilation_unit.compiler_version.version.split(".")[1]) >= version
358
359
360def get_version(solc: str, env: Optional[Dict[str, str]]) -> str:
361    """Obtains the version of the solc executable specified.
362
363    Args:
364        solc (str): The solc executable name to invoke.
365        env (Optional[Dict[str, str]]): An optional environment key-value store which can be used when invoking the solc executable.
366
367    Raises:
368        InvalidCompilation: If solc failed to run
369
370    Returns:
371        str: Returns the version of the provided solc executable.
372    """
373
374    cmd = [solc, "--version"]
375    LOGGER.info(
376        "'%s' running",
377        " ".join(cmd),
378    )
379    try:
380        with subprocess.Popen(
381            cmd,
382            stdout=subprocess.PIPE,
383            stderr=subprocess.PIPE,
384            env=env,
385            executable=shutil.which(cmd[0]),
386        ) as process:
387            stdout_bytes, stderr_bytes = process.communicate()
388            stdout, stderr = (
389                stdout_bytes.decode(errors="backslashreplace"),
390                stderr_bytes.decode(errors="backslashreplace"),
391            )  # convert bytestrings to unicode strings
392            version = re.findall(r"\d+\.\d+\.\d+", stdout)
393            if len(version) == 0:
394                raise InvalidCompilation(
395                    f"\nSolidity version not found:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
396                )
397            return version[0]
398    except OSError as error:
399        # pylint: disable=raise-missing-from
400        raise InvalidCompilation(error)
401
402
403def is_optimized(solc_arguments: Optional[str]) -> bool:
404    """Check if optimization are used
405
406    Args:
407        solc_arguments (Optional[str]): Solc arguments to check
408
409    Returns:
410        bool: True if the optimization are enabled
411    """
412    if solc_arguments:
413        return "--optimize" in solc_arguments
414    return False
415
416
417def _build_options(compiler_version: CompilerVersion, force_legacy_json: bool) -> str:
418    """
419    Build the solc command line options
420
421    Args:
422        compiler_version (CompilerVersion): compiler version
423        force_legacy_json (bool): true if the legacy json must be used
424
425    Returns:
426        str: options to be passed to the CI
427    """
428    old_04_versions = [f"0.4.{x}" for x in range(0, 12)]
429    # compact-format was introduced in 0.4.12 and made the default in solc 0.8.10
430    explicit_compact_format = (
431        [f"0.4.{x}" for x in range(12, 27)]
432        + [f"0.5.{x}" for x in range(0, 18)]
433        + [f"0.6.{x}" for x in range(0, 13)]
434        + [f"0.7.{x}" for x in range(0, 7)]
435        + [f"0.8.{x}" for x in range(0, 10)]
436    )
437    assert compiler_version.version
438    if compiler_version.version in old_04_versions or compiler_version.version.startswith("0.3"):
439        return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc"
440    if force_legacy_json:
441        return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes"
442    if compiler_version.version in explicit_compact_format:
443        return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes,compact-format"
444
445    return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes"
446
447
448# pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements
449def _run_solc(
450    compilation_unit: "CompilationUnit",
451    filename: str,
452    solc: str,
453    solc_disable_warnings: bool,
454    solc_arguments: Optional[str],
455    solc_remaps: Optional[Union[str, List[str]]] = None,
456    env: Optional[Dict] = None,
457    working_dir: Optional[Union[Path, str]] = None,
458    force_legacy_json: bool = False,
459) -> Dict:
460    """Run solc.
461    Ensure that crytic_compile.compiler_version is set prior calling _run_solc
462
463    Args:
464        compilation_unit (CompilationUnit): Associated compilation unit
465        filename (str): Solidity file to compile
466        solc (str): Solc binary
467        solc_disable_warnings (bool): If True, disable solc warnings
468        solc_arguments (Optional[str]): Additional solc cli arguments
469        solc_remaps (Optional[Union[str, List[str]]], optional): Solc remaps. Can be a string where remap are separated with space, or list of str, or a list of. Defaults to None.
470        env (Optional[Dict]): Environment variable when solc is run. Defaults to None.
471        working_dir (Optional[Union[Path, str]]): Working directory when solc is run. Defaults to None.
472        force_legacy_json (bool): Force to use the legacy json format. Defaults to False.
473
474    Raises:
475        InvalidCompilation: If solc failed to run or file is not a solidity file
476
477    Returns:
478        Dict: Json compilation artifacts
479    """
480    if not os.path.isfile(filename) and (
481        not working_dir or not os.path.isfile(os.path.join(str(working_dir), filename))
482    ):
483        if os.path.isdir(filename):
484            raise InvalidCompilation(
485                f"{filename} is a directory. Expected a Solidity file when not using a compilation framework."
486            )
487
488        raise InvalidCompilation(
489            f"{filename} does not exist. Are you in the correct working directory?"
490        )
491
492    if not filename.endswith(".sol"):
493        raise InvalidCompilation(f"{filename} is not the expected format '.sol'")
494
495    compilation_unit.compiler_version = CompilerVersion(
496        compiler="solc", version=get_version(solc, env), optimized=is_optimized(solc_arguments)
497    )
498
499    compiler_version = compilation_unit.compiler_version
500    assert compiler_version
501    options = _build_options(compiler_version, force_legacy_json)
502
503    cmd = [solc]
504    if solc_remaps:
505        if isinstance(solc_remaps, str):
506            solc_remaps = solc_remaps.split(" ")
507        cmd += solc_remaps
508    cmd += [filename, "--combined-json", options]
509    if solc_arguments:
510        # To parse, we first split the string on each '--'
511        solc_args = solc_arguments.split("--")
512        # Split each argument on the first space found
513        # One solc option may have multiple argument sepparated with ' '
514        # For example: --allow-paths /tmp .
515        # split() removes the delimiter, so we add it again
516        solc_args_ = [("--" + x).split(" ", 1) for x in solc_args if x]
517        # Flat the list of list
518        solc_args = [item.strip() for sublist in solc_args_ for item in sublist if item]
519        cmd += solc_args
520
521    additional_kwargs: Dict = {"cwd": working_dir} if working_dir else {}
522    if not compiler_version.version in [f"0.4.{x}" for x in range(0, 11)]:
523        # Add --allow-paths argument, if it isn't already specified
524        # We allow the CWD as well as the directory that contains the file
525        if "--allow-paths" not in cmd:
526            file_dir_start = os.path.normpath(os.path.dirname(filename))
527            # Paths in the --allow-paths arg can't contain commas, since this is the delimeter
528            # Try using absolute path; if it contains a comma, try using relative path instead
529            file_dir = os.path.abspath(file_dir_start)
530            if "," in file_dir:
531                try:
532                    file_dir = os.path.relpath(file_dir_start)
533                except ValueError:
534                    # relpath can fail if, for example, we're on Windows and the directory is on a different drive than CWD
535                    pass
536
537            # Even the relative path might have a comma in it, so we want to make sure first
538            if "," not in file_dir:
539                cmd += ["--allow-paths", ".," + file_dir]
540            else:
541                LOGGER.warning(
542                    "Solc filepath contains a comma; omitting the --allow-paths argument. This may result in failed imports.\n"
543                )
544
545    try:
546        LOGGER.info(
547            "'%s' running",
548            " ".join(cmd),
549        )
550        # pylint: disable=consider-using-with
551        if env:
552            process = subprocess.Popen(
553                cmd,
554                stdout=subprocess.PIPE,
555                stderr=subprocess.PIPE,
556                executable=shutil.which(cmd[0]),
557                env=env,
558                **additional_kwargs,
559            )
560        else:
561            process = subprocess.Popen(
562                cmd,
563                stdout=subprocess.PIPE,
564                stderr=subprocess.PIPE,
565                executable=shutil.which(cmd[0]),
566                **additional_kwargs,
567            )
568    except OSError as error:
569        # pylint: disable=raise-missing-from
570        raise InvalidCompilation(error)
571    stdout_, stderr_ = process.communicate()
572    stdout, stderr = (
573        stdout_.decode(encoding="utf-8", errors="ignore"),
574        stderr_.decode(encoding="utf-8", errors="ignore"),
575    )  # convert bytestrings to unicode strings
576
577    if stderr and (not solc_disable_warnings):
578        LOGGER.info("Compilation warnings/errors on %s:\n%s", filename, stderr)
579
580    try:
581        ret: Dict = json.loads(stdout)
582        return ret
583    except json.decoder.JSONDecodeError:
584        # pylint: disable=raise-missing-from
585        raise InvalidCompilation(f"Invalid solc compilation {stderr}")
586
587
588# pylint: disable=too-many-arguments
589def _run_solcs_path(
590    compilation_unit: "CompilationUnit",
591    filename: str,
592    solcs_path: Optional[Union[Dict, List[str]]],
593    solc_disable_warnings: bool,
594    solc_arguments: str,
595    solc_remaps: Optional[Union[str, List[str]]] = None,
596    env: Optional[Dict] = None,
597    working_dir: Optional[str] = None,
598    force_legacy_json: bool = False,
599) -> Dict:
600    """[summary]
601
602    Args:
603        compilation_unit (CompilationUnit): Associated compilation unit
604        filename (str): Solidity file to compile
605        solcs_path (Optional[Union[Dict, List[str]]]): List of solc binaries to try. If its a dict, in the form "version:path".
606        solc_disable_warnings (bool): If True, disable solc warnings
607        solc_arguments (str): Additional solc cli arguments
608        solc_remaps (Optional[Union[str, List[str]]], optional): Solc remaps. Can be a string where remap are separated with space, or list of str, or a list of. Defaults to None.
609        env (Optional[Dict]): Environment variable when solc is run. Defaults to None.
610        working_dir (Optional[Union[Path, str]], optional): Working directory when solc is run. Defaults to None.
611        force_legacy_json (bool): Force to use the legacy json format. Defaults to False.
612
613    Raises:
614        InvalidCompilation: [description]
615
616    Returns:
617        Dict: Json compilation artifacts
618    """
619    targets_json = None
620    if isinstance(solcs_path, dict):
621        guessed_solcs = _guess_solc(filename, working_dir)
622        compilation_errors = []
623        for guessed_solc in guessed_solcs:
624            if not guessed_solc in solcs_path:
625                continue
626            try:
627                targets_json = _run_solc(
628                    compilation_unit,
629                    filename,
630                    solcs_path[guessed_solc],
631                    solc_disable_warnings,
632                    solc_arguments,
633                    solc_remaps=solc_remaps,
634                    env=env,
635                    working_dir=working_dir,
636                    force_legacy_json=force_legacy_json,
637                )
638                break
639            except InvalidCompilation:
640                pass
641
642    if not targets_json:
643        if isinstance(solcs_path, dict):
644            solc_bins: List[str] = list(solcs_path.values())
645        elif solcs_path:
646            solc_bins = solcs_path
647        else:
648            solc_bins = []
649
650        for solc_bin in solc_bins:
651            try:
652                targets_json = _run_solc(
653                    compilation_unit,
654                    filename,
655                    solc_bin,
656                    solc_disable_warnings,
657                    solc_arguments,
658                    solc_remaps=solc_remaps,
659                    env=env,
660                    working_dir=working_dir,
661                    force_legacy_json=force_legacy_json,
662                )
663                break
664            except InvalidCompilation as ic:
665                compilation_errors.append(solc_bin + ": " + ic.args[0])
666
667    if not targets_json:
668        raise InvalidCompilation(
669            "Invalid solc compilation, none of the solc versions provided worked:\n"
670            + "\n".join(compilation_errors)
671        )
672
673    return targets_json
674
675
676# pylint: disable=too-many-arguments
677def _run_solcs_env(
678    compilation_unit: "CompilationUnit",
679    filename: str,
680    solc: str,
681    solc_disable_warnings: bool,
682    solc_arguments: str,
683    solc_remaps: Optional[Union[List[str], str]] = None,
684    env: Optional[Dict] = None,
685    working_dir: Optional[str] = None,
686    solcs_env: Optional[List[str]] = None,
687    force_legacy_json: bool = False,
688) -> Dict:
689    """Run different solc based on environment variable
690    This is mostly a legacy function for old solc-select usages
691
692    Args:
693        compilation_unit (CompilationUnit): Associated compilation unit
694        filename (str): Solidity file to compile
695        solc (str): Solc binary
696        solc_disable_warnings (bool): If True, disable solc warnings
697        solc_arguments (str): Additional solc cli arguments
698        solc_remaps (Optional[Union[str, List[str]]], optional): Solc remaps. Can be a string where remap are separated with space, or list of str, or a list of. Defaults to None.
699        env (Optional[Dict], optional): Environment variable when solc is run. Defaults to None.
700        working_dir (Optional[Union[Path, str]], optional): Working directory when solc is run. Defaults to None.
701        solcs_env (Optional[List[str]]): List of solc env variable to try. Defaults to None.
702        force_legacy_json (bool): Force to use the legacy json format. Defaults to False.
703
704    Raises:
705        InvalidCompilation: If solc failed
706
707    Returns:
708        Dict: Json compilation artifacts
709    """
710    env = dict(os.environ) if env is None else env
711    targets_json = None
712    guessed_solcs = _guess_solc(filename, working_dir)
713    compilation_errors = []
714    for guessed_solc in guessed_solcs:
715        if solcs_env and not guessed_solc in solcs_env:
716            continue
717        try:
718            env["SOLC_VERSION"] = guessed_solc
719            targets_json = _run_solc(
720                compilation_unit,
721                filename,
722                solc,
723                solc_disable_warnings,
724                solc_arguments,
725                solc_remaps=solc_remaps,
726                env=env,
727                working_dir=working_dir,
728                force_legacy_json=force_legacy_json,
729            )
730            break
731        except InvalidCompilation:
732            pass
733
734    if not targets_json:
735        solc_versions_env = solcs_env if solcs_env else []
736
737        for version_env in solc_versions_env:
738            try:
739                env["SOLC_VERSION"] = version_env
740                targets_json = _run_solc(
741                    compilation_unit,
742                    filename,
743                    solc,
744                    solc_disable_warnings,
745                    solc_arguments,
746                    solc_remaps=solc_remaps,
747                    env=env,
748                    working_dir=working_dir,
749                    force_legacy_json=force_legacy_json,
750                )
751                break
752            except InvalidCompilation as ic:
753                compilation_errors.append(version_env + ": " + ic.args[0])
754
755    if not targets_json:
756        raise InvalidCompilation(
757            "Invalid solc compilation, none of the solc versions provided worked:\n"
758            + "\n".join(compilation_errors)
759        )
760
761    return targets_json
762
763
764PATTERN = re.compile(r"pragma solidity\s*(?:\^|>=|<=)?\s*(\d+\.\d+\.\d+)")
765
766
767def _guess_solc(target: str, solc_working_dir: Optional[str]) -> List[str]:
768    """Guess the Solidity version (look for "pragma solidity")
769
770    Args:
771        target (str): Solidity filename
772        solc_working_dir (Optional[str]): Working directory
773
774    Returns:
775        List[str]: List of potential solidity version
776    """
777    if solc_working_dir:
778        target = os.path.join(solc_working_dir, target)
779    with open(target, encoding="utf8") as file_desc:
780        buf = file_desc.read()
781        return PATTERN.findall(buf)
782
783
784def relative_to_short(relative: Path) -> Path:
785    """Convert relative to short (does nothing for direct solc)
786
787    Args:
788        relative (Path): target
789
790    Returns:
791        Path: Converted path
792    """
793    return relative
LOGGER = <Logger CryticCompile (WARNING)>
def export_to_solc_from_compilation_unit( compilation_unit: crytic_compile.compilation_unit.CompilationUnit, key: str, export_dir: str) -> Union[str, NoneType]:
61def export_to_solc_from_compilation_unit(
62    compilation_unit: "CompilationUnit", key: str, export_dir: str
63) -> Optional[str]:
64    """Export the compilation unit to the standard solc output format.
65    The exported file will be $key.json
66
67    Args:
68        compilation_unit (CompilationUnit): Compilation unit to export
69        key (str): Filename Id
70        export_dir (str): Export directory
71
72    Returns:
73        Optional[str]: path to the file generated
74    """
75    contracts = _build_contract_data(compilation_unit)
76
77    # Create additional informational objects.
78    sources = {filename: {"AST": ast} for (filename, ast) in compilation_unit.asts.items()}
79    source_list = [x.absolute for x in compilation_unit.filenames]
80
81    # Create our root object to contain the contracts and other information.
82    output = {"sources": sources, "sourceList": source_list, "contracts": contracts}
83
84    # If we have an export directory specified, we output the JSON.
85    if export_dir:
86        if not os.path.exists(export_dir):
87            os.makedirs(export_dir)
88        path = os.path.join(export_dir, f"{key}.json")
89
90        with open(path, "w", encoding="utf8") as file_desc:
91            json.dump(output, file_desc)
92        return path
93    return None

Export the compilation unit to the standard solc output format. The exported file will be $key.json

Args: compilation_unit (CompilationUnit): Compilation unit to export key (str): Filename Id export_dir (str): Export directory

Returns: Optional[str]: path to the file generated

def export_to_solc( crytic_compile: crytic_compile.crytic_compile.CryticCompile, **kwargs: str) -> List[str]:
 96def export_to_solc(crytic_compile: "CryticCompile", **kwargs: str) -> List[str]:
 97    """Export all the compilation units to the standard solc output format.
 98    The files generated will be either
 99    - combined_solc.json, if there is one compilation unit (echidna legacy)
100    - $key.json, where $key is the compilation unit identifiant
101
102    Args:
103        crytic_compile (CryticCompile): CryticCompile object to export
104        **kwargs: optional arguments. Used: "export_dir"
105
106    Returns:
107        List[str]: List of filenames generated
108    """
109    # Obtain objects to represent each contract
110    export_dir = kwargs.get("export_dir", "crytic-export")
111
112    if len(crytic_compile.compilation_units) == 1:
113        compilation_unit = list(crytic_compile.compilation_units.values())[0]
114        path = export_to_solc_from_compilation_unit(compilation_unit, "combined_solc", export_dir)
115        if path:
116            return [path]
117        return []
118
119    paths = []
120    for key, compilation_unit in crytic_compile.compilation_units.items():
121        path = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir)
122        if path:
123            paths.append(path)
124    return paths

Export all the compilation units to the standard solc output format. The files generated will be either

  • combined_solc.json, if there is one compilation unit (echidna legacy)
  • $key.json, where $key is the compilation unit identifiant

Args: crytic_compile (CryticCompile): CryticCompile object to export **kwargs: optional arguments. Used: "export_dir"

Returns: List[str]: List of filenames generated

127class Solc(AbstractPlatform):
128    """
129    Solc platform
130    """
131
132    NAME = "solc"
133    PROJECT_URL = "https://github.com/ethereum/solidity"
134    TYPE = Type.SOLC
135
136    def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
137        """Run the compilation
138
139        Args:
140            crytic_compile (CryticCompile): Associated CryticCompile object
141            **kwargs: optional arguments. Used: "solc_working_dir", "solc_force_legacy_json"
142
143        Raises:
144            InvalidCompilation: If solc failed to run
145        """
146
147        solc_working_dir = kwargs.get("solc_working_dir", None)
148        force_legacy_json = kwargs.get("solc_force_legacy_json", False)
149        compilation_unit = CompilationUnit(crytic_compile, str(self._target))
150
151        targets_json = _get_targets_json(compilation_unit, self._target, **kwargs)
152
153        # there have been a couple of changes in solc starting from 0.8.x,
154        if force_legacy_json and _is_at_or_above_minor_version(compilation_unit, 8):
155            raise InvalidCompilation("legacy JSON not supported from 0.8.x onwards")
156
157        skip_filename = compilation_unit.compiler_version.version in [
158            f"0.4.{x}" for x in range(0, 10)
159        ]
160
161        if "sources" in targets_json:
162            for path, info in targets_json["sources"].items():
163                if skip_filename:
164                    path = convert_filename(
165                        self._target,
166                        relative_to_short,
167                        crytic_compile,
168                        working_dir=solc_working_dir,
169                    )
170                else:
171                    path = convert_filename(
172                        path, relative_to_short, crytic_compile, working_dir=solc_working_dir
173                    )
174                source_unit = compilation_unit.create_source_unit(path)
175                source_unit.ast = info["AST"]
176
177        solc_handle_contracts(
178            targets_json, skip_filename, compilation_unit, self._target, solc_working_dir
179        )
180
181    def clean(self, **_kwargs: str) -> None:
182        """Clean compilation artifacts
183
184        Args:
185            **_kwargs: unused.
186        """
187        return
188
189    @staticmethod
190    def is_supported(target: str, **kwargs: str) -> bool:
191        """Check if the target is a Solidity file
192
193        Args:
194            target (str): path to the target
195            **kwargs: optional arguments. Not used
196
197        Returns:
198            bool: True if the target is a Solidity file
199        """
200        return os.path.isfile(target) and target.endswith(".sol")
201
202    def is_dependency(self, _path: str) -> bool:
203        """Check if the path is a dependency (always false for direct solc)
204
205        Args:
206            _path (str): path to the target
207
208        Returns:
209            bool: True if the target is a dependency
210        """
211        return False
212
213    def _guessed_tests(self) -> List[str]:
214        """Guess the potential unit tests commands (always empty for direct solc)
215
216        Returns:
217            List[str]: The guessed unit tests commands
218        """
219        return []

Solc platform

NAME: str = 'solc'
PROJECT_URL: str = 'https://github.com/ethereum/solidity'
TYPE: crytic_compile.platform.types.Type = <Type.SOLC: 1>
def compile( self, crytic_compile: crytic_compile.crytic_compile.CryticCompile, **kwargs: str) -> None:
136    def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
137        """Run the compilation
138
139        Args:
140            crytic_compile (CryticCompile): Associated CryticCompile object
141            **kwargs: optional arguments. Used: "solc_working_dir", "solc_force_legacy_json"
142
143        Raises:
144            InvalidCompilation: If solc failed to run
145        """
146
147        solc_working_dir = kwargs.get("solc_working_dir", None)
148        force_legacy_json = kwargs.get("solc_force_legacy_json", False)
149        compilation_unit = CompilationUnit(crytic_compile, str(self._target))
150
151        targets_json = _get_targets_json(compilation_unit, self._target, **kwargs)
152
153        # there have been a couple of changes in solc starting from 0.8.x,
154        if force_legacy_json and _is_at_or_above_minor_version(compilation_unit, 8):
155            raise InvalidCompilation("legacy JSON not supported from 0.8.x onwards")
156
157        skip_filename = compilation_unit.compiler_version.version in [
158            f"0.4.{x}" for x in range(0, 10)
159        ]
160
161        if "sources" in targets_json:
162            for path, info in targets_json["sources"].items():
163                if skip_filename:
164                    path = convert_filename(
165                        self._target,
166                        relative_to_short,
167                        crytic_compile,
168                        working_dir=solc_working_dir,
169                    )
170                else:
171                    path = convert_filename(
172                        path, relative_to_short, crytic_compile, working_dir=solc_working_dir
173                    )
174                source_unit = compilation_unit.create_source_unit(path)
175                source_unit.ast = info["AST"]
176
177        solc_handle_contracts(
178            targets_json, skip_filename, compilation_unit, self._target, solc_working_dir
179        )

Run the compilation

Args: crytic_compile (CryticCompile): Associated CryticCompile object **kwargs: optional arguments. Used: "solc_working_dir", "solc_force_legacy_json"

Raises: InvalidCompilation: If solc failed to run

def clean(self, **_kwargs: str) -> None:
181    def clean(self, **_kwargs: str) -> None:
182        """Clean compilation artifacts
183
184        Args:
185            **_kwargs: unused.
186        """
187        return

Clean compilation artifacts

Args: **_kwargs: unused.

@staticmethod
def is_supported(target: str, **kwargs: str) -> bool:
189    @staticmethod
190    def is_supported(target: str, **kwargs: str) -> bool:
191        """Check if the target is a Solidity file
192
193        Args:
194            target (str): path to the target
195            **kwargs: optional arguments. Not used
196
197        Returns:
198            bool: True if the target is a Solidity file
199        """
200        return os.path.isfile(target) and target.endswith(".sol")

Check if the target is a Solidity file

Args: target (str): path to the target **kwargs: optional arguments. Not used

Returns: bool: True if the target is a Solidity file

def is_dependency(self, _path: str) -> bool:
202    def is_dependency(self, _path: str) -> bool:
203        """Check if the path is a dependency (always false for direct solc)
204
205        Args:
206            _path (str): path to the target
207
208        Returns:
209            bool: True if the target is a dependency
210        """
211        return False

Check if the path is a dependency (always false for direct solc)

Args: _path (str): path to the target

Returns: bool: True if the target is a dependency

def solc_handle_contracts( targets_json: Dict, skip_filename: bool, compilation_unit: crytic_compile.compilation_unit.CompilationUnit, target: str, solc_working_dir: Union[str, NoneType]) -> None:
292def solc_handle_contracts(
293    targets_json: Dict,
294    skip_filename: bool,
295    compilation_unit: "CompilationUnit",
296    target: str,
297    solc_working_dir: Optional[str],
298) -> None:
299    """Populate the compilation unit from the compilation json artifacts
300
301    Args:
302        targets_json (Dict): Compilation artifacts
303        skip_filename (bool): If true, skip the filename (for solc <0.4.10)
304        compilation_unit (CompilationUnit): Associated compilation unit
305        target (str): Path to the target
306        solc_working_dir (Optional[str]): Working directory for running solc
307    """
308    is_above_0_8 = _is_at_or_above_minor_version(compilation_unit, 8)
309
310    if "contracts" in targets_json:
311
312        for original_contract_name, info in targets_json["contracts"].items():
313            contract_name = extract_name(original_contract_name)
314            # for solc < 0.4.10 we cant retrieve the filename from the ast
315            if skip_filename:
316                filename = convert_filename(
317                    target,
318                    relative_to_short,
319                    compilation_unit.crytic_compile,
320                    working_dir=solc_working_dir,
321                )
322            else:
323                filename = convert_filename(
324                    extract_filename(original_contract_name),
325                    relative_to_short,
326                    compilation_unit.crytic_compile,
327                    working_dir=solc_working_dir,
328                )
329
330            source_unit = compilation_unit.create_source_unit(filename)
331
332            source_unit.add_contract_name(contract_name)
333            compilation_unit.filename_to_contracts[filename].add(contract_name)
334            source_unit.abis[contract_name] = (
335                json.loads(info["abi"]) if not is_above_0_8 else info["abi"]
336            )
337            source_unit.bytecodes_init[contract_name] = info["bin"]
338            source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"]
339            source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";")
340            source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";")
341            userdoc = json.loads(info.get("userdoc", "{}")) if not is_above_0_8 else info["userdoc"]
342            devdoc = json.loads(info.get("devdoc", "{}")) if not is_above_0_8 else info["devdoc"]
343            natspec = Natspec(userdoc, devdoc)
344            source_unit.natspec[contract_name] = natspec

Populate the compilation unit from the compilation json artifacts

Args: targets_json (Dict): Compilation artifacts skip_filename (bool): If true, skip the filename (for solc <0.4.10) compilation_unit (CompilationUnit): Associated compilation unit target (str): Path to the target solc_working_dir (Optional[str]): Working directory for running solc

def get_version(solc: str, env: Union[Dict[str, str], NoneType]) -> str:
361def get_version(solc: str, env: Optional[Dict[str, str]]) -> str:
362    """Obtains the version of the solc executable specified.
363
364    Args:
365        solc (str): The solc executable name to invoke.
366        env (Optional[Dict[str, str]]): An optional environment key-value store which can be used when invoking the solc executable.
367
368    Raises:
369        InvalidCompilation: If solc failed to run
370
371    Returns:
372        str: Returns the version of the provided solc executable.
373    """
374
375    cmd = [solc, "--version"]
376    LOGGER.info(
377        "'%s' running",
378        " ".join(cmd),
379    )
380    try:
381        with subprocess.Popen(
382            cmd,
383            stdout=subprocess.PIPE,
384            stderr=subprocess.PIPE,
385            env=env,
386            executable=shutil.which(cmd[0]),
387        ) as process:
388            stdout_bytes, stderr_bytes = process.communicate()
389            stdout, stderr = (
390                stdout_bytes.decode(errors="backslashreplace"),
391                stderr_bytes.decode(errors="backslashreplace"),
392            )  # convert bytestrings to unicode strings
393            version = re.findall(r"\d+\.\d+\.\d+", stdout)
394            if len(version) == 0:
395                raise InvalidCompilation(
396                    f"\nSolidity version not found:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
397                )
398            return version[0]
399    except OSError as error:
400        # pylint: disable=raise-missing-from
401        raise InvalidCompilation(error)

Obtains the version of the solc executable specified.

Args: solc (str): The solc executable name to invoke. env (Optional[Dict[str, str]]): An optional environment key-value store which can be used when invoking the solc executable.

Raises: InvalidCompilation: If solc failed to run

Returns: str: Returns the version of the provided solc executable.

def is_optimized(solc_arguments: Union[str, NoneType]) -> bool:
404def is_optimized(solc_arguments: Optional[str]) -> bool:
405    """Check if optimization are used
406
407    Args:
408        solc_arguments (Optional[str]): Solc arguments to check
409
410    Returns:
411        bool: True if the optimization are enabled
412    """
413    if solc_arguments:
414        return "--optimize" in solc_arguments
415    return False

Check if optimization are used

Args: solc_arguments (Optional[str]): Solc arguments to check

Returns: bool: True if the optimization are enabled

PATTERN = re.compile('pragma solidity\\s*(?:\\^|>=|<=)?\\s*(\\d+\\.\\d+\\.\\d+)')
def relative_to_short(relative: pathlib.Path) -> pathlib.Path:
785def relative_to_short(relative: Path) -> Path:
786    """Convert relative to short (does nothing for direct solc)
787
788    Args:
789        relative (Path): target
790
791    Returns:
792        Path: Converted path
793    """
794    return relative

Convert relative to short (does nothing for direct solc)

Args: relative (Path): target

Returns: Path: Converted path