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[lang]: add raw_create() builtin #4204

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
244 changes: 244 additions & 0 deletions tests/functional/builtins/codegen/test_create_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import vyper.ir.compile_ir as compile_ir
from tests.utils import ZERO_ADDRESS
from vyper.codegen.ir_node import IRnode
from vyper.compiler import compile_code
from vyper.compiler.settings import OptimizationLevel
from vyper.utils import EIP_170_LIMIT, ERC5202_PREFIX, checksum_encode, keccak256

Expand Down Expand Up @@ -746,3 +747,246 @@ def test(target: address) -> address:
c.test(c.address, value=2)
test1 = c.address
assert env.get_code(test1) == bytecode


def test_raw_create(get_contract, env):
to_deploy_code = """
foo: public(uint256)
"""

out = compile_code(to_deploy_code, output_formats=["bytecode", "bytecode_runtime"])
initcode = bytes.fromhex(out["bytecode"].removeprefix("0x"))
runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x"))

deployer_code = f"""
@external
def deploy_from_literal() -> address:
return raw_create({initcode})

@external
def deploy_from_calldata(s: Bytes[1024]) -> address:
return raw_create(s)

@external
def deploy_from_memory() -> address:
s: Bytes[1024] = {initcode}
return raw_create(s)
"""

deployer = get_contract(deployer_code)

res = deployer.deploy_from_literal()
assert env.get_code(res) == runtime

res = deployer.deploy_from_memory()
assert env.get_code(res) == runtime

res = deployer.deploy_from_calldata(initcode)
assert env.get_code(res) == runtime


def test_raw_create_double_eval(get_contract, env):
to_deploy_code = """
foo: public(uint256)


@deploy
def __init__(x: uint256):
self.foo = x
"""

out = compile_code(to_deploy_code, output_formats=["bytecode", "bytecode_runtime"])
initcode = bytes.fromhex(out["bytecode"].removeprefix("0x"))
runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x"))

deployer_code = """
interface Foo:
def foo() -> uint256: view

a: DynArray[uint256, 10]
counter: public(uint256)

@deploy
def __init__():
self.a.append(1)
self.a.append(2)

def get_index() -> uint256:
self.counter += 1
return 0

@external
def deploy_from_calldata(s: Bytes[1024]) -> address:
res: address = raw_create(s, self.a[self.get_index()])
assert staticcall Foo(res).foo() == 1
return res
"""

deployer = get_contract(deployer_code)

res = deployer.deploy_from_calldata(initcode)
assert env.get_code(res) == runtime

assert deployer.counter() == 1


def test_raw_create_salt(get_contract, env, create2_address_of, keccak):
to_deploy_code = """
foo: public(uint256)

@deploy
def __init__(arg: uint256):
self.foo = arg
"""

out = compile_code(to_deploy_code, output_formats=["bytecode", "bytecode_runtime"])
initcode = bytes.fromhex(out["bytecode"].removeprefix("0x"))
runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x"))

deployer_code = """
@external
def deploy_from_calldata(s: Bytes[1024], arg: uint256, salt: bytes32) -> address:
return raw_create(s, arg, salt=salt)
"""

deployer = get_contract(deployer_code)

salt = keccak(b"vyper")
arg = 42
res = deployer.deploy_from_calldata(initcode, arg, salt)

initcode = initcode + abi.encode("(uint256)", (arg,))

assert HexBytes(res) == create2_address_of(deployer.address, salt, initcode)

assert env.get_code(res) == runtime


# test raw_create with all combinations of value and revert_on_failure kwargs
# (including not present at all)
# additionally parametrize whether the constructor reverts or not
@pytest.mark.parametrize("constructor_reverts", [True, False])
@pytest.mark.parametrize("use_value", [True, False])
@pytest.mark.parametrize("revert_on_failure", [True, False, None])
def test_raw_create_revert_value_kws(
get_contract, env, tx_failed, constructor_reverts, revert_on_failure, use_value
):
value = 1
value_assert = f"assert msg.value == {value}" if use_value else ""
to_deploy_code = f"""
foo: public(uint256)

@deploy
@payable
def __init__(constructor_reverts: bool):
assert not constructor_reverts
{value_assert}
"""

out = compile_code(to_deploy_code, output_formats=["bytecode", "bytecode_runtime"])
initcode = bytes.fromhex(out["bytecode"].removeprefix("0x"))
runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x"))

value_kw = f", value={value}" if use_value else ""
revert_kw = f", revert_on_failure={revert_on_failure}" if revert_on_failure is not None else ""
deployer_code = f"""
@external
def deploy() -> address:
return raw_create({initcode},{constructor_reverts}{revert_kw}{value_kw})
"""

deployer = get_contract(deployer_code)
env.set_balance(deployer.address, value)

expect_revert = constructor_reverts and revert_on_failure in (True, None)

if expect_revert:
with tx_failed():
deployer.deploy()
else:
res = deployer.deploy()
if constructor_reverts:
assert res == ZERO_ADDRESS
assert env.get_code(res) == b""
else:
assert env.get_code(res) == runtime


# test that raw_create correctly interfaces with the abi encoder
# and can handle dynamic arguments
def test_raw_create_dynamic_arg(get_contract, env):
array = [1, 2, 3]

to_deploy_code = """
foo: public(uint256)

@deploy
@payable
def __init__(a: DynArray[uint256, 10]):
for i: uint256 in range(1, 4):
assert a[i - 1] == i
"""

out = compile_code(to_deploy_code, output_formats=["bytecode", "bytecode_runtime"])
initcode = bytes.fromhex(out["bytecode"].removeprefix("0x"))
runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x"))

deployer_code = f"""
@external
def deploy() -> address:
a: DynArray[uint256, 10] = {array}
return raw_create({initcode}, a)
"""

deployer = get_contract(deployer_code)

res = deployer.deploy()

assert env.get_code(res) == runtime


@pytest.mark.parametrize("arg", [12, 257, 2**256 - 1])
@pytest.mark.parametrize("length_offset", [-32, -1, 0, 1, 32])
def test_raw_create_change_initcode_size(
get_contract, deploy_blueprint_for, env, arg, length_offset
):
to_deploy_code = """
foo: public(uint256)

@deploy
def __init__(arg: uint256):
self.foo = arg

"""

out = compile_code(to_deploy_code, output_formats=["bytecode", "bytecode_runtime"])
initcode = bytes.fromhex(out["bytecode"].removeprefix("0x"))
runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x"))

dummy_bytes = b"\x02" * (len(initcode) + length_offset)

deployer_code = f"""
x:DynArray[Bytes[1024],1]

@internal
def change_initcode_length(v: Bytes[1024]) -> uint256:
self.x.pop()
self.x.append({dummy_bytes})
return {arg}

@external
def deploy(s: Bytes[1024]) -> address:
self.x.append(s)
contract: address = raw_create(self.x[0], self.change_initcode_length(s))
return contract
"""

deployer = get_contract(deployer_code)

res = deployer.deploy(initcode)
assert env.get_code(res) == runtime

_, FooContract = deploy_blueprint_for(to_deploy_code)
res = FooContract(res)

assert res.foo() == arg
44 changes: 44 additions & 0 deletions vyper/builtins/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
from vyper.utils import (
DECIMAL_DIVISOR,
EIP_170_LIMIT,
EIP_3860_LIMIT,
SHA3_PER_WORD,
MemoryPositions,
bytes_to_int,
Expand Down Expand Up @@ -1680,6 +1681,48 @@ def build_IR(self, expr, args, kwargs, context):
)


class RawCreate(_CreateBase):
_id = "raw_create"
_inputs = [("bytecode", BytesT(EIP_3860_LIMIT))]
_has_varargs = True

def _add_gas_estimate(self, args, should_use_create2):
return _create_addl_gas_estimate(EIP_170_LIMIT, should_use_create2)

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

ctor_args = [ensure_in_memory(arg, context) for arg in ctor_args]

# encode the varargs
to_encode = ir_tuple_from_args(ctor_args)
type_size_bound = to_encode.typ.abi_type.size_bound()
bufsz = initcode.typ.maxlen + type_size_bound

buf = context.new_internal_variable(get_type_for_exact_size(bufsz))

ret = ["seq"]

with scope_multi((initcode, value, salt), ("initcode", "value", "salt")) as (
b1,
(initcode, value, salt),
):
bytecode_len = get_bytearray_length(initcode)
with bytecode_len.cache_when_complex("initcode_len") as (b2, bytecode_len):
maxlen = initcode.typ.maxlen
ret.append(copy_bytes(buf, bytes_data_ptr(initcode), bytecode_len, maxlen))

argbuf = add_ofst(buf, bytecode_len)
argslen = abi_encode(
argbuf, to_encode, context, bufsz=type_size_bound, returns_len=True
)
total_len = add_ofst(bytecode_len, argslen)
ret.append(_create_ir(value, buf, total_len, salt, revert_on_failure))

return b1.resolve(b2.resolve(IRnode.from_list(ret)))


class CreateMinimalProxyTo(_CreateBase):
# create an EIP1167 "minimal proxy" to the target contract

Expand Down Expand Up @@ -2689,6 +2732,7 @@ def _try_fold(self, node):
"breakpoint": Breakpoint(),
"selfdestruct": SelfDestruct(),
"raw_call": RawCall(),
"raw_create": RawCreate(),
"raw_log": RawLog(),
"raw_revert": RawRevert(),
"create_minimal_proxy_to": CreateMinimalProxyTo(),
Expand Down
3 changes: 3 additions & 0 deletions vyper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,8 +499,11 @@ def quantize(d: decimal.Decimal, places=MAX_DECIMAL_PLACES, rounding_mode=decima


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

assert EIP_3860_LIMIT == 49152 # directly from the EIP

SHA3_BASE = 30
SHA3_PER_WORD = 6

Expand Down