Repository: crytic/slither

Issue:
Yul function with super and inheritance lead to incorrect access
We have a private codebase where this fails:

https://github.com/crytic/slither/blob/3bc22a9b143828edec956f170bdef7234d6707d6/slither/core/declarations/contract.py#L532-L533

From my quick debugging it looks like it's because of the yul function definition that are not correctly handled. This path is only taken if there is a call to super, however I wasn't yet able to reproduce a minimal PoC

We can investigate this issue while adding https://github.com/crytic/slither/issues/899


Discussion:

Resolving patch:
diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py
--- a/slither/core/declarations/contract.py
+++ b/slither/core/declarations/contract.py
@@ -11,7 +11,7 @@
 from slither.core.solidity_types.type import Type
 from slither.core.source_mapping.source_mapping import SourceMapping
 
-from slither.core.declarations.function import Function, FunctionType
+from slither.core.declarations.function import Function, FunctionType, FunctionLanguage
 from slither.utils.erc import (
     ERC20_signatures,
     ERC165_signatures,
@@ -540,7 +540,7 @@ def functions_and_modifiers_declared(self) -> List["Function"]:
     def available_elements_from_inheritances(
         self,
         elements: Dict[str, "Function"],
-        getter_available: Callable[["Contract"], List["Function"]],
+        getter_available: Callable[["Contract"], List["FunctionContract"]],
     ) -> Dict[str, "Function"]:
         """
 
@@ -551,14 +551,16 @@ def available_elements_from_inheritances(
         # keep track of the contracts visited
         # to prevent an ovveride due to multiple inheritance of the same contract
         # A is B, C, D is C, --> the second C was already seen
-        inherited_elements: Dict[str, "Function"] = {}
+        inherited_elements: Dict[str, "FunctionContract"] = {}
         accessible_elements = {}
         contracts_visited = []
         for father in self.inheritance_reverse:
-            functions: Dict[str, "Function"] = {
+            functions: Dict[str, "FunctionContract"] = {
                 v.full_name: v
                 for v in getter_available(father)
                 if v.contract not in contracts_visited
+                and v.function_language
+                != FunctionLanguage.Yul  # Yul functions are not propagated in the inheritance
             }
             contracts_visited.append(father)
             inherited_elements.update(functions)
diff --git a/slither/core/declarations/function.py b/slither/core/declarations/function.py
--- a/slither/core/declarations/function.py
+++ b/slither/core/declarations/function.py
@@ -104,6 +104,12 @@ def _filter_state_variables_written(expressions: List["Expression"]):
     return ret
 
 
+class FunctionLanguage(Enum):
+    Solidity = 0
+    Yul = 1
+    Vyper = 2
+
+
 class Function(SourceMapping, metaclass=ABCMeta):  # pylint: disable=too-many-public-methods
     """
     Function class
@@ -207,6 +213,9 @@ def __init__(self, compilation_unit: "SlitherCompilationUnit"):
 
         self.compilation_unit: "SlitherCompilationUnit" = compilation_unit
 
+        # Assume we are analyzing Solidty by default
+        self.function_language: FunctionLanguage = FunctionLanguage.Solidity
+
     ###################################################################################
     ###################################################################################
     # region General properties
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
@@ -10,7 +10,9 @@
     SolidityFunction,
     Contract,
 )
+from slither.core.declarations.function import FunctionLanguage
 from slither.core.declarations.function_contract import FunctionContract
+from slither.core.declarations.function_top_level import FunctionTopLevel
 from slither.core.expressions import (
     Literal,
     AssignmentOperation,
@@ -353,7 +355,13 @@ def convert_yul_block(
 def convert_yul_function_definition(
     root: YulScope, parent: YulNode, ast: Dict, node_scope: Union[Function, Scope]
 ) -> YulNode:
-    func = FunctionContract(root.compilation_unit)
+    top_node_scope = node_scope
+    while not isinstance(top_node_scope, Function):
+        top_node_scope = top_node_scope.father
+
+    assert isinstance(top_node_scope, (FunctionTopLevel, FunctionContract))
+    func = type(top_node_scope)(root.compilation_unit)
+    func.function_language = FunctionLanguage.Yul
     yul_function = YulFunction(func, root, ast, node_scope)
 
     root.contract.add_function(func)
@@ -707,6 +715,15 @@ def parse_yul_function_call(root: YulScope, node: YulNode, ast: Dict) -> Optiona
     raise SlitherException(f"unexpected function call target type {str(type(ident.value))}")
 
 
+def _check_for_state_variable_name(root: YulScope, potential_name: str) -> Optional[Identifier]:
+    root_function = root.function
+    if isinstance(root_function, FunctionContract):
+        var = root_function.contract.get_state_variable_from_name(potential_name)
+        if var:
+            return Identifier(var)
+    return None
+
+
 def parse_yul_identifier(root: YulScope, _node: YulNode, ast: Dict) -> Optional[Expression]:
     name = ast["name"]
 
@@ -739,17 +756,17 @@ def parse_yul_identifier(root: YulScope, _node: YulNode, ast: Dict) -> Optional[
     # check for magic suffixes
     if name.endswith("_slot") or name.endswith(".slot"):
         potential_name = name[:-5]
-        var = root.function.contract.get_state_variable_from_name(potential_name)
-        if var:
-            return Identifier(var)
+        variable_found = _check_for_state_variable_name(root, potential_name)
+        if variable_found:
+            return variable_found
         var = root.function.get_local_variable_from_name(potential_name)
         if var and var.is_storage:
             return Identifier(var)
     if name.endswith("_offset") or name.endswith(".offset"):
         potential_name = name[:-7]
-        var = root.function.contract.get_state_variable_from_name(potential_name)
-        if var:
-            return Identifier(var)
+        variable_found = _check_for_state_variable_name(root, potential_name)
+        if variable_found:
+            return variable_found
 
     raise SlitherException(f"unresolved reference to identifier {name}")
 


Test patch:
diff --git a/tests/ast-parsing/compile/yul-0.8.0-compact.zip b/tests/ast-parsing/compile/yul-0.8.0-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.0-compact.zip and b/tests/ast-parsing/compile/yul-0.8.0-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.1-compact.zip b/tests/ast-parsing/compile/yul-0.8.1-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.1-compact.zip and b/tests/ast-parsing/compile/yul-0.8.1-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.10-compact.zip b/tests/ast-parsing/compile/yul-0.8.10-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.10-compact.zip and b/tests/ast-parsing/compile/yul-0.8.10-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.2-compact.zip b/tests/ast-parsing/compile/yul-0.8.2-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.2-compact.zip and b/tests/ast-parsing/compile/yul-0.8.2-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.3-compact.zip b/tests/ast-parsing/compile/yul-0.8.3-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.3-compact.zip and b/tests/ast-parsing/compile/yul-0.8.3-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.4-compact.zip b/tests/ast-parsing/compile/yul-0.8.4-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.4-compact.zip and b/tests/ast-parsing/compile/yul-0.8.4-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.5-compact.zip b/tests/ast-parsing/compile/yul-0.8.5-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.5-compact.zip and b/tests/ast-parsing/compile/yul-0.8.5-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.6-compact.zip b/tests/ast-parsing/compile/yul-0.8.6-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.6-compact.zip and b/tests/ast-parsing/compile/yul-0.8.6-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.7-compact.zip b/tests/ast-parsing/compile/yul-0.8.7-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.7-compact.zip and b/tests/ast-parsing/compile/yul-0.8.7-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.8-compact.zip b/tests/ast-parsing/compile/yul-0.8.8-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.8-compact.zip and b/tests/ast-parsing/compile/yul-0.8.8-compact.zip differ
diff --git a/tests/ast-parsing/compile/yul-0.8.9-compact.zip b/tests/ast-parsing/compile/yul-0.8.9-compact.zip
Binary files a/tests/ast-parsing/compile/yul-0.8.9-compact.zip and b/tests/ast-parsing/compile/yul-0.8.9-compact.zip differ
diff --git a/tests/ast-parsing/yul-0.8.0.sol b/tests/ast-parsing/yul-0.8.0.sol
--- a/tests/ast-parsing/yul-0.8.0.sol
+++ b/tests/ast-parsing/yul-0.8.0.sol
@@ -50,3 +50,20 @@ contract C {
     }
 
 }
+
+// function from https://docs.soliditylang.org/en/v0.8.0/assembly.html
+function at(address _addr) view returns (bytes memory o_code) {
+    assembly {
+        // retrieve the size of the code, this needs assembly
+        let size := extcodesize(_addr)
+        // allocate output byte array - this could also be done without assembly
+        // by using o_code = new bytes(size)
+        o_code := mload(0x40)
+        // new "memory end" including padding
+        mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
+        // store length in memory
+        mstore(o_code, size)
+        // actually retrieve the code, this needs assembly
+        extcodecopy(_addr, add(o_code, 0x20), 0, size)
+    }
+}