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

feat: add deploy_as_blueprint to VVMDeployer #311

Merged
merged 16 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
36 changes: 34 additions & 2 deletions boa/contracts/vvm/vvm_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
from functools import cached_property

from boa.contracts.abi.abi_contract import ABIContractFactory, ABIFunction
from boa.contracts.vyper.compiler_utils import (
DEFAULT_BLUEPRINT_PREAMBLE,
generate_blueprint_bytecode,
)
from boa.environment import Env

# TODO: maybe this doesn't detect release candidates
Expand All @@ -18,7 +22,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
Expand All @@ -41,7 +57,7 @@ def constructor(self):
return ABIFunction(t, contract_name=self.filename)
return None

def deploy(self, *args, env=None):
def deploy(self, *args, env=None, **kwargs):
encoded_args = b""
if self.constructor is not None:
encoded_args = self.constructor.prepare_calldata(*args)
Expand All @@ -51,10 +67,26 @@ def deploy(self, *args, env=None):
if env is None:
env = Env.get_singleton()

address, _ = env.deploy_code(bytecode=self.bytecode + encoded_args)
address, _ = env.deploy_code(bytecode=self.bytecode + encoded_args, **kwargs)

return self.at(address)

def deploy_as_blueprint(
self, env=None, blueprint_preamble=DEFAULT_BLUEPRINT_PREAMBLE, **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.
"""
return VVMDeployer(
DanielSchiavini marked this conversation as resolved.
Show resolved Hide resolved
abi=[], # the blueprint may not be called directly, no public ABI
bytecode=generate_blueprint_bytecode(self.bytecode, blueprint_preamble),
filename=self.filename,
).deploy(env=env, **kwargs)

def __call__(self, *args, **kwargs):
return self.deploy(*args, **kwargs)

Expand Down
17 changes: 17 additions & 0 deletions boa/contracts/vyper/compiler_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,20 @@ def __boa_debug__() {return_sig}:
"""
)
return compile_vyper_function(wrapper_code, contract)


DEFAULT_BLUEPRINT_PREAMBLE = b"\xFE\x71\x00"


def generate_blueprint_bytecode(
DanielSchiavini marked this conversation as resolved.
Show resolved Hide resolved
contract_bytecode: bytes, blueprint_preamble: bytes = DEFAULT_BLUEPRINT_PREAMBLE
):
if blueprint_preamble is None:
blueprint_preamble = b""
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
21 changes: 8 additions & 13 deletions boa/contracts/vyper/vyper_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
from boa.contracts.vyper.ast_utils import get_fn_ancestor_from_node, reason_at
from boa.contracts.vyper.compiler_utils import (
_METHOD_ID_VAR,
DEFAULT_BLUEPRINT_PREAMBLE,
compile_vyper_function,
generate_blueprint_bytecode,
generate_bytecode_for_arbitrary_stmt,
generate_bytecode_for_internal_fn,
)
Expand Down Expand Up @@ -183,27 +185,20 @@ def __init__(
compiler_data,
env=None,
override_address=None,
blueprint_preamble=b"\xFE\x71\x00",
blueprint_preamble=DEFAULT_BLUEPRINT_PREAMBLE,
filename=None,
gas=None,
):
# note slight code duplication with VyperContract ctor,
# 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

addr, computation = self.env.deploy(
bytecode=deploy_bytecode, override_address=override_address, gas=gas
bytecode=generate_blueprint_bytecode(
compiler_data.bytecode, blueprint_preamble
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need this much nesting?

override_address=override_address,
gas=gas,
)
if computation.is_error:
raise computation.error
Expand Down
19 changes: 14 additions & 5 deletions tests/unitary/test_blueprints.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import pytest
import vyper
from eth_utils import to_canonical_address

import boa
from boa.util.eip5202 import get_create2_address

blueprint_code = """
# pragma version {}

@external
def some_function() -> uint256:
return 5
"""

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"]


def test_create2_address():
blueprint = boa.loads_partial(blueprint_code).deploy_as_blueprint()
factory = boa.loads(factory_code)
@pytest.mark.parametrize("version", VERSIONS)
def test_create2_address(version):
blueprint = boa.loads_partial(blueprint_code.format(version)).deploy_as_blueprint()
DanielSchiavini marked this conversation as resolved.
Show resolved Hide resolved
factory = boa.loads(factory_code.format(version))

salt = b"\x01" * 32

Expand All @@ -31,8 +39,9 @@ def test_create2_address():
)


def test_create2_address_bad_salt():
blueprint = boa.loads_partial(blueprint_code).deploy_as_blueprint()
@pytest.mark.parametrize("version", VERSIONS)
def test_create2_address_bad_salt(version):
blueprint = boa.loads_partial(blueprint_code.format(version)).deploy_as_blueprint()
blueprint_bytecode = boa.env.get_code(to_canonical_address(blueprint.address))
with pytest.raises(ValueError) as e:
get_create2_address(blueprint_bytecode, blueprint.address, salt=b"")
Expand Down
Loading