Skip to content

Commit

Permalink
feat[lang]: add revert_on_failure kwarg for create builtins (vyperl…
Browse files Browse the repository at this point in the history
…ang#3844)

per title. add `revert_on_failure=` kwarg for create builtins to mirror
`raw_call()`. among other things, this makes it easier to calculate
create addresses.

---------

Co-authored-by: Charles Cooper <cooper.charles.m@gmail.com>
  • Loading branch information
DanielSchiavini and charles-cooper authored Apr 7, 2024
1 parent c54d3b1 commit 8fcbde2
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 34 deletions.
9 changes: 6 additions & 3 deletions docs/built-in-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,14 @@ Vyper has three built-ins for contract creation; all three contract creation bui
* Invokes constructor, requires a special "blueprint" contract to be deployed
* Performs an ``EXTCODESIZE`` check to check there is code at ``target``

.. py:function:: create_minimal_proxy_to(target: address, value: uint256 = 0[, salt: bytes32]) -> address
.. py:function:: create_minimal_proxy_to(target: address, value: uint256 = 0, revert_on_failure: bool = True[, salt: bytes32]) -> address
Deploys a small, EIP1167-compliant "minimal proxy contract" that duplicates the logic of the contract at ``target``, but has its own state since every call to ``target`` is made using ``DELEGATECALL`` to ``target``. To the end user, this should be indistinguishable from an independently deployed contract with the same code as ``target``.


* ``target``: Address of the contract to proxy to
* ``value``: The wei value to send to the new contract address (Optional, default 0)
* ``revert_on_failure``: If ``False``, instead of reverting when the create operation fails, return the null address.
* ``salt``: A ``bytes32`` value utilized by the deterministic ``CREATE2`` opcode (Optional, if not supplied, ``CREATE`` is used)

Returns the address of the newly created proxy contract. If the create operation fails (for instance, in the case of a ``CREATE2`` collision), execution will revert.
Expand All @@ -163,12 +164,13 @@ Vyper has three built-ins for contract creation; all three contract creation bui
Before version 0.3.4, this function was named ``create_forwarder_to``.


.. py:function:: create_copy_of(target: address, value: uint256 = 0[, salt: bytes32]) -> address
.. py:function:: create_copy_of(target: address, value: uint256 = 0, revert_on_failure: bool = True[, salt: bytes32]) -> address
Create a physical copy of the runtime code at ``target``. The code at ``target`` is byte-for-byte copied into a newly deployed contract.

* ``target``: Address of the contract to copy
* ``value``: The wei value to send to the new contract address (Optional, default 0)
* ``revert_on_failure``: If ``False``, instead of reverting when the create operation fails, return the null address.
* ``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 there is no code at ``target``, execution will revert.
Expand All @@ -184,7 +186,7 @@ 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 = 3, [, salt: bytes32]) -> address
.. py:function:: create_from_blueprint(target: address, *args, value: uint256 = 0, raw_args: bool = False, code_offset: int = 3, revert_on_failure: bool = True[, 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.

Expand All @@ -193,6 +195,7 @@ Vyper has three built-ins for contract creation; all three contract creation bui
* ``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 3)
* ``revert_on_failure``: If ``False``, instead of reverting when the create operation fails, return the null address.
* ``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 Down
66 changes: 43 additions & 23 deletions tests/functional/builtins/codegen/test_create_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,17 @@ def test2(a: uint256) -> Bytes[100]:
assert receipt["gasUsed"] < GAS_SENT


def test_create_minimal_proxy_to_create2(get_contract, create2_address_of, keccak, tx_failed):
code = """
@pytest.mark.parametrize("revert_on_failure", [True, False, None])
def test_create_minimal_proxy_to_create2(
get_contract, create2_address_of, keccak, tx_failed, revert_on_failure
):
revert_arg = "" if revert_on_failure is None else f", revert_on_failure={revert_on_failure}"
code = f"""
main: address
@external
def test(_salt: bytes32) -> address:
self.main = create_minimal_proxy_to(self, salt=_salt)
self.main = create_minimal_proxy_to(self, salt=_salt{revert_arg})
return self.main
"""

Expand All @@ -129,16 +133,28 @@ def test(_salt: bytes32) -> address:

c.test(salt, transact={})
# revert on collision
with tx_failed():
c.test(salt, transact={})
if revert_on_failure is False:
assert not c.test(salt)
else:
with tx_failed():
c.test(salt, transact={})


# 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", ERC5202_PREFIX])
@pytest.mark.parametrize("revert_on_failure", [True, False, None])
def test_create_from_blueprint(
get_contract, deploy_blueprint_for, w3, keccak, create2_address_of, tx_failed, blueprint_prefix
get_contract,
deploy_blueprint_for,
w3,
keccak,
create2_address_of,
tx_failed,
blueprint_prefix,
revert_on_failure,
):
revert_arg = "" if revert_on_failure is None else f", revert_on_failure={revert_on_failure}"
code = """
@external
def foo() -> uint256:
Expand All @@ -151,14 +167,16 @@ def foo() -> uint256:
@external
def test(target: address):
self.created_address = create_from_blueprint(target, code_offset={prefix_len})
self.created_address = create_from_blueprint(target, code_offset={prefix_len}{revert_arg})
@external
def test2(target: address, salt: bytes32):
self.created_address = create_from_blueprint(target, code_offset={prefix_len}, salt=salt)
self.created_address = create_from_blueprint(
target, code_offset={prefix_len}, salt=salt{revert_arg}
)
"""

# deploy a foo so we can compare its bytecode with factory deployed version
# 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)

Expand Down Expand Up @@ -191,8 +209,11 @@ def test2(target: address, salt: bytes32):
assert HexBytes(test.address) == create2_address_of(d.address, salt, initcode)

# can't collide addresses
with tx_failed():
d.test2(f.address, salt)
if revert_on_failure is False:
assert not d.test2(f.address, salt)
else:
with tx_failed():
d.test2(f.address, salt)


# test blueprints with 0xfe7100 prefix, which is the EIP 5202 standard.
Expand Down Expand Up @@ -425,16 +446,18 @@ def should_fail(target: address, arg1: String[129], arg2: Bar):
w3.eth.send_transaction({"to": d.address, "data": f"{sig}{encoded}"})


def test_create_copy_of(get_contract, w3, keccak, create2_address_of, tx_failed):
code = """
@pytest.mark.parametrize("revert_on_failure", [True, False, None])
def test_create_copy_of(get_contract, w3, keccak, create2_address_of, tx_failed, revert_on_failure):
revert_arg = "" if revert_on_failure is None else f", revert_on_failure={revert_on_failure}"
code = f"""
created_address: public(address)
@internal
def _create_copy_of(target: address):
self.created_address = create_copy_of(target)
self.created_address = create_copy_of(target{revert_arg})
@internal
def _create_copy_of2(target: address, salt: bytes32):
self.created_address = create_copy_of(target, salt=salt)
self.created_address = create_copy_of(target, salt=salt{revert_arg})
@external
def test(target: address) -> address:
Expand Down Expand Up @@ -473,14 +496,11 @@ def test2(target: address, salt: bytes32) -> address:
assert HexBytes(test2) == create2_address_of(c.address, salt, vyper_initcode(bytecode))

# can't create2 where contract already exists
with tx_failed():
c.test2(c.address, salt, transact={})

# test single byte contract
# test2 = c.test2(b"\x01", salt)
# assert HexBytes(test2) == create2_address_of(c.address, salt, vyper_initcode(b"\x01"))
# with tx_failed():
# c.test2(bytecode, salt)
if revert_on_failure is False:
assert not c.test2(c.address, salt)
else:
with tx_failed():
c.test2(c.address, salt)


# XXX: these various tests to check the msize allocator for
Expand Down
20 changes: 12 additions & 8 deletions vyper/builtins/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1542,7 +1542,7 @@ def build_IR(self, expr, context):

# create helper functions
# generates CREATE op sequence + zero check for result
def _create_ir(value, buf, length, salt, checked=True):
def _create_ir(value, buf, length, salt, revert_on_failure=True):
args = [value, buf, length]
create_op = "create"
if salt is not CREATE2_SENTINEL:
Expand All @@ -1551,7 +1551,7 @@ def _create_ir(value, buf, length, salt, checked=True):

ret = IRnode.from_list(ensure_eval_once("create_builtin", [create_op, *args]))

if not checked:
if not revert_on_failure:
return ret

ret = clamp_nonzero(ret)
Expand Down Expand Up @@ -1652,6 +1652,7 @@ class _CreateBase(BuiltinFunctionT):
_kwargs = {
"value": KwargSettings(UINT256_T, zero_value),
"salt": KwargSettings(BYTES32_T, empty_value),
"revert_on_failure": KwargSettings(BoolT(), True, require_literal=True),
}
_return_type = AddressT()

Expand Down Expand Up @@ -1685,7 +1686,7 @@ def _add_gas_estimate(self, args, should_use_create2):
bytecode_len = 20 + len(b) + len(c)
return _create_addl_gas_estimate(bytecode_len, should_use_create2)

def _build_create_IR(self, expr, args, context, value, salt):
def _build_create_IR(self, expr, args, context, value, salt, revert_on_failure):
target_address = args[0]

buf = context.new_internal_variable(BytesT(96))
Expand Down Expand Up @@ -1713,7 +1714,7 @@ def _build_create_IR(self, expr, args, context, value, salt):
["mstore", buf, forwarder_preamble],
["mstore", ["add", buf, preamble_length], aligned_target],
["mstore", ["add", buf, preamble_length + 20], forwarder_post],
_create_ir(value, buf, buf_len, salt=salt),
_create_ir(value, buf, buf_len, salt, revert_on_failure),
]


Expand Down Expand Up @@ -1742,7 +1743,7 @@ def _add_gas_estimate(self, args, should_use_create2):
# max possible runtime length + preamble length
return _create_addl_gas_estimate(EIP_170_LIMIT + self._preamble_len, should_use_create2)

def _build_create_IR(self, expr, args, context, value, salt):
def _build_create_IR(self, expr, args, context, value, salt, revert_on_failure):
target = args[0]

# something we can pass to scope_multi
Expand Down Expand Up @@ -1776,7 +1777,7 @@ def _build_create_IR(self, expr, args, context, value, salt):
buf = add_ofst(mem_ofst, 32 - preamble_len)
buf_len = ["add", codesize, preamble_len]

ir.append(_create_ir(value, buf, buf_len, salt))
ir.append(_create_ir(value, buf, buf_len, salt, revert_on_failure))

return b1.resolve(b2.resolve(ir))

Expand All @@ -1789,6 +1790,7 @@ class CreateFromBlueprint(_CreateBase):
"salt": KwargSettings(BYTES32_T, empty_value),
"raw_args": KwargSettings(BoolT(), False, require_literal=True),
"code_offset": KwargSettings(UINT256_T, IRnode.from_list(3, typ=UINT256_T)),
"revert_on_failure": KwargSettings(BoolT(), True, require_literal=True),
}
_has_varargs = True

Expand All @@ -1798,7 +1800,9 @@ def _add_gas_estimate(self, args, should_use_create2):
maxlen = EIP_170_LIMIT + ctor_args.typ.abi_type.size_bound()
return _create_addl_gas_estimate(maxlen, should_use_create2)

def _build_create_IR(self, expr, args, context, value, salt, code_offset, raw_args):
def _build_create_IR(
self, expr, args, context, value, salt, code_offset, raw_args, revert_on_failure
):
target = args[0]
ctor_args = args[1:]

Expand Down Expand Up @@ -1874,7 +1878,7 @@ def _build_create_IR(self, expr, args, context, value, salt, code_offset, raw_ar

length = ["add", codesize, encoded_args_len]

ir.append(_create_ir(value, mem_ofst, length, salt))
ir.append(_create_ir(value, mem_ofst, length, salt, revert_on_failure))

return b1.resolve(b2.resolve(ir))

Expand Down

0 comments on commit 8fcbde2

Please sign in to comment.