diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py index aa02597d7..7b134e10d 100644 --- a/slither/core/declarations/contract.py +++ b/slither/core/declarations/contract.py @@ -10,7 +10,7 @@ from slither.core.cfg.scope import Scope from slither.core.source_mapping.source_mapping import SourceMapping -from slither.utils.using_for import USING_FOR, _merge_using_for +from slither.utils.using_for import USING_FOR, merge_using_for from slither.core.declarations.function import Function, FunctionType, FunctionLanguage from slither.utils.erc import ( ERC20_signatures, @@ -342,7 +342,7 @@ def using_for_complete(self) -> USING_FOR: result = self.using_for top_level_using_for = self.file_scope.using_for_directives for uftl in top_level_using_for: - result = _merge_using_for(result, uftl.using_for) + result = merge_using_for(result, uftl.using_for) self._using_for_complete = result return self._using_for_complete diff --git a/slither/core/declarations/function.py b/slither/core/declarations/function.py index 628961193..958a7d219 100644 --- a/slither/core/declarations/function.py +++ b/slither/core/declarations/function.py @@ -355,8 +355,7 @@ def is_checked(self) -> bool: @property def id(self) -> Optional[str]: """ - Return the ID of the funciton. For Solidity with compact-AST the ID is the reference ID - For other, the ID is None + Return the reference ID of the function, if available. :return: :rtype: diff --git a/slither/core/declarations/function_contract.py b/slither/core/declarations/function_contract.py index 8f68ab7b6..2e8aa3efe 100644 --- a/slither/core/declarations/function_contract.py +++ b/slither/core/declarations/function_contract.py @@ -65,7 +65,10 @@ def is_declared_by(self, contract: "Contract") -> bool: @property def file_scope(self) -> "FileScope": - return self.contract.file_scope + # This is the contract declarer's file scope because inherited functions have access + # to the file scope which their declared in. This scope may contain references not + # available in the child contract's scope. See inherited_function_scope.sol for an example. + return self.contract_declarer.file_scope # endregion ################################################################################### diff --git a/slither/core/declarations/function_top_level.py b/slither/core/declarations/function_top_level.py index e0dcd2557..39fa2d2bb 100644 --- a/slither/core/declarations/function_top_level.py +++ b/slither/core/declarations/function_top_level.py @@ -5,7 +5,7 @@ from slither.core.declarations import Function from slither.core.declarations.top_level import TopLevel -from slither.utils.using_for import USING_FOR, _merge_using_for +from slither.utils.using_for import USING_FOR, merge_using_for if TYPE_CHECKING: from slither.core.compilation_unit import SlitherCompilationUnit @@ -32,7 +32,7 @@ def using_for_complete(self) -> USING_FOR: if self._using_for_complete is None: result = {} for uftl in self.file_scope.using_for_directives: - result = _merge_using_for(result, uftl.using_for) + result = merge_using_for(result, uftl.using_for) self._using_for_complete = result return self._using_for_complete diff --git a/slither/core/declarations/import_directive.py b/slither/core/declarations/import_directive.py index 75c0406fe..19ea2cff9 100644 --- a/slither/core/declarations/import_directive.py +++ b/slither/core/declarations/import_directive.py @@ -11,8 +11,8 @@ class Import(SourceMapping): def __init__(self, filename: Path, scope: "FileScope") -> None: super().__init__() self._filename: Path = filename - self._alias: Optional[str] = None self.scope: "FileScope" = scope + self._alias: Optional[str] = None # Map local name -> original name self.renaming: Dict[str, str] = {} diff --git a/slither/core/declarations/using_for_top_level.py b/slither/core/declarations/using_for_top_level.py index ca73777e5..d6b21b829 100644 --- a/slither/core/declarations/using_for_top_level.py +++ b/slither/core/declarations/using_for_top_level.py @@ -1,6 +1,5 @@ -from typing import TYPE_CHECKING, List, Dict, Union +from typing import TYPE_CHECKING -from slither.core.solidity_types.type import Type from slither.core.declarations.top_level import TopLevel from slither.utils.using_for import USING_FOR @@ -11,7 +10,7 @@ class UsingForTopLevel(TopLevel): def __init__(self, scope: "FileScope") -> None: super().__init__() - self._using_for: Dict[Union[str, Type], List[Type]] = {} + self._using_for: USING_FOR = {} self.file_scope: "FileScope" = scope @property diff --git a/slither/core/scope/scope.py b/slither/core/scope/scope.py index 99fe00b25..ee2a98eb3 100644 --- a/slither/core/scope/scope.py +++ b/slither/core/scope/scope.py @@ -29,6 +29,7 @@ class FileScope: def __init__(self, filename: Filename) -> None: self.filename = filename self.accessible_scopes: List[FileScope] = [] + self.exported_symbols: Set[int] = set() self.contracts: Dict[str, Contract] = {} # Custom error are a list instead of a dict @@ -56,53 +57,39 @@ def __init__(self, filename: Filename) -> None: # Name -> type alias self.type_aliases: Dict[str, TypeAlias] = {} - def add_accesible_scopes(self) -> bool: # pylint: disable=too-many-branches + def add_accessible_scopes(self) -> bool: # pylint: disable=too-many-branches """ Add information from accessible scopes. Return true if new information was obtained - :return: :rtype: """ learn_something = False - for new_scope in self.accessible_scopes: - if not _dict_contain(new_scope.contracts, self.contracts): - self.contracts.update(new_scope.contracts) - learn_something = True - if not new_scope.custom_errors.issubset(self.custom_errors): - self.custom_errors |= new_scope.custom_errors - learn_something = True - if not _dict_contain(new_scope.enums, self.enums): - self.enums.update(new_scope.enums) - learn_something = True - if not new_scope.events.issubset(self.events): - self.events |= new_scope.events - learn_something = True - if not new_scope.functions.issubset(self.functions): - self.functions |= new_scope.functions - learn_something = True + # To support using for directives on user defined types and user defined functions, + # we need to propagate the using for directives from the imported file to the importing file + # since it is not reflected in the "exportedSymbols" field of the AST. if not new_scope.using_for_directives.issubset(self.using_for_directives): self.using_for_directives |= new_scope.using_for_directives learn_something = True - if not new_scope.imports.issubset(self.imports): - self.imports |= new_scope.imports - learn_something = True - if not new_scope.pragmas.issubset(self.pragmas): - self.pragmas |= new_scope.pragmas + if not _dict_contain(new_scope.type_aliases, self.type_aliases): + self.type_aliases.update(new_scope.type_aliases) learn_something = True - if not _dict_contain(new_scope.structures, self.structures): - self.structures.update(new_scope.structures) + if not new_scope.functions.issubset(self.functions): + self.functions |= new_scope.functions learn_something = True - if not _dict_contain(new_scope.variables, self.variables): - self.variables.update(new_scope.variables) + + # To get around this bug for aliases https://github.com/ethereum/solidity/pull/11881, + # we propagate the exported_symbols from the imported file to the importing file + # See tests/e2e/solc_parsing/test_data/top-level-nested-import-0.7.1.sol + if not new_scope.exported_symbols.issubset(self.exported_symbols): + self.exported_symbols |= new_scope.exported_symbols learn_something = True + + # This is need to support aliasing when we do a late lookup using SolidityImportPlaceholder if not _dict_contain(new_scope.renaming, self.renaming): self.renaming.update(new_scope.renaming) learn_something = True - if not _dict_contain(new_scope.type_aliases, self.type_aliases): - self.type_aliases.update(new_scope.type_aliases) - learn_something = True return learn_something diff --git a/slither/slither.py b/slither/slither.py index 4259b74b7..0f2218535 100644 --- a/slither/slither.py +++ b/slither/slither.py @@ -1,11 +1,10 @@ import logging -from typing import Union, List, ValuesView, Type, Dict, Optional +from typing import Union, List, Type, Dict, Optional from crytic_compile import CryticCompile, InvalidCompilation # pylint: disable= no-name-in-module from slither.core.compilation_unit import SlitherCompilationUnit -from slither.core.scope.scope import FileScope from slither.core.slither_core import SlitherCore from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification from slither.exceptions import SlitherError @@ -27,32 +26,71 @@ def _check_common_things( ) -> None: if not issubclass(cls, base_cls) or cls is base_cls: - raise Exception( + raise SlitherError( f"You can't register {cls!r} as a {thing_name}. You need to pass a class that inherits from {base_cls.__name__}" ) if any(type(obj) == cls for obj in instances_list): # pylint: disable=unidiomatic-typecheck - raise Exception(f"You can't register {cls!r} twice.") + raise SlitherError(f"You can't register {cls!r} twice.") -def _update_file_scopes(candidates: ValuesView[FileScope]): +def _update_file_scopes( + sol_parser: SlitherCompilationUnitSolc, +): # pylint: disable=too-many-branches """ - Because solc's import allows cycle in the import - We iterate until we aren't adding new information to the scope - + Since all definitions in a file are exported by default, including definitions from its (transitive) dependencies, + we can identify all top level items that could possibly be referenced within the file from its exportedSymbols. + It is not as straightforward for user defined types and functions as well as aliasing. See add_accessible_scopes for more details. """ + candidates = sol_parser.compilation_unit.scopes.values() learned_something = False + # Because solc's import allows cycle in the import graph, iterate until we aren't adding new information to the scope. while True: for candidate in candidates: - learned_something |= candidate.add_accesible_scopes() + learned_something |= candidate.add_accessible_scopes() if not learned_something: break learned_something = False + for scope in candidates: + for refId in scope.exported_symbols: + if refId in sol_parser.contracts_by_id: + contract = sol_parser.contracts_by_id[refId] + scope.contracts[contract.name] = contract + elif refId in sol_parser.functions_by_id: + functions = sol_parser.functions_by_id[refId] + assert len(functions) == 1 + scope.functions.add(functions[0]) + elif refId in sol_parser.imports_by_id: + import_directive = sol_parser.imports_by_id[refId] + scope.imports.add(import_directive) + elif refId in sol_parser.top_level_variables_by_id: + top_level_variable = sol_parser.top_level_variables_by_id[refId] + scope.variables[top_level_variable.name] = top_level_variable + elif refId in sol_parser.top_level_events_by_id: + top_level_event = sol_parser.top_level_events_by_id[refId] + scope.events.add(top_level_event) + elif refId in sol_parser.top_level_structures_by_id: + top_level_struct = sol_parser.top_level_structures_by_id[refId] + scope.structures[top_level_struct.name] = top_level_struct + elif refId in sol_parser.top_level_type_aliases_by_id: + top_level_type_alias = sol_parser.top_level_type_aliases_by_id[refId] + scope.type_aliases[top_level_type_alias.name] = top_level_type_alias + elif refId in sol_parser.top_level_enums_by_id: + top_level_enum = sol_parser.top_level_enums_by_id[refId] + scope.enums[top_level_enum.name] = top_level_enum + elif refId in sol_parser.top_level_errors_by_id: + top_level_custom_error = sol_parser.top_level_errors_by_id[refId] + scope.custom_errors.add(top_level_custom_error) + else: + logger.error( + f"Failed to resolved name for reference id {refId} in {scope.filename.absolute}." + ) + class Slither( SlitherCore -): # pylint: disable=too-many-instance-attributes,too-many-locals,too-many-statements +): # pylint: disable=too-many-instance-attributes,too-many-locals,too-many-statements,too-many-branches def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: """ Args: @@ -118,7 +156,18 @@ def __init__(self, target: Union[str, CryticCompile], **kwargs) -> None: sol_parser.parse_top_level_items(ast, path) self.add_source_code(path) - _update_file_scopes(compilation_unit_slither.scopes.values()) + for contract in sol_parser._underlying_contract_to_parser: + if contract.name.startswith("SlitherInternalTopLevelContract"): + raise SlitherError( + # region multi-line-string + """Your codebase has a contract named 'SlitherInternalTopLevelContract'. + Please rename it, this name is reserved for Slither's internals""" + # endregion multi-line + ) + sol_parser._contracts_by_id[contract.id] = contract + sol_parser._compilation_unit.contracts.append(contract) + + _update_file_scopes(sol_parser) if kwargs.get("generate_patches", False): self.generate_patches = True diff --git a/slither/slithir/convert.py b/slither/slithir/convert.py index 313033546..b8ce460bf 100644 --- a/slither/slithir/convert.py +++ b/slither/slithir/convert.py @@ -1502,7 +1502,6 @@ def convert_to_pop(ir: HighLevelCall, node: "Node") -> List[Operation]: def look_for_library_or_top_level( - contract: Contract, ir: HighLevelCall, using_for, t: Union[ @@ -1536,8 +1535,9 @@ def look_for_library_or_top_level( lib_contract = None if isinstance(destination, FunctionContract) and destination.contract.is_library: lib_contract = destination.contract - elif not isinstance(destination, FunctionTopLevel): - lib_contract = contract.file_scope.get_contract_from_name(str(destination)) + elif isinstance(destination, UserDefinedType) and isinstance(destination.type, Contract): + lib_contract = destination.type + if lib_contract: lib_call = LibraryCall( lib_contract, @@ -1561,20 +1561,14 @@ def look_for_library_or_top_level( def convert_to_library_or_top_level( ir: HighLevelCall, node: "Node", using_for ) -> Optional[Union[LibraryCall, InternalCall,]]: - # We use contract_declarer, because Solidity resolve the library - # before resolving the inheritance. - # Though we could use .contract as libraries cannot be shadowed - contract = ( - node.function.contract_declarer if isinstance(node.function, FunctionContract) else None - ) t = ir.destination.type if t in using_for: - new_ir = look_for_library_or_top_level(contract, ir, using_for, t) + new_ir = look_for_library_or_top_level(ir, using_for, t) if new_ir: return new_ir if "*" in using_for: - new_ir = look_for_library_or_top_level(contract, ir, using_for, "*") + new_ir = look_for_library_or_top_level(ir, using_for, "*") if new_ir: return new_ir @@ -1585,7 +1579,7 @@ def convert_to_library_or_top_level( and UserDefinedType(node.function.contract) in using_for ): new_ir = look_for_library_or_top_level( - contract, ir, using_for, UserDefinedType(node.function.contract) + ir, using_for, UserDefinedType(node.function.contract) ) if new_ir: return new_ir @@ -1740,7 +1734,10 @@ def convert_type_of_high_and_internal_level_call( ] for import_statement in contract.file_scope.imports: - if import_statement.alias and import_statement.alias == ir.contract_name: + if ( + import_statement.alias is not None + and import_statement.alias == ir.contract_name + ): imported_scope = contract.compilation_unit.get_scope(import_statement.filename) candidates += [ f @@ -1771,7 +1768,7 @@ def convert_type_of_high_and_internal_level_call( if func is None and candidates: func = _find_function_from_parameter(ir.arguments, candidates, False) - # lowlelvel lookup needs to be done at last step + # low level lookup needs to be done as last step if not func: if can_be_low_level(ir): return convert_to_low_level(ir) diff --git a/slither/solc_parsing/declarations/contract.py b/slither/solc_parsing/declarations/contract.py index 660aab176..ebab0e6df 100644 --- a/slither/solc_parsing/declarations/contract.py +++ b/slither/solc_parsing/declarations/contract.py @@ -585,7 +585,9 @@ def _analyze_params_elements( # pylint: disable=too-many-arguments,too-many-loc element.is_shadowed = True accessible_elements[element.full_name].shadows = True except (VariableNotFound, KeyError) as e: - self.log_incorrect_parsing(f"Missing params {e}") + self.log_incorrect_parsing( + f"Missing params {e} {self._contract.source_mapping.to_detailed_str()}" + ) return all_elements def analyze_constant_state_variables(self) -> None: diff --git a/slither/solc_parsing/declarations/function.py b/slither/solc_parsing/declarations/function.py index 21e860d37..57f4b26f1 100644 --- a/slither/solc_parsing/declarations/function.py +++ b/slither/solc_parsing/declarations/function.py @@ -63,10 +63,12 @@ def __init__( # Only present if compact AST if self.is_compact_ast: self._function.name = function_data["name"] - if "id" in function_data: - self._function.id = function_data["id"] else: self._function.name = function_data["attributes"][self.get_key()] + + if "id" in function_data: + self._function.id = function_data["id"] + self._functionNotParsed = function_data self._returnsNotParsed: List[dict] = [] self._params_was_analyzed = False diff --git a/slither/solc_parsing/expressions/find_variable.py b/slither/solc_parsing/expressions/find_variable.py index 404bcdee5..2d8fa58f6 100644 --- a/slither/solc_parsing/expressions/find_variable.py +++ b/slither/solc_parsing/expressions/find_variable.py @@ -305,6 +305,7 @@ def _find_variable_init( else: assert isinstance(underlying_function, FunctionContract) scope = underlying_function.contract.file_scope + elif isinstance(caller_context, StructureTopLevelSolc): direct_contracts = [] direct_functions_parser = [] @@ -505,4 +506,6 @@ def find_variable( if ret: return ret, False - raise VariableNotFound(f"Variable not found: {var_name} (context {contract})") + raise VariableNotFound( + f"Variable not found: {var_name} (context {contract} {contract.source_mapping.to_detailed_str()})" + ) diff --git a/slither/solc_parsing/slither_compilation_unit_solc.py b/slither/solc_parsing/slither_compilation_unit_solc.py index 721cf69fc..36efeef33 100644 --- a/slither/solc_parsing/slither_compilation_unit_solc.py +++ b/slither/solc_parsing/slither_compilation_unit_solc.py @@ -81,7 +81,16 @@ def __init__(self, compilation_unit: SlitherCompilationUnit) -> None: self._compilation_unit: SlitherCompilationUnit = compilation_unit self._contracts_by_id: Dict[int, Contract] = {} + # For top level functions, there should only be one `Function` since they can't be virtual and therefore can't be overridden. self._functions_by_id: Dict[int, List[Function]] = defaultdict(list) + self.imports_by_id: Dict[int, Import] = {} + self.top_level_events_by_id: Dict[int, EventTopLevel] = {} + self.top_level_errors_by_id: Dict[int, EventTopLevel] = {} + self.top_level_structures_by_id: Dict[int, StructureTopLevel] = {} + self.top_level_variables_by_id: Dict[int, TopLevelVariable] = {} + self.top_level_type_aliases_by_id: Dict[int, TypeAliasTopLevel] = {} + self.top_level_enums_by_id: Dict[int, EnumTopLevel] = {} + self._parsed = False self._analyzed = False self._is_compact_ast = False @@ -204,9 +213,11 @@ def _parse_enum(self, top_level_data: Dict, filename: str) -> None: scope = self.compilation_unit.get_scope(filename) enum = EnumTopLevel(name, canonicalName, values, scope) - scope.enums[name] = enum enum.set_offset(top_level_data["src"], self._compilation_unit) self._compilation_unit.enums_top_level.append(enum) + scope.enums[name] = enum + refId = top_level_data["id"] + self.top_level_enums_by_id[refId] = enum # pylint: disable=too-many-branches,too-many-statements,too-many-locals def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: @@ -217,8 +228,13 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: ) return + exported_symbols = {} if "nodeType" in data_loaded: self._is_compact_ast = True + exported_symbols = data_loaded.get("exportedSymbols", {}) + else: + attributes = data_loaded.get("attributes", {}) + exported_symbols = attributes.get("exportedSymbols", {}) if "sourcePaths" in data_loaded: for sourcePath in data_loaded["sourcePaths"]: @@ -236,7 +252,12 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: if self.get_children() not in data_loaded: return + scope = self.compilation_unit.get_scope(filename) + # Exported symbols includes a reference ID to all top-level definitions the file exports, + # including def's brought in by imports (even transitively) and def's local to the file. + for refId in exported_symbols.values(): + scope.exported_symbols |= set(refId) for top_level_data in data_loaded[self.get_children()]: if top_level_data[self.get_key()] == "ContractDefinition": @@ -269,6 +290,7 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: self._using_for_top_level_parser.append(usingFor_parser) elif top_level_data[self.get_key()] == "ImportDirective": + referenceId = top_level_data["id"] if self.is_compact_ast: import_directive = Import( Path( @@ -299,6 +321,7 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: import_directive.alias = top_level_data["attributes"]["unitAlias"] import_directive.set_offset(top_level_data["src"], self._compilation_unit) self._compilation_unit.import_directives.append(import_directive) + self.imports_by_id[referenceId] = import_directive get_imported_scope = self.compilation_unit.get_scope(import_directive.filename) scope.accessible_scopes.append(get_imported_scope) @@ -311,6 +334,8 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: self._compilation_unit.structures_top_level.append(st) self._structures_top_level_parser.append(st_parser) + referenceId = top_level_data["id"] + self.top_level_structures_by_id[referenceId] = st elif top_level_data[self.get_key()] == "EnumDefinition": # Note enum don't need a complex parser, so everything is directly done @@ -324,6 +349,9 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: self._compilation_unit.variables_top_level.append(var) self._variables_top_level_parser.append(var_parser) scope.variables[var.name] = var + referenceId = top_level_data["id"] + self.top_level_variables_by_id[referenceId] = var + elif top_level_data[self.get_key()] == "FunctionDefinition": func = FunctionTopLevel(self._compilation_unit, scope) scope.functions.add(func) @@ -342,6 +370,8 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: scope.custom_errors.add(custom_error) self._compilation_unit.custom_errors.append(custom_error) self._custom_error_parser.append(custom_error_parser) + referenceId = top_level_data["id"] + self.top_level_errors_by_id[referenceId] = custom_error elif top_level_data[self.get_key()] == "UserDefinedValueTypeDefinition": assert "name" in top_level_data @@ -360,6 +390,8 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: type_alias.set_offset(top_level_data["src"], self._compilation_unit) self._compilation_unit.type_aliases[alias] = type_alias scope.type_aliases[alias] = type_alias + referenceId = top_level_data["id"] + self.top_level_type_aliases_by_id[referenceId] = type_alias elif top_level_data[self.get_key()] == "EventDefinition": event = EventTopLevel(scope) @@ -369,6 +401,8 @@ def parse_top_level_items(self, data_loaded: Dict, filename: str) -> None: self._events_top_level_parser.append(event_parser) scope.events.add(event) self._compilation_unit.events_top_level.append(event) + referenceId = top_level_data["id"] + self.top_level_events_by_id[referenceId] = event else: raise SlitherException(f"Top level {top_level_data[self.get_key()]} not supported") @@ -432,19 +466,6 @@ def parse_contracts(self) -> None: # pylint: disable=too-many-statements,too-ma # pylint: disable=broad-exception-raised raise Exception("Contract analysis can be run only once!") - # First we save all the contracts in a dict - # the key is the contractid - for contract in self._underlying_contract_to_parser: - if contract.name.startswith("SlitherInternalTopLevelContract"): - raise SlitherException( - # region multi-line-string - """Your codebase has a contract named 'SlitherInternalTopLevelContract'. -Please rename it, this name is reserved for Slither's internals""" - # endregion multi-line - ) - self._contracts_by_id[contract.id] = contract - self._compilation_unit.contracts.append(contract) - def resolve_remapping_and_renaming(contract_parser: ContractSolc, want: str) -> Contract: contract_name = contract_parser.remapping[want] target = None diff --git a/slither/solc_parsing/solidity_types/type_parsing.py b/slither/solc_parsing/solidity_types/type_parsing.py index 06a91f911..6ca015127 100644 --- a/slither/solc_parsing/solidity_types/type_parsing.py +++ b/slither/solc_parsing/solidity_types/type_parsing.py @@ -238,7 +238,7 @@ def parse_type( renaming: Dict[str, str] type_aliases: Dict[str, TypeAlias] enums_direct_access: List["Enum"] = [] - # Note: for convenicence top level functions use the same parser than function in contract + # Note: for convenience top level functions use the same parser as function in contract # but contract_parser is set to None if isinstance(caller_context, SlitherCompilationUnitSolc) or ( isinstance(caller_context, FunctionSolc) and caller_context.contract_parser is None @@ -313,28 +313,28 @@ def parse_type( sl = caller_context.compilation_unit if isinstance(caller_context, FunctionSolc): underlying_func = caller_context.underlying_function - # If contract_parser is set to None, then underlying_function is a functionContract + # If contract_parser is set to None, then underlying_function is a FunctionContract # See note above assert isinstance(underlying_func, FunctionContract) contract = underlying_func.contract next_context = caller_context.contract_parser - scope = caller_context.underlying_function.file_scope + scope = underlying_func.file_scope else: contract = caller_context.underlying_contract next_context = caller_context - scope = caller_context.underlying_contract.file_scope + scope = contract.file_scope structures_direct_access = contract.structures - structures_direct_access += contract.file_scope.structures.values() - all_structuress = [c.structures for c in contract.file_scope.contracts.values()] + structures_direct_access += scope.structures.values() + all_structuress = [c.structures for c in scope.contracts.values()] all_structures = [item for sublist in all_structuress for item in sublist] - all_structures += contract.file_scope.structures.values() + all_structures += scope.structures.values() enums_direct_access += contract.enums - enums_direct_access += contract.file_scope.enums.values() - all_enumss = [c.enums for c in contract.file_scope.contracts.values()] + enums_direct_access += scope.enums.values() + all_enumss = [c.enums for c in scope.contracts.values()] all_enums = [item for sublist in all_enumss for item in sublist] - all_enums += contract.file_scope.enums.values() - contracts = contract.file_scope.contracts.values() + all_enums += scope.enums.values() + contracts = scope.contracts.values() functions = contract.functions + contract.modifiers renaming = scope.renaming @@ -495,4 +495,4 @@ def parse_type( return FunctionType(params_vars, return_values_vars) - raise ParsingError("Type name not found " + str(t)) + raise ParsingError(f"Type name not found {(t)} in {scope.filename}") diff --git a/slither/utils/using_for.py b/slither/utils/using_for.py index d8e6481eb..cc8527c85 100644 --- a/slither/utils/using_for.py +++ b/slither/utils/using_for.py @@ -1,15 +1,15 @@ from typing import Dict, List, TYPE_CHECKING, Union -from slither.core.solidity_types.type import Type +from slither.core.solidity_types import Type, UserDefinedType if TYPE_CHECKING: from slither.core.declarations import Function -USING_FOR_KEY = Union[str, Type] -USING_FOR_ITEM = List[Union[Type, "Function"]] +USING_FOR_KEY = Union[str, Type] # "*" is wildcard +USING_FOR_ITEM = List[Union[UserDefinedType, "Function"]] # UserDefinedType.type is a library USING_FOR = Dict[USING_FOR_KEY, USING_FOR_ITEM] -def _merge_using_for(uf1: USING_FOR, uf2: USING_FOR) -> USING_FOR: +def merge_using_for(uf1: USING_FOR, uf2: USING_FOR) -> USING_FOR: result = {**uf1, **uf2} for key, value in result.items(): if key in uf1 and key in uf2: diff --git a/tests/e2e/solc_parsing/test_ast_parsing.py b/tests/e2e/solc_parsing/test_ast_parsing.py index 96346bf36..ca3872f8c 100644 --- a/tests/e2e/solc_parsing/test_ast_parsing.py +++ b/tests/e2e/solc_parsing/test_ast_parsing.py @@ -473,6 +473,8 @@ def make_version(minor: int, patch_min: int, patch_max: int) -> List[str]: Test("enum-max-min.sol", ["0.8.19"]), Test("event-top-level.sol", ["0.8.22"]), Test("solidity-0.8.24.sol", ["0.8.24"], solc_args="--evm-version cancun"), + Test("scope/inherited_function_scope.sol", ["0.8.24"]), + Test("using_for_global_user_defined_operator_1.sol", ["0.8.24"]), ] # create the output folder if needed try: diff --git a/tests/e2e/solc_parsing/test_data/compile/scope/inherited_function_scope.sol-0.8.24-compact.zip b/tests/e2e/solc_parsing/test_data/compile/scope/inherited_function_scope.sol-0.8.24-compact.zip new file mode 100644 index 000000000..9bfeb835d Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/scope/inherited_function_scope.sol-0.8.24-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/compile/using_for_global_user_defined_operator_1.sol-0.8.24-compact.zip b/tests/e2e/solc_parsing/test_data/compile/using_for_global_user_defined_operator_1.sol-0.8.24-compact.zip new file mode 100644 index 000000000..ebab09f19 Binary files /dev/null and b/tests/e2e/solc_parsing/test_data/compile/using_for_global_user_defined_operator_1.sol-0.8.24-compact.zip differ diff --git a/tests/e2e/solc_parsing/test_data/expected/scope/inherited_function_scope.sol-0.8.24-compact.json b/tests/e2e/solc_parsing/test_data/expected/scope/inherited_function_scope.sol-0.8.24-compact.json new file mode 100644 index 000000000..0f54cab9e --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/expected/scope/inherited_function_scope.sol-0.8.24-compact.json @@ -0,0 +1,12 @@ +{ + "B": { + "_a()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n", + "b()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n", + "constructor()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + }, + "A": { + "_a()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n}\n", + "b()": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: EXPRESSION 1\n\"];\n}\n" + }, + "Z": {} +} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/expected/using_for_global_user_defined_operator_1.sol-0.8.24-compact.json b/tests/e2e/solc_parsing/test_data/expected/using_for_global_user_defined_operator_1.sol-0.8.24-compact.json new file mode 100644 index 000000000..ee2354fa9 --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/expected/using_for_global_user_defined_operator_1.sol-0.8.24-compact.json @@ -0,0 +1,9 @@ +{ + "BalanceDeltaLibrary": { + "amount0(BalanceDelta)": "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\"];\n2->3;\n3[label=\"Node Type: END INLINE ASM 3\n\"];\n3->4;\n4[label=\"Node Type: RETURN 4\n\"];\n}\n", + "amount1(BalanceDelta)": "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\"];\n2->3;\n3[label=\"Node Type: END INLINE ASM 3\n\"];\n3->4;\n4[label=\"Node Type: RETURN 4\n\"];\n}\n" + }, + "X": { + "get(BalanceDelta)": "digraph{\n0[label=\"Node Type: ENTRY_POINT 0\n\"];\n0->1;\n1[label=\"Node Type: NEW VARIABLE 1\n\"];\n1->2;\n2[label=\"Node Type: NEW VARIABLE 2\n\"];\n}\n" + } +} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/scope/inherited_function_scope.sol b/tests/e2e/solc_parsing/test_data/scope/inherited_function_scope.sol new file mode 100644 index 000000000..b372e3900 --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/scope/inherited_function_scope.sol @@ -0,0 +1,7 @@ +import {A} from "./scope_a.sol"; + +contract B is A { + constructor() { + b(); + } +} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/scope/scope_a.sol b/tests/e2e/solc_parsing/test_data/scope/scope_a.sol new file mode 100644 index 000000000..51bde3144 --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/scope/scope_a.sol @@ -0,0 +1,9 @@ +import {Z} from "./scope_z.sol"; +contract A { + function _a() private returns (Z) { + + } + function b() public { + _a(); + } +} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/scope/scope_z.sol b/tests/e2e/solc_parsing/test_data/scope/scope_z.sol new file mode 100644 index 000000000..fc63d3ded --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/scope/scope_z.sol @@ -0,0 +1 @@ +interface Z {} \ No newline at end of file diff --git a/tests/e2e/solc_parsing/test_data/using_for_global_user_defined_operator.sol b/tests/e2e/solc_parsing/test_data/using_for_global_user_defined_operator.sol new file mode 100644 index 000000000..7a6e53406 --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/using_for_global_user_defined_operator.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +type BalanceDelta is int256; + +using {add as +, sub as -, eq as ==} for BalanceDelta global; +using BalanceDeltaLibrary for BalanceDelta global; + +function toBalanceDelta(int128 _amount0, int128 _amount1) pure returns (BalanceDelta balanceDelta) { + /// @solidity memory-safe-assembly + assembly { + balanceDelta := + or(shl(128, _amount0), and(0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff, _amount1)) + } +} + +function add(BalanceDelta a, BalanceDelta b) pure returns (BalanceDelta) { + return toBalanceDelta(a.amount0() + b.amount0(), a.amount1() + b.amount1()); +} + +function sub(BalanceDelta a, BalanceDelta b) pure returns (BalanceDelta) { + return toBalanceDelta(a.amount0() - b.amount0(), a.amount1() - b.amount1()); +} + +function eq(BalanceDelta a, BalanceDelta b) pure returns (bool) { + return a.amount0() == b.amount0() && a.amount1() == b.amount1(); +} + +library BalanceDeltaLibrary { + function amount0(BalanceDelta balanceDelta) internal pure returns (int128 _amount0) { + /// @solidity memory-safe-assembly + assembly { + _amount0 := shr(128, balanceDelta) + } + } + + function amount1(BalanceDelta balanceDelta) internal pure returns (int128 _amount1) { + /// @solidity memory-safe-assembly + assembly { + _amount1 := balanceDelta + } + } +} diff --git a/tests/e2e/solc_parsing/test_data/using_for_global_user_defined_operator_1.sol b/tests/e2e/solc_parsing/test_data/using_for_global_user_defined_operator_1.sol new file mode 100644 index 000000000..f31c320a2 --- /dev/null +++ b/tests/e2e/solc_parsing/test_data/using_for_global_user_defined_operator_1.sol @@ -0,0 +1,9 @@ + +import {BalanceDelta} from "./using_for_global_user_defined_operator.sol"; +contract X { + + function get(BalanceDelta delta) external { + int128 amount0 = delta.amount0(); + int128 amount1 = delta.amount1(); + } +} \ No newline at end of file