diff --git a/examples/scripts/possible_paths.py b/examples/scripts/possible_paths.py new file mode 100644 index 000000000..e65ddb1c8 --- /dev/null +++ b/examples/scripts/possible_paths.py @@ -0,0 +1,192 @@ +import os +import argparse +from slither import Slither + + +def resolve_function(contract_name, function_name): + """ + Resolves a function instance, given a contract name and function. + :param contract_name: The name of the contract the function is declared in. + :param function_name: The name of the function to resolve. + :return: Returns the resolved function, raises an exception otherwise. + """ + # Obtain the target contract + contract = slither.get_contract_from_name(contract_name) + + # Verify the contract was resolved successfully + if contract is None: + raise ValueError(f"Could not resolve target contract: {contract_name}") + + # Obtain the target function + target_function = next((function for function in contract.functions if function.name == function_name), None) + + # Verify we have resolved the function specified. + if target_function is None: + raise ValueError(f"Could not resolve target function: {contract_name}.{function_name}") + + # Add the resolved function to the new list. + return target_function + + +def resolve_functions(functions): + """ + Resolves the provided function descriptors. + :param functions: A list of tuples (contract_name, function_name) or str (of form "ContractName.FunctionName") + to resolve into function objects. + :return: Returns a list of resolved functions. + """ + # Create the resolved list. + resolved = [] + + # Verify that the provided argument is a list. + if not isinstance(functions, list): + raise ValueError("Provided functions to resolve must be a list type.") + + # Loop for each item in the list. + for item in functions: + if isinstance(item, str): + # If the item is a single string, we assume it is of form 'ContractName.FunctionName'. + parts = item.split('.') + if len(parts) < 2: + raise ValueError("Provided string descriptor must be of form 'ContractName.FunctionName'") + resolved.append(resolve_function(parts[0], parts[1])) + elif isinstance(item, tuple): + # If the item is a tuple, it should be a 2-tuple providing contract and function names. + if len(item) != 2: + raise ValueError("Provided tuple descriptor must provide a contract and function name.") + resolved.append(resolve_function(item[0], item[1])) + else: + raise ValueError(f"Unexpected function descriptor type to resolve in list: {type(item)}") + + # Return the resolved list. + return resolved + + +def all_function_definitions(function): + """ + Obtains a list of representing this function and any base definitions + :param function: The function to obtain all definitions at and beneath. + :return: Returns a list composed of the provided function definition and any base definitions. + """ + return [function] + [f for c in function.contract.inheritance + for f in c.functions_and_modifiers_not_inherited + if f.full_name == function.full_name] + + +def __find_target_paths(target_function, current_path=[]): + + # Create our results list + results = set() + + # Add our current function to the path. + current_path = [target_function] + current_path + + # Obtain this target function and any base definitions. + all_target_functions = set(all_function_definitions(target_function)) + + # Look through all functions + for contract in slither.contracts: + for function in contract.functions_and_modifiers_not_inherited: + + # If the function is already in our path, skip it. + if function in current_path: + continue + + # Find all function calls in this function (except for low level) + called_functions = [f for (_, f) in function.high_level_calls + function.library_calls] + called_functions += function.internal_calls + called_functions = set(called_functions) + + # If any of our target functions are reachable from this function, it's a result. + if all_target_functions.intersection(called_functions): + path_results = __find_target_paths(function, current_path.copy()) + if path_results: + results = results.union(path_results) + + # If this path is external accessible from this point, we add the current path to the list. + if target_function.visibility in ['public', 'external'] and len(current_path) > 1: + results.add(tuple(current_path)) + + return results + + +def find_target_paths(target_functions): + """ + Obtains all functions which can lead to any of the target functions being called. + :param target_functions: The functions we are interested in reaching. + :return: Returns a list of all functions which can reach any of the target_functions. + """ + # Create our results list + results = set() + + # Loop for each target function + for target_function in target_functions: + results = results.union(__find_target_paths(target_function)) + + return results + + +def parse_args(): + """ + Parse the underlying arguments for the program. + :return: Returns the arguments for the program. + """ + parser = argparse.ArgumentParser(description='PossiblePaths', + usage='possible_paths.py [--is-truffle] filename [contract.function targets]') + + parser.add_argument('--is-truffle', + help='Indicates the filename refers to a truffle directory path.', + action='store_true', + default=False) + + parser.add_argument('filename', + help='The filename of the contract or truffle directory to analyze.') + + parser.add_argument('targets', nargs='+') + + return parser.parse_args() + + +# ------------------------------ +# PossiblePaths.py +# Usage: python3 possible_paths.py [--is-truffle] filename targets +# Example: python3 possible_paths.py contract.sol contract1.function1 contract2.function2 contract3.function3 +# ------------------------------ +# Parse all arguments +args = parse_args() + +# If this is a truffle project, verify we have a valid build directory. +if args.is_truffle: + cwd = os.path.abspath(args.filename) + build_dir = os.path.join(cwd, "build", "contracts") + if not os.path.exists(build_dir): + raise FileNotFoundError(f"Could not find truffle build directory at '{build_dir}'") + +# Perform slither analysis on the given filename +slither = Slither(args.filename, is_truffle=args.is_truffle) + +targets = resolve_functions(args.targets) + +# Print out all target functions. +print(f"Target functions:") +for target in targets: + print(f"-{target.contract.name}.{target.full_name}") +print("\n") + +# Obtain all paths which reach the target functions. +reaching_paths = find_target_paths(targets) +reaching_functions = set([y for x in reaching_paths for y in x if y not in targets]) + +# Print out all function names which can reach the targets. +print(f"The following functions reach the specified targets:") +for function_desc in sorted([f"{f.contract.name}.{f.full_name}" for f in reaching_functions]): + print(f"-{function_desc}") +print("\n") + +# Format all function paths. +reaching_paths_str = [' -> '.join([f"{f.contract.name}.{f.full_name}" for f in reaching_path]) for reaching_path in reaching_paths] + +# Print a sorted list of all function paths which can reach the targets. +print(f"The following paths reach the specified targets:") +for reaching_path in sorted(reaching_paths_str): + print(f"{reaching_path}\n") diff --git a/slither/detectors/abstract_detector.py b/slither/detectors/abstract_detector.py index d9caf0d92..0763ea75c 100644 --- a/slither/detectors/abstract_detector.py +++ b/slither/detectors/abstract_detector.py @@ -102,8 +102,10 @@ class AbstractDetector(metaclass=abc.ABCMeta): return def detect(self): - results = self._detect() - results = [r for r in results if self.slither.valid_result(r)] + all_results = self._detect() + results = [] + # only keep valid result, and remove dupplicate + [results.append(r) for r in all_results if self.slither.valid_result(r) and r not in results] if results: if self.logger: info = '\n'