-
Notifications
You must be signed in to change notification settings - Fork 979
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added possible_paths.py to example scripts: Provides information abou…
…t all functions/paths which reach specified target functions.
- Loading branch information
Showing
1 changed file
with
192 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") |