Source code for mythx_cli.payload.solidity

"""This module contains functions to generate Solidity-related payloads."""

import logging
import re
from pathlib import Path
from typing import Dict, Optional, Tuple

import click
import solcx
import solcx.exceptions

from mythx_cli.payload.util import patch_solc_bytecode

LOGGER = logging.getLogger("mythx-cli")
PRAGMA_PATTERN = r"pragma solidity [\^<>=]*(\d+\.\d+\.\d+);"


[docs]def generate_solidity_payload( file: str, version: Optional[str], contract: str = None, remappings: Tuple[str] = None, ) -> Dict: """Generate a MythX analysis request from a given Solidity file. This function will open the file, try to detect the used solc version from the pragma definition, and automatically compile it. If the given solc version is not installed on the client's system, it will be automatically downloaded. From the solc output, the following data is sent to the MythX API for analysis: * :code:`abi` * :code:`ast` * :code:`bin` * :code:`bin-runtime` * :code:`srcmap` * :code:`srcmap-runtime` :param file: The path pointing towards the Solidity file :param version: The solc version to use for compilation :param contract: The contract name(s) to submit :param remappings: Import remappings to pass to solcx :return: The payload dictionary to be sent to MythX """ with open(file) as f: source = f.read() solc_version = re.findall(PRAGMA_PATTERN, source) LOGGER.debug(f"solc version matches in {file}: {solc_version}") if not (solc_version or version): # no pragma found, user needs to specify the version raise click.exceptions.UsageError( "No pragma found - please specify a solc version with --solc-version" ) solc_version = f"v{version or solc_version[0]}" if solc_version not in solcx.get_installed_solc_versions(): try: LOGGER.debug(f"Installing solc {solc_version}") solcx.install_solc(solc_version) except Exception as e: raise click.exceptions.UsageError( f"Error installing solc version {solc_version}: {e}" ) solcx.set_solc_version(solc_version, silent=True) try: cwd = str(Path.cwd().absolute()) LOGGER.debug(f"Compiling {file} under allowed path {cwd}") result = solcx.compile_standard( input_data={ "language": "Solidity", "sources": {file: {"urls": [file]}}, "settings": { "remappings": [r.format(pwd=cwd) for r in remappings] or [ f"openzeppelin-solidity/={cwd}/node_modules/openzeppelin-solidity/", f"openzeppelin-zos/={cwd}/node_modules/openzeppelin-zos/", f"zos-lib/={cwd}/node_modules/zos-lib/", ], "outputSelection": { "*": { "*": [ "evm.bytecode.object", "evm.bytecode.sourceMap", "evm.deployedBytecode.object", "evm.deployedBytecode.sourceMap", ], "": ["ast"], } }, "optimizer": {"enabled": True, "runs": 200}, }, }, allow_paths=cwd, ) except solcx.exceptions.SolcError as e: raise click.exceptions.UsageError( f"Error compiling source with solc {solc_version}: {e}" ) compiled_sources = result.get("sources", {}) payload = { "sources": {}, "solc_version": solc_version, "main_source": file, "source_list": [None] * len(compiled_sources), } for file_path, file_data in compiled_sources.items(): # fill source list entry payload["source_list"][file_data.get("id")] = file_path payload_dict = payload["sources"][file_path] = {} # add AST for file if it's present ast = file_data.get("ast") if ast: payload_dict["ast"] = ast # add source from file path with open(file_path, newline="") as source_f: payload_dict["source"] = source_f.read() if contract: try: # if contract specified, set its bytecode and source mapping payload["contract_name"] = contract payload["bytecode"] = patch_solc_bytecode( result["contracts"][file][contract]["evm"]["bytecode"]["object"] ) payload["source_map"] = result["contracts"][file][contract]["evm"][ "bytecode" ]["sourceMap"] payload["deployed_bytecode"] = patch_solc_bytecode( result["contracts"][file][contract]["evm"]["deployedBytecode"]["object"] ) payload["deployed_source_map"] = result["contracts"][file][contract]["evm"][ "deployedBytecode" ]["sourceMap"] return payload except KeyError: LOGGER.warning( f"Could not find contract {contract} in compilation artifacts. The CLI will find the " f"largest bytecode artifact in the compilation output and submit it instead." ) # extract the largest bytecode from the compilation result and add it bytecode_max = 0 for file_path, file_element in result.get("contracts", {}).items(): for contract, contract_data in file_element.items(): contract_bytecode = contract_data["evm"]["bytecode"]["object"] contract_source_map = contract_data["evm"]["bytecode"]["sourceMap"] contract_deployed_bytecode = contract_data["evm"]["deployedBytecode"][ "object" ] contract_deployed_source_map = contract_data["evm"]["deployedBytecode"][ "sourceMap" ] bytecode_length = len(contract_bytecode) if bytecode_length > bytecode_max: bytecode_max = bytecode_length payload["contract_name"] = contract payload["bytecode"] = patch_solc_bytecode(contract_bytecode) payload["source_map"] = contract_source_map payload["deployed_bytecode"] = patch_solc_bytecode( contract_deployed_bytecode ) payload["deployed_source_map"] = contract_deployed_source_map return payload