diff --git a/README.md b/README.md index 03c8712b3..1f24121f2 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ If Slither is run on a directory, it will run on every `.sol` file of the direct * `--printer-quick-summary`: Print a quick summary of the contracts * `--printer-inheritance`: Print the inheritance relations * `--printer-inheritance-graph`: Print the inheritance graph in a file +* `--printer-call-graph`: Print the call graph in a file * `--printer-vars-and-auth`: Print the variables written and the check on `msg.sender` of each function ## Checks available diff --git a/examples/printers/call_graph.sol b/examples/printers/call_graph.sol new file mode 100644 index 000000000..182ccbf52 --- /dev/null +++ b/examples/printers/call_graph.sol @@ -0,0 +1,34 @@ +library Library { + function library_func() { + } +} + +contract ContractA { + uint256 public val = 0; + + function my_func_a() { + keccak256(0); + Library.library_func(); + } +} + +contract ContractB { + ContractA a; + + constructor() { + a = new ContractA(); + } + + function my_func_b() { + a.my_func_a(); + my_second_func_b(); + } + + function my_func_a() { + my_second_func_b(); + } + + function my_second_func_b(){ + a.val(); + } +} \ No newline at end of file diff --git a/examples/printers/call_graph.sol.dot b/examples/printers/call_graph.sol.dot new file mode 100644 index 000000000..5677b7a40 --- /dev/null +++ b/examples/printers/call_graph.sol.dot @@ -0,0 +1,28 @@ +strict digraph { +subgraph cluster_5_Library { +label = "Library" +"5_library_func" [label="library_func"] +} +subgraph cluster_22_ContractA { +label = "ContractA" +"22_my_func_a" [label="my_func_a"] +"22_val" [label="val"] +} +subgraph cluster_63_ContractB { +label = "ContractB" +"63_my_second_func_b" [label="my_second_func_b"] +"63_my_func_a" [label="my_func_a"] +"63_constructor" [label="constructor"] +"63_my_func_b" [label="my_func_b"] +"63_my_func_b" -> "63_my_second_func_b" +"63_my_func_a" -> "63_my_second_func_b" +} +subgraph cluster_solidity { +label = "[Solidity]" +"keccak256()" +"22_my_func_a" -> "keccak256()" +} +"22_my_func_a" -> "5_library_func" +"63_my_func_b" -> "22_my_func_a" +"63_my_second_func_b" -> "22_val" +} \ No newline at end of file diff --git a/examples/printers/call_graph.sol.dot.png b/examples/printers/call_graph.sol.dot.png new file mode 100644 index 000000000..a679acbc4 Binary files /dev/null and b/examples/printers/call_graph.sol.dot.png differ diff --git a/slither/__main__.py b/slither/__main__.py index fedfbdfc1..085ef8e0f 100644 --- a/slither/__main__.py +++ b/slither/__main__.py @@ -157,6 +157,7 @@ def main(): from slither.printers.summary.contract import ContractSummary from slither.printers.inheritance.inheritance import PrinterInheritance from slither.printers.inheritance.inheritance_graph import PrinterInheritanceGraph + from slither.printers.call.call_graph import PrinterCallGraph from slither.printers.functions.authorization import PrinterWrittenVariablesAndAuthorization from slither.printers.summary.slithir import PrinterSlithIR @@ -164,6 +165,7 @@ def main(): ContractSummary, PrinterInheritance, PrinterInheritanceGraph, + PrinterCallGraph, PrinterWrittenVariablesAndAuthorization, PrinterSlithIR] diff --git a/slither/printers/call/__init__.py b/slither/printers/call/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/slither/printers/call/call_graph.py b/slither/printers/call/call_graph.py new file mode 100644 index 000000000..18058c6c6 --- /dev/null +++ b/slither/printers/call/call_graph.py @@ -0,0 +1,155 @@ +""" + Module printing the call graph + + The call graph shows for each function, + what are the contracts/functions called. + The output is a dot file named filename.dot +""" + +from slither.printers.abstract_printer import AbstractPrinter +from slither.core.declarations.solidity_variables import SolidityFunction +from slither.core.declarations.function import Function +from slither.core.declarations.contract import Contract +from slither.core.expressions.member_access import MemberAccess +from slither.core.expressions.identifier import Identifier +from slither.core.variables.variable import Variable +from slither.core.solidity_types.user_defined_type import UserDefinedType + +# return unique id for contract to use as subgraph name +def _contract_subgraph(contract): + return f'cluster_{contract.id}_{contract.name}' + +# return unique id for contract function to use as node name +def _function_node(contract, function): + return f'{contract.id}_{function.name}' + +# return unique id for solidity function to use as node name +def _solidity_function_node(solidity_function): + return f'{solidity_function.name}' + +# return dot language string to add graph edge +def _edge(from_node, to_node): + return f'"{from_node}" -> "{to_node}"' + +# return dot language string to add graph node (with optional label) +def _node(node, label=None): + return ' '.join(( + f'"{node}"', + f'[label="{label}"]' if label is not None else '', + )) + +class PrinterCallGraph(AbstractPrinter): + ARGUMENT = 'call-graph' + HELP = 'the call graph' + + def __init__(self, slither, logger): + super(PrinterCallGraph, self).__init__(slither, logger) + + self.contract_functions = {} # contract -> contract functions nodes + self.contract_calls = {} # contract -> contract calls edges + + for contract in slither.contracts: + self.contract_functions[contract] = set() + self.contract_calls[contract] = set() + + self.solidity_functions = set() # solidity function nodes + self.solidity_calls = set() # solidity calls edges + + self.external_calls = set() # external calls edges + + self._process_contracts(slither.contracts) + + def _process_contracts(self, contracts): + for contract in contracts: + for function in contract.functions: + self._process_function(contract, function) + + def _process_function(self, contract, function): + self.contract_functions[contract].add( + _node(_function_node(contract, function), function.name), + ) + + for internal_call in function.internal_calls: + self._process_internal_call(contract, function, internal_call) + for external_call in function.high_level_calls: + self._process_external_call(contract, function, external_call) + + def _process_internal_call(self, contract, function, internal_call): + if isinstance(internal_call, (Function)): + self.contract_calls[contract].add(_edge( + _function_node(contract, function), + _function_node(contract, internal_call), + )) + elif isinstance(internal_call, (SolidityFunction)): + self.solidity_functions.add( + _node(_solidity_function_node(internal_call)), + ) + self.solidity_calls.add(_edge( + _function_node(contract, function), + _solidity_function_node(internal_call), + )) + + def _process_external_call(self, contract, function, external_call): + external_contract, external_function = external_call + + # add variable as node to respective contract + if isinstance(external_function, (Variable)): + self.contract_functions[external_contract].add(_node( + _function_node(external_contract, external_function), + external_function.name + )) + + self.external_calls.add(_edge( + _function_node(contract, function), + _function_node(external_contract, external_function), + )) + + def _render_internal_calls(self): + lines = [] + + for contract in self.contract_functions: + lines.append(f'subgraph {_contract_subgraph(contract)} {{') + lines.append(f'label = "{contract.name}"') + + lines.extend(self.contract_functions[contract]) + lines.extend(self.contract_calls[contract]) + + lines.append('}') + + return '\n'.join(lines) + + def _render_solidity_calls(self): + lines = [] + + lines.append('subgraph cluster_solidity {') + lines.append('label = "[Solidity]"') + + lines.extend(self.solidity_functions) + lines.extend(self.solidity_calls) + + lines.append('}') + + return '\n'.join(lines) + + def _render_external_calls(self): + return '\n'.join(self.external_calls) + + def output(self, filename): + """ + Output the graph in filename + Args: + filename(string) + """ + if not filename.endswith('.dot'): + filename += '.dot' + + self.info(f'Call Graph: {filename}') + + with open(filename, 'w') as f: + f.write('\n'.join([ + 'strict digraph {', + self._render_internal_calls(), + self._render_solidity_calls(), + self._render_external_calls(), + '}', + ]))