Skip to content

Commit

Permalink
feat: change default code_offset in create_from_blueprint (#3454)
Browse files Browse the repository at this point in the history
change default `code_offset` to 3 for `create_from_blueprint`, per
ERC5202 standard

---------

Co-authored-by: Charles Cooper <cooper.charles.m@gmail.com>
  • Loading branch information
bout3fiddy and charles-cooper committed Feb 21, 2024
1 parent 1ca243b commit 015cf81
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 12 deletions.
10 changes: 7 additions & 3 deletions docs/built-in-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,15 @@ Vyper has three built-ins for contract creation; all three contract creation bui
The implementation of ``create_copy_of`` assumes that the code at ``target`` is smaller than 16MB. While this is much larger than the EIP-170 constraint of 24KB, it is a conservative size limit intended to future-proof deployer contracts in case the EIP-170 constraint is lifted. If the code at ``target`` is larger than 16MB, the behavior of ``create_copy_of`` is undefined.


.. py:function:: create_from_blueprint(target: address, *args, value: uint256 = 0, raw_args: bool = False, code_offset: int = 0, [, salt: bytes32]) -> address
.. py:function:: create_from_blueprint(target: address, *args, value: uint256 = 0, raw_args: bool = False, code_offset: int = 3, [, salt: bytes32]) -> address
Copy the code of ``target`` into memory and execute it as initcode. In other words, this operation interprets the code at ``target`` not as regular runtime code, but directly as initcode. The ``*args`` are interpreted as constructor arguments, and are ABI-encoded and included when executing the initcode.

* ``target``: Address of the blueprint to invoke
* ``*args``: Constructor arguments to forward to the initcode.
* ``value``: The wei value to send to the new contract address (Optional, default 0)
* ``raw_args``: If ``True``, ``*args`` must be a single ``Bytes[...]`` argument, which will be interpreted as a raw bytes buffer to forward to the create operation (which is useful for instance, if pre- ABI-encoded data is passed in from elsewhere). (Optional, default ``False``)
* ``code_offset``: The offset to start the ``EXTCODECOPY`` from (Optional, default 0)
* ``code_offset``: The offset to start the ``EXTCODECOPY`` from (Optional, default 3)
* ``salt``: A ``bytes32`` value utilized by the deterministic ``CREATE2`` opcode (Optional, if not supplied, ``CREATE`` is used)

Returns the address of the created contract. If the create operation fails (for instance, in the case of a ``CREATE2`` collision), execution will revert. If ``code_offset >= target.codesize`` (ex. if there is no code at ``target``), execution will revert.
Expand All @@ -209,9 +209,13 @@ Vyper has three built-ins for contract creation; all three contract creation bui

To properly deploy a blueprint contract, special deploy bytecode must be used. The output of ``vyper -f blueprint_bytecode`` will produce bytecode which deploys an ERC-5202 compatible blueprint.

.. note::

Prior to Vyper version ``0.4.0``, the ``code_offset`` parameter defaulted to ``0``.

.. warning::

It is recommended to deploy blueprints with the ERC-5202 preamble ``0xFE7100`` to guard them from being called as regular contracts. This is particularly important for factories where the constructor has side effects (including ``SELFDESTRUCT``!), as those could get executed by *anybody* calling the blueprint contract directly. The ``code_offset=`` kwarg is provided to enable this pattern:
It is recommended to deploy blueprints with an `ERC-5202 <https://eips.ethereum.org/EIPS/eip-5202>`_ preamble like ``0xFE7100`` to guard them from being called as regular contracts. This is particularly important for factories where the constructor has side effects (including ``SELFDESTRUCT``!), as those could get executed by *anybody* calling the blueprint contract directly. The ``code_offset=`` kwarg is provided (and defaults to the ERC-5202 default of 3) to enable this pattern:

.. code-block:: vyper
Expand Down
5 changes: 4 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from vyper.compiler.input_bundle import FilesystemInputBundle, InputBundle
from vyper.compiler.settings import OptimizationLevel, Settings, _set_debug_mode
from vyper.ir import compile_ir, optimizer
from vyper.utils import ERC5202_PREFIX

# Import the base fixtures
pytest_plugins = ["tests.fixtures.memorymock"]
Expand Down Expand Up @@ -377,7 +378,9 @@ def get_contract_module(source_code, *args, **kwargs):
return get_contract_module


def _deploy_blueprint_for(w3, source_code, optimize, output_formats, initcode_prefix=b"", **kwargs):
def _deploy_blueprint_for(
w3, source_code, optimize, output_formats, initcode_prefix=ERC5202_PREFIX, **kwargs
):
settings = Settings()
settings.evm_version = kwargs.pop("evm_version", None)
settings.optimize = optimize
Expand Down
68 changes: 63 additions & 5 deletions tests/functional/builtins/codegen/test_create_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import vyper.ir.compile_ir as compile_ir
from vyper.codegen.ir_node import IRnode
from vyper.compiler.settings import OptimizationLevel
from vyper.utils import EIP_170_LIMIT, checksum_encode, keccak256
from vyper.utils import EIP_170_LIMIT, ERC5202_PREFIX, checksum_encode, keccak256


# initcode used by create_minimal_proxy_to
Expand Down Expand Up @@ -148,7 +148,7 @@ def test(_salt: bytes32) -> address:

# test blueprints with various prefixes - 0xfe would block calls to the blueprint
# contract, and 0xfe7100 is ERC5202 magic
@pytest.mark.parametrize("blueprint_prefix", [b"", b"\xfe", b"\xfe\71\x00"])
@pytest.mark.parametrize("blueprint_prefix", [b"", b"\xfe", ERC5202_PREFIX])
def test_create_from_blueprint(
get_contract, deploy_blueprint_for, w3, keccak, create2_address_of, tx_failed, blueprint_prefix
):
Expand Down Expand Up @@ -208,6 +208,66 @@ def test2(target: address, salt: bytes32):
d.test2(f.address, salt)


# test blueprints with 0xfe7100 prefix, which is the EIP 5202 standard.
# code offset by default should be 3 here.
def test_create_from_blueprint_default_offset(
get_contract, deploy_blueprint_for, w3, keccak, create2_address_of, tx_failed
):
code = """
@external
def foo() -> uint256:
return 123
"""

deployer_code = """
created_address: public(address)
@external
def test(target: address):
self.created_address = create_from_blueprint(target)
@external
def test2(target: address, salt: bytes32):
self.created_address = create_from_blueprint(target, salt=salt)
"""

# deploy a foo so we can compare its bytecode with factory deployed version
foo_contract = get_contract(code)
expected_runtime_code = w3.eth.get_code(foo_contract.address)

f, FooContract = deploy_blueprint_for(code)

d = get_contract(deployer_code)

d.test(f.address, transact={})

test = FooContract(d.created_address())
assert w3.eth.get_code(test.address) == expected_runtime_code
assert test.foo() == 123

# extcodesize check
zero_address = "0x" + "00" * 20
with tx_failed():
d.test(zero_address)

# now same thing but with create2
salt = keccak(b"vyper")
d.test2(f.address, salt, transact={})

test = FooContract(d.created_address())
assert w3.eth.get_code(test.address) == expected_runtime_code
assert test.foo() == 123

# check if the create2 address matches our offchain calculation
initcode = w3.eth.get_code(f.address)
initcode = initcode[len(ERC5202_PREFIX) :] # strip the prefix
assert HexBytes(test.address) == create2_address_of(d.address, salt, initcode)

# can't collide addresses
with tx_failed():
d.test2(f.address, salt)


def test_create_from_blueprint_bad_code_offset(
get_contract, get_contract_from_ir, deploy_blueprint_for, w3, tx_failed
):
Expand Down Expand Up @@ -238,8 +298,6 @@ def test(code_ofst: uint256) -> address:
tx_info = {"from": w3.eth.accounts[0], "value": 0, "gasPrice": 0}
tx_hash = deploy_transaction.transact(tx_info)
blueprint_address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"]
blueprint_code = w3.eth.get_code(blueprint_address)
print("BLUEPRINT CODE:", blueprint_code)

d = get_contract(deployer_code, blueprint_address)

Expand Down Expand Up @@ -320,7 +378,7 @@ def should_fail(target: address, arg1: String[129], arg2: Bar):

d = get_contract(deployer_code)

initcode = w3.eth.get_code(f.address)
initcode = w3.eth.get_code(f.address)[3:]

d.test(f.address, FOO, BAR, transact={})

Expand Down
2 changes: 1 addition & 1 deletion vyper/builtins/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1828,7 +1828,7 @@ class CreateFromBlueprint(_CreateBase):
"value": KwargSettings(UINT256_T, zero_value),
"salt": KwargSettings(BYTES32_T, empty_value),
"raw_args": KwargSettings(BoolT(), False, require_literal=True),
"code_offset": KwargSettings(UINT256_T, zero_value),
"code_offset": KwargSettings(UINT256_T, IRnode.from_list(3, typ=UINT256_T)),
}
_has_varargs = True

Expand Down
4 changes: 2 additions & 2 deletions vyper/compiler/phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from vyper.semantics.types.function import ContractFunctionT
from vyper.semantics.types.module import ModuleT
from vyper.typing import StorageLayout
from vyper.utils import ERC5202_PREFIX
from vyper.venom import generate_assembly_experimental, generate_ir

DEFAULT_CONTRACT_PATH = PurePath("VyperContract.vy")
Expand Down Expand Up @@ -228,8 +229,7 @@ def bytecode_runtime(self) -> bytes:

@cached_property
def blueprint_bytecode(self) -> bytes:
blueprint_preamble = b"\xFE\x71\x00" # ERC5202 preamble
blueprint_bytecode = blueprint_preamble + self.bytecode
blueprint_bytecode = ERC5202_PREFIX + self.bytecode

# the length of the deployed code in bytes
len_bytes = len(blueprint_bytecode).to_bytes(2, "big")
Expand Down
1 change: 1 addition & 0 deletions vyper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ class SizeLimits:


EIP_170_LIMIT = 0x6000 # 24kb
ERC5202_PREFIX = b"\xFE\x71\x00" # default prefix from ERC-5202

SHA3_BASE = 30
SHA3_PER_WORD = 6
Expand Down

0 comments on commit 015cf81

Please sign in to comment.