Improvements of the human summary printer.

Several of the new features are integrated within slither.core or slither.utils

- Add state_variable.signature function(s) to translate a state variable to a function signature
- Add several contract.is_erc_* methods
- Add contract.is_from_dependency that checks if the contract is from a dependency
- Add utils.erc with standard ERCs signatuers
- Add utils.standard_libraries with standard libraries

Several of these methods require crytic_compile 3ed5160924d08a4d3c86d7cddf9f1e17dfe51808
pull/228/head
Josselin 6 years ago
parent 1d4d681e2c
commit 3876b2b479
  1. 122
      slither/core/declarations/contract.py
  2. 45
      slither/core/variables/state_variable.py
  3. 4
      slither/detectors/erc/incorrect_erc20_interface.py
  4. 2
      slither/detectors/erc/incorrect_erc721_interface.py
  5. 83
      slither/printers/summary/human_summary.py
  6. 9
      slither/slither.py
  7. 71
      slither/utils/erc.py
  8. 193
      slither/utils/standard_libraries.py
  9. 3
      slither/utils/type.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
###################################################################################

@ -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
###################################################################################
###################################################################################

@ -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)]

@ -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)]

@ -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)

@ -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)

@ -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)

@ -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')

@ -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)

Loading…
Cancel
Save