All vulnerabilities
GHSA-2489-32vh-8rrc

vyper: Function variables can replace imported constant values and default arguments

vyper
CVSS
n/aMedium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed inNone listedNo source names a fixed version yet
Published7 Oct 2026Updated 7 Oct 2026GitHub

What fixes it

No source in this snapshot names a fixed version.

Description

Summary

Vyper can resolve names inside unfoldable constants and omitted default arguments against the function using them rather than their original declaration in an imported module. A same-named parameter, local, or loop variable can silently replace the intended value, including with values outside the declared type's range.

Details

These expressions are typechecked where declared but compiled at their use site. In vyper/codegen/expr.py, parse_Name checks context.vars before using the name's semantic binding. Consequently, a variable in the using function can override a reference that was validated as a module constant or immutable.

Unfoldable constant values are expanded by parse_Name and parse_Attribute. Omitted internal defaults are compiled through Expr(default, context) in self_call.py; external defaults are similarly compiled in the function's context by external_function.py.

The colliding declaration must live in an imported module: declaring the constant and same-named parameter in one module fails with NamespaceCollision. Expansion is recursive, so qualifying the outer constant as lib.CS does not protect bare names inside its value. Constant and internal-default expansion can capture in-scope parameters, locals, or loop variables; external defaults can capture parameters through expanded imported constants.

The replacement can also bypass type constraints: primitive setters trust internally encoded values and do not necessarily clamp them to the destination type. The using function's uint256 word can therefore enter a nominal uint8 slot unchanged. Fully folded expressions avoid the name lookup and are unaffected by this trigger.

Proof of concept

# lib.vy
struct S:
    a: uint8

E: constant(uint8) = empty(uint8)
CS: constant(S) = S(a=E)

@internal
@pure
def f(x: uint8 = E) -> uint8:
    return x
# main.vy
import lib

@external
@pure
def test(E: uint256) -> (uint256, uint256):
    return convert(lib.f(), uint256), convert(lib.CS.a, uint256)

Both results should be 0. Instead, test(300) returns (300, 300): the function parameter E replaces the library binding, and the value survives despite the nominal uint8 type.

Impact

The using function's variables can replace module-defined defaults or constants used in validation, routing, flags, or accounting. An external user controls the replacement only when the captured variable depends on user input, as in the PoC. The name collision is part of the developer-written source.

Values outside narrow integer ranges and noncanonical booleans can propagate internally or appear in ABI output. Exploitation requires a matching name and an expression that remains unfoldable; consequences depend on how the contract uses the captured value.

Affected versions

The issue affects versions from **0.4.0b1 through master (at 17e925db)

Remediation

Preserve declaration bindings when compiling constant values and default arguments, rather than resolving their names through the using function's local scope. Also enforce primitive type compatibility and destination constraints during internal assignments.

As a workaround, use literal or fully folded values, or avoid collisions with names throughout the expanded expressions. Explicitly passing a qualified immutable can avoid default capture; qualifying an unfoldable constant alone is insufficient. Deployed contracts may require recompilation and redeployment or an upgrade.

Text from GitHub Advisory Database

Affected versions 1

  • vyper, pip>= 0.4.0b1GitHub