diff --git a/docs/built-in-functions.rst b/docs/built-in-functions.rst index f2f6632906..0d2ad0c9e1 100644 --- a/docs/built-in-functions.rst +++ b/docs/built-in-functions.rst @@ -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. @@ -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. @@ -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. @@ -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. diff --git a/tests/functional/builtins/codegen/test_create_functions.py b/tests/functional/builtins/codegen/test_create_functions.py index c99942d99d..ce832cd3cb 100644 --- a/tests/functional/builtins/codegen/test_create_functions.py +++ b/tests/functional/builtins/codegen/test_create_functions.py @@ -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 """ @@ -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: @@ -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) @@ -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. @@ -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: @@ -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 diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 05d6dcb8b3..debf5c5c32 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -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: @@ -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) @@ -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() @@ -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)) @@ -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), ] @@ -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 @@ -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)) @@ -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 @@ -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:] @@ -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))