|
|
|
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_declared
|
|
|
|
if f.full_name == function.full_name
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def __find_target_paths(target_function, current_path=None):
|
|
|
|
current_path = current_path if current_path else []
|
|
|
|
# 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_declared:
|
|
|
|
|
|
|
|
# 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("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 = {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("The following functions reach the specified targets:")
|
|
|
|
for function_desc in sorted([f"{f.canonical_name}" for f in reaching_functions]):
|
|
|
|
print(f"-{function_desc}")
|
|
|
|
print("\n")
|
|
|
|
|
|
|
|
# Format all function paths.
|
|
|
|
reaching_paths_str = [
|
|
|
|
" -> ".join([f"{f.canonical_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("The following paths reach the specified targets:")
|
|
|
|
for reaching_path in sorted(reaching_paths_str):
|
|
|
|
print(f"{reaching_path}\n")
|