From 1b1ac45f8cd374c805ad986ab6ba25dd30b1d4ee Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Sun, 3 Nov 2024 11:45:14 +0000 Subject: [PATCH 01/12] forbid initialization of a stateless module --- vyper/semantics/analysis/module.py | 3 +++ vyper/semantics/analysis/utils.py | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 8a2beb61e6..e67276c5a2 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -37,6 +37,7 @@ check_modifiability, get_exact_type_from_node, get_expr_info, + is_stateless, ) from vyper.semantics.data_locations import DataLocation from vyper.semantics.namespace import Namespace, get_namespace, override_global_namespace @@ -409,6 +410,8 @@ def visit_InitializesDecl(self, node): module_info = get_expr_info(module_ref).module_info if module_info is None: raise StructureException("Not a module!", module_ref) + if is_stateless(module_info.module_node): + raise StructureException(f"Cannot initialize a stateless module {module_info.alias}!", module_ref) used_modules = {i.module_t: i for i in module_info.module_t.used_modules} diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index 9734087fc3..b76aebf02a 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -736,3 +736,16 @@ def validate_kwargs(node: vy_ast.Call, members: dict[str, VyperType], typeclass: msg = f"{typeclass} instantiation missing fields:" msg += f" {', '.join(list(missing))}" raise InstantiationException(msg, node) + +def is_stateless(module: vy_ast.Module): + """ + Determine whether a module is stateless by examining its top-level declarations. + A module has state if it contains storage variables, transient variables, or + immutables, or if it includes a "uses" or "initializes" declaration. + """ + for i in module.body: + if isinstance(i, (vy_ast.InitializesDecl, vy_ast.UsesDecl)): + return False + if isinstance(i, vy_ast.VariableDecl) and not i.is_constant: + return False + return True From 3a9e29dbc384aa801759ba2da459ed086eea424d Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Sun, 3 Nov 2024 11:51:41 +0000 Subject: [PATCH 02/12] fix tests so they do not initialize a stateless module --- tests/functional/codegen/modules/test_nonreentrant.py | 4 ++++ tests/functional/syntax/modules/test_initializers.py | 2 ++ tests/unit/compiler/test_abi.py | 8 ++++++++ 3 files changed, 14 insertions(+) diff --git a/tests/functional/codegen/modules/test_nonreentrant.py b/tests/functional/codegen/modules/test_nonreentrant.py index 69b17cbfa2..ea3297eea9 100644 --- a/tests/functional/codegen/modules/test_nonreentrant.py +++ b/tests/functional/codegen/modules/test_nonreentrant.py @@ -1,5 +1,7 @@ def test_export_nonreentrant(make_input_bundle, get_contract, tx_failed): lib1 = """ +phony: uint32 + interface Foo: def foo() -> uint256: nonpayable @@ -38,6 +40,8 @@ def __default__(): def test_internal_nonreentrant(make_input_bundle, get_contract, tx_failed): lib1 = """ +phony: uint32 + interface Foo: def foo() -> uint256: nonpayable diff --git a/tests/functional/syntax/modules/test_initializers.py b/tests/functional/syntax/modules/test_initializers.py index ead0fbcf6b..cac284fbba 100644 --- a/tests/functional/syntax/modules/test_initializers.py +++ b/tests/functional/syntax/modules/test_initializers.py @@ -835,6 +835,8 @@ def test_uses_skip_import(make_input_bundle): lib2 = """ import lib1 +phony: uint32 + @internal def foo(): pass diff --git a/tests/unit/compiler/test_abi.py b/tests/unit/compiler/test_abi.py index 430cd75344..f0f92d2ec4 100644 --- a/tests/unit/compiler/test_abi.py +++ b/tests/unit/compiler/test_abi.py @@ -218,6 +218,8 @@ def bar(x: {type}): def test_exports_abi(make_input_bundle): lib1 = """ +phony: uint32 + @external def foo(): pass @@ -330,6 +332,8 @@ def __init__(): def test_event_export_from_init(make_input_bundle): # test that events get exported when used in init functions lib1 = """ +phony: uint32 + event MyEvent: pass @@ -361,6 +365,8 @@ def __init__(): def test_event_export_from_function_export(make_input_bundle): # test events used in exported functions are exported lib1 = """ +phony: uint32 + event MyEvent: pass @@ -396,6 +402,8 @@ def foo(): def test_event_export_unused_function(make_input_bundle): # test events in unused functions are not exported lib1 = """ +phony: uint32 + event MyEvent: pass From 55272443e05a71743df39b382661f357898e0587 Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Sun, 3 Nov 2024 11:57:11 +0000 Subject: [PATCH 03/12] lint --- vyper/semantics/analysis/module.py | 4 +++- vyper/semantics/analysis/utils.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index e67276c5a2..9ed28d6267 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -411,7 +411,9 @@ def visit_InitializesDecl(self, node): if module_info is None: raise StructureException("Not a module!", module_ref) if is_stateless(module_info.module_node): - raise StructureException(f"Cannot initialize a stateless module {module_info.alias}!", module_ref) + raise StructureException( + f"Cannot initialize a stateless module {module_info.alias}!", module_ref + ) used_modules = {i.module_t: i for i in module_info.module_t.used_modules} diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index b76aebf02a..d092675aa2 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -737,6 +737,7 @@ def validate_kwargs(node: vy_ast.Call, members: dict[str, VyperType], typeclass: msg += f" {', '.join(list(missing))}" raise InstantiationException(msg, node) + def is_stateless(module: vy_ast.Module): """ Determine whether a module is stateless by examining its top-level declarations. From 3c695da9d0594881e02cc325e15ae69d162c687c Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Mon, 4 Nov 2024 10:55:29 +0000 Subject: [PATCH 04/12] set immutables as stateless --- vyper/semantics/analysis/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index d092675aa2..7955981244 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -747,6 +747,6 @@ def is_stateless(module: vy_ast.Module): for i in module.body: if isinstance(i, (vy_ast.InitializesDecl, vy_ast.UsesDecl)): return False - if isinstance(i, vy_ast.VariableDecl) and not i.is_constant: + if isinstance(i, vy_ast.VariableDecl) and not i.is_constant and not i.is_immutable: return False return True From 5000efc1693c0f9d9d3a05d9dd13a87c9cf0c1ec Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Mon, 4 Nov 2024 10:59:09 +0000 Subject: [PATCH 05/12] add tests --- .../syntax/modules/test_initializers.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tests/functional/syntax/modules/test_initializers.py b/tests/functional/syntax/modules/test_initializers.py index cac284fbba..f8aeb7057b 100644 --- a/tests/functional/syntax/modules/test_initializers.py +++ b/tests/functional/syntax/modules/test_initializers.py @@ -1420,3 +1420,103 @@ def set_some_mod(): compile_code(main, input_bundle=input_bundle) assert e.value._message == "module `lib3.vy` is used but never initialized!" assert e.value._hint is None + + +stateful_var_modules = [ + """ +phony: uint32 + """, + """ +ended: public(bool) + """, + """ +message: transient(bool) + """, +] + + +@pytest.mark.parametrize("module", stateful_var_modules) +def test_initializes_on_modules_with_state_related_vars(module, make_input_bundle): + main = """ +import lib +initializes: lib + """ + input_bundle = make_input_bundle({"lib.vy": module, "main.vy": main}) + compile_code(main, input_bundle=input_bundle) + + +stateless_modules = [ + """ + """, + """ +@deploy +def __init__(): + pass + """, + """ +@internal +@pure +def foo(x: uint256, y: uint256) -> uint256: + return unsafe_add(x & y, (x ^ y) >> 1) + """, + """ +FOO: constant(int128) = 128 + """, + """ +foo: immutable(int128) + +@deploy +def __init__(): + foo = 2 + """, +] + + +@pytest.mark.parametrize("module", stateless_modules) +def test_forbids_initializes_on_stateless_modules(module, make_input_bundle): + main = """ +import lib +initializes: lib + """ + input_bundle = make_input_bundle({"lib.vy": module, "main.vy": main}) + with pytest.raises(StructureException): + compile_code(main, input_bundle=input_bundle) + + +def test_initializes_on_modules_with_uses(make_input_bundle): + lib0 = """ +import lib1 +uses: lib1 + +@external +def foo() -> uint32: + return lib1.phony + """ + lib1 = """ +phony: uint32 + """ + main = """ +import lib1 +initializes: lib1 + +import lib0 +initializes: lib0[lib1 := lib1] + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "lib0.vy": lib0, "main.vy": main}) + compile_code(main, input_bundle=input_bundle) + + +def test_initializes_on_modules_with_initializes(make_input_bundle): + lib0 = """ +import lib1 +initializes: lib1 + """ + lib1 = """ +phony: uint32 + """ + main = """ +import lib0 +initializes: lib0 + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "lib0.vy": lib0, "main.vy": main}) + compile_code(main, input_bundle=input_bundle) From e3d758207abfe43b88ec578d0cf19627b02b541f Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Mon, 4 Nov 2024 11:32:18 +0000 Subject: [PATCH 06/12] fix stateless test --- tests/functional/codegen/features/decorators/test_pure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/codegen/features/decorators/test_pure.py b/tests/functional/codegen/features/decorators/test_pure.py index b2c5a0945b..867763ccc0 100644 --- a/tests/functional/codegen/features/decorators/test_pure.py +++ b/tests/functional/codegen/features/decorators/test_pure.py @@ -135,6 +135,7 @@ def foo() -> uint256: def test_invalid_module_immutable_access(make_input_bundle): lib1 = """ +phony: uint32 COUNTER: immutable(uint256) @deploy From c92e1adf25a5095c64743067a6ce841747206ea0 Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Mon, 4 Nov 2024 16:04:51 +0000 Subject: [PATCH 07/12] make immutables state --- tests/functional/codegen/features/decorators/test_pure.py | 1 - vyper/semantics/analysis/utils.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/functional/codegen/features/decorators/test_pure.py b/tests/functional/codegen/features/decorators/test_pure.py index 867763ccc0..b2c5a0945b 100644 --- a/tests/functional/codegen/features/decorators/test_pure.py +++ b/tests/functional/codegen/features/decorators/test_pure.py @@ -135,7 +135,6 @@ def foo() -> uint256: def test_invalid_module_immutable_access(make_input_bundle): lib1 = """ -phony: uint32 COUNTER: immutable(uint256) @deploy diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index 7955981244..d092675aa2 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -747,6 +747,6 @@ def is_stateless(module: vy_ast.Module): for i in module.body: if isinstance(i, (vy_ast.InitializesDecl, vy_ast.UsesDecl)): return False - if isinstance(i, vy_ast.VariableDecl) and not i.is_constant and not i.is_immutable: + if isinstance(i, vy_ast.VariableDecl) and not i.is_constant: return False return True From cf9178462a44ce9f5c07370c3befca8f53e49ff7 Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Mon, 4 Nov 2024 16:12:28 +0000 Subject: [PATCH 08/12] fix test --- .../syntax/modules/test_initializers.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/functional/syntax/modules/test_initializers.py b/tests/functional/syntax/modules/test_initializers.py index f8aeb7057b..5931be361a 100644 --- a/tests/functional/syntax/modules/test_initializers.py +++ b/tests/functional/syntax/modules/test_initializers.py @@ -1445,6 +1445,27 @@ def test_initializes_on_modules_with_state_related_vars(module, make_input_bundl compile_code(main, input_bundle=input_bundle) +def test_initializes_on_modules_with_immutables(make_input_bundle): + lib = """ +foo: immutable(int128) + +@deploy +def __init__(): + foo = 2 + """ + + main = """ +import lib +initializes: lib + +@deploy +def __init__(): + lib.__init__() + """ + input_bundle = make_input_bundle({"lib.vy": lib, "main.vy": main}) + compile_code(main, input_bundle=input_bundle) + + stateless_modules = [ """ """, @@ -1462,13 +1483,6 @@ def foo(x: uint256, y: uint256) -> uint256: """ FOO: constant(int128) = 128 """, - """ -foo: immutable(int128) - -@deploy -def __init__(): - foo = 2 - """, ] From 1ab6433ebb86510f6ef905c24a6abcc3e32fd7c9 Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Tue, 5 Nov 2024 09:57:25 +0000 Subject: [PATCH 09/12] test transient in cancun --- tests/functional/codegen/features/test_transient.py | 12 ++++++++++++ tests/functional/syntax/modules/test_initializers.py | 7 ++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/functional/codegen/features/test_transient.py b/tests/functional/codegen/features/test_transient.py index 2532def85b..cba5157259 100644 --- a/tests/functional/codegen/features/test_transient.py +++ b/tests/functional/codegen/features/test_transient.py @@ -570,3 +570,15 @@ def foo() -> (uint256[3], uint256, uint256, uint256): c = get_contract(main, input_bundle=input_bundle) assert c.foo() == ([1, 2, 3], 1, 2, 42) + + +def test_transient_is_state(make_input_bundle): + lib = """ +message: transient(bool) + """ + main = """ +import lib +initializes: lib + """ + input_bundle = make_input_bundle({"lib.vy": lib, "main.vy": main}) + compile_code(main, input_bundle=input_bundle) diff --git a/tests/functional/syntax/modules/test_initializers.py b/tests/functional/syntax/modules/test_initializers.py index 5931be361a..74b7e04a41 100644 --- a/tests/functional/syntax/modules/test_initializers.py +++ b/tests/functional/syntax/modules/test_initializers.py @@ -1422,20 +1422,17 @@ def set_some_mod(): assert e.value._hint is None -stateful_var_modules = [ +storage_var_modules = [ """ phony: uint32 """, """ ended: public(bool) """, - """ -message: transient(bool) - """, ] -@pytest.mark.parametrize("module", stateful_var_modules) +@pytest.mark.parametrize("module", storage_var_modules) def test_initializes_on_modules_with_state_related_vars(module, make_input_bundle): main = """ import lib From 28c9be759bab15b223c126fadad105c7d4a1233b Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Tue, 5 Nov 2024 10:52:49 +0000 Subject: [PATCH 10/12] make init function as state --- .../syntax/modules/test_initializers.py | 25 +++++++++++++++++++ vyper/semantics/analysis/utils.py | 2 ++ 2 files changed, 27 insertions(+) diff --git a/tests/functional/syntax/modules/test_initializers.py b/tests/functional/syntax/modules/test_initializers.py index 74b7e04a41..27498c3883 100644 --- a/tests/functional/syntax/modules/test_initializers.py +++ b/tests/functional/syntax/modules/test_initializers.py @@ -1531,3 +1531,28 @@ def test_initializes_on_modules_with_initializes(make_input_bundle): """ input_bundle = make_input_bundle({"lib1.vy": lib1, "lib0.vy": lib0, "main.vy": main}) compile_code(main, input_bundle=input_bundle) + + +def test_initializes_on_modules_with_init_function(make_input_bundle): + lib = """ +interface Foo: + def foo(): payable + +@deploy +def __init__(): + extcall Foo(self).foo() + """ + main = """ +import lib +initializes: lib + +@deploy +def __init__(): + lib.__init__() + +@external +def foo(): + pass + """ + input_bundle = make_input_bundle({"lib.vy": lib, "main.vy": main}) + compile_code(main, input_bundle=input_bundle) diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index d092675aa2..7f167a353d 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -749,4 +749,6 @@ def is_stateless(module: vy_ast.Module): return False if isinstance(i, vy_ast.VariableDecl) and not i.is_constant: return False + if isinstance(i, vy_ast.FunctionDef) and i.name == "__init__": + return False return True From 0c5f645edc2d7671d47edb28bf04412a5f5657ff Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Tue, 5 Nov 2024 10:58:06 +0000 Subject: [PATCH 11/12] remove one wrong test --- tests/functional/syntax/modules/test_initializers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/functional/syntax/modules/test_initializers.py b/tests/functional/syntax/modules/test_initializers.py index 27498c3883..1af24f49e1 100644 --- a/tests/functional/syntax/modules/test_initializers.py +++ b/tests/functional/syntax/modules/test_initializers.py @@ -1467,11 +1467,6 @@ def __init__(): """ """, """ -@deploy -def __init__(): - pass - """, - """ @internal @pure def foo(x: uint256, y: uint256) -> uint256: From 47efc8e3b83327fab2f145c6d60b331438ecf3bb Mon Sep 17 00:00:00 2001 From: Sand Bubbles Date: Sat, 9 Nov 2024 10:01:04 +0000 Subject: [PATCH 12/12] refactor is_stateless into ModuleT method --- vyper/semantics/analysis/module.py | 3 +-- vyper/semantics/analysis/utils.py | 16 ---------------- vyper/semantics/types/module.py | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 9ed28d6267..6fbb011aa1 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -37,7 +37,6 @@ check_modifiability, get_exact_type_from_node, get_expr_info, - is_stateless, ) from vyper.semantics.data_locations import DataLocation from vyper.semantics.namespace import Namespace, get_namespace, override_global_namespace @@ -410,7 +409,7 @@ def visit_InitializesDecl(self, node): module_info = get_expr_info(module_ref).module_info if module_info is None: raise StructureException("Not a module!", module_ref) - if is_stateless(module_info.module_node): + if module_info.module_t.is_stateless(): raise StructureException( f"Cannot initialize a stateless module {module_info.alias}!", module_ref ) diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index 7f167a353d..9734087fc3 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -736,19 +736,3 @@ def validate_kwargs(node: vy_ast.Call, members: dict[str, VyperType], typeclass: msg = f"{typeclass} instantiation missing fields:" msg += f" {', '.join(list(missing))}" raise InstantiationException(msg, node) - - -def is_stateless(module: vy_ast.Module): - """ - Determine whether a module is stateless by examining its top-level declarations. - A module has state if it contains storage variables, transient variables, or - immutables, or if it includes a "uses" or "initializes" declaration. - """ - for i in module.body: - if isinstance(i, (vy_ast.InitializesDecl, vy_ast.UsesDecl)): - return False - if isinstance(i, vy_ast.VariableDecl) and not i.is_constant: - return False - if isinstance(i, vy_ast.FunctionDef) and i.name == "__init__": - return False - return True diff --git a/vyper/semantics/types/module.py b/vyper/semantics/types/module.py index dabeaf21b6..52873ab902 100644 --- a/vyper/semantics/types/module.py +++ b/vyper/semantics/types/module.py @@ -559,3 +559,18 @@ def immutable_section_bytes(self): @cached_property def interface(self): return InterfaceT.from_ModuleT(self) + + def is_stateless(self): + """ + Determine whether ModuleT is stateless by examining its top-level declarations. + A module has state if it contains storage variables, transient variables, or + immutables, or if it includes a "uses" or "initializes" declaration. + """ + for i in self._module.body: + if isinstance(i, (vy_ast.InitializesDecl, vy_ast.UsesDecl)): + return False + if isinstance(i, vy_ast.VariableDecl) and not i.is_constant: + return False + if isinstance(i, vy_ast.FunctionDef) and i.name == "__init__": + return False + return True