Skip to content

feat[lang]: nonreentrancy by default#4563

Merged
charles-cooper merged 86 commits intovyperlang:masterfrom
charles-cooper:feat/nonreentrancy-by-default
Apr 28, 2025
Merged

feat[lang]: nonreentrancy by default#4563
charles-cooper merged 86 commits intovyperlang:masterfrom
charles-cooper:feat/nonreentrancy-by-default

Conversation

@charles-cooper
Copy link
Copy Markdown
Member

@charles-cooper charles-cooper commented Apr 6, 2025

What I did

implements #3380, #4509

How I did it

How to verify it

Commit message

to control the default nonreentrancy behavior on a module-by-module
basis. when the pragma is set to `on`, all the external functions and
getters in the file will be nonreentrant (excluding pure functions,
which can never be nonreentrant).

this is a step towards making nonreentrancy the default for the
language while providing a backwards-compatible upgrade path for users
in the 0.4.x series. in the future (probably starting in 0.5.0), the
default will be `pragma nonreentrancy on`, which will have to be opted
out of with the pragma.

Description for the changelog

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 6, 2025

Codecov Report

❌ Patch coverage is 91.37931% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.49%. Comparing base (756ff72) to head (15e51c8).
⚠️ Report is 61 commits behind head on master.

Files with missing lines Patch % Lines
vyper/ast/pre_parser.py 83.78% 5 Missing and 1 partial ⚠️
vyper/ast/nodes.py 77.77% 1 Missing and 1 partial ⚠️
vyper/compiler/settings.py 33.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4563      +/-   ##
==========================================
- Coverage   92.50%   92.49%   -0.01%     
==========================================
  Files         128      128              
  Lines       18432    18506      +74     
  Branches     3191     3208      +17     
==========================================
+ Hits        17050    17118      +68     
- Misses        941      944       +3     
- Partials      441      444       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cyberthirst
Copy link
Copy Markdown
Collaborator

as discussed offline, we should also process the pragma for imports

@cyberthirst
Copy link
Copy Markdown
Collaborator

this fails, imo we should only restrict the pragma to external functions

def test_nonreentrant_decorator(get_contract):
    code = """
# pragma nonreentrancy on

def foo():
    u: uint256 = 1

@external
def bar():
    self.foo()
    """
    c = get_contract(code)

    c.bar()

@cyberthirst
Copy link
Copy Markdown
Collaborator

# pragma nonreentrancy on

@external
@nonreentrant
def __default__():
    pass
vyper.exceptions.StructureException: used @nonreentrant decorator, but `#pragma nonreentrancy` is set

  contract "tests/custom/test.vy:5", line 5:0 
       4 @nonreentrant
  ---> 5 def __default__():
  -------^
       6     pass

which is problematic because __default__ is handled as reentrant by default

@charles-cooper charles-cooper force-pushed the feat/nonreentrancy-by-default branch from 6516d49 to 655ec47 Compare April 11, 2025 06:50
@cyberthirst
Copy link
Copy Markdown
Collaborator

per Anatomist:

Invalid variable annotations such as `public(public(TYPE))` are accepted by the compiler due to missing duplication checks for the first two variable traits.

@cyberthirst
Copy link
Copy Markdown
Collaborator

Per Anatomist:

Additionally, the grammar definition in the experimental parser is less restrictive than the actual implementation. For example, immutable(transient(TYPE)) is allowed in the experimental parser but should not be allowed according to the actual implementation.

I don't think this is a huge deal but if we can bake semantics into the grammar while not sacrificing simplicity, we should do it.

@cyberthirst
Copy link
Copy Markdown
Collaborator

Let's make sure the docs are up-to-date with the latest changes.

Including a comment from Anatomist. Didn't yet check whether it's relevant for the latest commit:

We recommend a few changes to the documentation:
• Since __default__ is no longer treated as a special case, the documentation should be updated accordingly.
• To clarify the purpose of the nonreentrant pragma, the documentation should specify that it applies only to non-pure external functions.
• Documentation for the reentrant modifier applied to global variables should be added.

@cyberthirst
Copy link
Copy Markdown
Collaborator

Per Anatomist:

Additionally, the grammar definition in the experimental parser is less restrictive than the actual implementation. For example, immutable(transient(TYPE)) is allowed in the experimental parser but should not be allowed according to the actual implementation.

I don't think this is a huge deal but if we can bake semantics into the grammar while not sacrificing simplicity, we should do it.

@trocher further mentions:

In grammar.lark, the grammar for constants is defined as:

constant: "constant" "(" type ")"
constant_private: ChainSecurity ":" constant
constant_with_getter: ChainSecurity ":" "public" "(" constant ")"
constant_def: (constant_private | constant_with_getter) "=" expr

However, this does not account for the reentrant decorator, which is a valid decorator for constant variables, since the below code is valid Vyper code, but fails to be parsed using the lark grammar:

a: public(reentrant(constant(uint256))) = 1

@charles-cooper
Copy link
Copy Markdown
Member Author

Per Anatomist:

Additionally, the grammar definition in the experimental parser is less restrictive than the actual implementation. For example, immutable(transient(TYPE)) is allowed in the experimental parser but should not be allowed according to the actual implementation.

I don't think this is a huge deal but if we can bake semantics into the grammar while not sacrificing simplicity, we should do it.

i think it's fine to allow these kinds of things in the grammar, immutable(transient(TYPE)) is arguably a semantic error, not a syntactical error

@cyberthirst
Copy link
Copy Markdown
Collaborator

Per @trocher

In the case no nonreentrant functions are present in the contract, but only public state variables with the pragma nonreentrancy on, the _generate_layout_export_r() function will not output the slot for the key, although it is allocated with _allocate_layout_r().

The following contracts yield the given layout when running vyper -f layout file.vy, although the transient storage slot 0 is allocated and used by the public getter function for the a variable:

# pragma nonreentrancy on

a: public(constant(uint256)) = 1

b: transient(uint256)
{"transient_storage_layout": {"b": {"type": "uint256", "n_slots": 1, "slot": 1}}}

@cyberthirst
Copy link
Copy Markdown
Collaborator

Per @trocher

In the case no nonreentrant functions are present in the contract, but only public state variables with the pragma nonreentrancy on, the _generate_layout_export_r() function will not output the slot for the key, although it is allocated with _allocate_layout_r().

The following contracts yield the given layout when running vyper -f layout file.vy, although the transient storage slot 0 is allocated and used by the public getter function for the a variable:

# pragma nonreentrancy on

a: public(constant(uint256)) = 1

b: transient(uint256)
{"transient_storage_layout": {"b": {"type": "uint256", "n_slots": 1, "slot": 1}}}

This has further impact down the line because we test for round-trip when using overrides. Because of the missing key, contracts will stop compiling:

# pragma evm-version shanghai

a: public(uint256)
{"a": {"type": "uint256", "n_slots": 1, "slot": 0}, "$.nonreentrant_key":  {"type": "nonreentrant lock", "n_slots": 1, "slot": 20}}

vyper.exceptions.CompilerPanic: Computed storage layout does not match override file!
expected: {"a": {"type": "uint256", "n_slots": 1, "slot": 0}, "$.nonreentrant_key": {"type": "nonreentrant lock", "n_slots": 1, "slot": 20}}

got:
{"a": {"type": "uint256", "n_slots": 1, "slot": 0}}

This is an unhandled internal compiler error. Please create an issue on Github to notify the developers!
https://github.com/vyperlang/vyper/issues/new?template=bug.md

note that i used shanghai because for roundtrip we use storage

@cyberthirst
Copy link
Copy Markdown
Collaborator

per Anatomist:

Invalid variable annotations such as `public(public(TYPE))` are accepted by the compiler due to missing duplication checks for the first two variable traits.

lifted this into a separate issue: #4610

@cyberthirst
Copy link
Copy Markdown
Collaborator

@trocher further mentions:

In grammar.lark, the grammar for constants is defined as:

constant: "constant" "(" type ")"
constant_private: ChainSecurity ":" constant
constant_with_getter: ChainSecurity ":" "public" "(" constant ")"
constant_def: (constant_private | constant_with_getter) "=" expr

However, this does not account for the reentrant decorator, which is a valid decorator for constant variables, since the below code is valid Vyper code, but fails to be parsed using the lark grammar:

a: public(reentrant(constant(uint256))) = 1

lifted this to: #4611

@cyberthirst
Copy link
Copy Markdown
Collaborator

Per @trocher

In the case no nonreentrant functions are present in the contract, but only public state variables with the pragma nonreentrancy on, the _generate_layout_export_r() function will not output the slot for the key, although it is allocated with _allocate_layout_r().

The following contracts yield the given layout when running vyper -f layout file.vy, although the transient storage slot 0 is allocated and used by the public getter function for the a variable:

# pragma nonreentrancy on

a: public(constant(uint256)) = 1

b: transient(uint256)
{"transient_storage_layout": {"b": {"type": "uint256", "n_slots": 1, "slot": 1}}}

lifted this up to #4612

The nonreentrant pragma
-----------------------

Beginning in 0.4.2, the ``#pragma nonreentrancy on`` pragma is available, and it enables nonreentrancy on all functions and public getters in the file. This is to prepare for a future release, probably in the 0.5.x series, where nonreentrant locks will be enabled by default language-wide.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on all external functions

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should document how it behaves for:

  • internal functions
  • pure external functions
  • view external functions

# pragma nonreentrancy on

x: public(uint256) # this has view-only reentrancy turned on
y: public(reentrant(uint256)) # this does not have view-only reentrancy
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does not have?

def __default__():
# the default function is reentrant by default, even when
# the nonreentrancy pragma is set
# this function is nonreentrant!
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for make_a_call the comment says # this function is protected from re-entrancy, the inconsistency makes it a bit hard to read

.. code-block:: vyper
# pragma nonreentrancy on

x: public(uint256) # this has view-only reentrancy turned on
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getters are by default nonreentrant with the pragma on

@charles-cooper charles-cooper enabled auto-merge (squash) April 28, 2025 13:21
@charles-cooper charles-cooper merged commit 59baa1b into vyperlang:master Apr 28, 2025
159 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants