-
-
Notifications
You must be signed in to change notification settings - Fork 818
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
fix[lang]: stateless modules should not be initialized #4347
base: master
Are you sure you want to change the base?
fix[lang]: stateless modules should not be initialized #4347
Conversation
vyper/semantics/analysis/utils.py
Outdated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this belongs better on ModuleT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, moved it.
vyper/semantics/analysis/module.py
Outdated
@@ -409,6 +410,10 @@ 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): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe should we also have this restriction on UsesDecl
-- @cyberthirst wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, missed the ping
i think that makes sense - uses
indicates that we're accessing the used module's state - and thus the module should actually have state
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this lgtm. paging @cyberthirst to take a look as well
@@ -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): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's please add a comment here that we're testing the initialization and how it relates to state
@@ -1,5 +1,7 @@ | |||
def test_export_nonreentrant(make_input_bundle, get_contract, tx_failed): | |||
lib1 = """ | |||
phony: uint32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this feels off, see my comment at is_stateless
vyper/semantics/types/module.py
Outdated
@@ -559,3 +559,19 @@ def immutable_section_bytes(self): | |||
@cached_property | |||
def interface(self): | |||
return InterfaceT.from_ModuleT(self) | |||
|
|||
@cached_property | |||
def is_stateless(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this definition omits reentrancy
locks - for cancun those are created inside transient
which we define as state. the locks aren't implemented as variables so this check would miss them
to further support the statement above, consider following:
#main.vy
import lib1
#initializes: lib1
exports: lib1.foo
#lib1.vy
@external
@nonreentrant
def foo() -> uint256:
return 5
which yields:
vyper.exceptions.ImmutableViolation: Cannot access `lib1` state!
note that use of the `@nonreentrant` decorator is also considered state access
contract "tests/custom/test.vy:5", line 5:9
4
---> 5 exports: lib1.foo
----------------^
6
(hint: add `uses: lib1` or `initializes: lib1` as a top-level statement to your contract)
transient variables, or immutables, or if it includes a "uses" or | ||
"initializes" declaration. | ||
""" | ||
if len(self.initializes_decls) > 0 or len(self.uses_decls) > 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we really check for uses_decl
? uses
refer to another's module state. or maybe i'm just debating the definition of "statlessness" - if it's about not accessing any state (in the sense of purity) then this code's ok
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #4347 +/- ##
==========================================
- Coverage 91.86% 90.91% -0.95%
==========================================
Files 119 119
Lines 16640 16654 +14
Branches 2801 2807 +6
==========================================
- Hits 15286 15141 -145
- Misses 929 1057 +128
- Partials 425 456 +31 ☔ View full report in Codecov by Sentry. |
This should be good for another review. |
vyper/semantics/types/module.py
Outdated
@@ -559,3 +559,24 @@ def immutable_section_bytes(self): | |||
@cached_property | |||
def interface(self): | |||
return InterfaceT.from_ModuleT(self) | |||
|
|||
@cached_property | |||
def is_not_initializable(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we can flip the logic and rename to is_initializable
?
vyper/semantics/types/module.py
Outdated
for fun in self.functions.values(): | ||
if fun.nonreentrant: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
convention: ContractFunctionT
is usually named fn_t
in loops (or other places with small scope)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for fun in self.functions.values(): | |
if fun.nonreentrant: | |
for fn_t in self.functions.values(): | |
if fn_t.nonreentrant: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually maybe this should be a loop over exposed functions (per @cyberthirst )?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that would skip internal functions with nonreentrancy locks - is that intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, not intentional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, maybe the exposed_functions
thing is a red herring -- we don't care about exposed_functions per se, we care about state in the recursion, but that is handled by checking if there are any initializes decls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup, i think as you said - we dont need to recurse in the function as the state of the imported modules is already handled. So the self.functions.values()
is okay?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes i think so, unless @cyberthirst thinks otherwise.
tests/unit/compiler/test_abi.py
Outdated
@@ -361,6 +363,8 @@ def __init__(): | |||
def test_event_export_from_function_export(make_input_bundle): | |||
# test events used in exported functions are exported | |||
lib1 = """ | |||
phony: uint32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe we shouldn't add phony here -- the code change is telling us to remove initializes: lib1
I've marked the In the tests, I removed the |
What I did
Forbid
initializes
on stateless modules as defined in #4050.How I did it
Define modules with storage variables, transient variables or with
uses
andinitializes
as stateful.When using
initializes
on stateless module, raise exception.How to verify it
Commit message
Commit message for the final, squashed PR. (Optional, but reviewers will appreciate it! Please see our commit message style guide for what we would ideally like to see in a commit message.)
Description for the changelog
Cute Animal Picture