def write_file( output_dir: Path, filename: str, content: str, allow_overwrite: bool = True, discard_if_exist: bool = False, ): """ Write the content into output_dir/filename :param output_dir: :param filename: :param content: :param allow_overwrite: If true, allows to overwrite existing file (default: true). Emit warning if overwrites :param discard_if_exist: If true, it will not emit warning or overwrite the file if it exists, (default: False) :return: """ file_to_write = Path(output_dir, filename) if file_to_write.exists(): if discard_if_exist: return if not allow_overwrite: logger.info( yellow( f"{file_to_write} already exist and will not be overwritten" )) return logger.info(yellow(f"Overwrite {file_to_write}")) else: logger.info(green(f"Write {file_to_write}")) with open(file_to_write, "w") as f: f.write(content)
def is_complex_code(self, contract): """ Check if the code is complex Heuristic, the code is complex if: - One function has a cyclomatic complexity > 7 Args: contract """ is_complex = self._is_complex_code(contract) result = red("Yes") if is_complex else green("No") return result
def get_detectors_result( self) -> Tuple[str, List[Dict], int, int, int, int, int]: ( all_results, optimization, informational, low, medium, high, ) = self._get_detectors_result() txt = "Number of optimization issues: {}\n".format(green(optimization)) txt += "Number of informational issues: {}\n".format( green(informational)) txt += "Number of low issues: {}\n".format(green(low)) if medium > 0: txt += "Number of medium issues: {}\n".format(yellow(medium)) else: txt += "Number of medium issues: {}\n".format(green(medium)) if high > 0: txt += "Number of high issues: {}\n".format(red(high)) else: txt += "Number of high issues: {}\n\n".format(green(high)) return txt, all_results, optimization, informational, low, medium, high
def get_summary_erc20(self, contract): txt = "" pause, mint_unlimited, race_condition_mitigated = self._get_summary_erc20( contract) if pause: txt += yellow("Pausable") + "\n" if mint_unlimited is None: txt += green("No Minting") + "\n" else: if mint_unlimited: txt += red("∞ Minting") + "\n" else: txt += yellow("Minting") + "\n" if not race_condition_mitigated: txt += red("Approve Race Cond.") + "\n" return txt
def output(self, _filename): """ _filename is not used Args: _filename(string) """ txt = "" if not self.fortress.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 = _extract_evm_info(self.fortress) for contract in self.fortress.contracts_derived: txt += blue("Contract {}\n".format(contract.name)) contract_file = self.fortress.source_code[ contract.source_mapping["filename_absolute"]].encode("utf-8") 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] 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: 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(), )) 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(), )) 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) res = self.generate_output(txt) return res
def output(self, _filename): # pylint: disable=too-many-locals """ _filename is not used Args: _filename(string) """ txt = "" all_contracts = [] for c in self.contracts: if c.is_top_level: continue 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.fortress.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.fortress.contracts_derived, }, ) # Order the function with # contract_declarer -> list_functions public = [(f.contract_declarer.name, f) for f in c.functions if (not f.is_shadowed and not f.is_constructor_variables) ] collect = collections.defaultdict(list) for a, b in public: collect[a].append(b) public = list(collect.items()) for contract, functions in public: txt += blue(" - From {}\n".format(contract)) functions = sorted(functions, key=lambda f: f.full_name) for function in functions: if function.visibility in ["external", "public"]: txt += green(" - {} ({})\n".format( function.full_name, function.visibility)) if function.visibility in ["internal", "private"]: txt += magenta(" - {} ({})\n".format( function.full_name, function.visibility)) if function.visibility not in [ "external", "public", "internal", "private", ]: txt += " - {} ({})\n".format( function.full_name, function.visibility) additional_fields.add( function, additional_fields={"visibility": function.visibility}) all_contracts.append((c, additional_fields.data)) self.info(txt) res = self.generate_output(txt) for contract, additional_fields in all_contracts: res.add(contract, additional_fields=additional_fields) return res
def output(self, filename): """ Output the inheritance relation _filename is not used Args: _filename(string) """ info = "Inheritance\n" if not self.contracts: return [] info += blue("Child_Contract -> ") + green("Immediate_Base_Contracts") info += green(" [Not_Immediate_Base_Contracts]") result = {"child_to_base": {}} for child in self.contracts: if child.is_top_level: continue info += blue(f"\n+ {child.name}\n") result["child_to_base"][child.name] = { "immediate": [], "not_immediate": [] } if child.inheritance: immediate = child.immediate_inheritance not_immediate = [ i for i in child.inheritance if i not in immediate ] info += " -> " + green(", ".join(map(str, immediate))) + "\n" result["child_to_base"][child.name]["immediate"] = list( map(str, immediate)) if not_immediate: info += ", [" + green(", ".join(map( str, not_immediate))) + "]\n" result["child_to_base"][ child.name]["not_immediate"] = list( map(str, not_immediate)) info += green("\n\nBase_Contract -> ") + blue( "Immediate_Child_Contracts") + "\n" info += blue(" [Not_Immediate_Child_Contracts]") + "\n" result["base_to_child"] = {} for base in self.contracts: if base.is_top_level: continue info += green(f"\n+ {base.name}") + "\n" children = list(self._get_child_contracts(base)) result["base_to_child"][base.name] = { "immediate": [], "not_immediate": [] } if children: immediate = [ child for child in children if base in child.immediate_inheritance ] not_immediate = [ child for child in children if not child in immediate ] info += " -> " + blue(", ".join(map(str, immediate))) + "\n" result["base_to_child"][base.name]["immediate"] = list( map(str, immediate)) if not_immediate: info += ", [" + blue(", ".join(map( str, not_immediate))) + "]" + "\n" result["base_to_child"][base.name]["not_immediate"] = list( map(str, immediate)) self.info(info) res = self.generate_output(info, additional_fields=result) return res
def generate_erc20( contract: Contract, type_property: str, addresses: Addresses ): # pylint: disable=too-many-locals """ Generate the ERC20 tests Files generated: - interfaces.sol: generic crytic interface - Properties[CONTRACTNAME].sol: erc20 properties - Test[CONTRACTNAME].sol: Target, its constructor needs to be manually updated - If truffle - migrations/x_Test[CONTRACTNAME].js - test/crytic/InitializationTest[CONTRACTNAME].js: unit tests to check that the contract is correctly configured - test/crytic/Test[CONTRACTNAME].js: ERC20 checks - echidna_config.yaml: configuration file :param addresses: :param contract: :param type_property: One of ERC20_PROPERTIES.keys() :return: """ if contract.fortress.crytic_compile is None: logging.error("Please compile with crytic-compile") return if contract.fortress.crytic_compile.type not in [ PlatformType.TRUFFLE, PlatformType.SOLC, ]: logging.error(f"{contract.fortress.crytic_compile.type} not yet supported by fortress-prop") return # Check if the contract is an ERC20 contract and if the functions have the correct visibility errors = _check_compatibility(contract) if errors: logger.error(red(errors)) return erc_properties = ERC20_PROPERTIES.get(type_property, None) if erc_properties is None: logger.error(f"{type_property} unknown. Types available {ERC20_PROPERTIES.keys()}") return properties = erc_properties.properties # Generate the output directory output_dir = _platform_to_output_dir(contract.fortress.crytic_compile.platform) output_dir.mkdir(exist_ok=True) # Get the properties solidity_properties, unit_tests = _get_properties(contract, properties) # Generate the contract containing the properties generate_solidity_interface(output_dir, addresses) property_file = generate_solidity_properties( contract, type_property, solidity_properties, output_dir ) # Generate the Test contract initialization_recommendation = _initialization_recommendation(type_property) contract_filename, contract_name = generate_test_contract( contract, type_property, output_dir, property_file, initialization_recommendation, ) # Generate Echidna config file echidna_config_filename = generate_echidna_config( Path(contract.fortress.crytic_compile.target).parent, addresses ) unit_test_info = "" # If truffle, generate unit tests if contract.fortress.crytic_compile.type == PlatformType.TRUFFLE: unit_test_info = generate_truffle_test(contract, type_property, unit_tests, addresses) logger.info("################################################") logger.info(green(f"Update the constructor in {Path(output_dir, contract_filename)}")) if unit_test_info: logger.info(green(unit_test_info)) logger.info(green("To run Echidna:")) txt = f"\t echidna-test {contract.fortress.crytic_compile.target} " txt += f"--contract {contract_name} --config {echidna_config_filename}" logger.info(green(txt))