diff --git a/slither/core/declarations/contract.py b/slither/core/declarations/contract.py index f53e2a717..f06bc3031 100644 --- a/slither/core/declarations/contract.py +++ b/slither/core/declarations/contract.py @@ -5,6 +5,9 @@ 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 +from slither.utils.erc import ERC20_signatures, \ + ERC165_signatures, ERC223_signatures, ERC721_signatures, \ + ERC1820_signatures, ERC777_signatures logger = logging.getLogger("Contract") @@ -35,6 +38,8 @@ class Contract(ChildSlither, SourceMapping): self._using_for = {} self._kind = None + self._signatures = None + self._initial_state_variables = [] # ssa @@ -212,6 +217,20 @@ class Contract(ChildSlither, SourceMapping): ################################################################################### ################################################################################### + @property + def functions_signatures(self): + """ + Return the signatures of all the public/eterxnal functions/state variables + :return: list(string) the signatures of all the functions that can be called + """ + if self._signatures == None: + sigs = [v.full_name for v in self.state_variables if v.visibility in ['public', + 'external']] + + sigs += set([f.full_name for f in self.functions if f.visibility in ['public', 'external']]) + self._signatures = list(set(sigs)) + return self._signatures + @property def functions(self): ''' @@ -534,50 +553,102 @@ class Contract(ChildSlither, SourceMapping): ################################################################################### ################################################################################### + def ercs(self): + """ + Return the ERC implemented + :return: list of string + """ + all = [('ERC20', lambda x: x.is_erc20()), + ('ERC165', lambda x: x.is_erc165()), + ('ERC1820', lambda x: x.is_erc1820()), + ('ERC223', lambda x: x.is_erc223()), + ('ERC721', lambda x: x.is_erc721()), + ('ERC777', lambda x: x.is_erc777())] + + return [erc[0] for erc in all if erc[1](self)] def is_erc20(self): """ Check if the contract is an erc20 token Note: it does not check for correct return values - Returns: - bool + :return: Returns a true if the contract is an erc20 """ - full_names = set([f.full_name for f in self.functions]) - return 'transfer(address,uint256)' in full_names and\ - 'transferFrom(address,address,uint256)' in full_names and\ - 'approve(address,uint256)' in full_names + full_names = self.functions_signatures + return all((s in full_names for s in ERC20_signatures)) + + def is_erc165(self): + """ + Check if the contract is an erc165 token + + Note: it does not check for correct return values + :return: Returns a true if the contract is an erc165 + """ + full_names = self.functions_signatures + return all((s in full_names for s in ERC165_signatures)) + + def is_erc1820(self): + """ + Check if the contract is an erc1820 + + Note: it does not check for correct return values + :return: Returns a true if the contract is an erc165 + """ + full_names = self.functions_signatures + return all((s in full_names for s in ERC1820_signatures)) + + def is_erc223(self): + """ + Check if the contract is an erc223 token + + Note: it does not check for correct return values + :return: Returns a true if the contract is an erc223 + """ + full_names = self.functions_signatures + return all((s in full_names for s in ERC223_signatures)) def is_erc721(self): - full_names = set([f.full_name for f in self.functions]) - return self.is_erc20() and\ - 'ownerOf(uint256)' in full_names and\ - 'safeTransferFrom(address,address,uint256,bytes)' in full_names and\ - 'safeTransferFrom(address,address,uint256)' in full_names and\ - 'setApprovalForAll(address,bool)' in full_names and\ - 'getApproved(uint256)' in full_names and\ - 'isApprovedForAll(address,address)' in full_names + """ + Check if the contract is an erc721 token + + Note: it does not check for correct return values + :return: Returns a true if the contract is an erc721 + """ + full_names = self.functions_signatures + return all((s in full_names for s in ERC721_signatures)) + + def is_erc777(self): + """ + Check if the contract is an erc777 - def has_an_erc20_function(self): + Note: it does not check for correct return values + :return: Returns a true if the contract is an erc165 + """ + full_names = self.functions_signatures + return all((s in full_names for s in ERC777_signatures)) + + def is_possible_erc20(self): """ Checks if the provided contract could be attempting to implement ERC20 standards. :param contract: The contract to check for token compatibility. :return: Returns a boolean indicating if the provided contract met the token standard. """ - full_names = set([f.full_name for f in self.functions]) + # We do not check for all the functions, as name(), symbol(), might give too many FPs + full_names = self.functions_signatures return 'transfer(address,uint256)' in full_names or \ 'transferFrom(address,address,uint256)' in full_names or \ 'approve(address,uint256)' in full_names - def has_an_erc721_function(self): + def is_possible_erc721(self): """ Checks if the provided contract could be attempting to implement ERC721 standards. :param contract: The contract to check for token compatibility. :return: Returns a boolean indicating if the provided contract met the token standard. """ - full_names = set([f.full_name for f in self.functions]) - return self.has_an_erc20_function() and \ - ('ownerOf(uint256)' in full_names or + # We do not check for all the functions, as name(), symbol(), might give too many FPs + full_names = self.functions_signatures + return ('approve(address,uint256)' in full_names or + 'ownerOf(uint256)' in full_names or 'safeTransferFrom(address,address,uint256,bytes)' in full_names or 'safeTransferFrom(address,address,uint256)' in full_names or 'setApprovalForAll(address,bool)' in full_names or @@ -585,6 +656,17 @@ class Contract(ChildSlither, SourceMapping): 'isApprovedForAll(address,address)' in full_names) + # endregion + ################################################################################### + ################################################################################### + # region Dependencies + ################################################################################### + ################################################################################### + + def is_from_dependency(self): + if self.slither.crytic_compile is None: + return False + return self.slither.crytic_compile.is_dependency(self.source_mapping['filename_absolute']) # endregion ################################################################################### diff --git a/slither/core/variables/state_variable.py b/slither/core/variables/state_variable.py index b8f46482e..e85ca2e09 100644 --- a/slither/core/variables/state_variable.py +++ b/slither/core/variables/state_variable.py @@ -1,8 +1,53 @@ from .variable import Variable from slither.core.children.child_contract import ChildContract +from slither.utils.type import export_nested_types_from_variable class StateVariable(ChildContract, Variable): + ################################################################################### + ################################################################################### + # region Signature + ################################################################################### + ################################################################################### + + @property + def signature(self): + """ + Return the signature of the state variable as a function signature + :return: (str, list(str), list(str)), as (name, list parameters type, list return values type) + """ + return self.name, [str(x) for x in export_nested_types_from_variable(self)], self.type + + @property + def signature_str(self): + """ + Return the signature of the state variable as a function signature + :return: str: func_name(type1,type2) returns(type3) + """ + name, parameters, returnVars = self.signature + return name+'('+','.join(parameters)+') returns('+','.join(returnVars)+')' + + # endregion + ################################################################################### + ################################################################################### + # region Name + ################################################################################### + ################################################################################### + @property def canonical_name(self): return '{}:{}'.format(self.contract.name, self.name) + + @property + def full_name(self): + """ + Return the name of the state variable as a function signaure + str: func_name(type1,type2) + :return: the function signature without the return values + """ + name, parameters, _ = self.signature + return name+'('+','.join(parameters)+')' + + # endregion + ################################################################################### + ################################################################################### diff --git a/slither/detectors/erc/incorrect_erc20_interface.py b/slither/detectors/erc/incorrect_erc20_interface.py index ab577fbab..5333f089c 100644 --- a/slither/detectors/erc/incorrect_erc20_interface.py +++ b/slither/detectors/erc/incorrect_erc20_interface.py @@ -62,12 +62,12 @@ contract Token{ list(str) : list of incorrect function signatures """ # Verify this is an ERC20 contract. - if not contract.has_an_erc20_function(): + if not contract.is_possible_erc20(): return [] # If this contract implements a function from ERC721, we can assume it is an ERC721 token. These tokens # offer functions which are similar to ERC20, but are not compatible. - if contract.has_an_erc721_function(): + if contract.is_possible_erc721(): return [] functions = [f for f in contract.functions if IncorrectERC20InterfaceDetection.incorrect_erc20_interface(f.signature)] diff --git a/slither/detectors/erc/incorrect_erc721_interface.py b/slither/detectors/erc/incorrect_erc721_interface.py index 5ddea37ff..336d549db 100644 --- a/slither/detectors/erc/incorrect_erc721_interface.py +++ b/slither/detectors/erc/incorrect_erc721_interface.py @@ -68,7 +68,7 @@ contract Token{ """ # Verify this is an ERC721 contract. - if not contract.has_an_erc721_function() or not contract.has_an_erc20_function(): + if not contract.is_possible_erc721() or not contract.is_possible_erc20(): return [] functions = [f for f in contract.functions if IncorrectERC721InterfaceDetection.incorrect_erc721_interface(f.signature)] diff --git a/slither/printers/summary/human_summary.py b/slither/printers/summary/human_summary.py index e82b7fef7..625a0aca9 100644 --- a/slither/printers/summary/human_summary.py +++ b/slither/printers/summary/human_summary.py @@ -6,7 +6,7 @@ import logging from slither.printers.abstract_printer import AbstractPrinter from slither.utils.code_complexity import compute_cyclomatic_complexity from slither.utils.colors import green, red, yellow - +from slither.utils.standard_libraries import is_standard_library class PrinterHumanSummary(AbstractPrinter): ARGUMENT = 'human-summary' @@ -88,8 +88,14 @@ class PrinterHumanSummary(AbstractPrinter): issues_informational, issues_low, issues_medium, issues_high = self._get_detectors_result() txt = "Number of informational issues: {}\n".format(green(issues_informational)) txt += "Number of low issues: {}\n".format(green(issues_low)) - txt += "Number of medium issues: {}\n".format(yellow(issues_medium)) - txt += "Number of high issues: {}\n".format(red(issues_high)) + if issues_medium > 0: + txt += "Number of medium issues: {}\n".format(yellow(issues_medium)) + else: + txt += "Number of medium issues: {}\n".format(green(issues_medium)) + if issues_high > 0: + txt += "Number of high issues: {}\n".format(red(issues_high)) + else: + txt += "Number of high issues: {}\n\n".format(green(issues_high)) return txt @@ -119,6 +125,49 @@ class PrinterHumanSummary(AbstractPrinter): def _number_functions(contract): return len(contract.functions) + def _lines_number(self): + if not self.slither.source_code: + return None + total_dep_lines = 0 + total_lines = 0 + for filename, source_code in self.slither.source_code.items(): + lines = len(source_code.splitlines()) + is_dep = False + if self.slither.crytic_compile: + is_dep = self.slither.crytic_compile.is_dependency(filename) + if is_dep: + total_dep_lines += lines + else: + total_lines += lines + return total_lines, total_dep_lines + + def _compilation_type(self): + if self.slither.crytic_compile is None: + return 'Compilation non standard\n' + return f'Compiled with {self.slither.crytic_compile.type}\n' + + def _number_contracts(self): + if self.slither.crytic_compile is None: + len(self.slither.contracts), 0 + deps = [c for c in self.slither.contracts if c.is_from_dependency()] + contracts = [c for c in self.slither.contracts if not c.is_from_dependency()] + return len(contracts), len(deps) + + def _standard_libraries(self): + libraries = [] + for contract in self.contracts: + lib = is_standard_library(contract) + if lib: + libraries.append(lib) + + return libraries + + def _ercs(self): + ercs = [] + for contract in self.contracts: + ercs += contract.ercs() + return list(set(ercs)) + def output(self, _filename): """ _filename is not used @@ -126,15 +175,37 @@ class PrinterHumanSummary(AbstractPrinter): _filename(string) """ - txt = "Analyze of {}\n".format(self.slither.filename) + txt = "\n" + txt += self._compilation_type() + + lines_number = self._lines_number() + if lines_number: + total_lines, total_dep_lines = lines_number + txt += f'Number of lines: {total_lines} (+ {total_dep_lines} in dependencies)\n' + + number_contracts, number_contracts_deps = self._number_contracts() + txt += f'Number of contracts: {number_contracts} (+ {number_contracts_deps} in dependencies) \n\n' + txt += self.get_detectors_result() + + libs = self._standard_libraries() + if libs: + txt += f'\nUse: {", ".join(libs)}\n' + + ercs = self._ercs() + if ercs: + txt += f'ERCs: {", ".join(ercs)}\n' + for contract in self.slither.contracts_derived: txt += "\nContract {}\n".format(contract.name) txt += self.is_complex_code(contract) + txt += '\tNumber of functions: {}\n'.format(self._number_functions(contract)) + ercs = contract.ercs() + if ercs: + txt += '\tERCs: ' + ','.join(ercs) + '\n' is_erc20 = contract.is_erc20() - txt += '\tNumber of functions:{}'.format(self._number_functions(contract)) - txt += "\tIs ERC20 token: {}\n".format(contract.is_erc20()) if is_erc20: + txt += '\tERC20 info:\n' txt += self.get_summary_erc20(contract) self.info(txt) diff --git a/slither/slither.py b/slither/slither.py index d30099426..acb25d619 100644 --- a/slither/slither.py +++ b/slither/slither.py @@ -45,9 +45,6 @@ class Slither(SlitherSolc): ''' - truffle_ignore = kwargs.get('truffle_ignore', False) - embark_ignore = kwargs.get('embark_ignore', False) - # list of files provided (see --splitted option) if isinstance(contract, list): self._init_from_list(contract) @@ -56,13 +53,13 @@ class Slither(SlitherSolc): else: super(Slither, self).__init__('') try: - cryticCompile = CryticCompile(contract, **kwargs) - self._crytic_compile = cryticCompile + crytic_compile = CryticCompile(contract, **kwargs) + self._crytic_compile = crytic_compile except InvalidCompilation as e: logger.error('Invalid compilation') logger.error(e) exit(-1) - for path, ast in cryticCompile.asts.items(): + for path, ast in crytic_compile.asts.items(): self._parse_contracts_from_loaded_json(ast, path) self._add_source_code(path) diff --git a/slither/utils/erc.py b/slither/utils/erc.py new file mode 100644 index 000000000..08aa7ad0d --- /dev/null +++ b/slither/utils/erc.py @@ -0,0 +1,71 @@ + +def erc_to_signatures(erc): + return [f'{e[0]}({",".join(e[1])})' for e in erc] + + +# Final +# https://eips.ethereum.org/EIPS/eip-20 +ERC20 = [('name', [], 'string'), + ('symbol', [], 'string'), + ('decimals', [], 'uint8'), + ('totalSupply', [], 'uint256'), + ('balanceOf', ['address'], 'uint256'), + ('transfer', ['address', 'uint256'], 'bool'), + ('transferFrom', ['address', 'address', 'uint256'], 'bool'), + ('approve', ['address', 'uint256'], 'bool'), + ('allowance', ['address', 'address'], 'uint256')] +ERC20_signatures = erc_to_signatures(ERC20) + +# Draft +# https://github.com/ethereum/eips/issues/223 +ERC223 = [('name', [], 'string'), + ('symbol', [], 'string'), + ('decimals', [], 'uint8'), + ('totalSupply', [], 'uint256'), + ('balanceOf', ['address'], 'uint256'), + ('transfer', ['address', 'uint256'], 'bool'), + ('transfer', ['address', 'uint256', 'bytes'], 'bool'), + ('transfer', ['address', 'uint256', 'bytes', 'string'], 'bool')] +ERC223_signatures = erc_to_signatures(ERC223) + +# Final +# https://eips.ethereum.org/EIPS/eip-165 +ERC165 = [('supportsInterface', ['bytes4'], 'bool')] +ERC165_signatures = erc_to_signatures(ERC165) + +# Final +# https://eips.ethereum.org/EIPS/eip-721 +# Must have ERC165 +# name, symbol, tokenURI are optionals +ERC721 = [('balanceOf', ['address'], 'uint256'), + ('ownerOf', ['uint256'], 'address'), + ('safeTransferFrom', ['address', 'address', 'uint256', 'bytes'], ''), + ('safeTransferFrom', ['address', 'address', 'uint256'], ''), + ('transferFrom', ['address', 'address', 'uint256'], ''), + ('approve', ['address', 'uint256'], ''), + ('setApprovalForAll', ['address', 'bool'], ''), + ('getApproved', ['uint256'], 'address'), + ('isApprovedForAll', ['address', 'address'], 'bool')] + ERC165 +ERC721_signatures = erc_to_signatures(ERC721) + +# Final +# https://eips.ethereum.org/EIPS/eip-1820 +ERC1820 = [('canImplementInterfaceForAddress', ['bytes32', 'address'], 'bytes32')] +ERC1820_signatures = erc_to_signatures(ERC1820) + +# Last Call +# https://eips.ethereum.org/EIPS/eip-777 +ERC777 = [('name', [], 'string'), + ('symbol', [], 'string'), + ('totalSupply', [], 'uint256'), + ('balanceOf', ['address'], 'uint256'), + ('granularity', [], 'uint256'), + ('defaultOperators', [], 'address[]'), + ('isOperatorFor', ['address', 'address'], 'bool'), + ('authorizeOperator', ['address'], ''), + ('revokeOperator', ['address'], ''), + ('send', ['address', 'uint256', 'bytes'], ''), + ('operatorSend', ['address', 'address', 'uint256', 'bytes', 'bytes'], ''), + ('burn', ['uint256', 'bytes'] , ''), + ('operatorBurn', ['address', 'uint256', 'bytes', 'bytes'] , '')] +ERC777_signatures = erc_to_signatures(ERC777) \ No newline at end of file diff --git a/slither/utils/standard_libraries.py b/slither/utils/standard_libraries.py new file mode 100644 index 000000000..c73a29b93 --- /dev/null +++ b/slither/utils/standard_libraries.py @@ -0,0 +1,193 @@ +from pathlib import Path + + +libraries = { + 'Openzeppelin-SafeMath': lambda x: is_openzepellin_safemath(x), + 'Openzeppelin-ECRecovery': lambda x: is_openzepellin_ecrecovery(x), + 'Openzeppelin-Ownable': lambda x: is_openzepellin_ownable(x), + 'Openzeppelin-ERC20': lambda x: is_openzepellin_erc20(x), + 'Openzeppelin-ERC721': lambda x: is_openzepellin_erc721(x), + 'Zos-Upgrade': lambda x: is_zos_initializable(x), + 'Dapphub-DSAuth': lambda x: is_dapphub_ds_auth(x), + 'Dapphub-DSMath': lambda x: is_dapphub_ds_math(x), + 'Dapphub-DSToken': lambda x: is_dapphub_ds_token(x), + 'Dapphub-DSProxy': lambda x: is_dapphub_ds_proxy(x), + 'Dapphub-DSGroup': lambda x: is_dapphub_ds_group(x), +} + +def is_standard_library(contract): + for name, is_lib in libraries.items(): + if is_lib(contract): + return name + return None + + +################################################################################### +################################################################################### +# region General libraries +################################################################################### +################################################################################### + + +def is_openzepellin(contract): + if not contract.is_from_dependency(): + return False + return 'openzeppelin-solidity' in Path(contract.source_mapping['filename_absolute']).parts + + +def is_zos(contract): + if not contract.is_from_dependency(): + return False + return 'zos-lib' in Path(contract.source_mapping['filename_absolute']).parts + + +# endregion +################################################################################### +################################################################################### +# region SafeMath +################################################################################### +################################################################################### + + +def is_safemath(contract): + return contract.name == "SafeMath" + + +def is_openzepellin_safemath(contract): + return is_safemath(contract) and is_openzepellin(contract) + +# endregion +################################################################################### +################################################################################### +# region ECRecovery +################################################################################### +################################################################################### + + +def is_ecrecovery(contract): + return contract.name == 'ECRecovery' + + +def is_openzepellin_ecrecovery(contract): + return is_ecrecovery(contract) and is_openzepellin(contract) + + +# endregion +################################################################################### +################################################################################### +# region Ownable +################################################################################### +################################################################################### + + +def is_ownable(contract): + return contract.name == 'Ownable' + + +def is_openzepellin_ownable(contract): + return is_ownable(contract) and is_openzepellin(contract) + + +# endregion +################################################################################### +################################################################################### +# region ERC20 +################################################################################### +################################################################################### + + +def is_erc20(contract): + return contract.name == 'ERC20' + + +def is_openzepellin_erc20(contract): + return is_erc20(contract) and is_openzepellin(contract) + + +# endregion +################################################################################### +################################################################################### +# region ERC721 +################################################################################### +################################################################################### + + +def is_erc721(contract): + return contract.name == 'ERC721' + + +def is_openzepellin_erc721(contract): + return is_erc721(contract) and is_openzepellin(contract) + + +# endregion +################################################################################### +################################################################################### +# region Zos Initializable +################################################################################### +################################################################################### + + +def is_initializable(contract): + return contract.name == 'Initializable' + + +def is_zos_initializable(contract): + return is_initializable(contract) and is_zos(contract) + + +# endregion +################################################################################### +################################################################################### +# region DappHub +################################################################################### +################################################################################### + +dapphubs = { + 'DSAuth': 'ds-auth', + 'DSMath': 'ds-math', + 'DSToken': 'ds-token', + 'DSProxy': 'ds-proxy', + 'DSGroup': 'ds-group', +} + + +def _is_ds(contract, name): + return contract.name == name + +def _is_dappdhub_ds(contract, name): + if not contract.is_from_dependency(): + return False + if not dapphubs[name] in Path(contract.source_mapping['filename_absolute']).parts: + return False + return _is_ds(contract, name) + +def is_ds_auth(contract): + return _is_ds(contract, 'DSAuth') + +def is_dapphub_ds_auth(contract): + return _is_dappdhub_ds(contract, 'DSAuth') + +def is_ds_math(contract): + return _is_ds(contract, 'DSMath') + +def is_dapphub_ds_math(contract): + return _is_dappdhub_ds(contract, 'DSMath') + +def is_ds_token(contract): + return _is_ds(contract, 'DSToken') + +def is_dapphub_ds_token(contract): + return _is_dappdhub_ds(contract, 'DSToken') + +def is_ds_proxy(contract): + return _is_ds(contract, 'DSProxy') + +def is_dapphub_ds_proxy(contract): + return _is_dappdhub_ds(contract, 'DSProxy') + +def is_ds_group(contract): + return _is_ds(contract, 'DSGroup') + +def is_dapphub_ds_group(contract): + return _is_dappdhub_ds(contract, 'DSGroup') diff --git a/slither/utils/type.py b/slither/utils/type.py index d5bca3720..3b0577987 100644 --- a/slither/utils/type.py +++ b/slither/utils/type.py @@ -1,16 +1,19 @@ from slither.core.solidity_types import (ArrayType, MappingType, ElementaryType) + def _add_mapping_parameter(t, l): while isinstance(t, MappingType): l.append(t.type_from) t = t.type_to _add_array_parameter(t, l) + def _add_array_parameter(t, l): while isinstance(t, ArrayType): l.append(ElementaryType('uint256')) t = t.type + def export_nested_types_from_variable(variable): """ Export the list of nested types (mapping/array)