From 8b5365290a6219f9e7297f3f35da6fe32945bbcf Mon Sep 17 00:00:00 2001 From: antazoey Date: Fri, 2 Feb 2024 16:21:36 -0600 Subject: [PATCH] fix: raise error when attempting to deploy a contract without init-code (#1909) --- src/ape/api/accounts.py | 5 +++ src/ape/contracts/base.py | 5 +++ src/ape/exceptions.py | 17 +++++++++ tests/functional/test_accounts.py | 22 +++++++++++- tests/functional/test_config.py | 6 ++-- tests/functional/test_contract_container.py | 33 ++++++++++++++--- tests/functional/test_contracts_cache.py | 39 +++++++++++---------- 7 files changed, 99 insertions(+), 28 deletions(-) diff --git a/src/ape/api/accounts.py b/src/ape/api/accounts.py index 6a55b22ae8..69a3442bf3 100644 --- a/src/ape/api/accounts.py +++ b/src/ape/api/accounts.py @@ -14,6 +14,7 @@ AccountsError, AliasAlreadyInUseError, MethodNonPayableError, + MissingDeploymentBytecodeError, SignatureError, TransactionError, ) @@ -239,6 +240,10 @@ def deploy( "a contract in your project." ) + bytecode = contract.contract_type.deployment_bytecode + if not bytecode or bytecode.bytecode in (None, "", "0x"): + raise MissingDeploymentBytecodeError(contract.contract_type) + txn = contract(*args, **kwargs) if kwargs.get("value") and not contract.contract_type.constructor.is_payable: raise MethodNonPayableError("Sending funds to a non-payable constructor.") diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py index 2c30a3ed6e..f4d3baca92 100644 --- a/src/ape/contracts/base.py +++ b/src/ape/contracts/base.py @@ -23,6 +23,7 @@ ContractNotFoundError, CustomError, MethodNonPayableError, + MissingDeploymentBytecodeError, TransactionNotFoundError, ) from ape.logging import logger @@ -1344,6 +1345,10 @@ def deploy(self, *args, publish: bool = False, **kwargs) -> ContractInstance: :class:`~ape.contracts.base.ContractInstance` """ + bytecode = self.contract_type.deployment_bytecode + if not bytecode or bytecode.bytecode in (None, "", "0x"): + raise MissingDeploymentBytecodeError(self.contract_type) + txn = self(*args, **kwargs) private = kwargs.get("private", False) diff --git a/src/ape/exceptions.py b/src/ape/exceptions.py index 711d8456b8..deac94890b 100644 --- a/src/ape/exceptions.py +++ b/src/ape/exceptions.py @@ -11,6 +11,7 @@ import click from eth_typing import Hash32 from eth_utils import humanize_hash +from ethpm_types import ContractType from ethpm_types.abi import ConstructorABI, ErrorABI, MethodABI from rich import print as rich_print @@ -77,6 +78,22 @@ class ContractDataError(ApeException): """ +class MissingDeploymentBytecodeError(ContractDataError): + """ + Raised when trying to deploy an interface or empty data. + """ + + def __init__(self, contract_type: ContractType): + message = "Cannot deploy: contract" + if name := contract_type.name: + message = f"{message} '{name}'" + + message = ( + f"{message} has no deployment-bytecode. Are you attempting to deploy an interface?" + ) + super().__init__(message) + + class ArgumentsLengthError(ContractDataError): """ Raised when calling a contract method with the wrong number of arguments. diff --git a/tests/functional/test_accounts.py b/tests/functional/test_accounts.py index 9ca0f71a84..f3f387c7bb 100644 --- a/tests/functional/test_accounts.py +++ b/tests/functional/test_accounts.py @@ -4,12 +4,15 @@ from eip712.messages import EIP712Message from eth_account.messages import encode_defunct from eth_pydantic_types import HexBytes +from ethpm_types import ContractType import ape from ape.api import ImpersonatedAccount +from ape.contracts import ContractContainer from ape.exceptions import ( AccountsError, AliasAlreadyInUseError, + MissingDeploymentBytecodeError, NetworkError, ProjectError, SignatureError, @@ -281,7 +284,7 @@ def test_deploy_proxy(owner, vyper_contract_instance, proxy_contract_container, assert implementation.contract_type == vyper_contract_instance.contract_type -def test_deploy_instance(owner, vyper_contract_instance, chain, clean_contracts_cache): +def test_deploy_instance(owner, vyper_contract_instance): """ Tests against a confusing scenario where you would get a SignatureError when trying to deploy a ContractInstance because Ape would attempt to create a tx @@ -297,6 +300,23 @@ def test_deploy_instance(owner, vyper_contract_instance, chain, clean_contracts_ owner.deploy(vyper_contract_instance) +@pytest.mark.parametrize("bytecode", (None, {}, {"bytecode": "0x"})) +def test_deploy_no_deployment_bytecode(owner, bytecode): + """ + https://github.com/ApeWorX/ape/issues/1904 + """ + expected = ( + r"Cannot deploy: contract 'Apes' has no deployment-bytecode\. " + r"Are you attempting to deploy an interface\?" + ) + contract_type = ContractType.model_validate( + {"abi": [], "contractName": "Apes", "deploymentBytecode": bytecode} + ) + contract = ContractContainer(contract_type) + with pytest.raises(MissingDeploymentBytecodeError, match=expected): + owner.deploy(contract) + + def test_send_transaction_with_bad_nonce(sender, receiver): # Bump the nonce so we can set one that is too low. sender.transfer(receiver, "1 gwei", type=0) diff --git a/tests/functional/test_config.py b/tests/functional/test_config.py index c7aca502f8..37eb61b77b 100644 --- a/tests/functional/test_config.py +++ b/tests/functional/test_config.py @@ -19,9 +19,9 @@ from tests.functional.conftest import PROJECT_WITH_LONG_CONTRACTS_FOLDER -def test_deployments(networks, owner, project_with_contract): +def test_deployments(networks, owner, vyper_contract_container): # First, obtain a "previously-deployed" contract. - instance = project_with_contract.ApeContract0.deploy(sender=owner) + instance = vyper_contract_container.deploy(1000200000, sender=owner) # Create a config using this new contract for a "later time". data = { @@ -33,7 +33,7 @@ def test_deployments(networks, owner, project_with_contract): assert config.root["ethereum"]["local"][0]["address"] == instance.address # Ensure we can reference the deployment on the project. - deployment = project_with_contract.ApeContract0.deployments[0] + deployment = vyper_contract_container.deployments[0] assert deployment.address == instance.address diff --git a/tests/functional/test_contract_container.py b/tests/functional/test_contract_container.py index 47b2eaa376..9ba478b640 100644 --- a/tests/functional/test_contract_container.py +++ b/tests/functional/test_contract_container.py @@ -1,8 +1,14 @@ import pytest +from ethpm_types import ContractType from ape import Contract -from ape.contracts import ContractInstance -from ape.exceptions import ArgumentsLengthError, NetworkError, ProjectError +from ape.contracts import ContractContainer, ContractInstance +from ape.exceptions import ( + ArgumentsLengthError, + MissingDeploymentBytecodeError, + NetworkError, + ProjectError, +) from ape_ethereum.ecosystem import ProxyType @@ -73,9 +79,26 @@ def test_deploy_privately(owner, contract_container): assert isinstance(deploy_1, ContractInstance) -def test_deployment_property(chain, owner, project_with_contract, eth_tester_provider): - initial_deployed_contract = project_with_contract.ApeContract0.deploy(sender=owner) - actual = project_with_contract.ApeContract0.deployments[-1].address +@pytest.mark.parametrize("bytecode", (None, {}, {"bytecode": "0x"})) +def test_deploy_no_deployment_bytecode(owner, bytecode): + """ + https://github.com/ApeWorX/ape/issues/1904 + """ + expected = ( + r"Cannot deploy: contract 'Apes' has no deployment-bytecode\. " + r"Are you attempting to deploy an interface\?" + ) + contract_type = ContractType.model_validate( + {"abi": [], "contractName": "Apes", "deploymentBytecode": bytecode} + ) + contract = ContractContainer(contract_type) + with pytest.raises(MissingDeploymentBytecodeError, match=expected): + contract.deploy(sender=owner) + + +def test_deployments(owner, eth_tester_provider, vyper_contract_container): + initial_deployed_contract = vyper_contract_container.deploy(10000000, sender=owner) + actual = vyper_contract_container.deployments[-1].address expected = initial_deployed_contract.address assert actual == expected diff --git a/tests/functional/test_contracts_cache.py b/tests/functional/test_contracts_cache.py index 97cda768dd..9f4c586526 100644 --- a/tests/functional/test_contracts_cache.py +++ b/tests/functional/test_contracts_cache.py @@ -9,13 +9,13 @@ @pytest.fixture -def contract_0(project_with_contract): - return project_with_contract.ApeContract0 +def contract_0(vyper_contract_container): + return vyper_contract_container @pytest.fixture -def contract_1(project_with_contract): - return project_with_contract.ApeContract1 +def contract_1(solidity_contract_container): + return solidity_contract_container def test_instance_at(chain, contract_instance): @@ -173,8 +173,8 @@ def test_get_deployments_local(chain, owner, contract_0, contract_1): chain.contracts._local_contract_types = {} starting_contracts_list_0 = chain.contracts.get_deployments(contract_0) starting_contracts_list_1 = chain.contracts.get_deployments(contract_1) - deployed_contract_0 = owner.deploy(contract_0) - deployed_contract_1 = owner.deploy(contract_1) + deployed_contract_0 = owner.deploy(contract_0, 900000000) + deployed_contract_1 = owner.deploy(contract_1, 900000001) # Act contracts_list_0 = chain.contracts.get_deployments(contract_0) @@ -195,8 +195,8 @@ def test_get_deployments_local(chain, owner, contract_0, contract_1): def test_get_deployments_live( chain, owner, contract_0, contract_1, remove_disk_writes_deployments, dummy_live_network ): - deployed_contract_0 = owner.deploy(contract_0, required_confirmations=0) - deployed_contract_1 = owner.deploy(contract_1, required_confirmations=0) + deployed_contract_0 = owner.deploy(contract_0, 8000000, required_confirmations=0) + deployed_contract_1 = owner.deploy(contract_1, 8000001, required_confirmations=0) # Act my_contracts_list_0 = chain.contracts.get_deployments(contract_0) @@ -214,12 +214,12 @@ def test_get_multiple_deployments_live( ): starting_contracts_list_0 = chain.contracts.get_deployments(contract_0) starting_contracts_list_1 = chain.contracts.get_deployments(contract_1) - initial_deployed_contract_0 = owner.deploy(contract_0, required_confirmations=0) - initial_deployed_contract_1 = owner.deploy(contract_1, required_confirmations=0) - owner.deploy(contract_0, required_confirmations=0) - owner.deploy(contract_1, required_confirmations=0) - final_deployed_contract_0 = owner.deploy(contract_0, required_confirmations=0) - final_deployed_contract_1 = owner.deploy(contract_1, required_confirmations=0) + initial_deployed_contract_0 = owner.deploy(contract_0, 700000, required_confirmations=0) + initial_deployed_contract_1 = owner.deploy(contract_1, 700001, required_confirmations=0) + owner.deploy(contract_0, 700002, required_confirmations=0) + owner.deploy(contract_1, 700003, required_confirmations=0) + final_deployed_contract_0 = owner.deploy(contract_0, 600000, required_confirmations=0) + final_deployed_contract_1 = owner.deploy(contract_1, 600001, required_confirmations=0) contracts_list_0 = chain.contracts.get_deployments(contract_0) contracts_list_1 = chain.contracts.get_deployments(contract_1) contract_type_map = { @@ -239,11 +239,11 @@ def test_get_multiple_deployments_live( def test_cache_updates_per_deploy(owner, chain, contract_0, contract_1): # Arrange / Act initial_contracts = chain.contracts.get_deployments(contract_0) - expected_first_contract = owner.deploy(contract_0) + expected_first_contract = owner.deploy(contract_0, 6787678) - owner.deploy(contract_0) - owner.deploy(contract_0) - expected_last_contract = owner.deploy(contract_0) + owner.deploy(contract_0, 6787679) + owner.deploy(contract_0, 6787680) + expected_last_contract = owner.deploy(contract_0, 6787681) actual_contracts = chain.contracts.get_deployments(contract_0) first_index = len(initial_contracts) # next index before deploys from this test @@ -294,7 +294,8 @@ def test_get_non_contract_address(chain, owner): def test_get_attempts_to_convert(chain): with pytest.raises(ConversionError): - chain.contracts.get("test.eth") + # NOTE: using eth2 suffix so still works if ape-ens is installed. + chain.contracts.get("test.eth2") def test_cache_non_checksum_address(chain, vyper_contract_instance):