From 7f1c700993f94cb7aacdfd45c28e6a5891b80b4f Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Thu, 3 Oct 2024 01:35:31 +0200 Subject: [PATCH] feat: add `deploy_as_blueprint` to `VVMDeployer` (#311) --------- Co-authored-by: Charles Cooper --- boa/contracts/vvm/vvm_contract.py | 39 +++++++++++++++++++++++++++ boa/contracts/vyper/vyper_contract.py | 16 ++++------- boa/util/eip5202.py | 17 ++++++++++++ tests/unitary/test_blueprints.py | 30 ++++++++++++++++++--- 4 files changed, 87 insertions(+), 15 deletions(-) diff --git a/boa/contracts/vvm/vvm_contract.py b/boa/contracts/vvm/vvm_contract.py index 2c2c783a..5c2afd45 100644 --- a/boa/contracts/vvm/vvm_contract.py +++ b/boa/contracts/vvm/vvm_contract.py @@ -3,6 +3,7 @@ from boa.contracts.abi.abi_contract import ABIContractFactory, ABIFunction from boa.environment import Env +from boa.util.eip5202 import generate_blueprint_bytecode # TODO: maybe this doesn't detect release candidates VERSION_RE = re.compile(r"\s*#\s*(pragma\s+version|@version)\s+(\d+\.\d+\.\d+)") @@ -18,7 +19,19 @@ def _detect_version(source_code: str): class VVMDeployer: + """ + A deployer that uses the Vyper Version Manager (VVM). + This allows deployment of contracts written in older versions of Vyper that + can interact with new versions using the ABI definition. + """ + def __init__(self, abi, bytecode, filename): + """ + Initialize a VVMDeployer instance. + :param abi: The contract's ABI. + :param bytecode: The contract's bytecode. + :param filename: The filename of the contract. + """ self.abi = abi self.bytecode = bytecode self.filename = filename @@ -55,6 +68,32 @@ def deploy(self, *args, env=None): return self.at(address) + @cached_property + def _blueprint_bytecode(self): + return generate_blueprint_bytecode(self.bytecode) + + @cached_property + def _blueprint_deployer(self): + # TODO: add filename + return ABIContractFactory.from_abi_dict([]) + + def deploy_as_blueprint(self, env=None, blueprint_preamble=None, **kwargs): + """ + Deploy a new blueprint from this contract. + :param blueprint_preamble: The preamble to use for the blueprint. + :param env: The environment to deploy the blueprint in. + :param kwargs: Keyword arguments to pass to the environment `deploy_code` method. + :returns: A contract instance. + """ + if env is None: + env = Env.get_singleton() + + address, _ = env.deploy_code(bytecode=self._blueprint_bytecode) + + ret = self._blueprint_deployer.at(address) + env.register_blueprint(self.bytecode, ret) + return ret + def __call__(self, *args, **kwargs): return self.deploy(*args, **kwargs) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index e2e7acec..cdf409c8 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -56,6 +56,7 @@ from boa.environment import Env from boa.profiling import cache_gas_used_for_computation from boa.util.abi import Address, abi_decode, abi_encode +from boa.util.eip5202 import generate_blueprint_bytecode from boa.util.lrudict import lrudict from boa.vm.gas_meters import ProfilingGasMeter from boa.vm.utils import to_bytes, to_int @@ -183,7 +184,7 @@ def __init__( compiler_data, env=None, override_address=None, - blueprint_preamble=b"\xFE\x71\x00", + blueprint_preamble=None, filename=None, gas=None, ): @@ -191,16 +192,9 @@ def __init__( # maybe use common base class? super().__init__(compiler_data, env, filename) - if blueprint_preamble is None: - blueprint_preamble = b"" - - blueprint_bytecode = blueprint_preamble + compiler_data.bytecode - - # the length of the deployed code in bytes - len_bytes = len(blueprint_bytecode).to_bytes(2, "big") - deploy_bytecode = b"\x61" + len_bytes + b"\x3d\x81\x60\x0a\x3d\x39\xf3" - - deploy_bytecode += blueprint_bytecode + deploy_bytecode = generate_blueprint_bytecode( + compiler_data.bytecode, blueprint_preamble + ) addr, computation = self.env.deploy( bytecode=deploy_bytecode, override_address=override_address, gas=gas diff --git a/boa/util/eip5202.py b/boa/util/eip5202.py index 2e6c6d31..10b7c321 100644 --- a/boa/util/eip5202.py +++ b/boa/util/eip5202.py @@ -3,6 +3,23 @@ from eth_utils import to_canonical_address, to_checksum_address from vyper.utils import keccak256 +DEFAULT_BLUEPRINT_PREAMBLE = b"\xFE\x71\x00" + + +def generate_blueprint_bytecode( + contract_bytecode: bytes, blueprint_preamble: bytes = None +): + if blueprint_preamble is None: + blueprint_preamble = DEFAULT_BLUEPRINT_PREAMBLE + + blueprint_bytecode = blueprint_preamble + contract_bytecode + + # the length of the deployed code in bytes + len_bytes = len(blueprint_bytecode).to_bytes(2, "big") + deploy_bytecode = b"\x61" + len_bytes + b"\x3d\x81\x60\x0a\x3d\x39\xf3" + + return deploy_bytecode + blueprint_bytecode + # TODO replace return type with upcoming AddressType wrapper def get_create2_address( diff --git a/tests/unitary/test_blueprints.py b/tests/unitary/test_blueprints.py index b3ef067c..3a5b3451 100644 --- a/tests/unitary/test_blueprints.py +++ b/tests/unitary/test_blueprints.py @@ -1,23 +1,45 @@ import pytest +import vyper from eth_utils import to_canonical_address import boa from boa.util.eip5202 import get_create2_address -blueprint_code = """ +_blueprint_code = """ +# pragma version {} + @external def some_function() -> uint256: return 5 """ -factory_code = """ +_factory_code = """ +# pragma version {} + @external def create_child(blueprint: address, salt: bytes32) -> address: return create_from_blueprint(blueprint, code_offset=3, salt=salt) """ +VERSIONS = [vyper.__version__, "0.3.10"] + + +@pytest.fixture(params=VERSIONS) +def version(request): + return request.param + + +@pytest.fixture +def blueprint_code(version): + return _blueprint_code.format(version) + + +@pytest.fixture +def factory_code(version): + return _factory_code.format(version) + -def test_create2_address(): +def test_create2_address(blueprint_code, factory_code): blueprint = boa.loads_partial(blueprint_code).deploy_as_blueprint() factory = boa.loads(factory_code) @@ -31,7 +53,7 @@ def test_create2_address(): ) -def test_create2_address_bad_salt(): +def test_create2_address_bad_salt(blueprint_code): blueprint = boa.loads_partial(blueprint_code).deploy_as_blueprint() blueprint_bytecode = boa.env.get_code(to_canonical_address(blueprint.address)) with pytest.raises(ValueError) as e: