VYPR
Moderate severityNVD Advisory· Published May 19, 2023· Updated Feb 12, 2025

Nonpayable default functions are sometimes payable in vyper

CVE-2023-32675

Description

Vyper is a pythonic Smart Contract Language for the ethereum virtual machine. In contracts with more than one regular nonpayable function, it is possible to send funds to the default function, even if the default function is marked nonpayable. This applies to contracts compiled with vyper versions prior to 0.3.8. This issue was fixed by the removal of the global calldatasize check in commit 02339dfda. Users are advised to upgrade to version 0.3.8. Users unable to upgrade should avoid use of nonpayable default functions.

AI Insight

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

In Vyper <0.3.8, contracts with multiple nonpayable functions allow funds to be sent to a nonpayable default function, bypassing intended restrictions.

Vulnerability

Vyper versions prior to 0.3.8 contain a flaw in the global calldatasize check that allows funds to be sent to a nonpayable default function when a contract has more than one regular nonpayable function [1]. The check intended to prevent ether transfers to nonpayable functions was improperly bypassed.

Exploitation

An attacker can exploit this by sending a transaction with arbitrary calldata (or empty data) to the contract, triggering the default function even if it is marked nonpayable. The issue occurs because the compiler only validated the calldatasize against the number of regular functions, not the default function [4].

Impact

This allows unintended ether transfers to the contract, potentially locking funds or violating the contract's economic invariants. The vulnerability undermines the guarantee that nonpayable functions cannot receive ether.

Mitigation

The fix was implemented in commit 02339dfda and released in Vyper 0.3.8 [4]. Users should upgrade to this version. As a workaround, avoid using nonpayable default functions in contracts with multiple other nonpayable functions [1].

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.3.80.3.8

Affected products

1

Patches

2
903727006c1e

Merge pull request from GHSA-vxmm-cwh2-q762

https://github.com/vyperlang/vyperCharles CooperMay 19, 2023via ghsa
1 file changed · +21 0
  • tests/parser/features/decorators/test_payable.py+21 0 modified
    @@ -372,3 +372,24 @@ def __default__():
         assert_tx_failed(
             lambda: w3.eth.send_transaction({"to": c.address, "value": 100, "data": "0x12345678"})
         )
    +
    +
    +def test_batch_nonpayable(get_contract, w3, assert_tx_failed):
    +    code = """
    +@external
    +def foo() -> bool:
    +    return True
    +
    +@external
    +def __default__():
    +    pass
    +    """
    +
    +    c = get_contract(code)
    +    w3.eth.send_transaction({"to": c.address, "value": 0, "data": "0x12345678"})
    +    data = bytes([1, 2, 3, 4])
    +    for i in range(5):
    +        calldata = "0x" + data[:i].hex()
    +        assert_tx_failed(
    +            lambda: w3.eth.send_transaction({"to": c.address, "value": 100, "data": calldata})
    +        )
    
02339dfda0f3

refactor: optimize calldatasize check (#3104)

https://github.com/vyperlang/vyperemc415Jan 20, 2023via ghsa
3 files changed · +24 9
  • tests/parser/features/test_init.py+6 5 modified
    @@ -15,12 +15,13 @@ def __init__(a: uint256):
         assert c.val() == 123
     
         # Make sure the init code does not access calldata
    -    opcodes = vyper.compile_code(code, ["opcodes"])["opcodes"].split(" ")
    -    ir_return_idx = opcodes.index("JUMP")
    +    assembly = vyper.compile_code(code, ["asm"])["asm"].split(" ")
    +    ir_return_idx_start = assembly.index("{")
    +    ir_return_idx_end = assembly.index("}")
     
    -    assert "CALLDATALOAD" in opcodes
    -    assert "CALLDATACOPY" not in opcodes[:ir_return_idx]
    -    assert "CALLDATALOAD" not in opcodes[:ir_return_idx]
    +    assert "CALLDATALOAD" in assembly
    +    assert "CALLDATACOPY" not in assembly[:ir_return_idx_start] + assembly[ir_return_idx_end:]
    +    assert "CALLDATALOAD" not in assembly[:ir_return_idx_start] + assembly[ir_return_idx_end:]
     
     
     def test_init_calls_internal(get_contract, assert_compile_failed, assert_tx_failed):
    
  • vyper/codegen/function_definitions/external_function.py+18 1 modified
    @@ -123,7 +123,24 @@ def handler_for(calldata_kwargs, default_kwargs):
     
             ret.append(["goto", sig.external_function_base_entry_label])
     
    -        ret = ["if", ["eq", "_calldata_method_id", method_id], ret]
    +        method_id_check = ["eq", "_calldata_method_id", method_id]
    +
    +        # if there is a function whose selector is 0, it won't be distinguished
    +        # from the case where nil calldata is supplied, b/c calldataload loads
    +        # 0s past the end of physical calldata (cf. yellow paper).
    +        # since supplying 0 calldata is expected to trigger the fallback fn,
    +        # we check that calldatasize > 0, which distinguishes the 0 selector
    +        # from the fallback function "selector"
    +        # (equiv. to "all selectors not in the selector table").
    +
    +        # note: cases where not enough calldata is supplied (besides
    +        # calldatasize==0) are not addressed here b/c a calldatasize
    +        # well-formedness check is already present in the function body
    +        # as part of abi validation
    +        if method_id.value == 0:
    +            method_id_check = ["and", ["gt", "calldatasize", 0], method_id_check]
    +
    +        ret = ["if", method_id_check, ret]
             return ret
     
         ret = ["seq"]
    
  • vyper/codegen/module.py+0 3 modified
    @@ -120,9 +120,6 @@ def _runtime_ir(runtime_functions, all_sigs, global_ctx):
     
         runtime = [
             "seq",
    -        # check that calldatasize is at least 4, otherwise
    -        # calldataload will load zeros (cf. yellow paper).
    -        ["if", ["lt", "calldatasize", 4], ["goto", "fallback"]],
             ["with", "_calldata_method_id", shr(224, ["calldataload", 0]), selector_section],
             close_selector_section,
             ["label", "fallback", ["var_list"], fallback_ir],
    

Vulnerability mechanics

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

References

7

News mentions

0

No linked articles in our index yet.