From 8e1bab85894e1e3a5d18885ab74dead85555748e Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Tue, 4 Jun 2019 16:23:23 +0530 Subject: [PATCH 01/21] Adds a new printer (WIP) to output the evm instructions of nodes in functions. --- slither/printers/all_printers.py | 1 + slither/printers/summary/evm.py | 86 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 slither/printers/summary/evm.py diff --git a/slither/printers/all_printers.py b/slither/printers/all_printers.py index 3c6ad58d3..bf339f8fd 100644 --- a/slither/printers/all_printers.py +++ b/slither/printers/all_printers.py @@ -13,3 +13,4 @@ from .summary.variable_order import VariableOrder from .summary.data_depenency import DataDependency from .summary.modifier_calls import Modifiers from .summary.require_calls import RequireOrAssert +from .summary.evm import PrinterEVM diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py new file mode 100644 index 000000000..9dbdf3fcd --- /dev/null +++ b/slither/printers/summary/evm.py @@ -0,0 +1,86 @@ +""" + Module printing evm mapping of the contract +""" + +import logging +try: + from evm_cfg_builder.cfg import CFG +except ImportError: + logger.error("ERROR: in order to use evm printer, you need to install evm-cfg-builder") + logger.error("pip install evm-cfg-builder") + sys.exit(-1) +from slither.printers.abstract_printer import AbstractPrinter +from slither.utils.colors import blue, green, magenta + +class PrinterEVM(AbstractPrinter): + + ARGUMENT = 'evm' + HELP = 'Print the evm instructions of nodes in functions' + + WIKI = 'https://github.com/trailofbits/slither/wiki/Printer-documentation#evm' + + def output(self, _filename): + """ + _filename is not used + Args: + _filename(string) + """ + + source_to_pc_mapping = self._process_evm_cfg(self.slither) + + for contract in self.slither.contracts_derived: + print('Contract {}'.format(contract.name)) + contract_file = self.slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') + contract_pcs = source_to_pc_mapping['mapping', contract.name] + print("contract_pcs: " + str(contract_pcs)) + contract_cfg = source_to_pc_mapping['cfg', contract.name] + for function in contract.functions: + print(f'\tFunction {function.canonical_name}') + for node in function.nodes: + print("\t\tNode: " + str(node)) + node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 + print('\t\t\tEVM Instructions:') + node_pcs = contract_pcs.get(node_source_line, "[]") + for pc in node_pcs: + print('\t\t\t\tINS: {}'.format(pc)) + for modifier in contract.modifiers: + print('\tModifier {}'.format(modifier.canonical_name)) + for node in modifier.nodes: + node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 + print('\t\tEVM Instructions:') + node_pcs = contract_pcs[node_source_line] + for pc in node_pcs: + print('\t\t\tINS: {}'.format(pc)) + + def _process_evm_cfg(self, slither): + source_to_pc_mapping = {} + for contract in slither.contracts_derived: + contract_bytecode_runtime = slither.crytic_compile.bytecode_runtime(contract.name) + contract_srcmap_runtime = slither.crytic_compile.srcmap_runtime(contract.name) + cfg = CFG(contract_bytecode_runtime) + source_to_pc_mapping['cfg', contract.name] = cfg + source_to_pc_mapping['mapping', contract.name] = self._generate_source_to_ins_mapping(cfg.instructions, + contract_srcmap_runtime, slither, + contract.source_mapping['filename_absolute']) + return(source_to_pc_mapping) + + def _generate_source_to_ins_mapping(self, evm_instructions, srcmap_runtime, slither, filename): + source_to_pc_mapping = {} + file_source = slither.source_code[filename].encode('utf-8') + prev_mapping = [] + for idx, mapping in enumerate(srcmap_runtime): + mapping_item = mapping.split(':') + mapping_item += prev_mapping[len(mapping_item):] + for i in range(len(mapping_item)): + if mapping_item[i] == '': + mapping_item[i] = int(prev_mapping[i]) + offset, length, file_id, _ = mapping_item + if file_id == '-1': + # Internal compiler-generated code snippets to be ignored + # See https://github.com/ethereum/solidity/issues/6119#issuecomment-467797635 + continue + offset = int(offset) + line_number = file_source[0:offset].count("\n".encode("utf-8")) + 1 + prev_mapping = mapping_item + source_to_pc_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) + return(source_to_pc_mapping) From ac65676ff98f3df600e1fcc2157235fdffe99caf Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Wed, 5 Jun 2019 10:26:39 +0530 Subject: [PATCH 02/21] Adds printing of source lines and minor formatting changes. --- slither/printers/summary/evm.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index 9dbdf3fcd..c82539a3c 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -31,26 +31,28 @@ class PrinterEVM(AbstractPrinter): for contract in self.slither.contracts_derived: print('Contract {}'.format(contract.name)) contract_file = self.slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') + contract_file_lines = open(contract.source_mapping['filename_absolute'],'r').readlines() contract_pcs = source_to_pc_mapping['mapping', contract.name] - print("contract_pcs: " + str(contract_pcs)) contract_cfg = source_to_pc_mapping['cfg', contract.name] for function in contract.functions: print(f'\tFunction {function.canonical_name}') for node in function.nodes: print("\t\tNode: " + str(node)) node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 - print('\t\t\tEVM Instructions:') + print('\t\tSource line {}: {}'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) + print('\t\tEVM Instructions:') node_pcs = contract_pcs.get(node_source_line, "[]") for pc in node_pcs: - print('\t\t\t\tINS: {}'.format(pc)) + print('\t\t\t{}'.format(contract_cfg.get_instruction_at(pc))) for modifier in contract.modifiers: print('\tModifier {}'.format(modifier.canonical_name)) for node in modifier.nodes: node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 + print('\t\tSource line {}: {}'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) print('\t\tEVM Instructions:') node_pcs = contract_pcs[node_source_line] for pc in node_pcs: - print('\t\t\tINS: {}'.format(pc)) + print('\t\t\t{}'.format(contract_cfg.get_instruction_at(pc))) def _process_evm_cfg(self, slither): source_to_pc_mapping = {} From 72c2fcc4aedab3eb8b6d1671ab93f921acd88ba2 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Thu, 6 Jun 2019 10:12:38 +0530 Subject: [PATCH 03/21] Adds printing of PCs for evm instructions. --- slither/printers/summary/evm.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index c82539a3c..6c4746226 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -41,18 +41,18 @@ class PrinterEVM(AbstractPrinter): node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 print('\t\tSource line {}: {}'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) print('\t\tEVM Instructions:') - node_pcs = contract_pcs.get(node_source_line, "[]") + node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: - print('\t\t\t{}'.format(contract_cfg.get_instruction_at(pc))) + print('\t\t\t0x{:x}: {}'.format(int(pc), contract_cfg.get_instruction_at(pc))) for modifier in contract.modifiers: print('\tModifier {}'.format(modifier.canonical_name)) for node in modifier.nodes: node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 - print('\t\tSource line {}: {}'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) + print('\t\tSource line {:x}: {}'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) print('\t\tEVM Instructions:') - node_pcs = contract_pcs[node_source_line] + node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: - print('\t\t\t{}'.format(contract_cfg.get_instruction_at(pc))) + print('\t\t\t0x{:x}: {}'.format(int(pc), contract_cfg.get_instruction_at(pc))) def _process_evm_cfg(self, slither): source_to_pc_mapping = {} From 2c8104364106166aecf0cf307c145936200bcb99 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Fri, 7 Jun 2019 09:34:44 +0530 Subject: [PATCH 04/21] Adds minor format changes --- slither/printers/summary/evm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index 6c4746226..856c42b4f 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -3,14 +3,13 @@ """ import logging +from slither.printers.abstract_printer import AbstractPrinter try: from evm_cfg_builder.cfg import CFG except ImportError: logger.error("ERROR: in order to use evm printer, you need to install evm-cfg-builder") logger.error("pip install evm-cfg-builder") sys.exit(-1) -from slither.printers.abstract_printer import AbstractPrinter -from slither.utils.colors import blue, green, magenta class PrinterEVM(AbstractPrinter): From 29f44a6f621aa642c7d596c68a3783507094a740 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Fri, 7 Jun 2019 10:20:36 +0530 Subject: [PATCH 05/21] Adds comments and some more formatting changes --- slither/printers/summary/evm.py | 43 ++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index 856c42b4f..2cdb12b3c 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -25,14 +25,14 @@ class PrinterEVM(AbstractPrinter): _filename(string) """ - source_to_pc_mapping = self._process_evm_cfg(self.slither) + evm_info = self._extract_evm_info(self.slither) for contract in self.slither.contracts_derived: print('Contract {}'.format(contract.name)) contract_file = self.slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') contract_file_lines = open(contract.source_mapping['filename_absolute'],'r').readlines() - contract_pcs = source_to_pc_mapping['mapping', contract.name] - contract_cfg = source_to_pc_mapping['cfg', contract.name] + contract_cfg = evm_info['cfg', contract.name] + contract_pcs = evm_info['mapping', contract.name] for function in contract.functions: print(f'\tFunction {function.canonical_name}') for node in function.nodes: @@ -44,32 +44,44 @@ class PrinterEVM(AbstractPrinter): for pc in node_pcs: print('\t\t\t0x{:x}: {}'.format(int(pc), contract_cfg.get_instruction_at(pc))) for modifier in contract.modifiers: - print('\tModifier {}'.format(modifier.canonical_name)) + print(f'\tModifier {modifier.canonical_name}') for node in modifier.nodes: node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 - print('\t\tSource line {:x}: {}'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) + print('\t\tSource line {}: {}'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) print('\t\tEVM Instructions:') node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: print('\t\t\t0x{:x}: {}'.format(int(pc), contract_cfg.get_instruction_at(pc))) - def _process_evm_cfg(self, slither): - source_to_pc_mapping = {} + def _extract_evm_info(self, slither): + ''' + Extract evm information for all derived contracts using evm_cfg_builder + + Returns: evm CFG and Solidity source to Program Counter (pc) mapping + ''' + evm_info = {} for contract in slither.contracts_derived: contract_bytecode_runtime = slither.crytic_compile.bytecode_runtime(contract.name) contract_srcmap_runtime = slither.crytic_compile.srcmap_runtime(contract.name) cfg = CFG(contract_bytecode_runtime) - source_to_pc_mapping['cfg', contract.name] = cfg - source_to_pc_mapping['mapping', contract.name] = self._generate_source_to_ins_mapping(cfg.instructions, + evm_info['cfg', contract.name] = cfg + evm_info['mapping', contract.name] = self._generate_source_to_evm_ins_mapping(cfg.instructions, contract_srcmap_runtime, slither, contract.source_mapping['filename_absolute']) - return(source_to_pc_mapping) + return(evm_info) - def _generate_source_to_ins_mapping(self, evm_instructions, srcmap_runtime, slither, filename): - source_to_pc_mapping = {} + def _generate_source_to_evm_ins_mapping(self, evm_instructions, srcmap_runtime, slither, filename): + ''' + Generate Solidity source to EVM instruction mapping using evm_cfg_builder:cfg.instructions and solc:srcmap_runtime + + Returns: Solidity source to EVM instruction mapping + ''' + source_to_evm_mapping = {} file_source = slither.source_code[filename].encode('utf-8') prev_mapping = [] for idx, mapping in enumerate(srcmap_runtime): + # Parse srcmap_runtime according to its format + # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings mapping_item = mapping.split(':') mapping_item += prev_mapping[len(mapping_item):] for i in range(len(mapping_item)): @@ -83,5 +95,8 @@ class PrinterEVM(AbstractPrinter): offset = int(offset) line_number = file_source[0:offset].count("\n".encode("utf-8")) + 1 prev_mapping = mapping_item - source_to_pc_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) - return(source_to_pc_mapping) + # Append evm instructions to the corresponding source line number + # Note: Some evm instructions in mapping are not necessarily in program execution order + # Note: The order depends on how solc creates the srcmap_runtime + source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) + return(source_to_evm_mapping) From 8f1c3e06f49b15d9cc66d0c677218442447763a6 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Fri, 7 Jun 2019 10:28:51 +0530 Subject: [PATCH 06/21] Improves code readability --- slither/printers/summary/evm.py | 35 ++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index 2cdb12b3c..d8ca25c0a 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -5,12 +5,15 @@ import logging from slither.printers.abstract_printer import AbstractPrinter try: + # Avoiding the addition of evm_cfg_builder as permanent dependency from evm_cfg_builder.cfg import CFG except ImportError: - logger.error("ERROR: in order to use evm printer, you need to install evm-cfg-builder") - logger.error("pip install evm-cfg-builder") + logger.error("To use evm printer, you need to install evm-cfg-builder from ToB") + logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") + logger.error("Installation: pip install evm-cfg-builder") sys.exit(-1) + class PrinterEVM(AbstractPrinter): ARGUMENT = 'evm' @@ -18,6 +21,7 @@ class PrinterEVM(AbstractPrinter): WIKI = 'https://github.com/trailofbits/slither/wiki/Printer-documentation#evm' + def output(self, _filename): """ _filename is not used @@ -29,10 +33,12 @@ class PrinterEVM(AbstractPrinter): for contract in self.slither.contracts_derived: print('Contract {}'.format(contract.name)) + contract_file = self.slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') contract_file_lines = open(contract.source_mapping['filename_absolute'],'r').readlines() contract_cfg = evm_info['cfg', contract.name] contract_pcs = evm_info['mapping', contract.name] + for function in contract.functions: print(f'\tFunction {function.canonical_name}') for node in function.nodes: @@ -43,6 +49,7 @@ class PrinterEVM(AbstractPrinter): node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: print('\t\t\t0x{:x}: {}'.format(int(pc), contract_cfg.get_instruction_at(pc))) + for modifier in contract.modifiers: print(f'\tModifier {modifier.canonical_name}') for node in modifier.nodes: @@ -53,13 +60,16 @@ class PrinterEVM(AbstractPrinter): for pc in node_pcs: print('\t\t\t0x{:x}: {}'.format(int(pc), contract_cfg.get_instruction_at(pc))) + def _extract_evm_info(self, slither): - ''' + """ Extract evm information for all derived contracts using evm_cfg_builder Returns: evm CFG and Solidity source to Program Counter (pc) mapping - ''' + """ + evm_info = {} + for contract in slither.contracts_derived: contract_bytecode_runtime = slither.crytic_compile.bytecode_runtime(contract.name) contract_srcmap_runtime = slither.crytic_compile.srcmap_runtime(contract.name) @@ -69,34 +79,45 @@ class PrinterEVM(AbstractPrinter): contract_srcmap_runtime, slither, contract.source_mapping['filename_absolute']) return(evm_info) + def _generate_source_to_evm_ins_mapping(self, evm_instructions, srcmap_runtime, slither, filename): - ''' + """ Generate Solidity source to EVM instruction mapping using evm_cfg_builder:cfg.instructions and solc:srcmap_runtime Returns: Solidity source to EVM instruction mapping - ''' + """ + source_to_evm_mapping = {} file_source = slither.source_code[filename].encode('utf-8') prev_mapping = [] + for idx, mapping in enumerate(srcmap_runtime): # Parse srcmap_runtime according to its format # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings + mapping_item = mapping.split(':') mapping_item += prev_mapping[len(mapping_item):] + for i in range(len(mapping_item)): if mapping_item[i] == '': mapping_item[i] = int(prev_mapping[i]) + offset, length, file_id, _ = mapping_item + if file_id == '-1': # Internal compiler-generated code snippets to be ignored # See https://github.com/ethereum/solidity/issues/6119#issuecomment-467797635 continue + offset = int(offset) line_number = file_source[0:offset].count("\n".encode("utf-8")) + 1 - prev_mapping = mapping_item + # Append evm instructions to the corresponding source line number # Note: Some evm instructions in mapping are not necessarily in program execution order # Note: The order depends on how solc creates the srcmap_runtime source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) + + prev_mapping = mapping_item + return(source_to_evm_mapping) From 8944eab3be7180d1fc7f192e8faef14034c01ab7 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Fri, 7 Jun 2019 14:34:48 +0530 Subject: [PATCH 07/21] Adds get_evm_instructions() API to script-ify getting evm instructions of Contract, Function or Node. --- examples/scripts/convert_to_evm_ins.py | 29 ++++++ examples/scripts/test_evm_api.sol | 5 + slither/evm/__init__.py | 0 slither/evm/convert.py | 121 +++++++++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 examples/scripts/convert_to_evm_ins.py create mode 100644 examples/scripts/test_evm_api.sol create mode 100644 slither/evm/__init__.py create mode 100644 slither/evm/convert.py diff --git a/examples/scripts/convert_to_evm_ins.py b/examples/scripts/convert_to_evm_ins.py new file mode 100644 index 000000000..2ac0c1027 --- /dev/null +++ b/examples/scripts/convert_to_evm_ins.py @@ -0,0 +1,29 @@ +import sys +from slither.slither import Slither +from slither.evm.convert import get_evm_instructions + + +if len(sys.argv) != 2: + print('python function_called.py functions_called.sol') + exit(-1) + +# Init slither +slither = Slither(sys.argv[1]) + +# Get the contract evm instructions +contract = slither.get_contract_from_name('Test') +contract_ins = get_evm_instructions(contract) +print("Contract evm instructions: " + str(contract_ins)) + +# Get the function evm instructions +function = contract.get_function_from_signature('foo()') +function_ins = get_evm_instructions(function) +print("Function evm instructions: " + str(function_ins)) + +# Get the node evm instructions +nodes = function.nodes +for node in nodes: + print("Node: " + str(node)) + node_ins = get_evm_instructions(node) + print("Node evm instructions: " + str(node_ins)) + diff --git a/examples/scripts/test_evm_api.sol b/examples/scripts/test_evm_api.sol new file mode 100644 index 000000000..b96d68a5c --- /dev/null +++ b/examples/scripts/test_evm_api.sol @@ -0,0 +1,5 @@ +contract Test { + function foo() { + uint i; + } +} diff --git a/slither/evm/__init__.py b/slither/evm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/slither/evm/convert.py b/slither/evm/convert.py new file mode 100644 index 000000000..62a6d721c --- /dev/null +++ b/slither/evm/convert.py @@ -0,0 +1,121 @@ +import logging +import sha3 +from slither.core.declarations import (Contract, Function) +from slither.core.cfg.node import Node + +logger = logging.getLogger('ConvertToEVM') + +def get_evm_instructions(obj): + + assert isinstance(obj, (Function, Contract, Node)) + + if "KEY_EVM_INS" not in obj.context: + + try: + # Avoiding the addition of evm_cfg_builder as permanent dependency + from evm_cfg_builder.cfg import CFG + except ImportError: + logger.error("To use evm printer, you need to install evm-cfg-builder from ToB") + logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") + logger.error("Installation: pip install evm-cfg-builder") + sys.exit(-1) + + slither = obj.slither + + if isinstance(obj, Node): + contract = obj.function.contract + elif isinstance(obj, Function): + contract = obj.contract + else: + contract = obj + + # Get contract bytecode, srcmap and cfg + contract_bytecode_runtime = slither.crytic_compile.bytecode_runtime(contract.name) + contract_srcmap_runtime = slither.crytic_compile.srcmap_runtime(contract.name) + contract_cfg = CFG(contract_bytecode_runtime) + + # Get evm instructions + if isinstance(obj, Contract): + obj.context["KEY_EVM_INS"] = contract_cfg.instructions + elif isinstance(obj, Function): + function = obj + # Get first four bytes of function singature's keccak-256 hash used as function selector + function_hash = "0x" + get_function_hash(function.full_name)[:8] + function_evm = get_function_evm(contract_cfg, function.name, function_hash) + function_ins = [] + for basic_block in sorted(function_evm.basic_blocks, key=lambda x:x.start.pc): + function_ins.append(basic_block.instructions) + obj.context["KEY_EVM_INS"] = function_ins + else: # Node obj + node = obj + function = node.function + contract_pcs = _generate_source_to_evm_ins_mapping(contract_cfg.instructions, + contract_srcmap_runtime, obj.slither, + contract.source_mapping['filename_absolute']) + contract_file = slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') + + # Get first four bytes of function singature's keccak-256 hash used as function selector + function_hash = "0x" + get_function_hash(function.full_name)[:8] + function_evm = get_function_evm(contract_cfg, function.name, function_hash) + node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 + node_pcs = contract_pcs.get(node_source_line, []) + node_ins = [] + for pc in node_pcs: + node_ins.append(contract_cfg.get_instruction_at(pc)) + obj.context["KEY_EVM_INS"] = node_ins + + return obj.context.get("KEY_EVM_INS", []) + +def get_function_hash(function_signature): + hash = sha3.keccak_256() + hash.update(function_signature.encode('utf-8')) + return hash.hexdigest() + +def get_function_evm(cfg, function_name, function_hash): + for function_evm in cfg.functions: + if function_evm.name[:2] == "0x" and function_evm.name == function_hash: + return function_evm + elif function_evm.name[:2] != "0x" and function_evm.name.split('(')[0] == function_name: + return function_evm + return "None" + +def _generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slither, filename): + """ + Generate Solidity source to EVM instruction mapping using evm_cfg_builder:cfg.instructions and solc:srcmap_runtime + + Returns: Solidity source to EVM instruction mapping + """ + + source_to_evm_mapping = {} + file_source = slither.source_code[filename].encode('utf-8') + prev_mapping = [] + + for idx, mapping in enumerate(srcmap_runtime): + # Parse srcmap_runtime according to its format + # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings + + mapping_item = mapping.split(':') + mapping_item += prev_mapping[len(mapping_item):] + + for i in range(len(mapping_item)): + if mapping_item[i] == '': + mapping_item[i] = int(prev_mapping[i]) + + offset, length, file_id, _ = mapping_item + + if file_id == '-1': + # Internal compiler-generated code snippets to be ignored + # See https://github.com/ethereum/solidity/issues/6119#issuecomment-467797635 + continue + + offset = int(offset) + line_number = file_source[0:offset].count("\n".encode("utf-8")) + 1 + + # Append evm instructions to the corresponding source line number + # Note: Some evm instructions in mapping are not necessarily in program execution order + # Note: The order depends on how solc creates the srcmap_runtime + source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) + + prev_mapping = mapping_item + + return(source_to_evm_mapping) From 209d3a31af5367e770240e0ab17cd34ea5e6b61c Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Thu, 13 Jun 2019 13:57:08 +0530 Subject: [PATCH 08/21] Fixes bug where prev_mapping was not being set when file_id == 1 --- slither/printers/summary/evm.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index d8ca25c0a..5e540474f 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -30,7 +30,7 @@ class PrinterEVM(AbstractPrinter): """ evm_info = self._extract_evm_info(self.slither) - + for contract in self.slither.contracts_derived: print('Contract {}'.format(contract.name)) @@ -95,7 +95,10 @@ class PrinterEVM(AbstractPrinter): for idx, mapping in enumerate(srcmap_runtime): # Parse srcmap_runtime according to its format # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings - + # In order to compress these source mappings especially for bytecode, the following rules are used: + # If a field is empty, the value of the preceding element is used. + # If a : is missing, all following fields are considered empty. + mapping_item = mapping.split(':') mapping_item += prev_mapping[len(mapping_item):] @@ -104,6 +107,7 @@ class PrinterEVM(AbstractPrinter): mapping_item[i] = int(prev_mapping[i]) offset, length, file_id, _ = mapping_item + prev_mapping = mapping_item if file_id == '-1': # Internal compiler-generated code snippets to be ignored @@ -118,6 +122,4 @@ class PrinterEVM(AbstractPrinter): # Note: The order depends on how solc creates the srcmap_runtime source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) - prev_mapping = mapping_item - return(source_to_evm_mapping) From 5434e6e11f5ea634d6aba4eb45d5057db15daa77 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Thu, 13 Jun 2019 14:14:46 +0530 Subject: [PATCH 09/21] Fixes bug where prev_mapping was not being set when file_id == 1 --- slither/evm/convert.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/slither/evm/convert.py b/slither/evm/convert.py index 62a6d721c..1e0196767 100644 --- a/slither/evm/convert.py +++ b/slither/evm/convert.py @@ -93,6 +93,9 @@ def _generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slithe for idx, mapping in enumerate(srcmap_runtime): # Parse srcmap_runtime according to its format # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings + # In order to compress these source mappings especially for bytecode, the following rules are used: + # If a field is empty, the value of the preceding element is used. + # If a : is missing, all following fields are considered empty. mapping_item = mapping.split(':') mapping_item += prev_mapping[len(mapping_item):] @@ -102,6 +105,7 @@ def _generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slithe mapping_item[i] = int(prev_mapping[i]) offset, length, file_id, _ = mapping_item + prev_mapping = mapping_item if file_id == '-1': # Internal compiler-generated code snippets to be ignored @@ -116,6 +120,4 @@ def _generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slithe # Note: The order depends on how solc creates the srcmap_runtime source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) - prev_mapping = mapping_item - return(source_to_evm_mapping) From b9e40b628dcec7b85940ddd5bd5862bc05c8b7e8 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Thu, 13 Jun 2019 17:15:06 +0530 Subject: [PATCH 10/21] Removes some unused code and adds checks and comments. --- examples/scripts/convert_to_evm_ins.py | 13 +++++++------ examples/scripts/test_evm_api.sol | 7 ++++++- slither/evm/convert.py | 18 ++++++++++++------ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/examples/scripts/convert_to_evm_ins.py b/examples/scripts/convert_to_evm_ins.py index 2ac0c1027..e3286b5d6 100644 --- a/examples/scripts/convert_to_evm_ins.py +++ b/examples/scripts/convert_to_evm_ins.py @@ -13,17 +13,18 @@ slither = Slither(sys.argv[1]) # Get the contract evm instructions contract = slither.get_contract_from_name('Test') contract_ins = get_evm_instructions(contract) -print("Contract evm instructions: " + str(contract_ins)) +print("## Contract evm instructions: {} ##".format(contract.name)) +print(contract_ins) # Get the function evm instructions function = contract.get_function_from_signature('foo()') +print("## Function evm instructions: {} ##".format(function.name)) function_ins = get_evm_instructions(function) -print("Function evm instructions: " + str(function_ins)) - +print(function_ins) + # Get the node evm instructions nodes = function.nodes for node in nodes: - print("Node: " + str(node)) node_ins = get_evm_instructions(node) - print("Node evm instructions: " + str(node_ins)) - + print("Node evm instructions: {}".format(str(node))) + print(node_ins) diff --git a/examples/scripts/test_evm_api.sol b/examples/scripts/test_evm_api.sol index b96d68a5c..f69e850b0 100644 --- a/examples/scripts/test_evm_api.sol +++ b/examples/scripts/test_evm_api.sol @@ -1,5 +1,10 @@ contract Test { - function foo() { + function foo() public returns (uint) { uint i; + return(i+10); + } + + function foobar(uint i) public returns (uint) { + return(i+10); } } diff --git a/slither/evm/convert.py b/slither/evm/convert.py index 1e0196767..a2efe2f7e 100644 --- a/slither/evm/convert.py +++ b/slither/evm/convert.py @@ -1,5 +1,6 @@ import logging import sha3 +import sys from slither.core.declarations import (Contract, Function) from slither.core.cfg.node import Node @@ -42,21 +43,24 @@ def get_evm_instructions(obj): # Get first four bytes of function singature's keccak-256 hash used as function selector function_hash = "0x" + get_function_hash(function.full_name)[:8] function_evm = get_function_evm(contract_cfg, function.name, function_hash) + if function_evm == "None": + logger.error("Function " + function.name + " not found") + sys.exit(-1) function_ins = [] for basic_block in sorted(function_evm.basic_blocks, key=lambda x:x.start.pc): - function_ins.append(basic_block.instructions) + for ins in basic_block.instructions: + function_ins.append(ins) obj.context["KEY_EVM_INS"] = function_ins else: # Node obj node = obj - function = node.function + + # Get evm instructions for node's contract contract_pcs = _generate_source_to_evm_ins_mapping(contract_cfg.instructions, contract_srcmap_runtime, obj.slither, contract.source_mapping['filename_absolute']) contract_file = slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') - - # Get first four bytes of function singature's keccak-256 hash used as function selector - function_hash = "0x" + get_function_hash(function.full_name)[:8] - function_evm = get_function_evm(contract_cfg, function.name, function_hash) + + # Get evm instructions corresponding to node's source line number node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 node_pcs = contract_pcs.get(node_source_line, []) node_ins = [] @@ -73,8 +77,10 @@ def get_function_hash(function_signature): def get_function_evm(cfg, function_name, function_hash): for function_evm in cfg.functions: + # Match function hash if function_evm.name[:2] == "0x" and function_evm.name == function_hash: return function_evm + # Match function name elif function_evm.name[:2] != "0x" and function_evm.name.split('(')[0] == function_name: return function_evm return "None" From cb13cb4a898b66868519779ddd2e700911b1f545 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Fri, 14 Jun 2019 16:27:41 +0530 Subject: [PATCH 11/21] Adds support for constructor's evm instructions using bytecode_init and srcmap_init. --- examples/scripts/convert_to_evm_ins.py | 18 +++++++--- examples/scripts/test_evm_api.sol | 7 ++++ slither/evm/convert.py | 49 +++++++++++++++++++++----- slither/printers/summary/evm.py | 15 ++++++-- 4 files changed, 74 insertions(+), 15 deletions(-) diff --git a/examples/scripts/convert_to_evm_ins.py b/examples/scripts/convert_to_evm_ins.py index e3286b5d6..f9b7220d2 100644 --- a/examples/scripts/convert_to_evm_ins.py +++ b/examples/scripts/convert_to_evm_ins.py @@ -4,7 +4,7 @@ from slither.evm.convert import get_evm_instructions if len(sys.argv) != 2: - print('python function_called.py functions_called.sol') + print('python3 function_called.py functions_called.sol') exit(-1) # Init slither @@ -14,17 +14,27 @@ slither = Slither(sys.argv[1]) contract = slither.get_contract_from_name('Test') contract_ins = get_evm_instructions(contract) print("## Contract evm instructions: {} ##".format(contract.name)) -print(contract_ins) +for ins in contract_ins: + print(str(ins)) +# Get the constructor evm instructions +constructor = contract.constructor +print("## Function evm instructions: {} ##".format(constructor.name)) +constructor_ins = get_evm_instructions(constructor) +for ins in constructor_ins: + print(str(ins)) + # Get the function evm instructions function = contract.get_function_from_signature('foo()') print("## Function evm instructions: {} ##".format(function.name)) function_ins = get_evm_instructions(function) -print(function_ins) +for ins in function_ins: + print(str(ins)) # Get the node evm instructions nodes = function.nodes for node in nodes: node_ins = get_evm_instructions(node) print("Node evm instructions: {}".format(str(node))) - print(node_ins) + for ins in node_ins: + print(str(ins)) diff --git a/examples/scripts/test_evm_api.sol b/examples/scripts/test_evm_api.sol index f69e850b0..7a43df074 100644 --- a/examples/scripts/test_evm_api.sol +++ b/examples/scripts/test_evm_api.sol @@ -1,4 +1,11 @@ contract Test { + + address owner; + + constructor () { + owner = msg.sender; + } + function foo() public returns (uint) { uint i; return(i+10); diff --git a/slither/evm/convert.py b/slither/evm/convert.py index a2efe2f7e..0f8ed3378 100644 --- a/slither/evm/convert.py +++ b/slither/evm/convert.py @@ -30,33 +30,63 @@ def get_evm_instructions(obj): else: contract = obj - # Get contract bytecode, srcmap and cfg + # Get contract runtime bytecode, srcmap and cfg contract_bytecode_runtime = slither.crytic_compile.bytecode_runtime(contract.name) contract_srcmap_runtime = slither.crytic_compile.srcmap_runtime(contract.name) contract_cfg = CFG(contract_bytecode_runtime) - # Get evm instructions + # Get contract init bytecode, srcmap and cfg + contract_bytecode_init= slither.crytic_compile.bytecode_init(contract.name) + contract_srcmap_init = slither.crytic_compile.srcmap_init(contract.name) + contract_cfg_init = CFG(contract_bytecode_init) + + # Get evm instructions for contract if isinstance(obj, Contract): - obj.context["KEY_EVM_INS"] = contract_cfg.instructions + # Combine the instructions of constructor and the rest of the contract + obj.context["KEY_EVM_INS"] = contract_cfg_init.instructions + contract_cfg.instructions + + # Get evm instructions for function elif isinstance(obj, Function): + function = obj - # Get first four bytes of function singature's keccak-256 hash used as function selector - function_hash = "0x" + get_function_hash(function.full_name)[:8] - function_evm = get_function_evm(contract_cfg, function.name, function_hash) + + # CFG depends on function being constructor or not + if function.is_constructor: + cfg = contract_cfg_init + name = "_dispatcher" + hash = "" + else: + cfg = contract_cfg + name = function.name + # Get first four bytes of function singature's keccak-256 hash used as function selector + hash = "0x" + get_function_hash(function.full_name)[:8] + + function_evm = get_function_evm(cfg, name, hash) if function_evm == "None": logger.error("Function " + function.name + " not found") sys.exit(-1) + function_ins = [] for basic_block in sorted(function_evm.basic_blocks, key=lambda x:x.start.pc): for ins in basic_block.instructions: function_ins.append(ins) + obj.context["KEY_EVM_INS"] = function_ins + else: # Node obj node = obj + # CFG and srcmap depend on function being constructor or not + if node.function.is_constructor: + cfg = contract_cfg_init + srcmap = contract_srcmap_init + else: + cfg = contract_cfg + srcmap = contract_srcmap_runtime + # Get evm instructions for node's contract - contract_pcs = _generate_source_to_evm_ins_mapping(contract_cfg.instructions, - contract_srcmap_runtime, obj.slither, + contract_pcs = _generate_source_to_evm_ins_mapping(cfg.instructions, + srcmap, obj.slither, contract.source_mapping['filename_absolute']) contract_file = slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') @@ -65,7 +95,8 @@ def get_evm_instructions(obj): node_pcs = contract_pcs.get(node_source_line, []) node_ins = [] for pc in node_pcs: - node_ins.append(contract_cfg.get_instruction_at(pc)) + node_ins.append(cfg.get_instruction_at(pc)) + obj.context["KEY_EVM_INS"] = node_ins return obj.context.get("KEY_EVM_INS", []) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index 5e540474f..52f89e87d 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -36,11 +36,15 @@ class PrinterEVM(AbstractPrinter): contract_file = self.slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') contract_file_lines = open(contract.source_mapping['filename_absolute'],'r').readlines() - contract_cfg = evm_info['cfg', contract.name] - contract_pcs = evm_info['mapping', contract.name] for function in contract.functions: print(f'\tFunction {function.canonical_name}') + if function.is_constructor: + contract_cfg = evm_info['cfg_init', contract.name] + contract_pcs = evm_info['mapping_init', contract.name] + else: + contract_cfg = evm_info['cfg', contract.name] + contract_pcs = evm_info['mapping', contract.name] for node in function.nodes: print("\t\tNode: " + str(node)) node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 @@ -78,6 +82,13 @@ class PrinterEVM(AbstractPrinter): evm_info['mapping', contract.name] = self._generate_source_to_evm_ins_mapping(cfg.instructions, contract_srcmap_runtime, slither, contract.source_mapping['filename_absolute']) + contract_bytecode_init= slither.crytic_compile.bytecode_init(contract.name) + contract_srcmap_init = slither.crytic_compile.srcmap_init(contract.name) + cfg_init = CFG(contract_bytecode_init) + evm_info['cfg_init', contract.name] = cfg_init + evm_info['mapping_init', contract.name] = self._generate_source_to_evm_ins_mapping(cfg_init.instructions, + contract_srcmap_init, slither, + contract.source_mapping['filename_absolute']) return(evm_info) From aa8cb34f39b1297e6c15376120d1d0988c51c111 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Tue, 18 Jun 2019 14:40:04 +0530 Subject: [PATCH 12/21] - Refactors code into smaller functions - Removes duplicate generate_source_to_evm_ins_mapping() in evm printer (reuse from SourceToEVM module) - Adds more comments for readability - Adds expected output for convert_to_evm_ins - Uses get_function_id from slither.utils.function --- .../convert_to_evm_ins.expected_output | 445 ++++++++++++++++++ examples/scripts/convert_to_evm_ins.py | 11 +- slither/evm/convert.py | 334 +++++++------ slither/printers/summary/evm.py | 80 +--- 4 files changed, 664 insertions(+), 206 deletions(-) create mode 100644 examples/scripts/convert_to_evm_ins.expected_output diff --git a/examples/scripts/convert_to_evm_ins.expected_output b/examples/scripts/convert_to_evm_ins.expected_output new file mode 100644 index 000000000..93c0019c6 --- /dev/null +++ b/examples/scripts/convert_to_evm_ins.expected_output @@ -0,0 +1,445 @@ +## Contract evm instructions: Test ## +PUSH1 0x80 +PUSH1 0x40 +MSTORE +CALLVALUE +DUP1 +ISZERO +PUSH2 0x10 +JUMPI +PUSH1 0x0 +DUP1 +REVERT +JUMPDEST +POP +CALLER +PUSH1 0x0 +DUP1 +PUSH2 0x100 +EXP +DUP2 +SLOAD +DUP2 +PUSH20 0xffffffffffffffffffffffffffffffffffffffff +MUL +NOT +AND +SWAP1 +DUP4 +PUSH20 0xffffffffffffffffffffffffffffffffffffffff +AND +MUL +OR +SWAP1 +SSTORE +POP +PUSH1 0xfa +DUP1 +PUSH2 0x5f +PUSH1 0x0 +CODECOPY +PUSH1 0x0 +RETURN +STOP +PUSH1 0x80 +PUSH1 0x40 +MSTORE +PUSH1 0x4 +CALLDATASIZE +LT +PUSH1 0x49 +JUMPI +PUSH1 0x0 +CALLDATALOAD +PUSH29 0x100000000000000000000000000000000000000000000000000000000 +SWAP1 +DIV +PUSH4 0xffffffff +AND +DUP1 +PUSH4 0x14ba3f12 +EQ +PUSH1 0x4e +JUMPI +DUP1 +PUSH4 0xc2985578 +EQ +PUSH1 0x8c +JUMPI +JUMPDEST +PUSH1 0x0 +DUP1 +REVERT +JUMPDEST +CALLVALUE +DUP1 +ISZERO +PUSH1 0x59 +JUMPI +PUSH1 0x0 +DUP1 +REVERT +JUMPDEST +POP +PUSH1 0x76 +PUSH1 0x4 +DUP1 +CALLDATASIZE +SUB +DUP2 +ADD +SWAP1 +DUP1 +DUP1 +CALLDATALOAD +SWAP1 +PUSH1 0x20 +ADD +SWAP1 +SWAP3 +SWAP2 +SWAP1 +POP +POP +POP +PUSH1 0xb4 +JUMP +JUMPDEST +PUSH1 0x40 +MLOAD +DUP1 +DUP3 +DUP2 +MSTORE +PUSH1 0x20 +ADD +SWAP2 +POP +POP +PUSH1 0x40 +MLOAD +DUP1 +SWAP2 +SUB +SWAP1 +RETURN +JUMPDEST +CALLVALUE +DUP1 +ISZERO +PUSH1 0x97 +JUMPI +PUSH1 0x0 +DUP1 +REVERT +JUMPDEST +POP +PUSH1 0x9e +PUSH1 0xc1 +JUMP +JUMPDEST +PUSH1 0x40 +MLOAD +DUP1 +DUP3 +DUP2 +MSTORE +PUSH1 0x20 +ADD +SWAP2 +POP +POP +PUSH1 0x40 +MLOAD +DUP1 +SWAP2 +SUB +SWAP1 +RETURN +JUMPDEST +PUSH1 0x0 +PUSH1 0xa +DUP3 +ADD +SWAP1 +POP +SWAP2 +SWAP1 +POP +JUMP +JUMPDEST +PUSH1 0x0 +DUP1 +PUSH1 0xa +DUP2 +ADD +SWAP2 +POP +POP +SWAP1 +JUMP +STOP +PUSH1 0x80 +PUSH1 0x40 +MSTORE +PUSH1 0x4 +CALLDATASIZE +LT +PUSH1 0x49 +JUMPI +PUSH1 0x0 +CALLDATALOAD +PUSH29 0x100000000000000000000000000000000000000000000000000000000 +SWAP1 +DIV +PUSH4 0xffffffff +AND +DUP1 +PUSH4 0x14ba3f12 +EQ +PUSH1 0x4e +JUMPI +DUP1 +PUSH4 0xc2985578 +EQ +PUSH1 0x8c +JUMPI +JUMPDEST +PUSH1 0x0 +DUP1 +REVERT +JUMPDEST +CALLVALUE +DUP1 +ISZERO +PUSH1 0x59 +JUMPI +PUSH1 0x0 +DUP1 +REVERT +JUMPDEST +POP +PUSH1 0x76 +PUSH1 0x4 +DUP1 +CALLDATASIZE +SUB +DUP2 +ADD +SWAP1 +DUP1 +DUP1 +CALLDATALOAD +SWAP1 +PUSH1 0x20 +ADD +SWAP1 +SWAP3 +SWAP2 +SWAP1 +POP +POP +POP +PUSH1 0xb4 +JUMP +JUMPDEST +PUSH1 0x40 +MLOAD +DUP1 +DUP3 +DUP2 +MSTORE +PUSH1 0x20 +ADD +SWAP2 +POP +POP +PUSH1 0x40 +MLOAD +DUP1 +SWAP2 +SUB +SWAP1 +RETURN +JUMPDEST +CALLVALUE +DUP1 +ISZERO +PUSH1 0x97 +JUMPI +PUSH1 0x0 +DUP1 +REVERT +JUMPDEST +POP +PUSH1 0x9e +PUSH1 0xc1 +JUMP +JUMPDEST +PUSH1 0x40 +MLOAD +DUP1 +DUP3 +DUP2 +MSTORE +PUSH1 0x20 +ADD +SWAP2 +POP +POP +PUSH1 0x40 +MLOAD +DUP1 +SWAP2 +SUB +SWAP1 +RETURN +JUMPDEST +PUSH1 0x0 +PUSH1 0xa +DUP3 +ADD +SWAP1 +POP +SWAP2 +SWAP1 +POP +JUMP +JUMPDEST +PUSH1 0x0 +DUP1 +PUSH1 0xa +DUP2 +ADD +SWAP2 +POP +POP +SWAP1 +JUMP +STOP +## Function evm instructions: constructor ## +PUSH1 0x80 +PUSH1 0x40 +MSTORE +CALLVALUE +DUP1 +ISZERO +PUSH2 0x10 +JUMPI +PUSH1 0x0 +DUP1 +REVERT +JUMPDEST +POP +CALLER +PUSH1 0x0 +DUP1 +PUSH2 0x100 +EXP +DUP2 +SLOAD +DUP2 +PUSH20 0xffffffffffffffffffffffffffffffffffffffff +MUL +NOT +AND +SWAP1 +DUP4 +PUSH20 0xffffffffffffffffffffffffffffffffffffffff +AND +MUL +OR +SWAP1 +SSTORE +POP +PUSH1 0xfa +DUP1 +PUSH2 0x5f +PUSH1 0x0 +CODECOPY +PUSH1 0x0 +RETURN +## Function evm instructions: foo ## +JUMPDEST +CALLVALUE +DUP1 +ISZERO +PUSH1 0x97 +JUMPI +PUSH1 0x0 +DUP1 +REVERT +JUMPDEST +POP +PUSH1 0x9e +PUSH1 0xc1 +JUMP +JUMPDEST +PUSH1 0x40 +MLOAD +DUP1 +DUP3 +DUP2 +MSTORE +PUSH1 0x20 +ADD +SWAP2 +POP +POP +PUSH1 0x40 +MLOAD +DUP1 +SWAP2 +SUB +SWAP1 +RETURN +JUMPDEST +PUSH1 0x0 +DUP1 +PUSH1 0xa +DUP2 +ADD +SWAP2 +POP +POP +SWAP1 +JUMP +Node evm instructions: ENTRY_POINT None +JUMPDEST +CALLVALUE +POP +PUSH1 0x9e +PUSH1 0xc1 +JUMP +JUMPDEST +PUSH1 0x40 +MLOAD +DUP1 +DUP3 +DUP2 +MSTORE +PUSH1 0x20 +ADD +SWAP2 +POP +POP +PUSH1 0x40 +MLOAD +DUP1 +SWAP2 +SUB +SWAP1 +RETURN +JUMPDEST +PUSH1 0x0 +POP +SWAP1 +JUMP +Node evm instructions: NEW VARIABLE None +DUP1 +Node evm instructions: RETURN (i + 10) +PUSH1 0xa +DUP2 +ADD +SWAP2 +POP diff --git a/examples/scripts/convert_to_evm_ins.py b/examples/scripts/convert_to_evm_ins.py index f9b7220d2..c6aa3ca2d 100644 --- a/examples/scripts/convert_to_evm_ins.py +++ b/examples/scripts/convert_to_evm_ins.py @@ -1,7 +1,6 @@ import sys from slither.slither import Slither -from slither.evm.convert import get_evm_instructions - +from slither.evm.convert import SourceToEVM if len(sys.argv) != 2: print('python3 function_called.py functions_called.sol') @@ -12,7 +11,7 @@ slither = Slither(sys.argv[1]) # Get the contract evm instructions contract = slither.get_contract_from_name('Test') -contract_ins = get_evm_instructions(contract) +contract_ins = SourceToEVM.get_evm_instructions(contract) print("## Contract evm instructions: {} ##".format(contract.name)) for ins in contract_ins: print(str(ins)) @@ -20,21 +19,21 @@ for ins in contract_ins: # Get the constructor evm instructions constructor = contract.constructor print("## Function evm instructions: {} ##".format(constructor.name)) -constructor_ins = get_evm_instructions(constructor) +constructor_ins = SourceToEVM.get_evm_instructions(constructor) for ins in constructor_ins: print(str(ins)) # Get the function evm instructions function = contract.get_function_from_signature('foo()') print("## Function evm instructions: {} ##".format(function.name)) -function_ins = get_evm_instructions(function) +function_ins = SourceToEVM.get_evm_instructions(function) for ins in function_ins: print(str(ins)) # Get the node evm instructions nodes = function.nodes for node in nodes: - node_ins = get_evm_instructions(node) + node_ins = SourceToEVM.get_evm_instructions(node) print("Node evm instructions: {}".format(str(node))) for ins in node_ins: print(str(ins)) diff --git a/slither/evm/convert.py b/slither/evm/convert.py index 0f8ed3378..b1ddb25ec 100644 --- a/slither/evm/convert.py +++ b/slither/evm/convert.py @@ -3,158 +3,200 @@ import sha3 import sys from slither.core.declarations import (Contract, Function) from slither.core.cfg.node import Node +from slither.utils.function import get_function_id logger = logging.getLogger('ConvertToEVM') -def get_evm_instructions(obj): - - assert isinstance(obj, (Function, Contract, Node)) - - if "KEY_EVM_INS" not in obj.context: - - try: - # Avoiding the addition of evm_cfg_builder as permanent dependency - from evm_cfg_builder.cfg import CFG - except ImportError: - logger.error("To use evm printer, you need to install evm-cfg-builder from ToB") - logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") - logger.error("Installation: pip install evm-cfg-builder") - sys.exit(-1) - - slither = obj.slither - - if isinstance(obj, Node): - contract = obj.function.contract - elif isinstance(obj, Function): - contract = obj.contract - else: - contract = obj - - # Get contract runtime bytecode, srcmap and cfg - contract_bytecode_runtime = slither.crytic_compile.bytecode_runtime(contract.name) - contract_srcmap_runtime = slither.crytic_compile.srcmap_runtime(contract.name) - contract_cfg = CFG(contract_bytecode_runtime) - - # Get contract init bytecode, srcmap and cfg - contract_bytecode_init= slither.crytic_compile.bytecode_init(contract.name) - contract_srcmap_init = slither.crytic_compile.srcmap_init(contract.name) - contract_cfg_init = CFG(contract_bytecode_init) - - # Get evm instructions for contract - if isinstance(obj, Contract): - # Combine the instructions of constructor and the rest of the contract - obj.context["KEY_EVM_INS"] = contract_cfg_init.instructions + contract_cfg.instructions - - # Get evm instructions for function - elif isinstance(obj, Function): - - function = obj +class SourceToEVM: + + @staticmethod + def get_evm_instructions (obj): + + assert isinstance(obj, (Function, Contract, Node)) + + if "KEY_EVM_INS" not in obj.context: + + try: + # Avoiding the addition of evm_cfg_builder as permanent dependency + from evm_cfg_builder.cfg import CFG + except ImportError: + logger.error("To use evm printer, you need to install evm-cfg-builder from ToB") + logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") + logger.error("Installation: pip install evm-cfg-builder") + sys.exit(-1) + + slither = obj.slither + + contract_info = {} + function_info = {} + node_info = {} - # CFG depends on function being constructor or not - if function.is_constructor: - cfg = contract_cfg_init - name = "_dispatcher" - hash = "" + if isinstance(obj, Node): + contract_info['contract'] = obj.function.contract + elif isinstance(obj, Function): + contract_info['contract'] = obj.contract else: - cfg = contract_cfg - name = function.name - # Get first four bytes of function singature's keccak-256 hash used as function selector - hash = "0x" + get_function_hash(function.full_name)[:8] - - function_evm = get_function_evm(cfg, name, hash) - if function_evm == "None": - logger.error("Function " + function.name + " not found") - sys.exit(-1) + contract_info['contract'] = obj + + # Get contract runtime bytecode, srcmap and cfg + contract_info['bytecode_runtime'] = slither.crytic_compile.bytecode_runtime( + contract_info['contract'].name) + contract_info['srcmap_runtime'] = slither.crytic_compile.srcmap_runtime( + contract_info['contract'].name) + contract_info['cfg'] = CFG(contract_info['bytecode_runtime']) + + # Get contract init bytecode, srcmap and cfg + contract_info['bytecode_init']= slither.crytic_compile.bytecode_init(contract_info['contract'].name) + contract_info['srcmap_init'] = slither.crytic_compile.srcmap_init(contract_info['contract'].name) + contract_info['cfg_init'] = CFG(contract_info['bytecode_init']) + + # Get evm instructions + if isinstance(obj, Contract): + # Get evm instructions for contract + obj.context["KEY_EVM_INS"] = SourceToEVM._get_evm_instructions_contract(contract_info) + + elif isinstance(obj, Function): + # Get evm instructions for function + function_info['function'] = obj + function_info['contract_info'] = contract_info + obj.context["KEY_EVM_INS"] = SourceToEVM._get_evm_instructions_function(function_info) - function_ins = [] - for basic_block in sorted(function_evm.basic_blocks, key=lambda x:x.start.pc): - for ins in basic_block.instructions: - function_ins.append(ins) - - obj.context["KEY_EVM_INS"] = function_ins - - else: # Node obj - node = obj - - # CFG and srcmap depend on function being constructor or not - if node.function.is_constructor: - cfg = contract_cfg_init - srcmap = contract_srcmap_init else: - cfg = contract_cfg - srcmap = contract_srcmap_runtime + # Get evm instructions for node + node_info['node'] = obj + + # CFG and srcmap depend on function being constructor or not + if node_info['node'].function.is_constructor: + cfg = contract_info['cfg_init'] + srcmap = contract_info['srcmap_init'] + else: + cfg = contract_info['cfg'] + srcmap = contract_info['srcmap_runtime'] + + node_info['cfg'] = cfg + node_info['srcmap'] = srcmap + node_info['contract'] = contract_info['contract'] + node_info['slither'] = slither - # Get evm instructions for node's contract - contract_pcs = _generate_source_to_evm_ins_mapping(cfg.instructions, - srcmap, obj.slither, - contract.source_mapping['filename_absolute']) - contract_file = slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') - - # Get evm instructions corresponding to node's source line number - node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 - node_pcs = contract_pcs.get(node_source_line, []) - node_ins = [] - for pc in node_pcs: - node_ins.append(cfg.get_instruction_at(pc)) - - obj.context["KEY_EVM_INS"] = node_ins + obj.context["KEY_EVM_INS"] = SourceToEVM._get_evm_instructions_node(node_info) + + return obj.context.get("KEY_EVM_INS", []) + + + @staticmethod + def _get_evm_instructions_contract (contract_info): + + # Combine the instructions of constructor and the rest of the contract + return contract_info['cfg_init'].instructions + contract_info['cfg'].instructions + + + @staticmethod + def _get_evm_instructions_function (function_info): + + function = function_info['function'] + + # CFG depends on function being constructor or not + if function.is_constructor: + cfg = function_info['contract_info']['cfg_init'] + # _dispatcher is the only function recognised by evm-cfg-builder in bytecode_init. + # _dispatcher serves the role of the constructor in init code, + # given that there are no other functions. + # Todo: Could rename it appropriately in evm-cfg-builder + # by detecting that init bytecode is being parsed. + name = "_dispatcher" + hash = "" + else: + cfg = function_info['contract_info']['cfg'] + name = function.name + # Get first four bytes of function singature's keccak-256 hash used as function selector + hash = str(hex(get_function_id(function.full_name))) + + function_evm = SourceToEVM._get_function_evm(cfg, name, hash) + if function_evm == "None": + logger.error("Function " + function.name + " not found") + sys.exit(-1) + + function_ins = [] + for basic_block in sorted(function_evm.basic_blocks, key=lambda x:x.start.pc): + for ins in basic_block.instructions: + function_ins.append(ins) + + return function_ins + + + @staticmethod + def _get_evm_instructions_node (node_info): + # Get evm instructions for node's contract + contract_pcs = SourceToEVM.generate_source_to_evm_ins_mapping(node_info['cfg'].instructions, + node_info['srcmap'], + node_info['slither'], + node_info['contract'].source_mapping['filename_absolute']) + contract_file = node_info['slither'].source_code[node_info['contract'].source_mapping['filename_absolute']].encode('utf-8') + + # Get evm instructions corresponding to node's source line number + node_source_line = contract_file[0:node_info['node'].source_mapping['start']].count("\n".encode("utf-8"))\ + + 1 + node_pcs = contract_pcs.get(node_source_line, []) + node_ins = [] + for pc in node_pcs: + node_ins.append(node_info['cfg'].get_instruction_at(pc)) - return obj.context.get("KEY_EVM_INS", []) - -def get_function_hash(function_signature): - hash = sha3.keccak_256() - hash.update(function_signature.encode('utf-8')) - return hash.hexdigest() - -def get_function_evm(cfg, function_name, function_hash): - for function_evm in cfg.functions: - # Match function hash - if function_evm.name[:2] == "0x" and function_evm.name == function_hash: - return function_evm - # Match function name - elif function_evm.name[:2] != "0x" and function_evm.name.split('(')[0] == function_name: - return function_evm - return "None" - -def _generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slither, filename): - """ - Generate Solidity source to EVM instruction mapping using evm_cfg_builder:cfg.instructions and solc:srcmap_runtime - - Returns: Solidity source to EVM instruction mapping - """ - - source_to_evm_mapping = {} - file_source = slither.source_code[filename].encode('utf-8') - prev_mapping = [] - - for idx, mapping in enumerate(srcmap_runtime): - # Parse srcmap_runtime according to its format - # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings - # In order to compress these source mappings especially for bytecode, the following rules are used: - # If a field is empty, the value of the preceding element is used. - # If a : is missing, all following fields are considered empty. - - mapping_item = mapping.split(':') - mapping_item += prev_mapping[len(mapping_item):] - - for i in range(len(mapping_item)): - if mapping_item[i] == '': - mapping_item[i] = int(prev_mapping[i]) - - offset, length, file_id, _ = mapping_item - prev_mapping = mapping_item - - if file_id == '-1': - # Internal compiler-generated code snippets to be ignored - # See https://github.com/ethereum/solidity/issues/6119#issuecomment-467797635 - continue - - offset = int(offset) - line_number = file_source[0:offset].count("\n".encode("utf-8")) + 1 - - # Append evm instructions to the corresponding source line number - # Note: Some evm instructions in mapping are not necessarily in program execution order - # Note: The order depends on how solc creates the srcmap_runtime - source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) - - return(source_to_evm_mapping) + return node_ins + + + @staticmethod + def _get_function_evm(cfg, function_name, function_hash): + for function_evm in cfg.functions: + # Match function hash + if function_evm.name[:2] == "0x" and function_evm.name == function_hash: + return function_evm + # Match function name + elif function_evm.name[:2] != "0x" and function_evm.name.split('(')[0] == function_name: + return function_evm + return "None" + + + @staticmethod + def generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slither, filename): + """ + Generate Solidity source to EVM instruction mapping using evm_cfg_builder:cfg.instructions + and solc:srcmap_runtime + + Returns: Solidity source to EVM instruction mapping + """ + + source_to_evm_mapping = {} + file_source = slither.source_code[filename].encode('utf-8') + prev_mapping = [] + + for idx, mapping in enumerate(srcmap_runtime): + # Parse srcmap_runtime according to its format + # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings + # In order to compress these source mappings especially for bytecode, the following rules are used: + # If a field is empty, the value of the preceding element is used. + # If a : is missing, all following fields are considered empty. + + mapping_item = mapping.split(':') + mapping_item += prev_mapping[len(mapping_item):] + + for i in range(len(mapping_item)): + if mapping_item[i] == '': + mapping_item[i] = int(prev_mapping[i]) + + offset, length, file_id, _ = mapping_item + prev_mapping = mapping_item + + if file_id == '-1': + # Internal compiler-generated code snippets to be ignored + # See https://github.com/ethereum/solidity/issues/6119#issuecomment-467797635 + continue + + offset = int(offset) + line_number = file_source[0:offset].count("\n".encode("utf-8")) + 1 + + # Append evm instructions to the corresponding source line number + # Note: Some evm instructions in mapping are not necessarily in program execution order + # Note: The order depends on how solc creates the srcmap_runtime + source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) + + return(source_to_evm_mapping) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index 52f89e87d..01f59cc5c 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -4,10 +4,13 @@ import logging from slither.printers.abstract_printer import AbstractPrinter +from slither.evm.convert import SourceToEVM + try: # Avoiding the addition of evm_cfg_builder as permanent dependency from evm_cfg_builder.cfg import CFG except ImportError: + logger = logging.getLogger('Printers') logger.error("To use evm printer, you need to install evm-cfg-builder from ToB") logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") logger.error("Installation: pip install evm-cfg-builder") @@ -39,16 +42,21 @@ class PrinterEVM(AbstractPrinter): for function in contract.functions: print(f'\tFunction {function.canonical_name}') + + # CFG and source mapping depend on function being constructor or not if function.is_constructor: contract_cfg = evm_info['cfg_init', contract.name] contract_pcs = evm_info['mapping_init', contract.name] else: contract_cfg = evm_info['cfg', contract.name] contract_pcs = evm_info['mapping', contract.name] + for node in function.nodes: print("\t\tNode: " + str(node)) - node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 - print('\t\tSource line {}: {}'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) + node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + + 1 + print('\t\tSource line {}: {}'.format(node_source_line, + contract_file_lines[node_source_line-1].rstrip())) print('\t\tEVM Instructions:') node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: @@ -57,8 +65,10 @@ class PrinterEVM(AbstractPrinter): for modifier in contract.modifiers: print(f'\tModifier {modifier.canonical_name}') for node in modifier.nodes: - node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 - print('\t\tSource line {}: {}'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) + node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + + 1 + print('\t\tSource line {}: {}'.format(node_source_line, + contract_file_lines[node_source_line-1].rstrip())) print('\t\tEVM Instructions:') node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: @@ -79,58 +89,20 @@ class PrinterEVM(AbstractPrinter): contract_srcmap_runtime = slither.crytic_compile.srcmap_runtime(contract.name) cfg = CFG(contract_bytecode_runtime) evm_info['cfg', contract.name] = cfg - evm_info['mapping', contract.name] = self._generate_source_to_evm_ins_mapping(cfg.instructions, - contract_srcmap_runtime, slither, - contract.source_mapping['filename_absolute']) + evm_info['mapping', contract.name] = SourceToEVM.generate_source_to_evm_ins_mapping( + cfg.instructions, + contract_srcmap_runtime, + slither, + contract.source_mapping['filename_absolute']) + contract_bytecode_init= slither.crytic_compile.bytecode_init(contract.name) contract_srcmap_init = slither.crytic_compile.srcmap_init(contract.name) cfg_init = CFG(contract_bytecode_init) - evm_info['cfg_init', contract.name] = cfg_init - evm_info['mapping_init', contract.name] = self._generate_source_to_evm_ins_mapping(cfg_init.instructions, - contract_srcmap_init, slither, - contract.source_mapping['filename_absolute']) - return(evm_info) - - - def _generate_source_to_evm_ins_mapping(self, evm_instructions, srcmap_runtime, slither, filename): - """ - Generate Solidity source to EVM instruction mapping using evm_cfg_builder:cfg.instructions and solc:srcmap_runtime - - Returns: Solidity source to EVM instruction mapping - """ - - source_to_evm_mapping = {} - file_source = slither.source_code[filename].encode('utf-8') - prev_mapping = [] - - for idx, mapping in enumerate(srcmap_runtime): - # Parse srcmap_runtime according to its format - # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings - # In order to compress these source mappings especially for bytecode, the following rules are used: - # If a field is empty, the value of the preceding element is used. - # If a : is missing, all following fields are considered empty. - - mapping_item = mapping.split(':') - mapping_item += prev_mapping[len(mapping_item):] - - for i in range(len(mapping_item)): - if mapping_item[i] == '': - mapping_item[i] = int(prev_mapping[i]) - - offset, length, file_id, _ = mapping_item - prev_mapping = mapping_item - if file_id == '-1': - # Internal compiler-generated code snippets to be ignored - # See https://github.com/ethereum/solidity/issues/6119#issuecomment-467797635 - continue - - offset = int(offset) - line_number = file_source[0:offset].count("\n".encode("utf-8")) + 1 - - # Append evm instructions to the corresponding source line number - # Note: Some evm instructions in mapping are not necessarily in program execution order - # Note: The order depends on how solc creates the srcmap_runtime - source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) + evm_info['cfg_init', contract.name] = cfg_init + evm_info['mapping_init', contract.name] = SourceToEVM.generate_source_to_evm_ins_mapping( + cfg_init.instructions, + contract_srcmap_init, slither, + contract.source_mapping['filename_absolute']) - return(source_to_evm_mapping) + return(evm_info) From d4be1d15984482c8de7fa69aa4c1f598053737ae Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Tue, 18 Jun 2019 16:17:02 +0530 Subject: [PATCH 13/21] Adds missing import sys. --- slither/printers/summary/evm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index 01f59cc5c..74a903942 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -2,6 +2,7 @@ Module printing evm mapping of the contract """ +import sys import logging from slither.printers.abstract_printer import AbstractPrinter from slither.evm.convert import SourceToEVM From 558b5b9b9e76646c53903850376e4c15d34bbcb6 Mon Sep 17 00:00:00 2001 From: Josselin Date: Tue, 18 Jun 2019 14:30:09 +0200 Subject: [PATCH 14/21] EVM printer: fix import --- examples/scripts/test_evm_api.sol | 2 +- slither/evm/__init__.py | 2 ++ slither/evm/convert.py | 11 ++--------- slither/evm/evm_cfg_builder.py | 15 +++++++++++++++ slither/printers/summary/evm.py | 17 ++++------------- 5 files changed, 24 insertions(+), 23 deletions(-) create mode 100644 slither/evm/evm_cfg_builder.py diff --git a/examples/scripts/test_evm_api.sol b/examples/scripts/test_evm_api.sol index 7a43df074..e3acd9d6b 100644 --- a/examples/scripts/test_evm_api.sol +++ b/examples/scripts/test_evm_api.sol @@ -2,7 +2,7 @@ contract Test { address owner; - constructor () { + constructor () public { owner = msg.sender; } diff --git a/slither/evm/__init__.py b/slither/evm/__init__.py index e69de29bb..0dc702cd3 100644 --- a/slither/evm/__init__.py +++ b/slither/evm/__init__.py @@ -0,0 +1,2 @@ +from .convert import SourceToEVM +from .evm_cfg_builder import load_evm_cfg_builder \ No newline at end of file diff --git a/slither/evm/convert.py b/slither/evm/convert.py index b1ddb25ec..b9a443f49 100644 --- a/slither/evm/convert.py +++ b/slither/evm/convert.py @@ -1,9 +1,9 @@ import logging -import sha3 import sys from slither.core.declarations import (Contract, Function) from slither.core.cfg.node import Node from slither.utils.function import get_function_id +from .evm_cfg_builder import load_evm_cfg_builder logger = logging.getLogger('ConvertToEVM') @@ -16,14 +16,7 @@ class SourceToEVM: if "KEY_EVM_INS" not in obj.context: - try: - # Avoiding the addition of evm_cfg_builder as permanent dependency - from evm_cfg_builder.cfg import CFG - except ImportError: - logger.error("To use evm printer, you need to install evm-cfg-builder from ToB") - logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") - logger.error("Installation: pip install evm-cfg-builder") - sys.exit(-1) + CFG = load_evm_cfg_builder() slither = obj.slither diff --git a/slither/evm/evm_cfg_builder.py b/slither/evm/evm_cfg_builder.py new file mode 100644 index 000000000..8024777c9 --- /dev/null +++ b/slither/evm/evm_cfg_builder.py @@ -0,0 +1,15 @@ +import logging +import sys + +logger = logging.getLogger('ConvertToEVM') + +def load_evm_cfg_builder(): + try: + # Avoiding the addition of evm_cfg_builder as permanent dependency + from evm_cfg_builder.cfg import CFG + return CFG + except ImportError: + logger.error("To use evm features, you need to install evm-cfg-builder from Trail of Bits") + logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") + logger.error("Installation: pip install evm-cfg-builder") + sys.exit(-1) \ No newline at end of file diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index 74a903942..bb48fa069 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -2,20 +2,9 @@ Module printing evm mapping of the contract """ -import sys -import logging from slither.printers.abstract_printer import AbstractPrinter -from slither.evm.convert import SourceToEVM +from slither.evm import SourceToEVM, load_evm_cfg_builder -try: - # Avoiding the addition of evm_cfg_builder as permanent dependency - from evm_cfg_builder.cfg import CFG -except ImportError: - logger = logging.getLogger('Printers') - logger.error("To use evm printer, you need to install evm-cfg-builder from ToB") - logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") - logger.error("Installation: pip install evm-cfg-builder") - sys.exit(-1) class PrinterEVM(AbstractPrinter): @@ -84,7 +73,9 @@ class PrinterEVM(AbstractPrinter): """ evm_info = {} - + + CFG = load_evm_cfg_builder() + for contract in slither.contracts_derived: contract_bytecode_runtime = slither.crytic_compile.bytecode_runtime(contract.name) contract_srcmap_runtime = slither.crytic_compile.srcmap_runtime(contract.name) From adfedbf7ce993ed7a7cc87bcfc64150ab4ee86e1 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Tue, 18 Jun 2019 18:08:04 +0530 Subject: [PATCH 15/21] Adds pragma and function visibility to satisfy solc 0.5.x --- examples/scripts/test_evm_api.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/scripts/test_evm_api.sol b/examples/scripts/test_evm_api.sol index 7a43df074..7e02a0228 100644 --- a/examples/scripts/test_evm_api.sol +++ b/examples/scripts/test_evm_api.sol @@ -1,8 +1,10 @@ +pragma solidity >=0.4.24 <0.5.4; + contract Test { address owner; - constructor () { + constructor () public { owner = msg.sender; } From 3ffe4d3615da03b0e42de7d4050edba956294fd4 Mon Sep 17 00:00:00 2001 From: rajeevgopalakrishna Date: Wed, 19 Jun 2019 14:18:55 +0530 Subject: [PATCH 16/21] - Fixes formatting bug - Replaces sys.exit with SlitherError - Replaces prints with self.info - Moves slither.evm to slither.analyses.evm - Replaces class SourceToEVM static methods with functions - Makes KEY_EVM_INS into a constant - Adds wiki entry for evm printer --- examples/printers/evm.sol | 9 + ...ed_output => test_evm_api.expected_output} | 0 slither/analyses/evm/__init__.py | 2 + slither/analyses/evm/convert.py | 189 +++++++++++++++++ slither/{ => analyses}/evm/evm_cfg_builder.py | 3 +- slither/evm/__init__.py | 2 - slither/evm/convert.py | 195 ------------------ slither/printers/summary/evm.py | 38 ++-- 8 files changed, 221 insertions(+), 217 deletions(-) create mode 100644 examples/printers/evm.sol rename examples/scripts/{convert_to_evm_ins.expected_output => test_evm_api.expected_output} (100%) create mode 100644 slither/analyses/evm/__init__.py create mode 100644 slither/analyses/evm/convert.py rename slither/{ => analyses}/evm/evm_cfg_builder.py (83%) delete mode 100644 slither/evm/__init__.py delete mode 100644 slither/evm/convert.py diff --git a/examples/printers/evm.sol b/examples/printers/evm.sol new file mode 100644 index 000000000..8ea86b9dd --- /dev/null +++ b/examples/printers/evm.sol @@ -0,0 +1,9 @@ +pragma solidity >=0.4.24 <0.5.4; + +contract Test { + + function foo() public returns (address) { + address from = msg.sender; + return(from); + } +} diff --git a/examples/scripts/convert_to_evm_ins.expected_output b/examples/scripts/test_evm_api.expected_output similarity index 100% rename from examples/scripts/convert_to_evm_ins.expected_output rename to examples/scripts/test_evm_api.expected_output diff --git a/slither/analyses/evm/__init__.py b/slither/analyses/evm/__init__.py new file mode 100644 index 000000000..bed1b751e --- /dev/null +++ b/slither/analyses/evm/__init__.py @@ -0,0 +1,2 @@ +from .convert import generate_source_to_evm_ins_mapping +from .evm_cfg_builder import load_evm_cfg_builder diff --git a/slither/analyses/evm/convert.py b/slither/analyses/evm/convert.py new file mode 100644 index 000000000..be13f9d19 --- /dev/null +++ b/slither/analyses/evm/convert.py @@ -0,0 +1,189 @@ +import logging +import sys +from slither.core.declarations import (Contract, Function) +from slither.core.cfg.node import Node +from slither.utils.function import get_function_id +from .evm_cfg_builder import load_evm_cfg_builder + +logger = logging.getLogger('ConvertToEVM') + +KEY_EVM_INS = "EVM_INSTRUCTIONS" + +def get_evm_instructions (obj): + + assert isinstance(obj, (Function, Contract, Node)) + + if KEY_EVM_INS not in obj.context: + + CFG = load_evm_cfg_builder() + + slither = obj.slither + + contract_info = {} + function_info = {} + node_info = {} + + if isinstance(obj, Node): + contract_info['contract'] = obj.function.contract + elif isinstance(obj, Function): + contract_info['contract'] = obj.contract + else: + contract_info['contract'] = obj + + # Get contract runtime bytecode, srcmap and cfg + contract_info['bytecode_runtime'] = slither.crytic_compile.bytecode_runtime( + contract_info['contract'].name) + contract_info['srcmap_runtime'] = slither.crytic_compile.srcmap_runtime( + contract_info['contract'].name) + contract_info['cfg'] = CFG(contract_info['bytecode_runtime']) + + # Get contract init bytecode, srcmap and cfg + contract_info['bytecode_init']= slither.crytic_compile.bytecode_init(contract_info['contract'].name) + contract_info['srcmap_init'] = slither.crytic_compile.srcmap_init(contract_info['contract'].name) + contract_info['cfg_init'] = CFG(contract_info['bytecode_init']) + + # Get evm instructions + if isinstance(obj, Contract): + # Get evm instructions for contract + obj.context[KEY_EVM_INS] = _get_evm_instructions_contract(contract_info) + + elif isinstance(obj, Function): + # Get evm instructions for function + function_info['function'] = obj + function_info['contract_info'] = contract_info + obj.context[KEY_EVM_INS] = _get_evm_instructions_function(function_info) + + else: + # Get evm instructions for node + node_info['node'] = obj + + # CFG and srcmap depend on function being constructor or not + if node_info['node'].function.is_constructor: + cfg = contract_info['cfg_init'] + srcmap = contract_info['srcmap_init'] + else: + cfg = contract_info['cfg'] + srcmap = contract_info['srcmap_runtime'] + + node_info['cfg'] = cfg + node_info['srcmap'] = srcmap + node_info['contract'] = contract_info['contract'] + node_info['slither'] = slither + + obj.context[KEY_EVM_INS] = _get_evm_instructions_node(node_info) + + return obj.context.get(KEY_EVM_INS, []) + + +def _get_evm_instructions_contract (contract_info): + + # Combine the instructions of constructor and the rest of the contract + return contract_info['cfg_init'].instructions + contract_info['cfg'].instructions + + +def _get_evm_instructions_function (function_info): + + function = function_info['function'] + + # CFG depends on function being constructor or not + if function.is_constructor: + cfg = function_info['contract_info']['cfg_init'] + # _dispatcher is the only function recognised by evm-cfg-builder in bytecode_init. + # _dispatcher serves the role of the constructor in init code, + # given that there are no other functions. + # Todo: Could rename it appropriately in evm-cfg-builder + # by detecting that init bytecode is being parsed. + name = "_dispatcher" + hash = "" + else: + cfg = function_info['contract_info']['cfg'] + name = function.name + # Get first four bytes of function singature's keccak-256 hash used as function selector + hash = str(hex(get_function_id(function.full_name))) + + function_evm = _get_function_evm(cfg, name, hash) + if function_evm == "None": + logger.error("Function " + function.name + " not found") + sys.exit(-1) + + function_ins = [] + for basic_block in sorted(function_evm.basic_blocks, key=lambda x:x.start.pc): + for ins in basic_block.instructions: + function_ins.append(ins) + + return function_ins + + +def _get_evm_instructions_node (node_info): + # Get evm instructions for node's contract + contract_pcs = generate_source_to_evm_ins_mapping(node_info['cfg'].instructions, + node_info['srcmap'], + node_info['slither'], + node_info['contract'].source_mapping['filename_absolute']) + contract_file = node_info['slither'].source_code[node_info['contract'].source_mapping['filename_absolute']].encode('utf-8') + + # Get evm instructions corresponding to node's source line number + node_source_line = contract_file[0:node_info['node'].source_mapping['start']].count("\n".encode("utf-8"))\ + + 1 + node_pcs = contract_pcs.get(node_source_line, []) + node_ins = [] + for pc in node_pcs: + node_ins.append(node_info['cfg'].get_instruction_at(pc)) + + return node_ins + + +def _get_function_evm(cfg, function_name, function_hash): + for function_evm in cfg.functions: + # Match function hash + if function_evm.name[:2] == "0x" and function_evm.name == function_hash: + return function_evm + # Match function name + elif function_evm.name[:2] != "0x" and function_evm.name.split('(')[0] == function_name: + return function_evm + return "None" + + +def generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slither, filename): + """ + Generate Solidity source to EVM instruction mapping using evm_cfg_builder:cfg.instructions + and solc:srcmap_runtime + + Returns: Solidity source to EVM instruction mapping + """ + + source_to_evm_mapping = {} + file_source = slither.source_code[filename].encode('utf-8') + prev_mapping = [] + + for idx, mapping in enumerate(srcmap_runtime): + # Parse srcmap_runtime according to its format + # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings + # In order to compress these source mappings especially for bytecode, the following rules are used: + # If a field is empty, the value of the preceding element is used. + # If a : is missing, all following fields are considered empty. + + mapping_item = mapping.split(':') + mapping_item += prev_mapping[len(mapping_item):] + + for i in range(len(mapping_item)): + if mapping_item[i] == '': + mapping_item[i] = int(prev_mapping[i]) + + offset, length, file_id, _ = mapping_item + prev_mapping = mapping_item + + if file_id == '-1': + # Internal compiler-generated code snippets to be ignored + # See https://github.com/ethereum/solidity/issues/6119#issuecomment-467797635 + continue + + offset = int(offset) + line_number = file_source[0:offset].count("\n".encode("utf-8")) + 1 + + # Append evm instructions to the corresponding source line number + # Note: Some evm instructions in mapping are not necessarily in program execution order + # Note: The order depends on how solc creates the srcmap_runtime + source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) + + return(source_to_evm_mapping) diff --git a/slither/evm/evm_cfg_builder.py b/slither/analyses/evm/evm_cfg_builder.py similarity index 83% rename from slither/evm/evm_cfg_builder.py rename to slither/analyses/evm/evm_cfg_builder.py index 8024777c9..1ac5593b4 100644 --- a/slither/evm/evm_cfg_builder.py +++ b/slither/analyses/evm/evm_cfg_builder.py @@ -1,5 +1,6 @@ import logging import sys +from slither.exceptions import SlitherError logger = logging.getLogger('ConvertToEVM') @@ -12,4 +13,4 @@ def load_evm_cfg_builder(): logger.error("To use evm features, you need to install evm-cfg-builder from Trail of Bits") logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") logger.error("Installation: pip install evm-cfg-builder") - sys.exit(-1) \ No newline at end of file + raise SlitherError("evm-cfg-builder not installed.") diff --git a/slither/evm/__init__.py b/slither/evm/__init__.py deleted file mode 100644 index 0dc702cd3..000000000 --- a/slither/evm/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .convert import SourceToEVM -from .evm_cfg_builder import load_evm_cfg_builder \ No newline at end of file diff --git a/slither/evm/convert.py b/slither/evm/convert.py deleted file mode 100644 index b9a443f49..000000000 --- a/slither/evm/convert.py +++ /dev/null @@ -1,195 +0,0 @@ -import logging -import sys -from slither.core.declarations import (Contract, Function) -from slither.core.cfg.node import Node -from slither.utils.function import get_function_id -from .evm_cfg_builder import load_evm_cfg_builder - -logger = logging.getLogger('ConvertToEVM') - -class SourceToEVM: - - @staticmethod - def get_evm_instructions (obj): - - assert isinstance(obj, (Function, Contract, Node)) - - if "KEY_EVM_INS" not in obj.context: - - CFG = load_evm_cfg_builder() - - slither = obj.slither - - contract_info = {} - function_info = {} - node_info = {} - - if isinstance(obj, Node): - contract_info['contract'] = obj.function.contract - elif isinstance(obj, Function): - contract_info['contract'] = obj.contract - else: - contract_info['contract'] = obj - - # Get contract runtime bytecode, srcmap and cfg - contract_info['bytecode_runtime'] = slither.crytic_compile.bytecode_runtime( - contract_info['contract'].name) - contract_info['srcmap_runtime'] = slither.crytic_compile.srcmap_runtime( - contract_info['contract'].name) - contract_info['cfg'] = CFG(contract_info['bytecode_runtime']) - - # Get contract init bytecode, srcmap and cfg - contract_info['bytecode_init']= slither.crytic_compile.bytecode_init(contract_info['contract'].name) - contract_info['srcmap_init'] = slither.crytic_compile.srcmap_init(contract_info['contract'].name) - contract_info['cfg_init'] = CFG(contract_info['bytecode_init']) - - # Get evm instructions - if isinstance(obj, Contract): - # Get evm instructions for contract - obj.context["KEY_EVM_INS"] = SourceToEVM._get_evm_instructions_contract(contract_info) - - elif isinstance(obj, Function): - # Get evm instructions for function - function_info['function'] = obj - function_info['contract_info'] = contract_info - obj.context["KEY_EVM_INS"] = SourceToEVM._get_evm_instructions_function(function_info) - - else: - # Get evm instructions for node - node_info['node'] = obj - - # CFG and srcmap depend on function being constructor or not - if node_info['node'].function.is_constructor: - cfg = contract_info['cfg_init'] - srcmap = contract_info['srcmap_init'] - else: - cfg = contract_info['cfg'] - srcmap = contract_info['srcmap_runtime'] - - node_info['cfg'] = cfg - node_info['srcmap'] = srcmap - node_info['contract'] = contract_info['contract'] - node_info['slither'] = slither - - obj.context["KEY_EVM_INS"] = SourceToEVM._get_evm_instructions_node(node_info) - - return obj.context.get("KEY_EVM_INS", []) - - - @staticmethod - def _get_evm_instructions_contract (contract_info): - - # Combine the instructions of constructor and the rest of the contract - return contract_info['cfg_init'].instructions + contract_info['cfg'].instructions - - - @staticmethod - def _get_evm_instructions_function (function_info): - - function = function_info['function'] - - # CFG depends on function being constructor or not - if function.is_constructor: - cfg = function_info['contract_info']['cfg_init'] - # _dispatcher is the only function recognised by evm-cfg-builder in bytecode_init. - # _dispatcher serves the role of the constructor in init code, - # given that there are no other functions. - # Todo: Could rename it appropriately in evm-cfg-builder - # by detecting that init bytecode is being parsed. - name = "_dispatcher" - hash = "" - else: - cfg = function_info['contract_info']['cfg'] - name = function.name - # Get first four bytes of function singature's keccak-256 hash used as function selector - hash = str(hex(get_function_id(function.full_name))) - - function_evm = SourceToEVM._get_function_evm(cfg, name, hash) - if function_evm == "None": - logger.error("Function " + function.name + " not found") - sys.exit(-1) - - function_ins = [] - for basic_block in sorted(function_evm.basic_blocks, key=lambda x:x.start.pc): - for ins in basic_block.instructions: - function_ins.append(ins) - - return function_ins - - - @staticmethod - def _get_evm_instructions_node (node_info): - # Get evm instructions for node's contract - contract_pcs = SourceToEVM.generate_source_to_evm_ins_mapping(node_info['cfg'].instructions, - node_info['srcmap'], - node_info['slither'], - node_info['contract'].source_mapping['filename_absolute']) - contract_file = node_info['slither'].source_code[node_info['contract'].source_mapping['filename_absolute']].encode('utf-8') - - # Get evm instructions corresponding to node's source line number - node_source_line = contract_file[0:node_info['node'].source_mapping['start']].count("\n".encode("utf-8"))\ - + 1 - node_pcs = contract_pcs.get(node_source_line, []) - node_ins = [] - for pc in node_pcs: - node_ins.append(node_info['cfg'].get_instruction_at(pc)) - - return node_ins - - - @staticmethod - def _get_function_evm(cfg, function_name, function_hash): - for function_evm in cfg.functions: - # Match function hash - if function_evm.name[:2] == "0x" and function_evm.name == function_hash: - return function_evm - # Match function name - elif function_evm.name[:2] != "0x" and function_evm.name.split('(')[0] == function_name: - return function_evm - return "None" - - - @staticmethod - def generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slither, filename): - """ - Generate Solidity source to EVM instruction mapping using evm_cfg_builder:cfg.instructions - and solc:srcmap_runtime - - Returns: Solidity source to EVM instruction mapping - """ - - source_to_evm_mapping = {} - file_source = slither.source_code[filename].encode('utf-8') - prev_mapping = [] - - for idx, mapping in enumerate(srcmap_runtime): - # Parse srcmap_runtime according to its format - # See https://solidity.readthedocs.io/en/v0.5.9/miscellaneous.html#source-mappings - # In order to compress these source mappings especially for bytecode, the following rules are used: - # If a field is empty, the value of the preceding element is used. - # If a : is missing, all following fields are considered empty. - - mapping_item = mapping.split(':') - mapping_item += prev_mapping[len(mapping_item):] - - for i in range(len(mapping_item)): - if mapping_item[i] == '': - mapping_item[i] = int(prev_mapping[i]) - - offset, length, file_id, _ = mapping_item - prev_mapping = mapping_item - - if file_id == '-1': - # Internal compiler-generated code snippets to be ignored - # See https://github.com/ethereum/solidity/issues/6119#issuecomment-467797635 - continue - - offset = int(offset) - line_number = file_source[0:offset].count("\n".encode("utf-8")) + 1 - - # Append evm instructions to the corresponding source line number - # Note: Some evm instructions in mapping are not necessarily in program execution order - # Note: The order depends on how solc creates the srcmap_runtime - source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) - - return(source_to_evm_mapping) diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index bb48fa069..b65696cb4 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -3,9 +3,8 @@ """ from slither.printers.abstract_printer import AbstractPrinter -from slither.evm import SourceToEVM, load_evm_cfg_builder - - +from slither.analyses.evm import generate_source_to_evm_ins_mapping, load_evm_cfg_builder +from slither.utils.colors import blue, green, magenta class PrinterEVM(AbstractPrinter): @@ -22,16 +21,17 @@ class PrinterEVM(AbstractPrinter): _filename(string) """ + txt = "" evm_info = self._extract_evm_info(self.slither) for contract in self.slither.contracts_derived: - print('Contract {}'.format(contract.name)) + txt += blue('Contract {}\n'.format(contract.name)) contract_file = self.slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') contract_file_lines = open(contract.source_mapping['filename_absolute'],'r').readlines() for function in contract.functions: - print(f'\tFunction {function.canonical_name}') + txt += blue(f'\tFunction {function.canonical_name}\n') # CFG and source mapping depend on function being constructor or not if function.is_constructor: @@ -42,28 +42,28 @@ class PrinterEVM(AbstractPrinter): contract_pcs = evm_info['mapping', contract.name] for node in function.nodes: - print("\t\tNode: " + str(node)) - node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) - + 1 - print('\t\tSource line {}: {}'.format(node_source_line, + txt += green("\t\tNode: " + str(node) + "\n") + node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 + txt += green('\t\tSource line {}: {}\n'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) - print('\t\tEVM Instructions:') + txt += magenta('\t\tEVM Instructions:\n') node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: - print('\t\t\t0x{:x}: {}'.format(int(pc), contract_cfg.get_instruction_at(pc))) + txt += magenta('\t\t\t0x{:x}: {}\n'.format(int(pc), contract_cfg.get_instruction_at(pc))) for modifier in contract.modifiers: - print(f'\tModifier {modifier.canonical_name}') + txt += blue(f'\tModifier {modifier.canonical_name}\n') for node in modifier.nodes: - node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) - + 1 - print('\t\tSource line {}: {}'.format(node_source_line, + txt += green("\t\tNode: " + str(node) + "\n") + node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 + txt += green('\t\tSource line {}: {}\n'.format(node_source_line, contract_file_lines[node_source_line-1].rstrip())) - print('\t\tEVM Instructions:') + txt += magenta('\t\tEVM Instructions:\n') node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: - print('\t\t\t0x{:x}: {}'.format(int(pc), contract_cfg.get_instruction_at(pc))) + txt += magenta('\t\t\t0x{:x}: {}\n'.format(int(pc), contract_cfg.get_instruction_at(pc))) + self.info(txt) def _extract_evm_info(self, slither): """ @@ -81,7 +81,7 @@ class PrinterEVM(AbstractPrinter): contract_srcmap_runtime = slither.crytic_compile.srcmap_runtime(contract.name) cfg = CFG(contract_bytecode_runtime) evm_info['cfg', contract.name] = cfg - evm_info['mapping', contract.name] = SourceToEVM.generate_source_to_evm_ins_mapping( + evm_info['mapping', contract.name] = generate_source_to_evm_ins_mapping( cfg.instructions, contract_srcmap_runtime, slither, @@ -92,7 +92,7 @@ class PrinterEVM(AbstractPrinter): cfg_init = CFG(contract_bytecode_init) evm_info['cfg_init', contract.name] = cfg_init - evm_info['mapping_init', contract.name] = SourceToEVM.generate_source_to_evm_ins_mapping( + evm_info['mapping_init', contract.name] = generate_source_to_evm_ins_mapping( cfg_init.instructions, contract_srcmap_init, slither, contract.source_mapping['filename_absolute']) From 3d5bad9fb170264db7bfd389246e68ccd717d568 Mon Sep 17 00:00:00 2001 From: Josselin Date: Wed, 13 Nov 2019 10:49:40 +0100 Subject: [PATCH 17/21] Minor changes in EVM analysis and printer --- slither/analyses/evm/convert.py | 81 ++++++++++++------------- slither/analyses/evm/evm_cfg_builder.py | 3 +- slither/printers/summary/evm.py | 42 +++++++------ 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/slither/analyses/evm/convert.py b/slither/analyses/evm/convert.py index be13f9d19..404e6bd97 100644 --- a/slither/analyses/evm/convert.py +++ b/slither/analyses/evm/convert.py @@ -1,16 +1,16 @@ import logging -import sys from slither.core.declarations import (Contract, Function) from slither.core.cfg.node import Node from slither.utils.function import get_function_id +from slither.exceptions import SlitherError from .evm_cfg_builder import load_evm_cfg_builder logger = logging.getLogger('ConvertToEVM') KEY_EVM_INS = "EVM_INSTRUCTIONS" -def get_evm_instructions (obj): +def get_evm_instructions(obj): assert isinstance(obj, (Function, Contract, Node)) if KEY_EVM_INS not in obj.context: @@ -38,7 +38,7 @@ def get_evm_instructions (obj): contract_info['cfg'] = CFG(contract_info['bytecode_runtime']) # Get contract init bytecode, srcmap and cfg - contract_info['bytecode_init']= slither.crytic_compile.bytecode_init(contract_info['contract'].name) + contract_info['bytecode_init'] = slither.crytic_compile.bytecode_init(contract_info['contract'].name) contract_info['srcmap_init'] = slither.crytic_compile.srcmap_init(contract_info['contract'].name) contract_info['cfg_init'] = CFG(contract_info['bytecode_init']) @@ -75,56 +75,55 @@ def get_evm_instructions (obj): return obj.context.get(KEY_EVM_INS, []) -def _get_evm_instructions_contract (contract_info): - +def _get_evm_instructions_contract(contract_info): # Combine the instructions of constructor and the rest of the contract return contract_info['cfg_init'].instructions + contract_info['cfg'].instructions -def _get_evm_instructions_function (function_info): - - function = function_info['function'] +def _get_evm_instructions_function(function_info): + function = function_info['function'] - # CFG depends on function being constructor or not - if function.is_constructor: - cfg = function_info['contract_info']['cfg_init'] - # _dispatcher is the only function recognised by evm-cfg-builder in bytecode_init. - # _dispatcher serves the role of the constructor in init code, - # given that there are no other functions. - # Todo: Could rename it appropriately in evm-cfg-builder - # by detecting that init bytecode is being parsed. - name = "_dispatcher" - hash = "" - else: - cfg = function_info['contract_info']['cfg'] - name = function.name - # Get first four bytes of function singature's keccak-256 hash used as function selector - hash = str(hex(get_function_id(function.full_name))) + # CFG depends on function being constructor or not + if function.is_constructor: + cfg = function_info['contract_info']['cfg_init'] + # _dispatcher is the only function recognised by evm-cfg-builder in bytecode_init. + # _dispatcher serves the role of the constructor in init code, + # given that there are no other functions. + # Todo: Could rename it appropriately in evm-cfg-builder + # by detecting that init bytecode is being parsed. + name = "_dispatcher" + hash = "" + else: + cfg = function_info['contract_info']['cfg'] + name = function.name + # Get first four bytes of function singature's keccak-256 hash used as function selector + hash = str(hex(get_function_id(function.full_name))) - function_evm = _get_function_evm(cfg, name, hash) - if function_evm == "None": - logger.error("Function " + function.name + " not found") - sys.exit(-1) + function_evm = _get_function_evm(cfg, name, hash) + if function_evm is None: + logger.error("Function " + function.name + " not found in the EVM code") + raise SlitherError("Function " + function.name + " not found in the EVM code") - function_ins = [] - for basic_block in sorted(function_evm.basic_blocks, key=lambda x:x.start.pc): - for ins in basic_block.instructions: - function_ins.append(ins) + function_ins = [] + for basic_block in sorted(function_evm.basic_blocks, key=lambda x: x.start.pc): + for ins in basic_block.instructions: + function_ins.append(ins) - return function_ins + return function_ins -def _get_evm_instructions_node (node_info): +def _get_evm_instructions_node(node_info): # Get evm instructions for node's contract contract_pcs = generate_source_to_evm_ins_mapping(node_info['cfg'].instructions, - node_info['srcmap'], - node_info['slither'], - node_info['contract'].source_mapping['filename_absolute']) - contract_file = node_info['slither'].source_code[node_info['contract'].source_mapping['filename_absolute']].encode('utf-8') + node_info['srcmap'], + node_info['slither'], + node_info['contract'].source_mapping['filename_absolute']) + contract_file = node_info['slither'].source_code[node_info['contract'].source_mapping['filename_absolute']].encode( + 'utf-8') # Get evm instructions corresponding to node's source line number - node_source_line = contract_file[0:node_info['node'].source_mapping['start']].count("\n".encode("utf-8"))\ - + 1 + node_source_line = contract_file[0:node_info['node'].source_mapping['start']].count("\n".encode("utf-8")) \ + + 1 node_pcs = contract_pcs.get(node_source_line, []) node_ins = [] for pc in node_pcs: @@ -141,7 +140,7 @@ def _get_function_evm(cfg, function_name, function_hash): # Match function name elif function_evm.name[:2] != "0x" and function_evm.name.split('(')[0] == function_name: return function_evm - return "None" + return None def generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slither, filename): @@ -186,4 +185,4 @@ def generate_source_to_evm_ins_mapping(evm_instructions, srcmap_runtime, slither # Note: The order depends on how solc creates the srcmap_runtime source_to_evm_mapping.setdefault(line_number, []).append(evm_instructions[idx].pc) - return(source_to_evm_mapping) + return source_to_evm_mapping diff --git a/slither/analyses/evm/evm_cfg_builder.py b/slither/analyses/evm/evm_cfg_builder.py index 1ac5593b4..5fc93d3f6 100644 --- a/slither/analyses/evm/evm_cfg_builder.py +++ b/slither/analyses/evm/evm_cfg_builder.py @@ -1,5 +1,4 @@ import logging -import sys from slither.exceptions import SlitherError logger = logging.getLogger('ConvertToEVM') @@ -10,7 +9,7 @@ def load_evm_cfg_builder(): from evm_cfg_builder.cfg import CFG return CFG except ImportError: - logger.error("To use evm features, you need to install evm-cfg-builder from Trail of Bits") + logger.error("To use evm features, you need to install evm-cfg-builder") logger.error("Documentation: https://github.com/crytic/evm_cfg_builder") logger.error("Installation: pip install evm-cfg-builder") raise SlitherError("evm-cfg-builder not installed.") diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index b65696cb4..58b6c2ccb 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -5,15 +5,14 @@ from slither.printers.abstract_printer import AbstractPrinter from slither.analyses.evm import generate_source_to_evm_ins_mapping, load_evm_cfg_builder from slither.utils.colors import blue, green, magenta - -class PrinterEVM(AbstractPrinter): + +class PrinterEVM(AbstractPrinter): ARGUMENT = 'evm' HELP = 'Print the evm instructions of nodes in functions' WIKI = 'https://github.com/trailofbits/slither/wiki/Printer-documentation#evm' - def output(self, _filename): """ _filename is not used @@ -23,16 +22,19 @@ class PrinterEVM(AbstractPrinter): txt = "" evm_info = self._extract_evm_info(self.slither) - + for contract in self.slither.contracts_derived: txt += blue('Contract {}\n'.format(contract.name)) - + contract_file = self.slither.source_code[contract.source_mapping['filename_absolute']].encode('utf-8') - contract_file_lines = open(contract.source_mapping['filename_absolute'],'r').readlines() + contract_file_lines = open(contract.source_mapping['filename_absolute'], 'r').readlines() + + contract_pcs = {} + contract_cfg = {} for function in contract.functions: txt += blue(f'\tFunction {function.canonical_name}\n') - + # CFG and source mapping depend on function being constructor or not if function.is_constructor: contract_cfg = evm_info['cfg_init', contract.name] @@ -40,38 +42,40 @@ class PrinterEVM(AbstractPrinter): else: contract_cfg = evm_info['cfg', contract.name] contract_pcs = evm_info['mapping', contract.name] - + for node in function.nodes: txt += green("\t\tNode: " + str(node) + "\n") node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 txt += green('\t\tSource line {}: {}\n'.format(node_source_line, - contract_file_lines[node_source_line-1].rstrip())) + contract_file_lines[node_source_line - 1].rstrip())) txt += magenta('\t\tEVM Instructions:\n') node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: txt += magenta('\t\t\t0x{:x}: {}\n'.format(int(pc), contract_cfg.get_instruction_at(pc))) - + for modifier in contract.modifiers: txt += blue(f'\tModifier {modifier.canonical_name}\n') for node in modifier.nodes: txt += green("\t\tNode: " + str(node) + "\n") node_source_line = contract_file[0:node.source_mapping['start']].count("\n".encode("utf-8")) + 1 txt += green('\t\tSource line {}: {}\n'.format(node_source_line, - contract_file_lines[node_source_line-1].rstrip())) + contract_file_lines[node_source_line - 1].rstrip())) txt += magenta('\t\tEVM Instructions:\n') node_pcs = contract_pcs.get(node_source_line, []) for pc in node_pcs: txt += magenta('\t\t\t0x{:x}: {}\n'.format(int(pc), contract_cfg.get_instruction_at(pc))) - self.info(txt) - + self.info(txt) + res = self.generate_output(txt) + return res + def _extract_evm_info(self, slither): """ Extract evm information for all derived contracts using evm_cfg_builder Returns: evm CFG and Solidity source to Program Counter (pc) mapping """ - + evm_info = {} CFG = load_evm_cfg_builder() @@ -86,15 +90,15 @@ class PrinterEVM(AbstractPrinter): contract_srcmap_runtime, slither, contract.source_mapping['filename_absolute']) - - contract_bytecode_init= slither.crytic_compile.bytecode_init(contract.name) + + contract_bytecode_init = slither.crytic_compile.bytecode_init(contract.name) contract_srcmap_init = slither.crytic_compile.srcmap_init(contract.name) cfg_init = CFG(contract_bytecode_init) - + evm_info['cfg_init', contract.name] = cfg_init evm_info['mapping_init', contract.name] = generate_source_to_evm_ins_mapping( cfg_init.instructions, contract_srcmap_init, slither, contract.source_mapping['filename_absolute']) - - return(evm_info) + + return evm_info From 4074bd36d226df5e7351f908ea53a0e0826d1742 Mon Sep 17 00:00:00 2001 From: Josselin Date: Wed, 13 Nov 2019 10:51:00 +0100 Subject: [PATCH 18/21] Improve travis test --- scripts/travis_test_printers.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/travis_test_printers.sh b/scripts/travis_test_printers.sh index 0e3eb8a0c..fca2b9d89 100755 --- a/scripts/travis_test_printers.sh +++ b/scripts/travis_test_printers.sh @@ -2,6 +2,9 @@ ### Test printer +# Needed for evm printer +pip install evm-cfg-builder + slither "tests/*.json" --print all if [ $? -ne 0 ]; then From b574ad753ecb4742e547679c42daf34ced258eef Mon Sep 17 00:00:00 2001 From: Josselin Date: Wed, 13 Nov 2019 11:07:35 +0100 Subject: [PATCH 19/21] EVM printer: fix bug + improve travis test --- scripts/travis_test_printers.sh | 1 + slither/analyses/evm/convert.py | 3 +++ slither/printers/summary/evm.py | 9 +++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/travis_test_printers.sh b/scripts/travis_test_printers.sh index fca2b9d89..bad5631f9 100755 --- a/scripts/travis_test_printers.sh +++ b/scripts/travis_test_printers.sh @@ -12,3 +12,4 @@ if [ $? -ne 0 ]; then exit 1 fi +slither examples/scripts/test_evm_api.sol --print evm --solc solc-0.5.1 diff --git a/slither/analyses/evm/convert.py b/slither/analyses/evm/convert.py index 404e6bd97..a86a4aaa5 100644 --- a/slither/analyses/evm/convert.py +++ b/slither/analyses/evm/convert.py @@ -19,6 +19,9 @@ def get_evm_instructions(obj): slither = obj.slither + if not slither.crytic_compile: + raise SlitherError('EVM features require to compile with crytic-compile') + contract_info = {} function_info = {} node_info = {} diff --git a/slither/printers/summary/evm.py b/slither/printers/summary/evm.py index 58b6c2ccb..98de8391a 100644 --- a/slither/printers/summary/evm.py +++ b/slither/printers/summary/evm.py @@ -1,10 +1,9 @@ """ Module printing evm mapping of the contract """ - from slither.printers.abstract_printer import AbstractPrinter from slither.analyses.evm import generate_source_to_evm_ins_mapping, load_evm_cfg_builder -from slither.utils.colors import blue, green, magenta +from slither.utils.colors import blue, green, magenta, red class PrinterEVM(AbstractPrinter): @@ -21,6 +20,12 @@ class PrinterEVM(AbstractPrinter): """ txt = "" + + if not self.slither.crytic_compile: + txt = 'The EVM printer requires to compile with crytic-compile' + self.info(red(txt)) + res = self.generate_output(txt) + return res evm_info = self._extract_evm_info(self.slither) for contract in self.slither.contracts_derived: From 459ff2ed3c7f4e3e01e558da126675d69ed94369 Mon Sep 17 00:00:00 2001 From: Josselin Date: Wed, 13 Nov 2019 11:56:04 +0100 Subject: [PATCH 20/21] Improvements on the contract summary Contract summary: Show upgreadble information (fix #358) and if the contract is a most derived one Contract: add is_upgradeable/is_upgradeable_proxy properties Parsing: add inline assembly source code to the node --- slither/core/cfg/node.py | 15 ++++++ slither/core/declarations/contract.py | 48 +++++++++++++++++++ slither/printers/summary/contract.py | 25 ++++++++-- slither/solc_parsing/declarations/function.py | 9 ++-- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/slither/core/cfg/node.py b/slither/core/cfg/node.py index 5bcb000b3..11435f3a5 100644 --- a/slither/core/cfg/node.py +++ b/slither/core/cfg/node.py @@ -204,6 +204,8 @@ class Node(SourceMapping, ChildFunction): self._can_reenter = None self._can_send_eth = None + self._asm_source_code = None + ################################################################################### ################################################################################### # region General's properties @@ -515,6 +517,19 @@ class Node(SourceMapping, ChildFunction): return True return False + # endregion + ################################################################################### + ################################################################################### + # region EVM + ################################################################################### + ################################################################################### + + @property + def inline_asm(self): + return self._asm_source_code + + def add_inline_asm(self, asm): + self._asm_source_code = asm # endregion diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py index 813848a44..1d8b0fd47 100644 --- a/slither/core/declarations/contract.py +++ b/slither/core/declarations/contract.py @@ -2,6 +2,7 @@ Contract module """ import logging + from slither.core.children.child_slither import ChildSlither from slither.core.source_mapping.source_mapping import SourceMapping from slither.core.declarations.function import Function @@ -42,6 +43,9 @@ class Contract(ChildSlither, SourceMapping): self._signatures = None + self._is_upgradeable = None + self._is_upgradeable_proxy = None + self._initial_state_variables = [] # ssa @@ -811,6 +815,50 @@ class Contract(ChildSlither, SourceMapping): for function in self.functions + self.modifiers: function.update_read_write_using_ssa() + # endregion + ################################################################################### + ################################################################################### + # region Upgradeability + ################################################################################### + ################################################################################### + + @property + def is_upgradeable(self): + if self._is_upgradeable is None: + self._is_upgradeable = False + initializable = self.slither.get_contract_from_name('Initializable') + if initializable: + if initializable in self.inheritance: + self._is_upgradeable = True + else: + for c in self.inheritance + [self]: + # This might lead to false positive + if 'upgradeable' in c.name.lower() or 'upgradable' in c.name.lower(): + self._is_upgradeable = True + break + return self._is_upgradeable + + @property + def is_upgradeable_proxy(self): + from slither.core.cfg.node import NodeType + from slither.slithir.operations import LowLevelCall + if self._is_upgradeable_proxy is None: + self._is_upgradeable_proxy = False + for f in self.functions: + if f.is_fallback: + for node in f.nodes: + for ir in node.irs: + if isinstance(ir, LowLevelCall) and ir.function_name == 'delegatecall': + self._is_upgradeable_proxy = True + return self._is_upgradeable_proxy + if node.type == NodeType.ASSEMBLY: + inline_asm = node.inline_asm + if inline_asm: + if 'delegatecall' in inline_asm: + self._is_upgradeable_proxy = True + return self._is_upgradeable_proxy + return self._is_upgradeable_proxy + # endregion ################################################################################### ################################################################################### diff --git a/slither/printers/summary/contract.py b/slither/printers/summary/contract.py index e8f91d897..37139299d 100644 --- a/slither/printers/summary/contract.py +++ b/slither/printers/summary/contract.py @@ -24,8 +24,27 @@ class ContractSummary(AbstractPrinter): all_contracts = [] for c in self.contracts: - txt += blue("\n+ Contract %s\n" % c.name) - additional_fields = output.Output('') + + is_upgradeable_proxy = c.is_upgradeable_proxy + is_upgradeable = c.is_upgradeable + + additional_txt_info = '' + + if is_upgradeable_proxy: + additional_txt_info += ' (Upgradeable Proxy)' + + if is_upgradeable: + additional_txt_info += ' (Upgradeable)' + + if c in self.slither.contracts_derived: + additional_txt_info += ' (Most derived contract)' + + txt += blue(f"\n+ Contract {c.name}{additional_txt_info}\n") + additional_fields = output.Output('', additional_fields={ + 'is_upgradeable_proxy': is_upgradeable_proxy, + 'is_upgradeable': is_upgradeable, + 'is_most_derived': c in self.slither.contracts_derived + }) # Order the function with # contract_declarer -> list_functions @@ -49,7 +68,7 @@ class ContractSummary(AbstractPrinter): txt += " - {}  ({})\n".format(function, function.visibility) additional_fields.add(function, additional_fields={"visibility": - function.visibility}) + function.visibility,}) all_contracts.append((c, additional_fields.data)) diff --git a/slither/solc_parsing/declarations/function.py b/slither/solc_parsing/declarations/function.py index e54f7cce6..0a61bad4a 100644 --- a/slither/solc_parsing/declarations/function.py +++ b/slither/solc_parsing/declarations/function.py @@ -680,10 +680,13 @@ class FunctionSolc(Function): elif name == 'Block': node = self._parse_block(statement, node) elif name == 'InlineAssembly': - break_node = self._new_node(NodeType.ASSEMBLY, statement['src']) + asm_node = self._new_node(NodeType.ASSEMBLY, statement['src']) self._contains_assembly = True - link_nodes(node, break_node) - node = break_node + # Added with solc 0.4.12 + if 'operations' in statement: + asm_node.add_inline_asm(statement['operations']) + link_nodes(node, asm_node) + node = asm_node elif name == 'DoWhileStatement': node = self._parse_dowhile(statement, node) # For Continue / Break / Return / Throw From 6983474290633af9d9a88439a972a0bdfea332b6 Mon Sep 17 00:00:00 2001 From: Josselin Date: Wed, 13 Nov 2019 12:24:01 +0100 Subject: [PATCH 21/21] Uninitialized state variable detector: remove FP due to delegatecall proxy --- .../uninitialized_state_variables.py | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/slither/detectors/variables/uninitialized_state_variables.py b/slither/detectors/variables/uninitialized_state_variables.py index 795d74e39..255709784 100644 --- a/slither/detectors/variables/uninitialized_state_variables.py +++ b/slither/detectors/variables/uninitialized_state_variables.py @@ -44,7 +44,7 @@ Initialize all the variables. If a variable is meant to be initialized to zero, ''' @staticmethod - def written_variables(contract): + def _written_variables(contract): ret = [] for f in contract.all_functions_called + contract.modifiers: for n in f.nodes: @@ -57,24 +57,48 @@ Initialize all the variables. If a variable is meant to be initialized to zero, for param in ir.function.parameters: if param.location == 'storage': ret.append(ir.arguments[idx]) - idx = idx+1 + idx = idx + 1 return ret + def _variable_written_in_proxy(self): + # Hack to memoize without having it define in the init + if hasattr(self, '__variables_written_in_proxy'): + return self.__variables_written_in_proxy + + variables_written_in_proxy = [] + for c in self.slither.contracts: + if c.is_upgradeable_proxy: + variables_written_in_proxy += self._written_variables(c) + + self.__variables_written_in_proxy = list(set([v.name for v in variables_written_in_proxy])) + return self.__variables_written_in_proxy + + def _written_variables_in_proxy(self, contract): + variables = [] + if contract.is_upgradeable: + variables_name_written_in_proxy = self._variable_written_in_proxy() + if variables_name_written_in_proxy: + variables_in_contract = [contract.get_state_variable_from_name(v) for v in variables_name_written_in_proxy] + variables_in_contract = [v for v in variables_in_contract if v] + variables += variables_in_contract + return list(set(variables)) + @staticmethod - def read_variables(contract): + def _read_variables(contract): ret = [] for f in contract.all_functions_called + contract.modifiers: ret += f.state_variables_read return ret - def detect_uninitialized(self, contract): - written_variables = self.written_variables(contract) - read_variables = self.read_variables(contract) + def _detect_uninitialized(self, contract): + written_variables = self._written_variables(contract) + written_variables += self._written_variables_in_proxy(contract) + read_variables = self._read_variables(contract) return [(variable, contract.get_functions_reading_from_variable(variable)) - for variable in contract.state_variables if variable not in written_variables and\ - not variable.expression and\ - variable in read_variables] + for variable in contract.state_variables if variable not in written_variables and \ + not variable.expression and \ + variable in read_variables] def _detect(self): """ Detect uninitialized state variables @@ -85,7 +109,7 @@ Initialize all the variables. If a variable is meant to be initialized to zero, """ results = [] for c in self.slither.contracts_derived: - ret = self.detect_uninitialized(c) + ret = self._detect_uninitialized(c) for variable, functions in ret: info = [variable, " is never initialized. It is used in:\n"]