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            abi = abi.replace(" ", "")
 47            exported_name = combine_filename_name(filename.absolute, contract_name)
 48            contracts[exported_name] = {
 49                "srcmap": ";".join(source_unit.srcmap_init(contract_name)),
 50                "srcmap-runtime": ";".join(source_unit.srcmap_runtime(contract_name)),
 51                "abi": abi,
 52                "bin": source_unit.bytecode_init(contract_name, libraries_to_update),
 53                "bin-runtime": source_unit.bytecode_runtime(contract_name, libraries_to_update),
 54                "userdoc": source_unit.natspec[contract_name].userdoc.export(),
 55                "devdoc": source_unit.natspec[contract_name].devdoc.export(),
 56                "libraries": dict(libraries) if libraries else {},
 57            }
 58    return contracts
 59
 60
 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
 94
 95
 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
125
126
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 []
220
221
222def _get_targets_json(compilation_unit: "CompilationUnit", target: str, **kwargs: Any) -> Dict:
223    """Run the compilation, population the compilation info, and returns the json compilation artifacts
224
225    Args:
226        compilation_unit (CompilationUnit): Compilation unit
227        target (str): path to the solidity file
228        **kwargs: optional arguments. Used: "solc", "solc_disable_warnings", "solc_args", "solc_remaps",
229            "solc_solcs_bin", "solc_solcs_select", "solc_working_dir", "solc_force_legacy_json"
230
231    Returns:
232        Dict: Json of the compilation artifacts
233    """
234    solc: str = kwargs.get("solc", "solc")
235    solc_disable_warnings: bool = kwargs.get("solc_disable_warnings", False)
236    solc_arguments: str = kwargs.get("solc_args", "")
237    solc_remaps: Optional[Union[str, List[str]]] = kwargs.get("solc_remaps", None)
238    # From config file, solcs is a dict (version -> path)
239    # From command line, solc is a list
240    # The guessing of version only works from config file
241    # This is to prevent too complex command line
242    solcs_path_: Optional[Union[str, Dict, List[str]]] = kwargs.get("solc_solcs_bin")
243    solcs_path: Optional[Union[Dict, List[str]]] = None
244    if solcs_path_:
245        if isinstance(solcs_path_, str):
246            solcs_path = solcs_path_.split(",")
247        else:
248            solcs_path = solcs_path_
249    # solcs_env is always a list. It matches solc-select list
250    solcs_env = kwargs.get("solc_solcs_select")
251    solc_working_dir = kwargs.get("solc_working_dir", None)
252    force_legacy_json = kwargs.get("solc_force_legacy_json", False)
253
254    if solcs_path:
255        return _run_solcs_path(
256            compilation_unit,
257            target,
258            solcs_path,
259            solc_disable_warnings,
260            solc_arguments,
261            solc_remaps=solc_remaps,
262            working_dir=solc_working_dir,
263            force_legacy_json=force_legacy_json,
264        )
265
266    if solcs_env:
267        solcs_env_list = solcs_env.split(",")
268        return _run_solcs_env(
269            compilation_unit,
270            target,
271            solc,
272            solc_disable_warnings,
273            solc_arguments,
274            solcs_env=solcs_env_list,
275            solc_remaps=solc_remaps,
276            working_dir=solc_working_dir,
277            force_legacy_json=force_legacy_json,
278        )
279
280    return _run_solc(
281        compilation_unit,
282        target,
283        solc,
284        solc_disable_warnings,
285        solc_arguments,
286        solc_remaps=solc_remaps,
287        working_dir=solc_working_dir,
288        force_legacy_json=force_legacy_json,
289    )
290
291
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
345
346
347def _is_at_or_above_minor_version(compilation_unit: "CompilationUnit", version: int) -> bool:
348    """Checks if the solc version is at or above(=newer) a given minor (0.x.0) version
349
350    Args:
351        compilation_unit (CompilationUnit): Associated compilation unit
352        version (int): version to check
353
354    Returns:
355        bool: True if the compilation unit version is above or equal to the provided version
356    """
357    assert compilation_unit.compiler_version.version
358    return int(compilation_unit.compiler_version.version.split(".")[1]) >= version
359
360
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)
402
403
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
416
417
418def _build_options(compiler_version: CompilerVersion, force_legacy_json: bool) -> str:
419    """
420    Build the solc command line options
421
422    Args:
423        compiler_version (CompilerVersion): compiler version
424        force_legacy_json (bool): true if the legacy json must be used
425
426    Returns:
427        str: options to be passed to the CI
428    """
429    old_04_versions = [f"0.4.{x}" for x in range(0, 12)]
430    # compact-format was introduced in 0.4.12 and made the default in solc 0.8.10
431    explicit_compact_format = (
432        [f"0.4.{x}" for x in range(12, 27)]
433        + [f"0.5.{x}" for x in range(0, 18)]
434        + [f"0.6.{x}" for x in range(0, 13)]
435        + [f"0.7.{x}" for x in range(0, 7)]
436        + [f"0.8.{x}" for x in range(0, 10)]
437    )
438    assert compiler_version.version
439    if compiler_version.version in old_04_versions or compiler_version.version.startswith("0.3"):
440        return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc"
441    if force_legacy_json:
442        return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes"
443    if compiler_version.version in explicit_compact_format:
444        return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes,compact-format"
445
446    return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes"
447
448
449# pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements
450def _run_solc(
451    compilation_unit: "CompilationUnit",
452    filename: str,
453    solc: str,
454    solc_disable_warnings: bool,
455    solc_arguments: Optional[str],
456    solc_remaps: Optional[Union[str, List[str]]] = None,
457    env: Optional[Dict] = None,
458    working_dir: Optional[Union[Path, str]] = None,
459    force_legacy_json: bool = False,
460) -> Dict:
461    """Run solc.
462    Ensure that crytic_compile.compiler_version is set prior calling _run_solc
463
464    Args:
465        compilation_unit (CompilationUnit): Associated compilation unit
466        filename (str): Solidity file to compile
467        solc (str): Solc binary
468        solc_disable_warnings (bool): If True, disable solc warnings
469        solc_arguments (Optional[str]): Additional solc cli arguments
470        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.
471        env (Optional[Dict]): Environment variable when solc is run. Defaults to None.
472        working_dir (Optional[Union[Path, str]]): Working directory when solc is run. Defaults to None.
473        force_legacy_json (bool): Force to use the legacy json format. Defaults to False.
474
475    Raises:
476        InvalidCompilation: If solc failed to run or file is not a solidity file
477
478    Returns:
479        Dict: Json compilation artifacts
480    """
481    if not os.path.isfile(filename) and (
482        not working_dir or not os.path.isfile(os.path.join(str(working_dir), filename))
483    ):
484        if os.path.isdir(filename):
485            raise InvalidCompilation(
486                f"{filename} is a directory. Expected a Solidity file when not using a compilation framework."
487            )
488
489        raise InvalidCompilation(
490            f"{filename} does not exist. Are you in the correct working directory?"
491        )
492
493    if not filename.endswith(".sol"):
494        raise InvalidCompilation(f"{filename} is not the expected format '.sol'")
495
496    compilation_unit.compiler_version = CompilerVersion(
497        compiler="solc", version=get_version(solc, env), optimized=is_optimized(solc_arguments)
498    )
499
500    compiler_version = compilation_unit.compiler_version
501    assert compiler_version
502    options = _build_options(compiler_version, force_legacy_json)
503
504    cmd = [solc]
505    if solc_remaps:
506        if isinstance(solc_remaps, str):
507            solc_remaps = solc_remaps.split(" ")
508        cmd += solc_remaps
509    cmd += [filename, "--combined-json", options]
510    if solc_arguments:
511        # To parse, we first split the string on each '--'
512        solc_args = solc_arguments.split("--")
513        # Split each argument on the first space found
514        # One solc option may have multiple argument sepparated with ' '
515        # For example: --allow-paths /tmp .
516        # split() removes the delimiter, so we add it again
517        solc_args_ = [("--" + x).split(" ", 1) for x in solc_args if x]
518        # Flat the list of list
519        solc_args = [item.strip() for sublist in solc_args_ for item in sublist if item]
520        cmd += solc_args
521
522    additional_kwargs: Dict = {"cwd": working_dir} if working_dir else {}
523    if not compiler_version.version in [f"0.4.{x}" for x in range(0, 11)]:
524        # Add --allow-paths argument, if it isn't already specified
525        # We allow the CWD as well as the directory that contains the file
526        if "--allow-paths" not in cmd:
527            file_dir_start = os.path.normpath(os.path.dirname(filename))
528            # Paths in the --allow-paths arg can't contain commas, since this is the delimeter
529            # Try using absolute path; if it contains a comma, try using relative path instead
530            file_dir = os.path.abspath(file_dir_start)
531            if "," in file_dir:
532                try:
533                    file_dir = os.path.relpath(file_dir_start)
534                except ValueError:
535                    # relpath can fail if, for example, we're on Windows and the directory is on a different drive than CWD
536                    pass
537
538            # Even the relative path might have a comma in it, so we want to make sure first
539            if "," not in file_dir:
540                cmd += ["--allow-paths", ".," + file_dir]
541            else:
542                LOGGER.warning(
543                    "Solc filepath contains a comma; omitting the --allow-paths argument. This may result in failed imports.\n"
544                )
545
546    try:
547        LOGGER.info(
548            "'%s' running",
549            " ".join(cmd),
550        )
551        # pylint: disable=consider-using-with
552        if env:
553            process = subprocess.Popen(
554                cmd,
555                stdout=subprocess.PIPE,
556                stderr=subprocess.PIPE,
557                executable=shutil.which(cmd[0]),
558                env=env,
559                **additional_kwargs,
560            )
561        else:
562            process = subprocess.Popen(
563                cmd,
564                stdout=subprocess.PIPE,
565                stderr=subprocess.PIPE,
566                executable=shutil.which(cmd[0]),
567                **additional_kwargs,
568            )
569    except OSError as error:
570        # pylint: disable=raise-missing-from
571        raise InvalidCompilation(error)
572    stdout_, stderr_ = process.communicate()
573    stdout, stderr = (
574        stdout_.decode(encoding="utf-8", errors="ignore"),
575        stderr_.decode(encoding="utf-8", errors="ignore"),
576    )  # convert bytestrings to unicode strings
577
578    if stderr and (not solc_disable_warnings):
579        LOGGER.info("Compilation warnings/errors on %s:\n%s", filename, stderr)
580
581    try:
582        ret: Dict = json.loads(stdout)
583        return ret
584    except json.decoder.JSONDecodeError:
585        # pylint: disable=raise-missing-from
586        raise InvalidCompilation(f"Invalid solc compilation {stderr}")
587
588
589# pylint: disable=too-many-arguments
590def _run_solcs_path(
591    compilation_unit: "CompilationUnit",
592    filename: str,
593    solcs_path: Optional[Union[Dict, List[str]]],
594    solc_disable_warnings: bool,
595    solc_arguments: str,
596    solc_remaps: Optional[Union[str, List[str]]] = None,
597    env: Optional[Dict] = None,
598    working_dir: Optional[str] = None,
599    force_legacy_json: bool = False,
600) -> Dict:
601    """[summary]
602
603    Args:
604        compilation_unit (CompilationUnit): Associated compilation unit
605        filename (str): Solidity file to compile
606        solcs_path (Optional[Union[Dict, List[str]]]): List of solc binaries to try. If its a dict, in the form "version:path".
607        solc_disable_warnings (bool): If True, disable solc warnings
608        solc_arguments (str): Additional solc cli arguments
609        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.
610        env (Optional[Dict]): Environment variable when solc is run. Defaults to None.
611        working_dir (Optional[Union[Path, str]], optional): Working directory when solc is run. Defaults to None.
612        force_legacy_json (bool): Force to use the legacy json format. Defaults to False.
613
614    Raises:
615        InvalidCompilation: [description]
616
617    Returns:
618        Dict: Json compilation artifacts
619    """
620    targets_json = None
621    if isinstance(solcs_path, dict):
622        guessed_solcs = _guess_solc(filename, working_dir)
623        compilation_errors = []
624        for guessed_solc in guessed_solcs:
625            if not guessed_solc in solcs_path:
626                continue
627            try:
628                targets_json = _run_solc(
629                    compilation_unit,
630                    filename,
631                    solcs_path[guessed_solc],
632                    solc_disable_warnings,
633                    solc_arguments,
634                    solc_remaps=solc_remaps,
635                    env=env,
636                    working_dir=working_dir,
637                    force_legacy_json=force_legacy_json,
638                )
639                break
640            except InvalidCompilation:
641                pass
642
643    if not targets_json:
644        if isinstance(solcs_path, dict):
645            solc_bins: List[str] = list(solcs_path.values())
646        elif solcs_path:
647            solc_bins = solcs_path
648        else:
649            solc_bins = []
650
651        for solc_bin in solc_bins:
652            try:
653                targets_json = _run_solc(
654                    compilation_unit,
655                    filename,
656                    solc_bin,
657                    solc_disable_warnings,
658                    solc_arguments,
659                    solc_remaps=solc_remaps,
660                    env=env,
661                    working_dir=working_dir,
662                    force_legacy_json=force_legacy_json,
663                )
664                break
665            except InvalidCompilation as ic:
666                compilation_errors.append(solc_bin + ": " + ic.args[0])
667
668    if not targets_json:
669        raise InvalidCompilation(
670            "Invalid solc compilation, none of the solc versions provided worked:\n"
671            + "\n".join(compilation_errors)
672        )
673
674    return targets_json
675
676
677# pylint: disable=too-many-arguments
678def _run_solcs_env(
679    compilation_unit: "CompilationUnit",
680    filename: str,
681    solc: str,
682    solc_disable_warnings: bool,
683    solc_arguments: str,
684    solc_remaps: Optional[Union[List[str], str]] = None,
685    env: Optional[Dict] = None,
686    working_dir: Optional[str] = None,
687    solcs_env: Optional[List[str]] = None,
688    force_legacy_json: bool = False,
689) -> Dict:
690    """Run different solc based on environment variable
691    This is mostly a legacy function for old solc-select usages
692
693    Args:
694        compilation_unit (CompilationUnit): Associated compilation unit
695        filename (str): Solidity file to compile
696        solc (str): Solc binary
697        solc_disable_warnings (bool): If True, disable solc warnings
698        solc_arguments (str): Additional solc cli arguments
699        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.
700        env (Optional[Dict], optional): Environment variable when solc is run. Defaults to None.
701        working_dir (Optional[Union[Path, str]], optional): Working directory when solc is run. Defaults to None.
702        solcs_env (Optional[List[str]]): List of solc env variable to try. Defaults to None.
703        force_legacy_json (bool): Force to use the legacy json format. Defaults to False.
704
705    Raises:
706        InvalidCompilation: If solc failed
707
708    Returns:
709        Dict: Json compilation artifacts
710    """
711    env = dict(os.environ) if env is None else env
712    targets_json = None
713    guessed_solcs = _guess_solc(filename, working_dir)
714    compilation_errors = []
715    for guessed_solc in guessed_solcs:
716        if solcs_env and not guessed_solc in solcs_env:
717            continue
718        try:
719            env["SOLC_VERSION"] = guessed_solc
720            targets_json = _run_solc(
721                compilation_unit,
722                filename,
723                solc,
724                solc_disable_warnings,
725                solc_arguments,
726                solc_remaps=solc_remaps,
727                env=env,
728                working_dir=working_dir,
729                force_legacy_json=force_legacy_json,
730            )
731            break
732        except InvalidCompilation:
733            pass
734
735    if not targets_json:
736        solc_versions_env = solcs_env if solcs_env else []
737
738        for version_env in solc_versions_env:
739            try:
740                env["SOLC_VERSION"] = version_env
741                targets_json = _run_solc(
742                    compilation_unit,
743                    filename,
744                    solc,
745                    solc_disable_warnings,
746                    solc_arguments,
747                    solc_remaps=solc_remaps,
748                    env=env,
749                    working_dir=working_dir,
750                    force_legacy_json=force_legacy_json,
751                )
752                break
753            except InvalidCompilation as ic:
754                compilation_errors.append(version_env + ": " + ic.args[0])
755
756    if not targets_json:
757        raise InvalidCompilation(
758            "Invalid solc compilation, none of the solc versions provided worked:\n"
759            + "\n".join(compilation_errors)
760        )
761
762    return targets_json
763
764
765PATTERN = re.compile(r"pragma solidity\s*(?:\^|>=|<=)?\s*(\d+\.\d+\.\d+)")
766
767
768def _guess_solc(target: str, solc_working_dir: Optional[str]) -> List[str]:
769    """Guess the Solidity version (look for "pragma solidity")
770
771    Args:
772        target (str): Solidity filename
773        solc_working_dir (Optional[str]): Working directory
774
775    Returns:
776        List[str]: List of potential solidity version
777    """
778    if solc_working_dir:
779        target = os.path.join(solc_working_dir, target)
780    with open(target, encoding="utf8") as file_desc:
781        buf = file_desc.read()
782        return PATTERN.findall(buf)
783
784
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
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]:
62def export_to_solc_from_compilation_unit(
63    compilation_unit: "CompilationUnit", key: str, export_dir: str
64) -> Optional[str]:
65    """Export the compilation unit to the standard solc output format.
66    The exported file will be $key.json
67
68    Args:
69        compilation_unit (CompilationUnit): Compilation unit to export
70        key (str): Filename Id
71        export_dir (str): Export directory
72
73    Returns:
74        Optional[str]: path to the file generated
75    """
76    contracts = _build_contract_data(compilation_unit)
77
78    # Create additional informational objects.
79    sources = {filename: {"AST": ast} for (filename, ast) in compilation_unit.asts.items()}
80    source_list = [x.absolute for x in compilation_unit.filenames]
81
82    # Create our root object to contain the contracts and other information.
83    output = {"sources": sources, "sourceList": source_list, "contracts": contracts}
84
85    # If we have an export directory specified, we output the JSON.
86    if export_dir:
87        if not os.path.exists(export_dir):
88            os.makedirs(export_dir)
89        path = os.path.join(export_dir, f"{key}.json")
90
91        with open(path, "w", encoding="utf8") as file_desc:
92            json.dump(output, file_desc)
93        return path
94    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]:
 97def export_to_solc(crytic_compile: "CryticCompile", **kwargs: str) -> List[str]:
 98    """Export all the compilation units to the standard solc output format.
 99    The files generated will be either
100    - combined_solc.json, if there is one compilation unit (echidna legacy)
101    - $key.json, where $key is the compilation unit identifiant
102
103    Args:
104        crytic_compile (CryticCompile): CryticCompile object to export
105        **kwargs: optional arguments. Used: "export_dir"
106
107    Returns:
108        List[str]: List of filenames generated
109    """
110    # Obtain objects to represent each contract
111    export_dir = kwargs.get("export_dir", "crytic-export")
112
113    if len(crytic_compile.compilation_units) == 1:
114        compilation_unit = list(crytic_compile.compilation_units.values())[0]
115        path = export_to_solc_from_compilation_unit(compilation_unit, "combined_solc", export_dir)
116        if path:
117            return [path]
118        return []
119
120    paths = []
121    for key, compilation_unit in crytic_compile.compilation_units.items():
122        path = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir)
123        if path:
124            paths.append(path)
125    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

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

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:
182    def clean(self, **_kwargs: str) -> None:
183        """Clean compilation artifacts
184
185        Args:
186            **_kwargs: unused.
187        """
188        return

Clean compilation artifacts

Args: **_kwargs: unused.

@staticmethod
def is_supported(target: str, **kwargs: str) -> bool:
190    @staticmethod
191    def is_supported(target: str, **kwargs: str) -> bool:
192        """Check if the target is a Solidity file
193
194        Args:
195            target (str): path to the target
196            **kwargs: optional arguments. Not used
197
198        Returns:
199            bool: True if the target is a Solidity file
200        """
201        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:
203    def is_dependency(self, _path: str) -> bool:
204        """Check if the path is a dependency (always false for direct solc)
205
206        Args:
207            _path (str): path to the target
208
209        Returns:
210            bool: True if the target is a dependency
211        """
212        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:
293def solc_handle_contracts(
294    targets_json: Dict,
295    skip_filename: bool,
296    compilation_unit: "CompilationUnit",
297    target: str,
298    solc_working_dir: Optional[str],
299) -> None:
300    """Populate the compilation unit from the compilation json artifacts
301
302    Args:
303        targets_json (Dict): Compilation artifacts
304        skip_filename (bool): If true, skip the filename (for solc <0.4.10)
305        compilation_unit (CompilationUnit): Associated compilation unit
306        target (str): Path to the target
307        solc_working_dir (Optional[str]): Working directory for running solc
308    """
309    is_above_0_8 = _is_at_or_above_minor_version(compilation_unit, 8)
310
311    if "contracts" in targets_json:
312
313        for original_contract_name, info in targets_json["contracts"].items():
314            contract_name = extract_name(original_contract_name)
315            # for solc < 0.4.10 we cant retrieve the filename from the ast
316            if skip_filename:
317                filename = convert_filename(
318                    target,
319                    relative_to_short,
320                    compilation_unit.crytic_compile,
321                    working_dir=solc_working_dir,
322                )
323            else:
324                filename = convert_filename(
325                    extract_filename(original_contract_name),
326                    relative_to_short,
327                    compilation_unit.crytic_compile,
328                    working_dir=solc_working_dir,
329                )
330
331            source_unit = compilation_unit.create_source_unit(filename)
332
333            source_unit.add_contract_name(contract_name)
334            compilation_unit.filename_to_contracts[filename].add(contract_name)
335            source_unit.abis[contract_name] = (
336                json.loads(info["abi"]) if not is_above_0_8 else info["abi"]
337            )
338            source_unit.bytecodes_init[contract_name] = info["bin"]
339            source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"]
340            source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";")
341            source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";")
342            userdoc = json.loads(info.get("userdoc", "{}")) if not is_above_0_8 else info["userdoc"]
343            devdoc = json.loads(info.get("devdoc", "{}")) if not is_above_0_8 else info["devdoc"]
344            natspec = Natspec(userdoc, devdoc)
345            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:
362def get_version(solc: str, env: Optional[Dict[str, str]]) -> str:
363    """Obtains the version of the solc executable specified.
364
365    Args:
366        solc (str): The solc executable name to invoke.
367        env (Optional[Dict[str, str]]): An optional environment key-value store which can be used when invoking the solc executable.
368
369    Raises:
370        InvalidCompilation: If solc failed to run
371
372    Returns:
373        str: Returns the version of the provided solc executable.
374    """
375
376    cmd = [solc, "--version"]
377    LOGGER.info(
378        "'%s' running",
379        " ".join(cmd),
380    )
381    try:
382        with subprocess.Popen(
383            cmd,
384            stdout=subprocess.PIPE,
385            stderr=subprocess.PIPE,
386            env=env,
387            executable=shutil.which(cmd[0]),
388        ) as process:
389            stdout_bytes, stderr_bytes = process.communicate()
390            stdout, stderr = (
391                stdout_bytes.decode(errors="backslashreplace"),
392                stderr_bytes.decode(errors="backslashreplace"),
393            )  # convert bytestrings to unicode strings
394            version = re.findall(r"\d+\.\d+\.\d+", stdout)
395            if len(version) == 0:
396                raise InvalidCompilation(
397                    f"\nSolidity version not found:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
398                )
399            return version[0]
400    except OSError as error:
401        # pylint: disable=raise-missing-from
402        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:
405def is_optimized(solc_arguments: Optional[str]) -> bool:
406    """Check if optimization are used
407
408    Args:
409        solc_arguments (Optional[str]): Solc arguments to check
410
411    Returns:
412        bool: True if the optimization are enabled
413    """
414    if solc_arguments:
415        return "--optimize" in solc_arguments
416    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:
786def relative_to_short(relative: Path) -> Path:
787    """Convert relative to short (does nothing for direct solc)
788
789    Args:
790        relative (Path): target
791
792    Returns:
793        Path: Converted path
794    """
795    return relative

Convert relative to short (does nothing for direct solc)

Args: relative (Path): target

Returns: Path: Converted path