From cf13ac19b971af5cb4fa86e4d242afe30a2ef2d7 Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Fri, 2 Aug 2024 13:03:35 +0200 Subject: [PATCH 1/9] fix check for recursive call --- vyper/semantics/analysis/module.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index d6bbea1b48..9a7cc163e1 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -161,6 +161,10 @@ def _compute_reachable_set(fn_t: ContractFunctionT, path: list[ContractFunctionT message = " -> ".join([f.name for f in path]) raise CallViolation(f"Contract contains cyclic function call: {message}") + if g == fn_t: + message = f"{g.name} -> {g.name}" + raise CallViolation(f"Contract contains recursion: {message}") + _compute_reachable_set(g, path=path) g_reachable = g.reachable_internal_functions From 4e44664336e60be5bbc6f4305217cf0987ae0c95 Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Fri, 2 Aug 2024 13:03:48 +0200 Subject: [PATCH 2/9] add test for recursive call --- .../analysis/test_cyclic_function_calls.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit/semantics/analysis/test_cyclic_function_calls.py b/tests/unit/semantics/analysis/test_cyclic_function_calls.py index 990c839fde..009d3bc91e 100644 --- a/tests/unit/semantics/analysis/test_cyclic_function_calls.py +++ b/tests/unit/semantics/analysis/test_cyclic_function_calls.py @@ -54,6 +54,21 @@ def potato(): analyze_module(vyper_module, dummy_input_bundle) +def test_recursive_function_call(dummy_input_bundle): + code = """ +@external +def foo(): + self.bar() + +@internal +def bar(): + self.bar() + """ + vyper_module = parse_to_ast(code) + with pytest.raises(CallViolation): + analyze_module(vyper_module, dummy_input_bundle) + + def test_global_ann_assign_callable_no_crash(dummy_input_bundle): code = """ balanceOf: public(HashMap[address, uint256]) From 0247f205e0d679fdff0afe170ea5b4035939dc50 Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Fri, 2 Aug 2024 13:52:09 +0200 Subject: [PATCH 3/9] add context constancy check to raw_log --- vyper/builtins/functions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 672d978455..674efda7ce 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -1267,6 +1267,8 @@ def infer_arg_types(self, node, expected_return_typ=None): @process_inputs def build_IR(self, expr, args, kwargs, context): + context.check_is_not_constant(f"use {self._id}", expr) + topics_length = len(expr.args[0].elements) topics = args[0].args topics = [unwrap_location(topic) for topic in topics] From ad367ca7cef8c914694523134693a13bc52684eb Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Mon, 5 Aug 2024 10:16:00 +0200 Subject: [PATCH 4/9] add message check for recursion exception --- tests/unit/semantics/analysis/test_cyclic_function_calls.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/semantics/analysis/test_cyclic_function_calls.py b/tests/unit/semantics/analysis/test_cyclic_function_calls.py index 009d3bc91e..e3c36bf2b1 100644 --- a/tests/unit/semantics/analysis/test_cyclic_function_calls.py +++ b/tests/unit/semantics/analysis/test_cyclic_function_calls.py @@ -65,9 +65,11 @@ def bar(): self.bar() """ vyper_module = parse_to_ast(code) - with pytest.raises(CallViolation): + with pytest.raises(CallViolation) as e: analyze_module(vyper_module, dummy_input_bundle) + assert e.value.message == "Contract contains recursion: bar -> bar" + def test_global_ann_assign_callable_no_crash(dummy_input_bundle): code = """ From f386487ee2749f528937f9d31bce38f663da2147 Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Mon, 5 Aug 2024 18:20:15 +0200 Subject: [PATCH 5/9] Revert "add context constancy check to raw_log" This reverts commit 0247f205e0d679fdff0afe170ea5b4035939dc50. --- vyper/builtins/functions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 674efda7ce..672d978455 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -1267,8 +1267,6 @@ def infer_arg_types(self, node, expected_return_typ=None): @process_inputs def build_IR(self, expr, args, kwargs, context): - context.check_is_not_constant(f"use {self._id}", expr) - topics_length = len(expr.args[0].elements) topics = args[0].args topics = [unwrap_location(topic) for topic in topics] From 5ac3ad037b4dcc023b2941609a0506b908b49b3b Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Tue, 6 Aug 2024 08:30:15 +0200 Subject: [PATCH 6/9] simplify cycle detection --- vyper/semantics/analysis/module.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 9a7cc163e1..a5dbf52c7c 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -150,21 +150,18 @@ def _compute_reachable_set(fn_t: ContractFunctionT, path: list[ContractFunctionT path = path or [] path.append(fn_t) - root = path[0] for g in fn_t.called_functions: if g in fn_t.reachable_internal_functions: # already seen continue - if g == root: - message = " -> ".join([f.name for f in path]) + if g in path: + cycle_start = path.index(g) + extended_path = path[cycle_start:] + [g] + message = " -> ".join([f.name for f in extended_path]) raise CallViolation(f"Contract contains cyclic function call: {message}") - if g == fn_t: - message = f"{g.name} -> {g.name}" - raise CallViolation(f"Contract contains recursion: {message}") - _compute_reachable_set(g, path=path) g_reachable = g.reachable_internal_functions From 43c13308fda5dec2ce822270b96be88553116e0b Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Tue, 6 Aug 2024 08:30:44 +0200 Subject: [PATCH 7/9] add tests for cycle detection --- .../analysis/test_cyclic_function_calls.py | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/tests/unit/semantics/analysis/test_cyclic_function_calls.py b/tests/unit/semantics/analysis/test_cyclic_function_calls.py index e3c36bf2b1..14bda86bd7 100644 --- a/tests/unit/semantics/analysis/test_cyclic_function_calls.py +++ b/tests/unit/semantics/analysis/test_cyclic_function_calls.py @@ -12,9 +12,28 @@ def foo(): self.foo() """ vyper_module = parse_to_ast(code) - with pytest.raises(CallViolation): + with pytest.raises(CallViolation) as e: + analyze_module(vyper_module, dummy_input_bundle) + + assert e.value.message == "Contract contains cyclic function call: foo -> foo" + + +def test_self_function_call2(dummy_input_bundle): + code = """ +@external +def foo(): + self.bar() + +@internal +def bar(): + self.bar() + """ + vyper_module = parse_to_ast(code) + with pytest.raises(CallViolation) as e: analyze_module(vyper_module, dummy_input_bundle) + assert e.value.message == "Contract contains cyclic function call: bar -> bar" + def test_cyclic_function_call(dummy_input_bundle): code = """ @@ -27,9 +46,11 @@ def bar(): self.foo() """ vyper_module = parse_to_ast(code) - with pytest.raises(CallViolation): + with pytest.raises(CallViolation) as e: analyze_module(vyper_module, dummy_input_bundle) + assert e.value.message == "Contract contains cyclic function call: foo -> bar -> foo" + def test_multi_cyclic_function_call(dummy_input_bundle): code = """ @@ -50,17 +71,44 @@ def potato(): self.foo() """ vyper_module = parse_to_ast(code) - with pytest.raises(CallViolation): + with pytest.raises(CallViolation) as e: analyze_module(vyper_module, dummy_input_bundle) + expected_message = "Contract contains cyclic function call: foo -> bar -> baz -> potato -> foo" + + assert e.value.message == expected_message + -def test_recursive_function_call(dummy_input_bundle): +def test_multi_cyclic_function_call2(dummy_input_bundle): code = """ -@external +@internal def foo(): self.bar() @internal +def bar(): + self.baz() + +@internal +def baz(): + self.potato() + +@internal +def potato(): + self.bar() + """ + vyper_module = parse_to_ast(code) + with pytest.raises(CallViolation) as e: + analyze_module(vyper_module, dummy_input_bundle) + + expected_message = "Contract contains cyclic function call: bar -> baz -> potato -> bar" + + assert e.value.message == expected_message + + +def test_cyclic_function_call_without_external(dummy_input_bundle): + code = """ +@internal def bar(): self.bar() """ @@ -68,7 +116,7 @@ def bar(): with pytest.raises(CallViolation) as e: analyze_module(vyper_module, dummy_input_bundle) - assert e.value.message == "Contract contains recursion: bar -> bar" + assert e.value.message == "Contract contains cyclic function call: bar -> bar" def test_global_ann_assign_callable_no_crash(dummy_input_bundle): From 4e97e81f30ee1c2997bd58ea79483eb9ef507872 Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Tue, 6 Aug 2024 08:36:16 +0200 Subject: [PATCH 8/9] remove a duplicate test --- .../analysis/test_cyclic_function_calls.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/unit/semantics/analysis/test_cyclic_function_calls.py b/tests/unit/semantics/analysis/test_cyclic_function_calls.py index 14bda86bd7..8fab1395fc 100644 --- a/tests/unit/semantics/analysis/test_cyclic_function_calls.py +++ b/tests/unit/semantics/analysis/test_cyclic_function_calls.py @@ -106,19 +106,6 @@ def potato(): assert e.value.message == expected_message -def test_cyclic_function_call_without_external(dummy_input_bundle): - code = """ -@internal -def bar(): - self.bar() - """ - vyper_module = parse_to_ast(code) - with pytest.raises(CallViolation) as e: - analyze_module(vyper_module, dummy_input_bundle) - - assert e.value.message == "Contract contains cyclic function call: bar -> bar" - - def test_global_ann_assign_callable_no_crash(dummy_input_bundle): code = """ balanceOf: public(HashMap[address, uint256]) From e2c20c7256fb5a4478a530c32f2f1a455760da77 Mon Sep 17 00:00:00 2001 From: cyberthirst Date: Tue, 6 Aug 2024 14:16:16 +0200 Subject: [PATCH 9/9] report full call path in cyclic call violation exception --- tests/unit/semantics/analysis/test_cyclic_function_calls.py | 4 ++-- vyper/semantics/analysis/module.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/unit/semantics/analysis/test_cyclic_function_calls.py b/tests/unit/semantics/analysis/test_cyclic_function_calls.py index 8fab1395fc..406adc00ab 100644 --- a/tests/unit/semantics/analysis/test_cyclic_function_calls.py +++ b/tests/unit/semantics/analysis/test_cyclic_function_calls.py @@ -32,7 +32,7 @@ def bar(): with pytest.raises(CallViolation) as e: analyze_module(vyper_module, dummy_input_bundle) - assert e.value.message == "Contract contains cyclic function call: bar -> bar" + assert e.value.message == "Contract contains cyclic function call: foo -> bar -> bar" def test_cyclic_function_call(dummy_input_bundle): @@ -101,7 +101,7 @@ def potato(): with pytest.raises(CallViolation) as e: analyze_module(vyper_module, dummy_input_bundle) - expected_message = "Contract contains cyclic function call: bar -> baz -> potato -> bar" + expected_message = "Contract contains cyclic function call: foo -> bar -> baz -> potato -> bar" assert e.value.message == expected_message diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index a5dbf52c7c..bfcff135b8 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -157,8 +157,7 @@ def _compute_reachable_set(fn_t: ContractFunctionT, path: list[ContractFunctionT continue if g in path: - cycle_start = path.index(g) - extended_path = path[cycle_start:] + [g] + extended_path = path + [g] message = " -> ".join([f.name for f in extended_path]) raise CallViolation(f"Contract contains cyclic function call: {message}")