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

POC feat[lang]: add EIP-3074 support #3958

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
18 changes: 17 additions & 1 deletion vyper/ast/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1252,13 +1252,17 @@ class Call(ExprNode):
def is_extcall(self):
return isinstance(self._parent, ExtCall)

@property
def is_authcall(self):
return isinstance(self._parent, AuthCall)

@property
def is_staticcall(self):
return isinstance(self._parent, StaticCall)

@property
def is_plain_call(self):
return not (self.is_extcall or self.is_staticcall)
return not (self.is_extcall or self.is_staticcall or self.is_authcall)

@property
def kind_str(self):
Expand Down Expand Up @@ -1308,6 +1312,18 @@ def validate(self):
hint="did you forget parentheses?",
)

class AuthCall(ExprNode):
__slots__ = ("value",)

def validate(self):
if not isinstance(self.value, Call):
# TODO: investigate wrong col_offset for `self.value`
raise StructureException(
"`extcall` must be followed by a function call",
self.value,
hint="did you forget parentheses?",
)


class keyword(VyperNode):
__slots__ = ("arg", "value")
Expand Down
3 changes: 3 additions & 0 deletions vyper/ast/nodes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ class ExtCall(VyperNode):
class StaticCall(VyperNode):
value: Call = ...

class AuthCall(VyperNode):
value: Call = ...

class UnaryOp(ExprNode):
op: VyperNode = ...
operand: ExprNode = ...
Expand Down
2 changes: 1 addition & 1 deletion vyper/ast/pre_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def consume(self, token):
# simple statements that are replaced with `yield`
CUSTOM_STATEMENT_TYPES = {"log": "Log"}
# expression types that are replaced with `await`
CUSTOM_EXPRESSION_TYPES = {"extcall": "ExtCall", "staticcall": "StaticCall"}
CUSTOM_EXPRESSION_TYPES = {"extcall": "ExtCall", "staticcall": "StaticCall", "authcall": "AuthCall"}


def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]:
Expand Down
29 changes: 27 additions & 2 deletions vyper/builtins/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,25 @@ def infer_kwarg_types(self, node):
return {"output_type": TYPE_T(output_type)}


# EIP-3074 authorization
class Authorize(BuiltinFunctionT):
_id = "authorize"
_inputs = [
("address", AddressT()),
("buf", BytesT(97)),
# maybe add a `checked=True`?
]

@process_inputs
def build_IR(self, expr, args, kwargs, context):
address = args[0]
buf = args[1]
with ensure_in_memory(buf, context).cache_when_complex("buf") as (b1, buf):
auth = ["auth", address, bytes_data_ptr(buf), get_bytearray_length(buf)]
ret = IRnode.from_list(["assert", auth])
return b1.resolve(ret)


class ECRecover(BuiltinFunctionT):
_id = "ecrecover"
_inputs = [
Expand Down Expand Up @@ -1005,6 +1024,7 @@ def build_IR(self, expr, args, kwargs, context):

zero_value = IRnode.from_list(0, typ=UINT256_T)
empty_value = IRnode.from_list(0, typ=BYTES32_T)
zero_address = IRnode.from_list(0, typ=AddressT())


class RawCall(BuiltinFunctionT):
Expand All @@ -1016,6 +1036,7 @@ class RawCall(BuiltinFunctionT):
"value": KwargSettings(UINT256_T, zero_value),
"is_delegate_call": KwargSettings(BoolT(), False, require_literal=True),
"is_static_call": KwargSettings(BoolT(), False, require_literal=True),
"is_auth_call": KwargSettings(AddressT(), False, require_literal=True),
"revert_on_failure": KwargSettings(BoolT(), True, require_literal=True),
}

Expand Down Expand Up @@ -1060,16 +1081,17 @@ def infer_arg_types(self, node, expected_return_typ=None):
def build_IR(self, expr, args, kwargs, context):
to, data = args
# TODO: must compile in source code order, left-to-right
gas, value, outsize, delegate_call, static_call, revert_on_failure = (
gas, value, outsize, delegate_call, static_call, revert_on_failure, auth_call = (
kwargs["gas"],
kwargs["value"],
kwargs["max_outsize"],
kwargs["is_delegate_call"],
kwargs["is_static_call"],
kwargs["is_auth_call"],
kwargs["revert_on_failure"],
)

if delegate_call and static_call:
if delegate_call + static_call + auth_call > 1:
raise ArgumentException(
"Call may use one of `is_delegate_call` or `is_static_call`, not both"
)
Expand Down Expand Up @@ -1124,6 +1146,8 @@ def build_IR(self, expr, args, kwargs, context):
call_op = ["delegatecall", gas, to, *common_call_args]
elif static_call:
call_op = ["staticcall", gas, to, *common_call_args]
elif auth_call:
call_op = ["authcall", gas, to, value, *common_call_args]
else:
call_op = ["call", gas, to, value, *common_call_args]

Expand Down Expand Up @@ -2625,6 +2649,7 @@ def _try_fold(self, node):
"breakpoint": Breakpoint(),
"selfdestruct": SelfDestruct(),
"raw_call": RawCall(),
"authorize": Authorize(),
"raw_log": RawLog(),
"raw_revert": RawRevert(),
"create_minimal_proxy_to": CreateMinimalProxyTo(),
Expand Down
3 changes: 3 additions & 0 deletions vyper/codegen/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,9 @@ def handle_external_call(cls, expr, context):

return external_call.ir_for_external_call(call_node, context)

def parse_AuthCall(self):
return self.handle_external_call(self.expr, self.context)

def parse_ExtCall(self):
return self.handle_external_call(self.expr, self.context)

Expand Down
4 changes: 4 additions & 0 deletions vyper/codegen/external_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,15 @@ def _external_call_helper(contract_address, args_ir, call_kwargs, call_expr, con
value = call_kwargs.value

use_staticcall = fn_type.mutability in (StateMutability.VIEW, StateMutability.PURE)
use_authcall = call_expr.is_authcall
assert not (use_staticcall and use_authcall)
if context.is_constant():
assert use_staticcall, "typechecker missed this"

if use_staticcall:
call_op = ["staticcall", gas, contract_address, args_ofst, args_len, buf, ret_len]
elif use_authcall:
call_op = ["authcall", gas, contract_address, value, args_ofst, args_len, buf, ret_len]
else:
call_op = ["call", gas, contract_address, value, args_ofst, args_len, buf, ret_len]

Expand Down
2 changes: 2 additions & 0 deletions vyper/evm/opcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@
"RETURN": (0xF3, 2, 0, 0),
"DELEGATECALL": (0xF4, 6, 1, 2100),
"CREATE2": (0xF5, 4, 1, 32000),
"AUTH": (0xF6, 3, 1, 3100), # TODO: assign fork
"AUTHCALL": (0xF7, 7, 1, 2100), # TODO: assign fork
"SELFDESTRUCT": (0xFF, 1, 0, 25000),
"STATICCALL": (0xFA, 6, 1, 2100),
"REVERT": (0xFD, 2, 0, 0),
Expand Down
7 changes: 5 additions & 2 deletions vyper/semantics/analysis/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,14 +485,14 @@ def visit_Expr(self, node):
)

# NOTE: standalone staticcalls are banned!
if not isinstance(node.value, (vy_ast.Call, vy_ast.ExtCall)):
if not isinstance(node.value, (vy_ast.Call, vy_ast.ExtCall, vy_ast.AuthCall)):
raise StructureException(
"Expressions without assignment are disallowed",
node,
hint="did you mean to assign the result to a variable?",
)

if isinstance(node.value, vy_ast.ExtCall):
if isinstance(node.value, (vy_ast.ExtCall, vy_ast.AuthCall)):
call_node = node.value.value
else:
call_node = node.value
Expand Down Expand Up @@ -746,6 +746,9 @@ def _check_call_mutability(self, call_mutability: StateMutability):
def visit_ExtCall(self, node, typ):
return self.visit(node.value, typ)

def visit_AuthCall(self, node, typ):
return self.visit(node.value, typ)

def visit_StaticCall(self, node, typ):
return self.visit(node.value, typ)

Expand Down
4 changes: 4 additions & 0 deletions vyper/semantics/analysis/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ def types_from_ExtCall(self, node):
call_node = node.value
return self._find_fn(call_node)(call_node)

def types_from_AuthCall(self, node):
call_node = node.value
return self._find_fn(call_node)(call_node)

def types_from_StaticCall(self, node):
call_node = node.value
return self._find_fn(call_node)(call_node)
Expand Down
3 changes: 2 additions & 1 deletion vyper/semantics/types/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
)
from vyper.semantics.data_locations import DataLocation
from vyper.semantics.types.base import KwargSettings, VyperType
from vyper.semantics.types.primitives import BoolT
from vyper.semantics.types.primitives import BoolT, AddressT

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
vyper.semantics.types.primitives
begins an import cycle.
from vyper.semantics.types.shortcuts import UINT256_T
from vyper.semantics.types.subscriptable import TupleT
from vyper.semantics.types.utils import type_from_abi, type_from_annotation
Expand Down Expand Up @@ -191,6 +191,7 @@
"gas": KwargSettings(UINT256_T, "gas"),
"value": KwargSettings(UINT256_T, 0),
"skip_contract_check": KwargSettings(BoolT(), False, require_literal=True),
"auth": KwargSettings(AddressT(), 0),
"default_return_value": KwargSettings(self.return_type, None),
}

Expand Down
Loading