CVE-2023-30629: Always-Incorrect Control Flow Implementation (CWE-670)
value: 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`.
CWE description: The code contains a control flow path that does not reflect the algorithm that the path is intended to implement, leading to incorrect behavior any time this path is navigated.
CVSS v3 base score: 7.5
Published: 2023-04-24T22:15Z
Repository: https://github.com/vyperlang/vyper
Language: Python
Files changed: tests/parser/functions/test_raw_call.py, vyper/builtins/functions.py
Commit date: 2023-04-24 10:41:02 -0700
Commit message: Merge pull request from GHSA-w9g2-3w7p-72g9

it was getting tagged as a pointer instead of a stack value during
codegen
Diff: diff --git a/tests/parser/functions/test_raw_call.py b/tests/parser/functions/test_raw_call.py
index 4fbff331e..95db070ff 100644
--- a/tests/parser/functions/test_raw_call.py
+++ b/tests/parser/functions/test_raw_call.py
@@ -294,12 +294,14 @@ def test_checkable_raw_call(get_contract, assert_tx_failed):
 baz: int128
 @external
 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:
         self.baz = self.baz + 1
     return self.baz
@@ -318,10 +320,11 @@ def foo(_addr: address, should_raise: bool) -> uint256:
         is_static_call=True,
         revert_on_failure=False,
     )
     assert success == (not should_raise)
     return 1
+
 @external
 @view
 def bar(_addr: address, should_raise: bool) -> uint256:
     success: bool = True
     response: Bytes[32] = b""
@@ -332,19 +335,34 @@ def bar(_addr: address, should_raise: bool) -> uint256:
         is_static_call=True,
         revert_on_failure=False,
     )
     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)
     caller = get_contract(caller_source)
 
     assert caller.foo(target.address, True) == 1
     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 = [
     (
         """
diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py
index f81fb20a6..8d39c3b05 100644
--- a/vyper/builtins/functions.py
+++ b/vyper/builtins/functions.py
@@ -1186,30 +1186,38 @@ class RawCall(BuiltinFunction):
 
             bytes_ty = BytesT(outsize)
 
             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 = [
                     "multi",
                     # use IRnode.from_list to make sure the types are
                     # set properly on the "multi" members
                     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):
     _id = "send"
     _inputs = [("to", AddressT()), ("value", UINT256_T)]

Vulnerable code:             if revert_on_failure:
                typ = None
                ret_ir = check_external_call(call_ir)
            else:
                typ = bool_ty
                ret_ir = call_ir
        return IRnode.from_list(ret_ir, typ=typ, location=MEMORY)
Fixed code: 
# (fail2 fails because of staticcall)


# 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
    assert caller.baz(target.address, True) == 3
    assert caller.baz(target.address, False) == 3
                # check the call success flag, and store returndata in memory
                return IRnode.from_list(ret_ir, typ=typ, 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)
            # 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)
        raise CompilerPanic("unreachable!")