Merge pull request #478 from crytic/dev-summary-printer

Add new APIs and improve human summary printer
pull/480/head
Feist Josselin 5 years ago committed by GitHub
commit 72b609be47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 44
      slither/core/declarations/contract.py
  2. 6
      slither/core/declarations/pragma_directive.py
  3. 148
      slither/printers/summary/human_summary.py
  4. 10
      slither/utils/standard_libraries.py
  5. 49
      slither/utils/tests_pattern.py

@ -2,6 +2,9 @@
Contract module
"""
import logging
from pathlib import Path
from crytic_compile.platform import Type as PlatformType
from slither.core.children.child_slither import ChildSlither
from slither.core.source_mapping.source_mapping import SourceMapping
@ -9,6 +12,7 @@ from slither.core.declarations.function import Function
from slither.utils.erc import ERC20_signatures, \
ERC165_signatures, ERC223_signatures, ERC721_signatures, \
ERC1820_signatures, ERC777_signatures
from slither.utils.tests_pattern import is_test_contract
logger = logging.getLogger("Contract")
@ -781,6 +785,14 @@ class Contract(ChildSlither, SourceMapping):
full_names = self.functions_signatures
return all((s in full_names for s in ERC777_signatures))
@property
def is_token(self) -> bool:
"""
Check if the contract follows one of the standard ERC token
:return:
"""
return self.is_erc20() or self.is_erc721() or self.is_erc165() or self.is_erc223() or self.is_erc777()
def is_possible_erc20(self):
"""
Checks if the provided contract could be attempting to implement ERC20 standards.
@ -808,6 +820,13 @@ class Contract(ChildSlither, SourceMapping):
'getApproved(uint256)' in full_names or
'isApprovedForAll(address,address)' in full_names)
@property
def is_possible_token(self) -> bool:
"""
Check if the contract is a potential token (it might not implement all the functions)
:return:
"""
return self.is_possible_erc20() or self.is_possible_erc721()
# endregion
###################################################################################
@ -821,6 +840,31 @@ class Contract(ChildSlither, SourceMapping):
return False
return self.slither.crytic_compile.is_dependency(self.source_mapping['filename_absolute'])
# endregion
###################################################################################
###################################################################################
# region Test
###################################################################################
###################################################################################
@property
def is_truffle_migration(self) -> bool:
"""
Return true if the contract is the Migrations contract needed for Truffle
:return:
"""
if self.slither.crytic_compile:
if self.slither.crytic_compile.platform == PlatformType.TRUFFLE:
if self.name == 'Migrations':
paths = Path(self.source_mapping['filename_absolute']).parts
if len(paths) >= 2:
return paths[-2] == 'contracts' and paths[-1] == 'migrations.sol'
return False
@property
def is_test(self) -> bool:
return is_test_contract(self) or self.is_truffle_migration
# endregion
###################################################################################
###################################################################################

@ -27,5 +27,11 @@ class Pragma(SourceMapping):
return self._directive[0].lower() == 'solidity'
return False
@property
def is_abi_encoder_v2(self):
if len(self._directive) == 2:
return self._directive[0] == 'experimental' and self._directive[1] == 'ABIEncoderV2'
return False
def __str__(self):
return 'pragma '+''.join(self.directive)

@ -2,14 +2,20 @@
Module printing summary of the contract
"""
import logging
from pathlib import Path
from typing import Tuple, List, Dict
from slither.core.declarations import SolidityFunction, Function
from slither.core.variables.state_variable import StateVariable
from slither.printers.abstract_printer import AbstractPrinter
from slither.slithir.operations import LowLevelCall, HighLevelCall, Transfer, Send, SolidityCall
from slither.utils import output
from slither.utils.code_complexity import compute_cyclomatic_complexity
from slither.utils.colors import green, red, yellow
from slither.utils.myprettytable import MyPrettyTable
from slither.utils.standard_libraries import is_standard_library
from slither.core.cfg.node import NodeType
from slither.utils.tests_pattern import is_test_file
class PrinterHumanSummary(AbstractPrinter):
@ -27,40 +33,36 @@ class PrinterHumanSummary(AbstractPrinter):
pause = 'pause' in functions_name
if 'mint' in functions_name:
if not 'mintingFinished' in state_variables:
mint_limited = False
if 'mintingFinished' in state_variables:
mint_unlimited = False
else:
mint_limited = True
mint_unlimited = True
else:
mint_limited = None # no minting
mint_unlimited = None # no minting
race_condition_mitigated = 'increaseApproval' in functions_name or \
'safeIncreaseAllowance' in functions_name
return pause, mint_limited, race_condition_mitigated
return pause, mint_unlimited, race_condition_mitigated
def get_summary_erc20(self, contract):
txt = ''
pause, mint_limited, race_condition_mitigated = self._get_summary_erc20(contract)
pause, mint_unlimited, race_condition_mitigated = self._get_summary_erc20(contract)
if pause:
txt += "\t\t Can be paused? : {}\n".format(yellow('Yes'))
else:
txt += "\t\t Can be paused? : {}\n".format(green('No'))
txt += yellow("Pausable") + "\n"
if mint_limited is None:
txt += "\t\t Minting restriction? : {}\n".format(green('No Minting'))
if mint_unlimited is None:
txt += green("No Minting") + "\n"
else:
if mint_limited:
txt += "\t\t Minting restriction? : {}\n".format(red('Yes'))
if mint_unlimited:
txt += red("∞ Minting") + "\n"
else:
txt += "\t\t Minting restriction? : {}\n".format(yellow('No'))
txt += yellow("Minting") + "\n"
if race_condition_mitigated:
txt += "\t\t ERC20 race condition mitigation: {}\n".format(green('Yes'))
else:
txt += "\t\t ERC20 race condition mitigation: {}\n".format(red('No'))
if not race_condition_mitigated:
txt += red("Approve Race Cond.") + "\n"
return txt
@ -139,8 +141,7 @@ class PrinterHumanSummary(AbstractPrinter):
is_complex = self._is_complex_code(contract)
result = red('Yes') if is_complex else green('No')
return "\tComplex code? {}\n".format(result)
return result
@staticmethod
def _number_functions(contract):
@ -151,6 +152,8 @@ class PrinterHumanSummary(AbstractPrinter):
return None
total_dep_lines = 0
total_lines = 0
total_tests_lines = 0
for filename, source_code in self.slither.source_code.items():
lines = len(source_code.splitlines())
is_dep = False
@ -159,8 +162,11 @@ class PrinterHumanSummary(AbstractPrinter):
if is_dep:
total_dep_lines += lines
else:
total_lines += lines
return total_lines, total_dep_lines
if is_test_file(Path(filename)):
total_tests_lines += lines
else:
total_lines += lines
return total_lines, total_dep_lines, total_tests_lines
def _get_number_of_assembly_lines(self):
total_asm_lines = 0
@ -176,14 +182,14 @@ class PrinterHumanSummary(AbstractPrinter):
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'
return f'Compiled with {str(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)
tests = [c for c in self.slither.contracts if c.is_test]
return len(self.slither.contracts) - len(deps) - len(tests), len(deps), len(tests)
def _standard_libraries(self):
libraries = []
@ -200,6 +206,59 @@ class PrinterHumanSummary(AbstractPrinter):
ercs += contract.ercs()
return list(set(ercs))
def _get_features(self, contract):
has_payable = False
can_send_eth = False
can_selfdestruct = False
has_ecrecover = False
can_delegatecall = False
has_token_interaction = False
has_assembly = False
use_abi_encoder = False
for pragma in self.slither.pragma_directives:
if pragma.source_mapping["filename_absolute"] == contract.source_mapping["filename_absolute"]:
if pragma.is_abi_encoder_v2:
use_abi_encoder = True
for function in contract.functions:
if function.payable:
has_payable = True
if function.contains_assembly:
has_assembly = True
for ir in function.slithir_operations:
if isinstance(ir, (LowLevelCall, HighLevelCall, Send, Transfer)) and ir.call_value:
can_send_eth = True
if isinstance(ir, SolidityCall) and ir.function in [SolidityFunction("suicide(address)"),
SolidityFunction("selfdestruct(address)")]:
can_selfdestruct = True
if (isinstance(ir, SolidityCall) and
ir.function == SolidityFunction("ecrecover(bytes32,uint8,bytes32,bytes32)")):
has_ecrecover = True
if isinstance(ir, LowLevelCall) and ir.function_name in ["delegatecall", "callcode"]:
can_delegatecall = True
if isinstance(ir, HighLevelCall):
if isinstance(ir.function, (Function, StateVariable)) and ir.function.contract.is_possible_token:
has_token_interaction = True
return {
"Receive ETH": has_payable,
"Send ETH": can_send_eth,
"Selfdestruct": can_selfdestruct,
"Ecrecover": has_ecrecover,
"Delegatecall": can_delegatecall,
"Tokens interaction": has_token_interaction,
"AbiEncoderV2": use_abi_encoder,
"Assembly": has_assembly,
"Upgradeable": contract.is_upgradeable,
"Proxy": contract.is_upgradeable_proxy,
}
def output(self, _filename):
"""
_filename is not used
@ -225,16 +284,16 @@ class PrinterHumanSummary(AbstractPrinter):
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'
total_lines, total_dep_lines, total_tests_lines = lines_number
txt += f'Number of lines: {total_lines} (+ {total_dep_lines} in dependencies, + {total_tests_lines} in tests)\n'
results['number_lines'] = total_lines
results['number_lines__dependencies'] = total_dep_lines
total_asm_lines = self._get_number_of_assembly_lines()
txt += f"Number of assembly lines: {total_asm_lines}\n"
results['number_lines_assembly'] = total_asm_lines
number_contracts, number_contracts_deps = self._number_contracts()
txt += f'Number of contracts: {number_contracts} (+ {number_contracts_deps} in dependencies) \n\n'
number_contracts, number_contracts_deps, number_contracts_tests = self._number_contracts()
txt += f'Number of contracts: {number_contracts} (+ {number_contracts_deps} in dependencies, + {number_contracts_tests} tests) \n\n'
txt_detectors, detectors_results, optimization, info, low, medium, high = self.get_detectors_result()
txt += txt_detectors
@ -258,26 +317,36 @@ class PrinterHumanSummary(AbstractPrinter):
txt += f'ERCs: {", ".join(ercs)}\n'
results['ercs'] = [str(e) for e in ercs]
table = MyPrettyTable(["Name", "# functions", "ERCS", "ERC20 info", "Complex code", "Features"])
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'
if contract.is_from_dependency() or contract.is_test:
continue
is_complex = self.is_complex_code(contract)
number_functions = self._number_functions(contract)
ercs = ','.join(contract.ercs())
is_erc20 = contract.is_erc20()
erc20_info = ''
if is_erc20:
txt += '\tERC20 info:\n'
txt += self.get_summary_erc20(contract)
erc20_info += self.get_summary_erc20(contract)
features = "\n".join([name for name, to_print in self._get_features(contract).items() if to_print])
self.info(txt)
table.add_row([contract.name, number_functions, ercs, erc20_info, is_complex, features])
self.info(txt + '\n' + str(table))
results_contract = output.Output('')
for contract in self.slither.contracts_derived:
if contract.is_test or contract.is_from_dependency():
continue
contract_d = {'contract_name': contract.name,
'is_complex_code': self._is_complex_code(contract),
'is_erc20': contract.is_erc20(),
'number_functions': self._number_functions(contract)}
'number_functions': self._number_functions(contract),
'features': [name for name, to_print in self._get_features(contract).items() if to_print]}
if contract_d['is_erc20']:
pause, mint_limited, race_condition_mitigated = self._get_summary_erc20(contract)
contract_d['erc20_pause'] = pause
@ -287,7 +356,6 @@ class PrinterHumanSummary(AbstractPrinter):
else:
contract_d['erc20_can_mint'] = False
contract_d['erc20_race_condition_mitigated'] = race_condition_mitigated
results_contract.add_contract(contract, additional_fields=contract_d)
results['contracts']['elements'] = results_contract.elements

@ -50,7 +50,15 @@ def is_standard_library(contract):
def is_openzepellin(contract):
if not contract.is_from_dependency():
return False
return 'openzeppelin-solidity' in Path(contract.source_mapping['filename_absolute']).parts
path = Path(contract.source_mapping['filename_absolute']).parts
is_zep = 'openzeppelin-solidity' in Path(contract.source_mapping['filename_absolute']).parts
try:
is_zep |= path[path.index('@openzeppelin') + 1] == 'contracts'
except IndexError:
pass
except ValueError:
pass
return is_zep
def is_zos(contract):

@ -0,0 +1,49 @@
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from slither.core.declarations.contract import Contract
_TESTS_PATTERNS = ["Test", "test", "Mock", "mock"]
TESTS_PATTERNS = _TESTS_PATTERNS + [x + "s" for x in _TESTS_PATTERNS]
def _is_test_pattern(txt: str, pattern: str) -> bool:
"""
Check if the txt starts with the pattern, or ends with it
:param pattern:
:return:
"""
if txt.endswith(pattern):
return True
if not txt.startswith(pattern):
return False
length = len(pattern)
if len(txt) <= length:
return True
return txt[length] == "_" or txt[length].isupper()
def is_test_file(path: Path) -> bool:
"""
Check if the given path points to a test/mock file
:param path:
:return:
"""
return any((test_pattern in path.parts for test_pattern in TESTS_PATTERNS))
def is_test_contract(contract: "Contract") -> bool:
"""
Check if the contract is a test/mock
:param contract:
:return:
"""
return (
_is_test_pattern(contract.name, "Test")
or _is_test_pattern(contract.name, "Mock")
or (
contract.source_mapping["filename_absolute"]
and is_test_file(Path(contract.source_mapping["filename_absolute"]))
)
)
Loading…
Cancel
Save