Repository: crytic/slither

Issue:
[Bug]: unresolved reference to yul function identifier in another function
### Describe the issue:

When one inline-assembly (yul) function calls another yul function, Slither will raise an exception `slither.exceptions.SlitherException: unresolved reference to identifier <callee>`.

This issue is different from #1610, where the unresolved identifier is declared outside the inline-assembly block. 
In this issue, the unresolved identifier is another function declared inside the inline-assembly block. 

### Code example to reproduce the issue:

```solidity
contract A {
    function foo() public {
        assembly {
            function f() {}
            function g() {
                f()
            }
            g()
        }
    }
}
```

### Version:

0.9.3

### Relevant log output:

```shell
Error
Traceback (most recent call last):
  File "/slither_test.py", line 326, in test_assembly_function
    with TestSourceCode(code, solc_version="0.6.12") as slither:
  File "/lib/test_util.py", line 22, in __enter__
    return Slither(comp)
  File "/python-3.10/lib/python3.10/site-packages/slither/slither.py", line 135, in __init__
    self._init_parsing_and_analyses(kwargs.get("skip_analyze", False))
  File "/python-3.10/lib/python3.10/site-packages/slither/slither.py", line 145, in _init_parsing_and_analyses
    raise e
  File "/python-3.10/lib/python3.10/site-packages/slither/slither.py", line 141, in _init_parsing_and_analyses
    parser.parse_contracts()
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/slither_compilation_unit_solc.py", line 518, in parse_contracts
    self._analyze_third_part(contracts_to_be_analyzed, libraries)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/slither_compilation_unit_solc.py", line 628, in _analyze_third_part
    self._analyze_variables_modifiers_functions(contract)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/slither_compilation_unit_solc.py", line 727, in _analyze_variables_modifiers_functions
    contract.analyze_content_functions()
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/declarations/contract.py", line 418, in analyze_content_functions
    function_parser.analyze_content()
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/declarations/function.py", line 286, in analyze_content
    self._parse_cfg(body)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/declarations/function.py", line 1073, in _parse_cfg
    self._parse_block(cfg, node, check_arithmetic=check_arithmetic)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/declarations/function.py", line 1036, in _parse_block
    node = self._parse_statement(statement, node, new_scope)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/declarations/function.py", line 934, in _parse_statement
    exitpoint = yul_object.convert(statement["AST"])
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 328, in convert
    return convert_yul(self, self._entrypoint, ast, self.node_scope)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 628, in convert_yul
    return converters.get(ast["nodeType"], convert_yul_unsupported)(root, parent, ast, node_scope)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 362, in convert_yul_block
    parent = convert_yul(root, parent, statement, node_scope)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 628, in convert_yul
    return converters.get(ast["nodeType"], convert_yul_unsupported)(root, parent, ast, node_scope)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 393, in convert_yul_function_definition
    yul_function.parse_body()
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 273, in parse_body
    node.analyze_expressions()
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 65, in analyze_expressions
    expression = parse_yul(self._scope, self, self._unparsed_expression)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 858, in parse_yul
    op: Expression = parsers.get(ast["nodeType"], parse_yul_unsupported)(root, node, ast)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 703, in parse_yul_function_call
    ident = parse_yul(root, node, ast["functionName"])
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 858, in parse_yul
    op: Expression = parsers.get(ast["nodeType"], parse_yul_unsupported)(root, node, ast)
  File "/python-3.10/lib/python3.10/site-packages/slither/solc_parsing/yul/parse_yul.py", line 820, in parse_yul_identifier
    raise SlitherException(f"unresolved reference to identifier {name}")
slither.exceptions.SlitherException: unresolved reference to identifier f
```



Discussion:

Resolving patch:
diff --git a/slither/solc_parsing/yul/parse_yul.py b/slither/solc_parsing/yul/parse_yul.py
--- a/slither/solc_parsing/yul/parse_yul.py
+++ b/slither/solc_parsing/yul/parse_yul.py
@@ -181,7 +181,7 @@ def get_yul_local_variable_from_name(self, variable_name: str) -> Optional["YulL
     def add_yul_local_function(self, func: "YulFunction") -> None:
         self._yul_local_functions.append(func)
 
-    def get_yul_local_function_from_name(self, func_name: str) -> Optional["YulLocalVariable"]:
+    def get_yul_local_function_from_name(self, func_name: str) -> Optional["YulFunction"]:
         return next(
             (v for v in self._yul_local_functions if v.underlying.name == func_name),
             None,
@@ -252,6 +252,10 @@ def underlying(self) -> Function:
     def function(self) -> Function:
         return self._function
 
+    @property
+    def root(self) -> YulScope:
+        return self._root
+
     def convert_body(self) -> None:
         node = self.new_node(NodeType.ENTRYPOINT, self._ast["src"])
         link_underlying_nodes(self._entrypoint, node)
@@ -271,6 +275,9 @@ def convert_body(self) -> None:
     def parse_body(self) -> None:
         for node in self._nodes:
             node.analyze_expressions()
+        for f in self._yul_local_functions:
+            if f != self:
+                f.parse_body()
 
     def new_node(self, node_type: NodeType, src: str) -> YulNode:
         if self._function:
@@ -325,7 +332,10 @@ def new_node(self, node_type: NodeType, src: Union[str, Dict]) -> YulNode:
         return yul_node
 
     def convert(self, ast: Dict) -> YulNode:
-        return convert_yul(self, self._entrypoint, ast, self.node_scope)
+        yul_node = convert_yul(self, self._entrypoint, ast, self.node_scope)
+        for f in self._yul_local_functions:
+            f.parse_body()
+        return yul_node
 
     def analyze_expressions(self) -> None:
         for node in self._nodes:
@@ -390,7 +400,6 @@ def convert_yul_function_definition(
     root.add_yul_local_function(yul_function)
 
     yul_function.convert_body()
-    yul_function.parse_body()
 
     return parent
 
@@ -778,6 +787,7 @@ def _parse_yul_magic_suffixes(name: str, root: YulScope) -> Optional[Expression]
     return None
 
 
+# pylint: disable=too-many-branches
 def parse_yul_identifier(root: YulScope, _node: YulNode, ast: Dict) -> Optional[Expression]:
     name = ast["name"]
 
@@ -809,6 +819,23 @@ def parse_yul_identifier(root: YulScope, _node: YulNode, ast: Dict) -> Optional[
     if func:
         return Identifier(func.underlying)
 
+    # check yul-block scoped function
+    if isinstance(root, YulFunction):
+        yul_block = root.root
+
+        # Iterate until we searched in all the scopes until the YulBlock scope
+        while not isinstance(yul_block, YulBlock):
+            func = yul_block.get_yul_local_function_from_name(name)
+            if func:
+                return Identifier(func.underlying)
+
+            if isinstance(yul_block, YulFunction):
+                yul_block = yul_block.root
+
+        func = yul_block.get_yul_local_function_from_name(name)
+        if func:
+            return Identifier(func.underlying)
+
     magic_suffix = _parse_yul_magic_suffixes(name, root)
     if magic_suffix:
         return magic_suffix


Test patch:
diff --git a/tests/e2e/solc_parsing/test_ast_parsing.py b/tests/e2e/solc_parsing/test_ast_parsing.py
--- a/tests/e2e/solc_parsing/test_ast_parsing.py
+++ b/tests/e2e/solc_parsing/test_ast_parsing.py
@@ -453,6 +453,10 @@ def make_version(minor: int, patch_min: int, patch_max: int) -> List[str]:
     Test("complex_imports/import_aliases_issue_1319/test.sol", ["0.5.12"]),
     Test("yul-state-constant-access.sol", ["0.8.16"]),
     Test("negate-unary-element.sol", ["0.8.16"]),
+    Test(
+        "assembly-functions.sol",
+        ["0.6.9", "0.7.6", "0.8.16"],
+    ),
 ]
 # create the output folder if needed
 try:
diff --git a/tests/e2e/solc_parsing/test_data/assembly-functions.sol b/tests/e2e/solc_parsing/test_data/assembly-functions.sol
new file mode 100644
--- /dev/null
+++ b/tests/e2e/solc_parsing/test_data/assembly-functions.sol
@@ -0,0 +1,12 @@
+contract A {
+    function foo() public {
+        assembly {
+            function f() { function z() { function x() { g() } x() } z() }
+            function w() { function a() {} function b() { a() } b() }
+            function g() {
+                f()
+            }
+            g()
+        }
+    }
+}
diff --git a/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.6.9-compact.zip b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.6.9-compact.zip
new file mode 100644
Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.6.9-compact.zip differ
diff --git a/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.6.9-legacy.zip b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.6.9-legacy.zip
new file mode 100644
Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.6.9-legacy.zip differ
diff --git a/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.7.6-compact.zip b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.7.6-compact.zip
new file mode 100644
Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.7.6-compact.zip differ
diff --git a/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.7.6-legacy.zip b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.7.6-legacy.zip
new file mode 100644
Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.7.6-legacy.zip differ
diff --git a/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.8.16-compact.zip b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.8.16-compact.zip
new file mode 100644
Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/assembly-functions.sol-0.8.16-compact.zip differ
diff --git a/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.6.9-compact.json b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.6.9-compact.json
new file mode 100644
--- /dev/null
+++ b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.6.9-compact.json
@@ -0,0 +1,12 @@
+{
+  "A": {
+    "foo()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: INLINE ASM 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.f()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.f.z()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.f.z.x()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.w()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.w.a()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n}\n",
+    "foo.asm_0.w.b()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.g()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n"
+  }
+}
\ No newline at end of file
diff --git a/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.6.9-legacy.json b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.6.9-legacy.json
new file mode 100644
--- /dev/null
+++ b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.6.9-legacy.json
@@ -0,0 +1,5 @@
+{
+  "A": {
+    "foo()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: INLINE ASM 1\n\"];\n}\n"
+  }
+}
\ No newline at end of file
diff --git a/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.7.6-compact.json b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.7.6-compact.json
new file mode 100644
--- /dev/null
+++ b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.7.6-compact.json
@@ -0,0 +1,12 @@
+{
+  "A": {
+    "foo()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: INLINE ASM 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.f()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.f.z()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.f.z.x()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.w()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.w.a()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n}\n",
+    "foo.asm_0.w.b()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.g()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n"
+  }
+}
\ No newline at end of file
diff --git a/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.7.6-legacy.json b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.7.6-legacy.json
new file mode 100644
--- /dev/null
+++ b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.7.6-legacy.json
@@ -0,0 +1,5 @@
+{
+  "A": {
+    "foo()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: INLINE ASM 1\n\"];\n}\n"
+  }
+}
\ No newline at end of file
diff --git a/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.8.16-compact.json b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.8.16-compact.json
new file mode 100644
--- /dev/null
+++ b/tests/e2e/solc_parsing/test_data/expected/assembly-functions.sol-0.8.16-compact.json
@@ -0,0 +1,12 @@
+{
+  "A": {
+    "foo()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: INLINE ASM 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.f()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.f.z()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.f.z.x()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.w()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.w.a()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n}\n",
+    "foo.asm_0.w.b()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n",
+    "foo.asm_0.g()": "digraph{\n0[label=\"Node Type: INLINE ASM 0\n\"];\n0->1;\n1[label=\"Node Type: ENTRY_POINT 1\n\"];\n1->2;\n2[label=\"Node Type: EXPRESSION 2\n\"];\n}\n"
+  }
+}
\ No newline at end of file