diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc71c580..9c22f706c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/eth-brownie/brownie) ### Added - Arguments from the command line can now be passed to brownie scripts. ([#398](https://github.com/eth-brownie/brownie/issues/398)) +- Fix etherscan verification w/ new solidity flattener ([#1283](https://github.com/eth-brownie/brownie/pull/1283)) ## [1.16.4](https://github.com/eth-brownie/brownie/tree/v1.16.4) - 2021-09-21 ### Added diff --git a/brownie/network/contract.py b/brownie/network/contract.py index f8de57a88..5b7077cd8 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -1,11 +1,11 @@ #!/usr/bin/python3 +import io import json import os import re import time import warnings -from collections import defaultdict from pathlib import Path from textwrap import TextWrapper from threading import get_ident # noqa @@ -14,7 +14,6 @@ import eth_abi import requests -import solcast import solcx from eth_utils import remove_0x_prefix from hexbytes import HexBytes @@ -40,9 +39,9 @@ ) from brownie.project import compiler, ethpm from brownie.project.compiler.solidity import SOLIDITY_ERROR_CODES +from brownie.project.flattener import Flattener from brownie.typing import AccountsType, TransactionReceiptType from brownie.utils import color -from brownie.utils.toposort import toposort_flatten from . import accounts, chain from .event import _add_deployment_topics, _get_topics @@ -154,6 +153,10 @@ def __init__(self, project: Any, build: Dict) -> None: self.deploy = ContractConstructor(self, self._name) _revert_register(self) + # messes with tests if it is created on init + # instead we create when it's requested, but still define it here + self._flattener: Flattener = None # type: ignore + def __iter__(self) -> Iterator: return iter(self._contracts) @@ -258,118 +261,35 @@ def get_verification_info(self) -> Dict: "for vyper contracts. You need to verify the source manually" ) elif language == "Solidity": - # Scan the AST tree for needed information - nodes_source = [ - {"node": solcast.from_ast(self._build["ast"]), "src": self._build["source"]} - ] - for name in self._build["dependencies"]: - build_json = self._project._build.get(name) - if "ast" in build_json: - nodes_source.append( - {"node": solcast.from_ast(build_json["ast"]), "src": build_json["source"]} + if self._flattener is None: + source_fp = ( + Path(self._project._path) + .joinpath(self._build["sourcePath"]) + .resolve() + .as_posix() + ) + config = self._project._compiler_config + remaps = dict( + map( + lambda s: s.split("=", 1), + compiler._get_solc_remappings(config["solc"]["remappings"]), ) + ) + compiler_settings = { + "evmVersion": self._build["compiler"]["evm_version"], + "optimizer": config["solc"]["optimizer"], + } + self._flattener = Flattener(source_fp, self._name, remaps, compiler_settings) - pragma_statements = set() - global_structs = set() - global_enums = set() - import_aliases: Dict = defaultdict(list) - for n, src in [ns.values() for ns in nodes_source]: - for pragma in n.children(filters={"nodeType": "PragmaDirective"}): - pragma_statements.add(src[slice(*pragma.offset)]) - - for enum in n.children(filters={"nodeType": "EnumDefinition"}): - if enum.parent() == n: - # parent == source node -> global enum - global_enums.add(src[slice(*enum.offset)]) - - for struct in n.children(filters={"nodeType": "StructDefinition"}): - if struct.parent() == n: - # parent == source node -> global struct - global_structs.add(src[(slice(*struct.offset))]) - - for imp in n.children(filters={"nodeType": "ImportDirective"}): - if isinstance(imp.get("symbolAliases"), list): - for symbol_alias in imp.get("symbolAliases"): - if symbol_alias["local"] is not None: - import_aliases[imp.get("absolutePath")].append( - symbol_alias["local"], - ) - - abiencoder_str = "" - for pragma in ("pragma experimental ABIEncoderV2;", "pragma abicoder v2;"): - if pragma in pragma_statements: - abiencoder_str = f"{abiencoder_str}\n{pragma}" - - # build dependency tree - dependency_tree: Dict = defaultdict(set) - dependency_tree["__root_node__"] = set(self._build["dependencies"]) - for name in self._build["dependencies"]: - build_json = self._project._build.get(name) - if "dependencies" in build_json: - dependency_tree[name].update(build_json["dependencies"]) - - # sort dependencies, process them and insert them into the flattened file - flattened_source = "" - for name in toposort_flatten(dependency_tree): - if name == "__root_node__": - continue - build_json = self._project._build.get(name) - offset = build_json["offset"] - contract_name = build_json["contractName"] - source = self._slice_source(build_json["source"], offset) - # Check for import aliases and duplicate the contract with different name - if "sourcePath" in build_json: - for alias in import_aliases[build_json["sourcePath"]]: - # slice to contract definition and replace contract name - a_source = build_json["source"][offset[0] :] - a_source = re.sub( - rf"^(abstract)?(\s*)({build_json['type']})(\s+)({contract_name})", - rf"\1\2\3\4{alias}", - a_source, - ) - # restore source, adjust offsets and slice source - a_source = f"{build_json['source'][:offset[0]]}{a_source}" - a_offset = [offset[0], offset[1] + (len(alias) - len(contract_name))] - a_source = self._slice_source(a_source, a_offset) - # add alias source to flattened file - a_name = f"{name} (Alias import as {alias})" - flattened_source = f"{flattened_source}\n\n// Part: {a_name}\n\n{a_source}" - - flattened_source = f"{flattened_source}\n\n// Part: {name}\n\n{source}" - - # Top level contract, defines compiler and license build_json = self._build - version = build_json["compiler"]["version"] - version_short = re.findall(r"^[^+]+", version)[0] - offset = build_json["offset"] - source = self._slice_source(build_json["source"], offset) - file_name = Path(build_json["sourcePath"]).parts[-1] - licenses = re.findall( - r"SPDX-License-Identifier:(.*)\n", build_json["source"][: offset[0]] - ) - license_identifier = licenses[0].strip() if len(licenses) >= 1 else "NONE" - - # combine to final flattened source - lb = "\n" - is_global = len(global_enums) + len(global_structs) > 0 - global_str = "// Global Enums and Structs\n\n" if is_global else "" - enum_structs = f"{lb.join(global_enums)}\n\n{lb.join(global_structs)}" - flattened_source = ( - f"// SPDX-License-Identifier: {license_identifier}\n\n" - f"pragma solidity {version_short};" - f"{abiencoder_str}\n\n{global_str}" - f"{enum_structs if is_global else ''}" - f"{flattened_source}\n\n" - f"// File: {file_name}\n\n{source}\n" - ) return { - "flattened_source": flattened_source, + "standard_json_input": self._flattener.standard_input_json, "contract_name": build_json["contractName"], - "compiler_version": version, + "compiler_version": build_json["compiler"]["version"], "optimizer_enabled": build_json["compiler"]["optimizer"]["enabled"], "optimizer_runs": build_json["compiler"]["optimizer"]["runs"], - "license_identifier": license_identifier, + "license_identifier": self._flattener.license, "bytecode_len": len(build_json["bytecode"]), } else: @@ -407,7 +327,7 @@ def publish_source(self, contract: Any, silent: bool = False) -> bool: address = _resolve_address(contract.address) - # Get flattened source code and contract/compiler information + # Get source code and contract/compiler information contract_info = self.get_verification_info() # Select matching license code (https://etherscan.io/contract-license-types) @@ -483,9 +403,9 @@ def publish_source(self, contract: Any, silent: bool = False) -> bool: "module": "contract", "action": "verifysourcecode", "contractaddress": address, - "sourceCode": contract_info["flattened_source"], - "codeformat": "solidity-single-file", - "contractname": contract_info["contract_name"], + "sourceCode": io.StringIO(json.dumps(self._flattener.standard_input_json)), + "codeformat": "solidity-standard-json-input", + "contractname": f"{self._flattener.contract_file}:{self._flattener.contract_name}", "compilerversion": f"v{contract_info['compiler_version']}", "optimizationUsed": 1 if contract_info["optimizer_enabled"] else 0, "runs": contract_info["optimizer_runs"], diff --git a/brownie/project/flattener.py b/brownie/project/flattener.py new file mode 100644 index 000000000..b18fb9a08 --- /dev/null +++ b/brownie/project/flattener.py @@ -0,0 +1,139 @@ +import re +from collections import defaultdict +from pathlib import Path +from typing import DefaultDict, Dict, Set + +from brownie.utils.toposort import toposort_flatten + +# Patten matching Solidity `import-directive`, capturing path component +# https://docs.soliditylang.org/en/latest/grammar.html#a4.SolidityParser.importDirective +IMPORT_PATTERN = re.compile(r"(?<=\n)?import(?P.*)\"(?P.*)\"(?P.*)(?=\n)") +PRAGMA_PATTERN = re.compile(r"^pragma.*;$", re.MULTILINE) +LICENSE_PATTERN = re.compile(r"^// SPDX-License-Identifier: (.*)$", re.MULTILINE) + + +class Flattener: + """Brownie's Robust Solidity Flattener.""" + + def __init__( + self, primary_source_fp: str, contract_name: str, remappings: dict, compiler_settings: dict + ) -> None: + self.sources: Dict[str, str] = {} + self.dependencies: DefaultDict[str, Set[str]] = defaultdict(set) + self.compiler_settings = compiler_settings + self.contract_name = contract_name + self.contract_file = Path(primary_source_fp).name + self.remappings = remappings + + self.traverse(primary_source_fp) + + license_search = LICENSE_PATTERN.search(self.sources[Path(primary_source_fp).name]) + self.license = license_search.group(1) if license_search else "NONE" + + def traverse(self, fp: str) -> None: + """Traverse a contract source files dependencies. + + Files are read in, import statement path components are substituted for their absolute + path, and the modified source is saved along with it's dependencies. + + Args: + fp: The contract source file to traverse, if it's already been traversed, return early. + """ + # if already traversed file, return early + fp_obj = Path(fp) + if fp_obj.name in self.sources: + return + + # read in the source file + source = fp_obj.read_text() + + # path sanitization lambda fn + sanitize = lambda path: self.make_import_absolute( # noqa: E731 + self.remap_import(path), fp_obj.parent.as_posix() + ) + # replacement function for re.sub, we just sanitize the path + repl = ( # noqa: E731 + lambda m: f'import{m.group("prefix")}' + + f'"{Path(sanitize(m.group("path"))).name}"' + + f'{m.group("suffix")}' + ) + + self.sources[fp_obj.name] = IMPORT_PATTERN.sub(repl, source) + if fp_obj.name not in self.dependencies: + self.dependencies[fp_obj.name] = set() + + # traverse dependency files - can circular imports happen? + for m in IMPORT_PATTERN.finditer(source): + import_path = sanitize(m.group("path")) + self.dependencies[fp_obj.name].add(Path(import_path).name) + self.traverse(import_path) + + @property + def flattened_source(self) -> str: + """The flattened source code for use verifying.""" + # all source files in the correct order for concatenation + sources = [self.sources[k] for k in toposort_flatten(self.dependencies)] + # all pragma statements, we already have the license used + know which compiler + # version is used via the build info + pragmas = set((match.strip() for src in sources for match in PRAGMA_PATTERN.findall(src))) + # now we go thorugh and remove all imports/pragmas/license stuff + wipe = lambda src: PRAGMA_PATTERN.sub( # noqa: E731 + "", LICENSE_PATTERN.sub("", IMPORT_PATTERN.sub("", src)) + ) + + sources = [ + f"// File: {file}\n\n{wipe(src)}" + for src, file in zip(sources, toposort_flatten(self.dependencies)) + ] + + flat = ( + "\n".join([pragma for pragma in pragmas if "pragma solidity" not in pragma]) + + "\n\n" + + "\n".join(sources) + ) + # hopefully this doesn't mess up anything pretty, but just gotta remove all + # that extraneous whitespace + return re.sub(r"\n{3,}", "\n\n", flat) + + @property + def standard_input_json(self) -> Dict: + """Useful for etherscan verification via solidity-standard-json-input mode. + + Sadly programmatic upload of this isn't available at the moment (2021-10-11) + """ + return { + "language": "Solidity", + "sources": {k: {"content": v} for k, v in self.sources.items()}, + "settings": self.compiler_settings, + } + + def remap_import(self, import_path: str) -> str: + """Remap imports in a solidity source file. + + Args: + import_path: The path component of an import directive from a solidity source file. + + Returns: + str: The import path string correctly remapped. + """ + for k, v in self.remappings.items(): + if import_path.startswith(k): + return import_path.replace(k, v, 1) + return import_path + + @staticmethod + def make_import_absolute(import_path: str, source_file_dir: str) -> str: + """Make an import path absolute, if it is not already. + + Args: + source_file_dir: The parent directory of the source file where the import path appears. + import_path: The path component of an import directive (should already remapped). + + Returns: + str: The import path string in absolute form. + """ + path: Path = Path(import_path) + if path.is_absolute(): + return path.as_posix() + + return (Path(source_file_dir) / path).resolve().as_posix() diff --git a/requirements-dev.txt b/requirements-dev.txt index 06f37cfdb..a902297f9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile requirements-dev.in @@ -26,7 +26,7 @@ certifi==2021.5.30 # requests cffi==1.14.6 # via cryptography -charset-normalizer==2.0.3 +charset-normalizer==2.0.6 # via # -c requirements.txt # requests @@ -83,12 +83,12 @@ markupsafe==2.0.1 # via jinja2 mccabe==0.6.1 # via flake8 +mypy==0.720 + # via -r requirements-dev.in mypy-extensions==0.4.3 # via # -c requirements.txt # mypy -mypy==0.720 - # via -r requirements-dev.in packaging==21.0 # via # -c requirements.txt @@ -102,9 +102,11 @@ pip-tools==6.2.0 # via -r requirements-dev.in pkginfo==1.7.1 # via twine -platformdirs==2.0.2 - # via virtualenv -pluggy==0.13.1 +platformdirs==2.3.0 + # via + # -c requirements.txt + # virtualenv +pluggy==1.0.0 # via # -c requirements.txt # pytest @@ -120,7 +122,7 @@ pycparser==2.20 # via cffi pyflakes==2.3.1 # via flake8 -pygments==2.9.0 +pygments==2.10.0 # via # -c requirements.txt # readme-renderer @@ -129,27 +131,27 @@ pyparsing==2.4.7 # via # -c requirements.txt # packaging -pytest-cov==2.12.1 - # via -r requirements-dev.in -pytest-mock==3.6.1 - # via -r requirements-dev.in -pytest==6.2.4 +pytest==6.2.5 # via # -c requirements.txt # pytest-cov # pytest-mock +pytest-cov==2.12.1 + # via -r requirements-dev.in +pytest-mock==3.6.1 + # via -r requirements-dev.in pytz==2021.1 # via babel readme-renderer==29.0 # via twine -requests-toolbelt==0.9.1 - # via twine requests==2.26.0 # via # -c requirements.txt # requests-toolbelt # sphinx # twine +requests-toolbelt==0.9.1 + # via twine rfc3986==1.5.0 # via twine secretstorage==3.3.1 @@ -163,12 +165,12 @@ six==1.16.0 # virtualenv snowballstemmer==2.1.0 # via sphinx -sphinx-rtd-theme==0.5.2 - # via -r requirements-dev.in sphinx==4.1.1 # via # -r requirements-dev.in # sphinx-rtd-theme +sphinx-rtd-theme==0.5.2 + # via -r requirements-dev.in sphinxcontrib-applehelp==1.0.2 # via sphinx sphinxcontrib-devhelp==1.0.2 @@ -187,13 +189,13 @@ toml==0.10.2 # pytest # pytest-cov # tox -tomli==1.0.4 +tomli==1.2.1 # via # -c requirements.txt # pep517 tox==3.24.0 # via -r requirements-dev.in -tqdm==4.61.2 +tqdm==4.62.3 # via # -c requirements.txt # twine @@ -201,7 +203,7 @@ twine==3.4.2 # via -r requirements-dev.in typed-ast==1.4.3 # via mypy -typing-extensions==3.10.0.0 +typing-extensions==3.10.0.2 # via # -c requirements.txt # mypy diff --git a/tests/conftest.py b/tests/conftest.py index 66738baec..a7f2bd754 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,6 @@ from ethpm._utils.ipfs import dummy_ipfs_pin from ethpm.backends.ipfs import BaseIPFSBackend from prompt_toolkit.input.defaults import create_pipe_input -from semantic_version import Version import brownie from brownie._cli.console import Console @@ -96,27 +95,6 @@ def pytest_generate_tests(metafunc): metafunc.parametrize("evmtester", params, indirect=True) -# travis cannot call github ethereum/solidity API, so this method is patched -def pytest_sessionstart(session): - if not session.config.getoption("--evm"): - monkeypatch_session = MonkeyPatch() - monkeypatch_session.setattr( - "solcx.get_installable_solc_versions", - lambda: [ - Version("0.6.7"), - Version("0.6.2"), - Version("0.6.0"), - Version("0.5.15"), - Version("0.5.8"), - Version("0.5.7"), - Version("0.5.0"), - Version("0.4.25"), - Version("0.4.24"), - Version("0.4.22"), - ], - ) - - @pytest.fixture(scope="session") def network_name(): return _dev_network diff --git a/tests/network/contract/conftest.py b/tests/network/contract/conftest.py deleted file mode 100644 index 8cec1d9a3..000000000 --- a/tests/network/contract/conftest.py +++ /dev/null @@ -1,313 +0,0 @@ -import pytest - - -@pytest.fixture -def brownie_tester_flat(): - yield """// SPDX-License-Identifier: NONE - -pragma solidity 0.5.17; -pragma experimental ABIEncoderV2; - - - -// Part: BrownieTester - -pragma solidity ^0.5.0; -pragma experimental ABIEncoderV2; - -import "./SafeMath.sol"; - -/** @notice This is the main contract used to test Brownie functionality */ -contract BrownieTester { - - using SafeMath for uint; - - address payable public owner; - uint256 num; - - struct Nested { - string a; - bytes32 b; - } - - struct Base { - string str; - address addr; - Nested nested; - } - - mapping (address => Base) testMap; - - event TupleEvent(address addr, Base base); - event Debug(uint a); - event IndexedEvent(string indexed str, uint256 indexed num); - - constructor (bool success) public { - require(success); - owner = msg.sender; - } - - function () external payable { - require(msg.value >= 1 ether); - emit Debug(31337); - } - - function sendEth() external returns (bool) { - owner.transfer(address(this).balance); - return true; - } - - function receiveEth() external payable returns (bool) { - return true; - } - - function doNothing() external returns (bool) { - return true; - } - - function emitEvents(string calldata str, uint256 num) external returns (bool) { - emit Debug(num); - emit IndexedEvent(str, num); - emit Debug(num + 2); - return true; - } - - function revertStrings(uint a) external returns (bool) { - emit Debug(a); - require (a != 0, "zero"); - require (a != 1); // dev: one - require (a != 2, "two"); // dev: error - require (a != 3); // error - if (a != 31337) { - return true; - } - revert(); // dev: great job - } - - function setTuple(Base memory _base) public { - testMap[_base.addr] = _base; - emit TupleEvent(_base.addr, _base); - } - - function getTuple(address _addr) public view returns (Base memory) { - return testMap[_addr]; - } - - function setNum(uint _num) external returns (bool) { - num = _num; - return true; - } - - function manyValues( - uint a, - bool[] calldata b, - address c, - bytes32[2][] calldata d - ) - external - view - returns (uint _num, bool[] memory _bool, address _addr, bytes32[2][] memory _bytes) - { - return (a, b, c, d); - } - - function useSafeMath(uint a, uint b) external returns (uint) { - uint c = a.mul(b); - return c; - } - - function makeExternalCall(ExternalCallTester other, uint a) external returns (bool) { - bool ok = other.getCalled(a); - return ok; - } - - function makeInternalCalls(bool callPublic, bool callPrivate) external returns (bool) { - if (callPublic) { - getCalled(0); - } - if (callPrivate) { - _getCalled(0); - } - return true; - } - - function getCalled(uint a) public returns (bool) { - return _getCalled(a); - } - - function _getCalled(uint a) internal returns (bool) { - return false; - } - -} - -// Part: SafeMath - -library SafeMath { - function add(uint a, uint b) internal pure returns (uint c) { - c = a + b; - require(c >= a); - } - function sub(uint a, uint b) internal pure returns (uint c) { - require(b <= a); - c = a - b; - } - function mul(uint a, uint b) internal pure returns (uint c) { - c = a * b; - require(a == 0 || c / a == b); - } - function div(uint a, uint b) internal pure returns (uint c) { - require(b > 0); - c = a / b; - } -} - -// Part: ExternalCallTester - -contract ExternalCallTester { - - function getCalled(uint a) external returns (bool) { - if (a > 2) { - return true; - } - revert(); // dev: should jump to a revert - } - - function makeExternalCall(BrownieTester other, uint a) external returns (bool) { - other.revertStrings(a); - return true; - } - -} - -// File: BrownieTester.sol - -pragma solidity ^0.5.0; -pragma experimental ABIEncoderV2; - -import "./SafeMath.sol"; - -/** @notice This is the main contract used to test Brownie functionality */ -contract BrownieTester { - - using SafeMath for uint; - - address payable public owner; - uint256 num; - - struct Nested { - string a; - bytes32 b; - } - - struct Base { - string str; - address addr; - Nested nested; - } - - mapping (address => Base) testMap; - - event TupleEvent(address addr, Base base); - event Debug(uint a); - event IndexedEvent(string indexed str, uint256 indexed num); - - constructor (bool success) public { - require(success); - owner = msg.sender; - } - - function () external payable { - require(msg.value >= 1 ether); - emit Debug(31337); - } - - function sendEth() external returns (bool) { - owner.transfer(address(this).balance); - return true; - } - - function receiveEth() external payable returns (bool) { - return true; - } - - function doNothing() external returns (bool) { - return true; - } - - function emitEvents(string calldata str, uint256 num) external returns (bool) { - emit Debug(num); - emit IndexedEvent(str, num); - emit Debug(num + 2); - return true; - } - - function revertStrings(uint a) external returns (bool) { - emit Debug(a); - require (a != 0, "zero"); - require (a != 1); // dev: one - require (a != 2, "two"); // dev: error - require (a != 3); // error - if (a != 31337) { - return true; - } - revert(); // dev: great job - } - - function setTuple(Base memory _base) public { - testMap[_base.addr] = _base; - emit TupleEvent(_base.addr, _base); - } - - function getTuple(address _addr) public view returns (Base memory) { - return testMap[_addr]; - } - - function setNum(uint _num) external returns (bool) { - num = _num; - return true; - } - - function manyValues( - uint a, - bool[] calldata b, - address c, - bytes32[2][] calldata d - ) - external - view - returns (uint _num, bool[] memory _bool, address _addr, bytes32[2][] memory _bytes) - { - return (a, b, c, d); - } - - function useSafeMath(uint a, uint b) external returns (uint) { - uint c = a.mul(b); - return c; - } - - function makeExternalCall(ExternalCallTester other, uint a) external returns (bool) { - bool ok = other.getCalled(a); - return ok; - } - - function makeInternalCalls(bool callPublic, bool callPrivate) external returns (bool) { - if (callPublic) { - getCalled(0); - } - if (callPrivate) { - _getCalled(0); - } - return true; - } - - function getCalled(uint a) public returns (bool) { - return _getCalled(a); - } - - function _getCalled(uint a) internal returns (bool) { - return false; - } - -} -""" diff --git a/tests/network/contract/test_verification.py b/tests/network/contract/test_verification.py index bdaff3eb9..01bb817ad 100644 --- a/tests/network/contract/test_verification.py +++ b/tests/network/contract/test_verification.py @@ -1,11 +1,99 @@ -def test_verification_info(BrownieTester, brownie_tester_flat): - v = BrownieTester.get_verification_info() - assert v["contract_name"] == "BrownieTester" - assert v["compiler_version"].startswith("0.5.") - assert v["optimizer_enabled"] is True - assert v["optimizer_runs"] == 200 - assert v["license_identifier"] == "NONE" - assert v["bytecode_len"] == 9842 - - # skip version pragma, because it is inconsistent - assert v["flattened_source"][58:] == brownie_tester_flat[58:] +from pathlib import Path + +import pytest +import solcx + +from brownie.project import load, new +from brownie.project.compiler.solidity import find_best_solc_version + +sources = [ + ( + "contracts/Foo.sol", + """ +contract Foo { + uint256 value_; + function value() external view returns(uint256) { + return value_; + } +} + """, + ), + ( + "contracts/Baz.sol", + """ +enum Test { + A, + B, + C, + D +} + +contract Baz {} + """, + ), + ( + "contracts/Bar.sol", + """ +import {Foo as FooSomething} from "./Foo.sol"; +import "./Baz.sol"; + +struct Helper { + address x; + uint256 y; + uint8 z; +} + +contract Bar is FooSomething {} + """, + ), +] + + +@pytest.mark.parametrize("version", ("0.6.0", "0.7.3", "0.8.6")) +def test_verification_info(tmp_path_factory, version): + header = f""" +// SPDX-License-Identifier: MIT +pragma solidity {version}; + + + """ + + # setup directory + dir: Path = tmp_path_factory.mktemp("verify-project") + # initialize brownie project + new(dir.as_posix()) + + modded_sources = {} + for fp, src in sources: + with dir.joinpath(fp).open("w") as f: + f.write(header + src) + modded_sources[fp] = header + src + + find_best_solc_version(modded_sources, install_needed=True) + + project = load(dir, "TestImportProject") + + for contract_name in ("Foo", "Bar", "Baz"): + contract = getattr(project, contract_name) + input_data = contract.get_verification_info()["standard_json_input"] + + # output selection isn't included in the verification info because + # etherscan replaces it regardless. Here we just replicate with what they + # would include + input_data["settings"]["outputSelection"] = { + "*": {"*": ["evm.bytecode", "evm.deployedBytecode", "abi"]} + } + + compiler_version, _ = contract._build["compiler"]["version"].split("+") + output_data = solcx.compile_standard(input_data, solc_version=compiler_version) + # keccak256 = 0xd61b13a841b15bc814760b36086983db80788946ca38aa90a06bebf287a67205 + build_info = output_data["contracts"][f"{contract_name}.sol"][contract_name] + + assert build_info["abi"] == contract.abi + # ignore the metadata at the end of the bytecode, etherscan does the same + assert build_info["evm"]["bytecode"]["object"][:-96] == contract.bytecode[:-96] + assert ( + build_info["evm"]["deployedBytecode"]["object"][:-96] + == contract._build["deployedBytecode"][:-96] + ) + project.close() diff --git a/tests/project/compiler/test_solidity.py b/tests/project/compiler/test_solidity.py index e172256e3..154add3bd 100644 --- a/tests/project/compiler/test_solidity.py +++ b/tests/project/compiler/test_solidity.py @@ -53,6 +53,21 @@ def msolc(monkeypatch): ] monkeypatch.setattr("solcx.get_installed_solc_versions", lambda: installed) monkeypatch.setattr("solcx.install_solc", lambda k, **z: installed.append(k)) + monkeypatch.setattr( + "solcx.get_installable_solc_versions", + lambda: [ + Version("0.6.7"), + Version("0.6.2"), + Version("0.6.0"), + Version("0.5.15"), + Version("0.5.8"), + Version("0.5.7"), + Version("0.5.0"), + Version("0.4.25"), + Version("0.4.24"), + Version("0.4.22"), + ], + ) yield installed @@ -157,12 +172,12 @@ def test_find_solc_versions_install(find_version, msolc): assert Version("0.4.25") not in msolc assert Version("0.5.10") not in msolc find_version("^0.4.24", install_needed=True) - assert msolc.pop() == Version("0.4.25") + assert msolc.pop() == Version("0.4.26") find_version("^0.4.22", install_latest=True) - assert msolc.pop() == Version("0.4.25") - find_version("^0.4.24 || >=0.5.10", install_needed=True) + assert msolc.pop() == Version("0.4.26") + find_version("^0.4.24 || >=0.5.10 <=0.6.7", install_needed=True) assert msolc.pop() == Version("0.6.7") - find_version(">=0.4.24", install_latest=True) + find_version(">=0.4.24 <=0.6.7", install_latest=True) assert msolc.pop() == Version("0.6.7")