VYPR
Moderate severityNVD Advisory· Published Sep 18, 2023· Updated Sep 24, 2024

Vyper has incorrect re-entrancy lock when key is empty string

CVE-2023-42441

Description

Vyper is a Pythonic Smart Contract Language for the Ethereum Virtual Machine (EVM). Starting in version 0.2.9 and prior to version 0.3.10, locks of the type @nonreentrant("") or @nonreentrant('') do not produce reentrancy checks at runtime. This issue is fixed in version 0.3.10. As a workaround, ensure the lock name is a non-empty string.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

In Vyper versions 0.2.9 to 0.3.10, the `@nonreentrant("")` decorator fails to enforce reentrancy locks, allowing unsafe reentrant calls in smart contracts.

Vulnerability

Analysis

CVE-2023-42441 is a vulnerability in the Vyper smart contract language, affecting versions 0.2.9 through 0.3.10. The issue lies in the @nonreentrant decorator, which is designed to prevent reentrancy attacks by locking a function during execution. When the lock name is specified as an empty string ("" or ''), the decorator does not produce any reentrancy checks at runtime, effectively making the protection useless [1]. This was identified in the Vyper source code and fixed in version 0.3.10 [2].

Exploitation

An attacker can exploit this by crafting a contract that uses @nonreentrant("") on a function and then calling that function in a reentrant manner, such as via a fallback function that re-enters the same function. No special privileges are needed other than the ability to interact with the vulnerable contract. The root cause is that the compiler fails to correctly process an empty string key for the reentrancy lock, omitting the runtime check entirely [2].

Impact

Successful exploitation could allow an attacker to execute reentrant calls on functions meant to be protected, potentially leading to draining of funds, state manipulation, or other unintended behavior. The impact is limited to contracts compiled with vulnerable Vyper versions that use the empty string lock name [1].

Mitigation

The fix has been implemented in Vyper version 0.3.10 [1]. Developers should upgrade to this version or later. As a workaround, ensure that the lock name argument to @nonreentrant is always a non-empty string [1]. The vulnerability is tracked in the PyPI advisory database as PYSEC-2023-305 [4].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
vyperPyPI
>= 0.2.9, < 0.3.100.3.10

Affected products

1

Patches

1
0b740280c1e3

fix: only allow valid identifiers to be nonreentrant keys (#3605)

https://github.com/vyperlang/vyperCharles CooperSep 15, 2023via ghsa
9 files changed · +147 126
  • tests/parser/exceptions/test_structure_exception.py+20 3 modified
    @@ -56,9 +56,26 @@ def double_nonreentrant():
         """,
         """
     @external
    -@nonreentrant("B")
    -@nonreentrant("C")
    -def double_nonreentrant():
    +@nonreentrant(" ")
    +def invalid_nonreentrant_key():
    +    pass
    +    """,
    +    """
    +@external
    +@nonreentrant("")
    +def invalid_nonreentrant_key():
    +    pass
    +    """,
    +    """
    +@external
    +@nonreentrant("123")
    +def invalid_nonreentrant_key():
    +    pass
    +    """,
    +    """
    +@external
    +@nonreentrant("!123abcd")
    +def invalid_nonreentrant_key():
         pass
         """,
         """
    
  • tests/parser/features/decorators/test_nonreentrant.py+2 2 modified
    @@ -142,7 +142,7 @@ def set_callback(c: address):
     
     @external
     @payable
    -@nonreentrant('default')
    +@nonreentrant("lock")
     def protected_function(val: String[100], do_callback: bool) -> uint256:
         self.special_value = val
         _amount: uint256 = msg.value
    @@ -166,7 +166,7 @@ def unprotected_function(val: String[100], do_callback: bool):
     
     @external
     @payable
    -@nonreentrant('default')
    +@nonreentrant("lock")
     def __default__():
         pass
         """
    
  • tests/parser/test_call_graph_stability.py+1 1 modified
    @@ -6,8 +6,8 @@
     from hypothesis import given, settings
     
     import vyper.ast as vy_ast
    +from vyper.ast.identifiers import RESERVED_KEYWORDS
     from vyper.compiler.phases import CompilerData
    -from vyper.semantics.namespace import RESERVED_KEYWORDS
     
     
     def _valid_identifier(attr):
    
  • tests/parser/types/test_identifier_naming.py+1 1 modified
    @@ -1,10 +1,10 @@
     import pytest
     
     from vyper.ast.folding import BUILTIN_CONSTANTS
    +from vyper.ast.identifiers import RESERVED_KEYWORDS
     from vyper.builtins.functions import BUILTIN_FUNCTIONS
     from vyper.codegen.expr import ENVIRONMENT_VARIABLES
     from vyper.exceptions import NamespaceCollision, StructureException, SyntaxException
    -from vyper.semantics.namespace import RESERVED_KEYWORDS
     from vyper.semantics.types.primitives import AddressT
     
     BUILTIN_CONSTANTS = set(BUILTIN_CONSTANTS.keys())
    
  • vyper/ast/identifiers.py+111 0 added
    @@ -0,0 +1,111 @@
    +import re
    +
    +from vyper.exceptions import StructureException
    +
    +
    +def validate_identifier(attr, ast_node=None):
    +    if not re.match("^[_a-zA-Z][a-zA-Z0-9_]*$", attr):
    +        raise StructureException(f"'{attr}' contains invalid character(s)", ast_node)
    +    if attr.lower() in RESERVED_KEYWORDS:
    +        raise StructureException(f"'{attr}' is a reserved keyword", ast_node)
    +
    +
    +# https://docs.python.org/3/reference/lexical_analysis.html#keywords
    +# note we don't technically need to block all python reserved keywords,
    +# but do it for hygiene
    +_PYTHON_RESERVED_KEYWORDS = {
    +    "False",
    +    "None",
    +    "True",
    +    "and",
    +    "as",
    +    "assert",
    +    "async",
    +    "await",
    +    "break",
    +    "class",
    +    "continue",
    +    "def",
    +    "del",
    +    "elif",
    +    "else",
    +    "except",
    +    "finally",
    +    "for",
    +    "from",
    +    "global",
    +    "if",
    +    "import",
    +    "in",
    +    "is",
    +    "lambda",
    +    "nonlocal",
    +    "not",
    +    "or",
    +    "pass",
    +    "raise",
    +    "return",
    +    "try",
    +    "while",
    +    "with",
    +    "yield",
    +}
    +_PYTHON_RESERVED_KEYWORDS = {s.lower() for s in _PYTHON_RESERVED_KEYWORDS}
    +
    +# Cannot be used for variable or member naming
    +RESERVED_KEYWORDS = _PYTHON_RESERVED_KEYWORDS | {
    +    # decorators
    +    "public",
    +    "external",
    +    "nonpayable",
    +    "constant",
    +    "immutable",
    +    "transient",
    +    "internal",
    +    "payable",
    +    "nonreentrant",
    +    # "class" keywords
    +    "interface",
    +    "struct",
    +    "event",
    +    "enum",
    +    # EVM operations
    +    "unreachable",
    +    # special functions (no name mangling)
    +    "init",
    +    "_init_",
    +    "___init___",
    +    "____init____",
    +    "default",
    +    "_default_",
    +    "___default___",
    +    "____default____",
    +    # more control flow and special operations
    +    "range",
    +    # more special operations
    +    "indexed",
    +    # denominations
    +    "ether",
    +    "wei",
    +    "finney",
    +    "szabo",
    +    "shannon",
    +    "lovelace",
    +    "ada",
    +    "babbage",
    +    "gwei",
    +    "kwei",
    +    "mwei",
    +    "twei",
    +    "pwei",
    +    # sentinal constant values
    +    # TODO remove when these are removed from the language
    +    "zero_address",
    +    "empty_bytes32",
    +    "max_int128",
    +    "min_int128",
    +    "max_decimal",
    +    "min_decimal",
    +    "max_uint256",
    +    "zero_wei",
    +}
    
  • vyper/exceptions.py+3 1 modified
    @@ -54,7 +54,9 @@ def __init__(self, message="Error Message not found.", *items):
                 # support older exceptions that don't annotate - remove this in the future!
                 self.lineno, self.col_offset = items[0][:2]
             else:
    -            self.annotations = items
    +            # strip out None sources so that None can be passed as a valid
    +            # annotation (in case it is only available optionally)
    +            self.annotations = [k for k in items if k is not None]
     
         def with_annotation(self, *annotations):
             """
    
  • vyper/semantics/namespace.py+3 116 modified
    @@ -1,12 +1,7 @@
     import contextlib
    -import re
    -
    -from vyper.exceptions import (
    -    CompilerPanic,
    -    NamespaceCollision,
    -    StructureException,
    -    UndeclaredDefinition,
    -)
    +
    +from vyper.ast.identifiers import validate_identifier
    +from vyper.exceptions import CompilerPanic, NamespaceCollision, UndeclaredDefinition
     from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions
     
     
    @@ -121,111 +116,3 @@ def override_global_namespace(ns):
         finally:
             # unclobber
             _namespace = tmp
    -
    -
    -def validate_identifier(attr):
    -    if not re.match("^[_a-zA-Z][a-zA-Z0-9_]*$", attr):
    -        raise StructureException(f"'{attr}' contains invalid character(s)")
    -    if attr.lower() in RESERVED_KEYWORDS:
    -        raise StructureException(f"'{attr}' is a reserved keyword")
    -
    -
    -# https://docs.python.org/3/reference/lexical_analysis.html#keywords
    -# note we don't technically need to block all python reserved keywords,
    -# but do it for hygiene
    -_PYTHON_RESERVED_KEYWORDS = {
    -    "False",
    -    "None",
    -    "True",
    -    "and",
    -    "as",
    -    "assert",
    -    "async",
    -    "await",
    -    "break",
    -    "class",
    -    "continue",
    -    "def",
    -    "del",
    -    "elif",
    -    "else",
    -    "except",
    -    "finally",
    -    "for",
    -    "from",
    -    "global",
    -    "if",
    -    "import",
    -    "in",
    -    "is",
    -    "lambda",
    -    "nonlocal",
    -    "not",
    -    "or",
    -    "pass",
    -    "raise",
    -    "return",
    -    "try",
    -    "while",
    -    "with",
    -    "yield",
    -}
    -_PYTHON_RESERVED_KEYWORDS = {s.lower() for s in _PYTHON_RESERVED_KEYWORDS}
    -
    -# Cannot be used for variable or member naming
    -RESERVED_KEYWORDS = _PYTHON_RESERVED_KEYWORDS | {
    -    # decorators
    -    "public",
    -    "external",
    -    "nonpayable",
    -    "constant",
    -    "immutable",
    -    "transient",
    -    "internal",
    -    "payable",
    -    "nonreentrant",
    -    # "class" keywords
    -    "interface",
    -    "struct",
    -    "event",
    -    "enum",
    -    # EVM operations
    -    "unreachable",
    -    # special functions (no name mangling)
    -    "init",
    -    "_init_",
    -    "___init___",
    -    "____init____",
    -    "default",
    -    "_default_",
    -    "___default___",
    -    "____default____",
    -    # more control flow and special operations
    -    "range",
    -    # more special operations
    -    "indexed",
    -    # denominations
    -    "ether",
    -    "wei",
    -    "finney",
    -    "szabo",
    -    "shannon",
    -    "lovelace",
    -    "ada",
    -    "babbage",
    -    "gwei",
    -    "kwei",
    -    "mwei",
    -    "twei",
    -    "pwei",
    -    # sentinal constant values
    -    # TODO remove when these are removed from the language
    -    "zero_address",
    -    "empty_bytes32",
    -    "max_int128",
    -    "min_int128",
    -    "max_decimal",
    -    "min_decimal",
    -    "max_uint256",
    -    "zero_wei",
    -}
    
  • vyper/semantics/types/base.py+1 1 modified
    @@ -3,6 +3,7 @@
     
     from vyper import ast as vy_ast
     from vyper.abi_types import ABIType
    +from vyper.ast.identifiers import validate_identifier
     from vyper.exceptions import (
         CompilerPanic,
         InvalidLiteral,
    @@ -12,7 +13,6 @@
         UnknownAttribute,
     )
     from vyper.semantics.analysis.levenshtein_utils import get_levenshtein_error_suggestions
    -from vyper.semantics.namespace import validate_identifier
     
     
     # Some fake type with an overridden `compare_type` which accepts any RHS
    
  • vyper/semantics/types/function.py+5 1 modified
    @@ -5,6 +5,7 @@
     from typing import Any, Dict, List, Optional, Tuple
     
     from vyper import ast as vy_ast
    +from vyper.ast.identifiers import validate_identifier
     from vyper.ast.validation import validate_call_args
     from vyper.exceptions import (
         ArgumentException,
    @@ -220,7 +221,10 @@ def from_FunctionDef(
                             msg = "Nonreentrant decorator disallowed on `__init__`"
                             raise FunctionDeclarationException(msg, decorator)
     
    -                    kwargs["nonreentrant"] = decorator.args[0].value
    +                    nonreentrant_key = decorator.args[0].value
    +                    validate_identifier(nonreentrant_key, decorator.args[0])
    +
    +                    kwargs["nonreentrant"] = nonreentrant_key
     
                     elif isinstance(decorator, vy_ast.Name):
                         if FunctionVisibility.is_valid_value(decorator.id):
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.