CVE-2022-29255 (published 2022-06-09T09:15Z)

Vyper is a Pythonic Smart Contract Language for the ethereum virtual machine. In versions prior to 0.3.4 when a calling an external contract with no return value, the contract address (including side effects) could be evaluated twice. This may result in incorrect outcomes for contracts. This issue has been addressed in v0.3.4.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-670
cwe_name: Always-Incorrect Control Flow Implementation
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.
extended_description: This weakness captures cases in which a particular code segment is always incorrect with respect to the algorithm that it is implementing. For example, if a C programmer intends to include multiple statements in a single block but does not include the enclosing braces (CWE-483), then the logic is always incorrect. This issue is in contrast to most weaknesses in which the code usually behaves correctly, except when it is externally manipulated in malicious ways.
         

Fix information:
repo_url: https://github.com/vyperlang/vyper
commit_details: repo_url: https://github.com/vyperlang/vyper
author: Charles Cooper
author_date: 2022-06-06 11:38:28-07:00
msg: Merge pull request from GHSA-4v9q-cgpw-cf38

in external call codegen, when `extcodesize` is called on the target
address, the IR for evaluating the target address can be evaluated
twice. this can result in any side effects (embedded in the evaluation
of the target address) being executed twice.
repository: repo_url: https://github.com/vyperlang/vyper
repo_name: vyperlang/vyper
description: Pythonic Smart Contract Language for the EVM
repo_language: Python
owner: vyperlang
file_changes: filename: test_external_contract_calls.py
change_type: ModificationType.MODIFY
diff: @@ -2411,3 +2411,44 @@ def bar(foo: Foo):
     # fails due to returndatasize being nonzero but also lt 64
     assert_tx_failed(lambda: c.bar(bad_1.address))
     c.bar(bad_2.address)
+
+
+def test_contract_address_evaluation(get_contract):
+    callee_code = """
+# implements: Foo
+
+interface Counter:
+    def increment_counter(): nonpayable
+
+@external
+def foo():
+    pass
+
+@external
+def bar() -> address:
+    Counter(msg.sender).increment_counter()
+    return self
+    """
+    code = """
+# implements: Counter
+
+interface Foo:
+    def foo(): nonpayable
+    def bar() -> address: nonpayable
+
+counter: uint256
+
+@external
+def increment_counter():
+    self.counter += 1
+
+@external
+def do_stuff(f: Foo) -> uint256:
+    Foo(f.bar()).foo()
+    return self.counter
+    """
+
+    c1 = get_contract(code)
+    c2 = get_contract(callee_code)
+
+    assert c1.do_stuff(c2.address) == 1

programming_language: Python
filename: external_call.py
change_type: ModificationType.MODIFY
diff: @@ -168,15 +168,7 @@ def _extcodesize_check(address):
     return ["assert", ["extcodesize", address]]
 
 
-def ir_for_external_call(call_expr, context):
-    from vyper.codegen.expr import Expr  # TODO rethink this circular import
-
-    contract_address = Expr.parse_value_expr(call_expr.func.value, context)
-    call_kwargs = _parse_kwargs(call_expr, context)
-    args_ir = [Expr(x, context).ir_node for x in call_expr.args]
-
-    assert isinstance(contract_address.typ, InterfaceType)
-
+def _external_call_helper(contract_address, args_ir, call_kwargs, call_expr, context):
     # expr.func._metadata["type"].return_type is more accurate
     # than fn_sig.return_type in the case of JSON interfaces.
     fn_type = call_expr.func._metadata["type"]
@@ -223,3 +215,17 @@ def ir_for_external_call(call_expr, context):
         ret.append(ret_unpacker)
 
     return IRnode.from_list(ret, typ=return_t, location=MEMORY)
+
+
+def ir_for_external_call(call_expr, context):
+    from vyper.codegen.expr import Expr  # TODO rethink this circular import
+
+    contract_address = Expr.parse_value_expr(call_expr.func.value, context)
+    assert isinstance(contract_address.typ, InterfaceType)
+    args_ir = [Expr(x, context).ir_node for x in call_expr.args]
+    call_kwargs = _parse_kwargs(call_expr, context)
+
+    with contract_address.cache_when_complex("external_contract") as (b1, contract_address):
+        return b1.resolve(
+            _external_call_helper(contract_address, args_ir, call_kwargs, call_expr, context)
+        )

programming_language: Python