Source code for crytic_compile.platform.solc

"""
Solc platform
"""
import os
import json
import logging
import subprocess
import re

from typing import TYPE_CHECKING, Union, List, Dict, Optional

from crytic_compile.platform.abstract_platform import AbstractPlatform
from crytic_compile.platform.types import Type
from crytic_compile.utils.naming import (
    extract_filename,
    extract_name,
    combine_filename_name,
    convert_filename,
)
from crytic_compile.platform.exceptions import InvalidCompilation
from crytic_compile.compiler.compiler import CompilerVersion

# Cycle dependency
from crytic_compile.utils.natspec import Natspec

if TYPE_CHECKING:
    from crytic_compile import CryticCompile

LOGGER = logging.getLogger("CryticCompile")


[docs]def export_to_solc(crytic_compile: "CryticCompile", **kwargs: str) -> Union[str, None]: """ Export the project to the solc format :param crytic_compile: :param kwargs: :return: """ # Obtain objects to represent each contract contracts = dict() for contract_name in crytic_compile.contracts_names: abi = str(crytic_compile.abi(contract_name)) abi = abi.replace("'", '"') abi = abi.replace("True", "true") abi = abi.replace("False", "false") abi = abi.replace(" ", "") exported_name = combine_filename_name( crytic_compile.contracts_filenames[contract_name].absolute, contract_name ) contracts[exported_name] = { "srcmap": ";".join(crytic_compile.srcmap_init(contract_name)), "srcmap-runtime": ";".join(crytic_compile.srcmap_runtime(contract_name)), "abi": abi, "bin": crytic_compile.bytecode_init(contract_name), "bin-runtime": crytic_compile.bytecode_runtime(contract_name), "userdoc": crytic_compile.natspec[contract_name].userdoc.export(), "devdoc": crytic_compile.natspec[contract_name].devdoc.export(), } # Create additional informational objects. sources = {filename: {"AST": ast} for (filename, ast) in crytic_compile.asts.items()} source_list = [x.absolute for x in crytic_compile.filenames] # Create our root object to contain the contracts and other information. output = {"sources": sources, "sourceList": source_list, "contracts": contracts} # If we have an export directory specified, we output the JSON. export_dir = kwargs.get("export_dir", "crytic-export") if export_dir: if not os.path.exists(export_dir): os.makedirs(export_dir) path = os.path.join(export_dir, "combined_solc.json") with open(path, "w", encoding="utf8") as file_desc: json.dump(output, file_desc) return path return None
[docs]class Solc(AbstractPlatform): """ Solc platform """ NAME = "solc" PROJECT_URL = "https://github.com/ethereum/solidity" TYPE = Type.SOLC
[docs] def compile(self, crytic_compile: "CryticCompile", **kwargs: str): """ Compile the target :param crytic_compile: :param kwargs: :return: """ solc = kwargs.get("solc", "solc") solc_disable_warnings = kwargs.get("solc_disable_warnings", False) solc_arguments = kwargs.get("solc_args", "") solc_remaps = kwargs.get("solc_remaps", None) solc_working_dir = kwargs.get("solc_working_dir", None) crytic_compile.compiler_version = CompilerVersion( compiler="solc", version=get_version(solc), optimized=is_optimized(solc_arguments) ) # From config file, solcs is a dict (version -> path) # From command line, solc is a list # The guessing of version only works from config file # This is to prevent too complex command line solcs_path: Optional[Union[str, Dict, List[str]]] = kwargs.get("solc_solcs_bin") # solcs_env is always a list. It matches solc-select list solcs_env = kwargs.get("solc_solcs_select") if solcs_path: if isinstance(solcs_path, str): solcs_path = solcs_path.split(",") targets_json = _run_solcs_path( crytic_compile, self._target, solcs_path, solc_disable_warnings, solc_arguments, solc_remaps=solc_remaps, working_dir=solc_working_dir, ) elif solcs_env: solcs_env_list = solcs_env.split(",") targets_json = _run_solcs_env( crytic_compile, self._target, solc, solc_disable_warnings, solc_arguments, solcs_env=solcs_env_list, solc_remaps=solc_remaps, working_dir=solc_working_dir, ) else: targets_json = _run_solc( crytic_compile, self._target, solc, solc_disable_warnings, solc_arguments, solc_remaps=solc_remaps, working_dir=solc_working_dir, ) skip_filename = crytic_compile.compiler_version.version in [ f"0.4.{x}" for x in range(0, 10) ] if "contracts" in targets_json: for original_contract_name, info in targets_json["contracts"].items(): contract_name = extract_name(original_contract_name) contract_filename = extract_filename(original_contract_name) # for solc < 0.4.10 we cant retrieve the filename from the ast if skip_filename: contract_filename = convert_filename( self._target, relative_to_short, crytic_compile, working_dir=solc_working_dir, ) else: contract_filename = convert_filename( contract_filename, relative_to_short, crytic_compile, working_dir=solc_working_dir, ) crytic_compile.contracts_names.add(contract_name) crytic_compile.contracts_filenames[contract_name] = contract_filename crytic_compile.abis[contract_name] = json.loads(info["abi"]) crytic_compile.bytecodes_init[contract_name] = info["bin"] crytic_compile.bytecodes_runtime[contract_name] = info["bin-runtime"] crytic_compile.srcmaps_init[contract_name] = info["srcmap"].split(";") crytic_compile.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") userdoc = json.loads(info.get("userdoc", "{}")) devdoc = json.loads(info.get("devdoc", "{}")) natspec = Natspec(userdoc, devdoc) crytic_compile.natspec[contract_name] = natspec if "sources" in targets_json: for path, info in targets_json["sources"].items(): if skip_filename: path = convert_filename( self._target, relative_to_short, crytic_compile, working_dir=solc_working_dir, ) else: path = convert_filename( path, relative_to_short, crytic_compile, working_dir=solc_working_dir ) crytic_compile.filenames.add(path) crytic_compile.asts[path.absolute] = info["AST"]
[docs] @staticmethod def is_supported(target: str, **kwargs: str) -> bool: """ Check if the target is a solc project :param target: :return: """ return os.path.isfile(target) and target.endswith(".sol")
[docs] def is_dependency(self, _path: str) -> bool: """ Always return false :param _path: :return: """ return False
def _guessed_tests(self) -> List[str]: """ Guess the potential unit tests commands :return: """ return []
[docs]def get_version(solc: str) -> str: """ Get the compiler verison used :param solc: :return: """ cmd = [solc, "--version"] try: process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as error: raise InvalidCompilation(error) stdout_bytes, _ = process.communicate() stdout = stdout_bytes.decode() # convert bytestrings to unicode strings version = re.findall(r"\d+\.\d+\.\d+", stdout) if len(version) == 0: raise InvalidCompilation(f"Solidity version not found: {stdout}") return version[0]
[docs]def is_optimized(solc_arguments: str) -> bool: """ Check if optimization are used :param solc_arguments: :return: """ if solc_arguments: return "--optimize" in solc_arguments return False
def _run_solc( crytic_compile: "CryticCompile", filename: str, solc: str, solc_disable_warnings, solc_arguments, solc_remaps=None, env=None, working_dir=None, ): """ Note: Ensure that crytic_compile.compiler_version is set prior calling _run_solc :param crytic_compile: :param filename: :param solc: :param solc_disable_warnings: :param solc_arguments: :param solc_remaps: :param env: :param working_dir: :return: """ if not os.path.isfile(filename): raise InvalidCompilation( "{} does not exist (are you in the correct directory?)".format(filename) ) if not filename.endswith(".sol"): raise InvalidCompilation("Incorrect file format") compiler_version = crytic_compile.compiler_version assert compiler_version old_04_versions = [f"0.4.{x}" for x in range(0, 12)] if compiler_version.version in old_04_versions or compiler_version.version.startswith("0.3"): options = "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc" else: options = ( "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes,compact-format" ) cmd = [solc] if solc_remaps: if isinstance(solc_remaps, str): solc_remaps = solc_remaps.split(" ") cmd += solc_remaps cmd += [filename, "--combined-json", options] if solc_arguments: # To parse, we first split the string on each '--' solc_args = solc_arguments.split("--") # Split each argument on the first space found # One solc option may have multiple argument sepparated with ' ' # For example: --allow-paths /tmp . # split() removes the delimiter, so we add it again solc_args = [("--" + x).split(" ", 1) for x in solc_args if x] # Flat the list of list solc_args = [item for sublist in solc_args for item in sublist if item] cmd += solc_args additional_kwargs = {"cwd": working_dir} if working_dir else {} if not compiler_version.version in [f"0.4.{x}" for x in range(0, 11)]: # Add . as default allowed path if "--allow-paths" not in cmd: relative_filepath = filename if not working_dir: working_dir = os.getcwd() if relative_filepath.startswith(working_dir): relative_filepath = relative_filepath[len(working_dir) + 1 :] cmd += ["--allow-paths", ".", relative_filepath] try: if env: process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, **additional_kwargs ) else: process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **additional_kwargs ) except OSError as error: raise InvalidCompilation(error) stdout, stderr = process.communicate() stdout, stderr = (stdout.decode(), stderr.decode()) # convert bytestrings to unicode strings if stderr and (not solc_disable_warnings): LOGGER.info("Compilation warnings/errors on %s:\n%s", filename, stderr) try: ret = json.loads(stdout) return ret except json.decoder.JSONDecodeError: raise InvalidCompilation(f"Invalid solc compilation {stderr}") def _run_solcs_path( crytic_compile, filename, solcs_path, solc_disable_warnings, solc_arguments, solc_remaps=None, env=None, working_dir=None, ): targets_json = None if isinstance(solcs_path, dict): guessed_solcs = _guess_solc(filename, working_dir) for guessed_solc in guessed_solcs: if not guessed_solc in solcs_path: continue try: targets_json = _run_solc( crytic_compile, filename, solcs_path[guessed_solc], solc_disable_warnings, solc_arguments, solc_remaps=solc_remaps, env=env, working_dir=working_dir, ) except InvalidCompilation: pass if not targets_json: solc_bins = solcs_path.values() if isinstance(solcs_path, dict) else solcs_path for solc_bin in solc_bins: try: targets_json = _run_solc( crytic_compile, filename, solc_bin, solc_disable_warnings, solc_arguments, solc_remaps=solc_remaps, env=env, working_dir=working_dir, ) except InvalidCompilation: pass if not targets_json: raise InvalidCompilation( "Invalid solc compilation, none of the solc versions provided worked" ) return targets_json def _run_solcs_env( crytic_compile, filename, solc, solc_disable_warnings, solc_arguments, solc_remaps=None, env=None, working_dir=None, solcs_env=None, ): env = dict(os.environ) if env is None else env targets_json = None guessed_solcs = _guess_solc(filename, working_dir) for guessed_solc in guessed_solcs: if not guessed_solc in solcs_env: continue try: env["SOLC_VERSION"] = guessed_solc targets_json = _run_solc( crytic_compile, filename, solc, solc_disable_warnings, solc_arguments, solc_remaps=solc_remaps, env=env, working_dir=working_dir, ) except InvalidCompilation: pass if not targets_json: solc_versions_env = solcs_env for version_env in solc_versions_env: try: env["SOLC_VERSION"] = version_env targets_json = _run_solc( crytic_compile, filename, solc, solc_disable_warnings, solc_arguments, solc_remaps=solc_remaps, env=env, working_dir=working_dir, ) except InvalidCompilation: pass if not targets_json: raise InvalidCompilation( "Invalid solc compilation, none of the solc versions provided worked" ) return targets_json PATTERN = re.compile(r"pragma solidity[\^|>=|<=]?[ ]+?(\d+\.\d+\.\d+)") def _guess_solc(target, solc_working_dir): if solc_working_dir: target = os.path.join(solc_working_dir, target) with open(target, encoding="utf8") as file_desc: buf = file_desc.read() return PATTERN.findall(buf)
[docs]def relative_to_short(relative): """ Convert relative to short :param relative: :return: """ return relative