Vyper's raw_call with outsize=0 and revert_on_failure=False returns incorrect success value
Description
Vyper is a Pythonic Smart Contract Language for the ethereum virtual machine. In versions 0.3.1 through 0.3.7, the Vyper compiler generates the wrong bytecode. Any contract that uses the raw_call with revert_on_failure=False and max_outsize=0 receives the wrong response from raw_call. Depending on the memory garbage, the result can be either True or False. A patch is available and, as of time of publication, anticipated to be part of Vyper 0.3.8. As a workaround, one may always put max_outsize>0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Vyper 0.3.1–0.3.7 contracts using raw_call with revert_on_failure=False and max_outsize=0 may return incorrect boolean due to bytecode bug.
In Vyper versions 0.3.1 through 0.3.7, a bytecode generation flaw in the compiler causes the built-in raw_call function to return an incorrect boolean value when invoked with revert_on_failure=False and max_outsize=0 [1][2]. The return value depends on uninitialized memory, making it unpredictable.
To exploit, an attacker would need to interact with a contract that uses raw_call in this specific configuration. No special privileges are required, as the bug occurs during normal operation. However, the attacker would need to understand the specific conditions under which the memory garbage yields True or False [4].
This can lead to logical errors in smart contracts that rely on the return value of raw_call for control flow. For example, a contract might incorrectly assume a call succeeded when it failed, or vice versa, potentially leading to loss of funds or unauthorized state changes [2][4].
A patch is available in Vyper 0.3.8, and users are advised to upgrade [2]. As a workaround, setting max_outsize>0 avoids the bug [1]. The Lido protocol identified this issue in their gate-seals contract and applied a fix [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.
| Package | Affected versions | Patched versions |
|---|---|---|
vyperPyPI | >= 0.3.1, < 0.3.8 | 0.3.8 |
Affected products
1Patches
1851f7a1b3aa2Merge pull request from GHSA-w9g2-3w7p-72g9
2 files changed · +33 −7
tests/parser/functions/test_raw_call.py+18 −0 modified@@ -296,8 +296,10 @@ def test_checkable_raw_call(get_contract, assert_tx_failed): def fail1(should_raise: bool): if should_raise: raise "fail" + # test both paths for raw_call - # they are different depending if callee has or doesn't have returntype +# (fail2 fails because of staticcall) @external def fail2(should_raise: bool) -> int128: if should_raise: @@ -320,6 +322,7 @@ def foo(_addr: address, should_raise: bool) -> uint256: ) assert success == (not should_raise) return 1 + @external @view def bar(_addr: address, should_raise: bool) -> uint256: @@ -334,6 +337,19 @@ def bar(_addr: address, should_raise: bool) -> uint256: ) assert success == (not should_raise) return 2 + +# test max_outsize not set case +@external +@nonpayable +def baz(_addr: address, should_raise: bool) -> uint256: + success: bool = True + success = raw_call( + _addr, + _abi_encode(should_raise, method_id=method_id("fail1(bool)")), + revert_on_failure=False, + ) + assert success == (not should_raise) + return 3 """ target = get_contract(target_source) @@ -343,6 +359,8 @@ def bar(_addr: address, should_raise: bool) -> uint256: assert caller.foo(target.address, False) == 1 assert caller.bar(target.address, True) == 2 assert caller.bar(target.address, False) == 2 + assert caller.baz(target.address, True) == 3 + assert caller.baz(target.address, False) == 3 uncompilable_code = [
vyper/builtins/functions.py+15 −7 modified@@ -1188,7 +1188,9 @@ def build_IR(self, expr, args, kwargs, context): if revert_on_failure: typ = bytes_ty + # check the call success flag, and store returndata in memory ret_ir = ["seq", check_external_call(call_ir), store_output_size] + return IRnode.from_list(ret_ir, typ=typ, location=MEMORY) else: typ = TupleT([bool_ty, bytes_ty]) ret_ir = [ @@ -1198,16 +1200,22 @@ def build_IR(self, expr, args, kwargs, context): IRnode.from_list(call_ir, typ=bool_ty), IRnode.from_list(store_output_size, typ=bytes_ty, location=MEMORY), ] + # return an IR tuple of call success flag and returndata pointer + return IRnode.from_list(ret_ir, typ=typ) + + # max_outsize is 0. + + if not revert_on_failure: + # return call flag as stack item + typ = bool_ty + return IRnode.from_list(call_ir, typ=typ) else: - if revert_on_failure: - typ = None - ret_ir = check_external_call(call_ir) - else: - typ = bool_ty - ret_ir = call_ir + # check the call success flag and don't return anything + ret_ir = check_external_call(call_ir) + return IRnode.from_list(ret_ir, typ=None) - return IRnode.from_list(ret_ir, typ=typ, location=MEMORY) + raise CompilerPanic("unreachable!") class Send(BuiltinFunction):
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-w9g2-3w7p-72g9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-30629ghsaADVISORY
- docs.vyperlang.org/en/v0.3.7/built-in-functions.htmlghsax_refsource_MISCWEB
- github.com/lidofinance/gate-seals/blob/051593e74df01a4131c485b4fda52e691cd4b7d8/contracts/GateSeal.vyghsax_refsource_MISCWEB
- github.com/lidofinance/gate-seals/pull/5/filesghsax_refsource_MISCWEB
- github.com/pypa/advisory-database/tree/main/vulns/vyper/PYSEC-2023-131.yamlghsaWEB
- github.com/vyperlang/vyper/commit/851f7a1b3aa2a36fd041e3d0ed38f9355a58c8aeghsax_refsource_MISCWEB
- github.com/vyperlang/vyper/security/advisories/GHSA-w9g2-3w7p-72g9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.