Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: viaIR would not verify on etherscan #129

Merged
merged 4 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ape_etherscan/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ape_etherscan.config import EtherscanConfig
from ape_etherscan.exceptions import (
ContractNotVerifiedError,
IncompatibleCompilerSettingsError,
UnhandledResultError,
UnsupportedEcosystemError,
UnsupportedNetworkError,
Expand Down Expand Up @@ -354,6 +355,7 @@ def verify_source_code(
evm_version: Optional[str] = None,
license_type: Optional[int] = None,
libraries: Optional[dict[str, str]] = None,
via_ir: bool = False,
) -> str:
libraries = libraries or {}
if len(libraries) > 10:
Expand Down Expand Up @@ -389,6 +391,9 @@ def verify_source_code(
json_dict[f"libraryaddress{iterator}"] = lib_address
iterator += 1

if code_format == "solidity-single-file" and via_ir:
raise IncompatibleCompilerSettingsError("Solidity", "via_ir", via_ir)

headers = {"Content-Type": "application/x-www-form-urlencoded"}
return str(self._post(json_dict=json_dict, headers=headers).value)

Expand Down
12 changes: 11 additions & 1 deletion ape_etherscan/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Any, Union

from ape.exceptions import ApeException
from requests import Response
Expand Down Expand Up @@ -87,6 +87,16 @@ class ContractVerificationError(ApeEtherscanException):
"""


class IncompatibleCompilerSettingsError(ApeEtherscanException):
"""
An error that occurs when unable to verify or publish a contract because viaIR (or some other)
is enabled and the compiler settings are not compatible with the API.
"""

def __init__(self, compiler: str, setting: str, value: Any):
super().__init__(f"Incompatible {compiler} setting: '{setting}={value}'.")


def get_request_error(response: Response, ecosystem: str) -> EtherscanResponseError:
response_data = response.json()
if "result" in response_data and response_data["result"]:
Expand Down
56 changes: 41 additions & 15 deletions ape_etherscan/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
from ethpm_types import Compiler, ContractType

from ape_etherscan.client import AccountClient, ClientFactory, ContractClient
from ape_etherscan.exceptions import ContractVerificationError, EtherscanResponseError
from ape_etherscan.exceptions import (
ContractVerificationError,
EtherscanResponseError,
IncompatibleCompilerSettingsError,
)

DEFAULT_OPTIMIZATION_RUNS = 200
_SPDX_ID_TO_API_CODE = {
Expand All @@ -38,7 +42,6 @@
"busl-1.1": 14,
}
_SPDX_ID_KEY = "SPDX-License-Identifier: "

ECOSYSTEMS_VERIFY_USING_JSON = ("arbitrum", "base", "blast", "ethereum")


Expand Down Expand Up @@ -169,6 +172,11 @@ def from_spdx_id(cls, spdx_id: str) -> "LicenseType":
return cls.NO_LICENSE


class VerificationApproach(Enum):
STANDARD_JSON = "STANDARD_JSON"
FLATTEN = "FLATTEN"


class SourceVerifier(ManagerAccessMixin):
def __init__(
self,
Expand Down Expand Up @@ -276,34 +284,40 @@ def compiler(self) -> Compiler:
# Build a default one and hope for the best.
return Compiler(name=self.compiler_name, contractType=[self.contract_name], version="")

def attempt_verification(self):
def attempt_verification(
self, compiler: Optional[Compiler] = None, approach: Optional[VerificationApproach] = None
):
"""
Attempt to verify the source code.
If the bytecode is already verified, Etherscan will use the existing bytecode
and this method will still succeed.

Args:
compiler (ethpm_types.Compiler): Optionally provide the compiler. Defaults to
looking it up from Ape.
approach (VerificationApproach): The approach to use when verifying. Defaults
to figuring it out from settings.

Raises:
:class:`~ape_etherscan.exceptions.ContractVerificationError`: - When fails
to validate the contract.
"""

version = str(self.compiler.version)

compiler = self.compiler
compiler = compiler or self.compiler
valid = True
settings = {}
if compiler:
settings = self.compiler.settings or {}
settings = compiler.settings or {}
output_contracts = settings.get("outputSelection", {})
for contract_id in self.compiler.contractTypes or []:
for contract_id in compiler.contractTypes or []:
parts = contract_id.split(":")
cname = None
if len(parts) == 2:
_, cname = parts

else:
elif len(parts) == 1:
cname = parts[0]

if cname not in output_contracts:
if not cname or cname not in output_contracts:
valid = False
break

Expand All @@ -313,12 +327,18 @@ def attempt_verification(self):
optimizer = settings.get("optimizer", {})
optimized = optimizer.get("enabled", False)
runs = optimizer.get("runs", DEFAULT_OPTIMIZATION_RUNS)
source_id = self.contract_type.source_id
standard_input_json = self._get_standard_input_json(source_id, **settings)
via_ir = settings.get("viaIR", settings.get("via_ir", False))
source_id = self.contract_type.source_id or ""
standard_input_json = self._get_standard_input_json(
source_id, approach=approach, **settings
)
evm_version = settings.get("evmVersion")
license_code = self.license_code
license_code_value = license_code.value if license_code else None

if "sourceCode" in standard_input_json and via_ir:
raise IncompatibleCompilerSettingsError(compiler.name, "viaIR", via_ir)

if logger.level == LogLevel.DEBUG:
logger.debug("Dumping standard JSON output:\n")
standard_json = json.dumps(standard_input_json, indent=2)
Expand All @@ -342,6 +362,7 @@ def attempt_verification(self):
constructor_arguments=self.constructor_arguments,
evm_version=evm_version,
license_type=license_code_value,
via_ir=via_ir,
)
except EtherscanResponseError as err:
if "source code already verified" in str(err):
Expand All @@ -367,7 +388,9 @@ def _get_new_settings(self, version: str) -> dict:
# Hack to allow any Version object work.
return {str(v): s for v, s in all_settings.items() if str(v) == version}[version]

def _get_standard_input_json(self, source_id: str, **settings) -> dict:
def _get_standard_input_json(
self, source_id: str, approach: Optional[VerificationApproach] = None, **settings
) -> dict:
source_path = self.local_project.sources.lookup(source_id)
compiler = self.compiler_manager.registered_compilers[source_path.suffix]
sources = {source_id: {"content": source_path.read_text()}}
Expand All @@ -392,7 +415,10 @@ def flatten_source(_source_id: str) -> str:
# libraries are handled below.
settings.pop("libraries")

if self.provider.network.ecosystem.name in ECOSYSTEMS_VERIFY_USING_JSON:
if approach is VerificationApproach.STANDARD_JSON or (
approach is None
and self.provider.network.ecosystem.name in ECOSYSTEMS_VERIFY_USING_JSON
):
# Use standard input json format
data = {
"language": compiler.name.capitalize(),
Expand Down
43 changes: 28 additions & 15 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,22 +485,35 @@ def _get_mock_response(


@pytest.fixture
def verification_params(address_to_verify, standard_input_json):
ctor_args = "" # noqa: E501
def get_base_verification_params():
def fn(
ctor_args,
address,
std_json,
**kwargs,
):
return {
"action": "verifysourcecode",
"codeformat": "solidity-standard-json-input",
"constructorArguements": ctor_args,
"contractaddress": address,
"contractname": "tests/contracts/subcontracts/foo.sol:foo",
"evmversion": None,
"licenseType": LicenseType.AGLP_3.value,
"module": "contract",
"optimizationUsed": 1,
"runs": 200,
"sourceCode": StringIO(json.dumps(std_json)),
**kwargs,
}

return {
"action": "verifysourcecode",
"codeformat": "solidity-standard-json-input",
"constructorArguements": ctor_args,
"contractaddress": address_to_verify,
"contractname": "tests/contracts/subcontracts/foo.sol:foo",
"evmversion": None,
"licenseType": LicenseType.AGLP_3.value,
"module": "contract",
"optimizationUsed": 1,
"runs": 200,
"sourceCode": StringIO(json.dumps(standard_input_json)),
}
return fn


@pytest.fixture
def verification_params(address_to_verify, standard_input_json, get_base_verification_params):
ctor_args = "" # noqa: E501
return get_base_verification_params(ctor_args, address_to_verify, standard_input_json)


@pytest.fixture(scope="session")
Expand Down
28 changes: 25 additions & 3 deletions tests/test_etherscan.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import pytest
from ape.api.query import AccountTransactionQuery

from ape_etherscan.exceptions import EtherscanResponseError, EtherscanTooManyRequestsError
from ape_etherscan.exceptions import (
EtherscanResponseError,
EtherscanTooManyRequestsError,
IncompatibleCompilerSettingsError,
)
from ape_etherscan.verify import SourceVerifier, VerificationApproach

from ._utils import ecosystems_and_networks

Expand Down Expand Up @@ -87,12 +92,13 @@ def sim(self):
def setup_verification_test(
mock_backend, verification_params, verification_tester_cls, contract_to_verify
):
def setup(found_handler: Callable, threshold: int = 2):
def setup(found_handler: Callable, threshold: int = 2, params=None):
params = params or verification_params
overrides = _acct_tx_overrides(contract_to_verify)
mock_backend.setup_mock_account_transactions_response(
address=contract_to_verify.address, **overrides
)
mock_backend.add_handler("POST", "contract", verification_params, return_value=PUBLISH_GUID)
mock_backend.add_handler("POST", "contract", params, return_value=PUBLISH_GUID)
verification_tester = verification_tester_cls(found_handler, threshold=threshold)
mock_backend.add_handler(
"GET",
Expand Down Expand Up @@ -262,6 +268,22 @@ def test_publish_contract_with_ctor_args(
assert caplog.records[-1].message == expected_verification_log_with_ctor_args


def test_publish_contract_flatten_via_ir(mocker, address_to_verify):
client = mocker.MagicMock()
project = mocker.MagicMock()

compiler = mocker.MagicMock()
compiler.settings = {"viaIR": True, "outputSelection": {"Contract": []}}
compiler.contractTypes = ["Contract"]
compiler.name = "Solidity"
source_verifier = SourceVerifier(address_to_verify, client, project=project)
expected = "Incompatible Solidity setting: 'viaIR=True'."
with pytest.raises(IncompatibleCompilerSettingsError, match=expected):
source_verifier.attempt_verification(
compiler=compiler, approach=VerificationApproach.FLATTEN
)


def _acct_tx_overrides(contract, args=None):
suffix = args or ""
if suffix.startswith("0x"):
Expand Down
Loading