Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new APIs and improve human summary printer #478

Merged
merged 2 commits into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions slither/core/declarations/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
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
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")

Expand Down Expand Up @@ -781,6 +785,14 @@ def is_erc777(self):
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.
Expand Down Expand Up @@ -808,6 +820,13 @@ def is_possible_erc721(self):
'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
###################################################################################
Expand All @@ -821,6 +840,31 @@ def is_from_dependency(self):
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
###################################################################################
###################################################################################
Expand Down
6 changes: 6 additions & 0 deletions slither/core/declarations/pragma_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,11 @@ def is_solidity_version(self):
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)
148 changes: 108 additions & 40 deletions slither/printers/summary/human_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -27,40 +33,36 @@ def _get_summary_erc20(contract):
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

Expand Down Expand Up @@ -139,8 +141,7 @@ def is_complex_code(self, contract):
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):
Expand All @@ -151,6 +152,8 @@ def _lines_number(self):
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
Expand All @@ -159,8 +162,11 @@ def _lines_number(self):
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
Expand All @@ -176,14 +182,14 @@ def _get_number_of_assembly_lines(self):
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 = []
Expand All @@ -200,6 +206,59 @@ def _ercs(self):
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
Expand All @@ -225,16 +284,16 @@ def output(self, _filename):

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
Expand All @@ -258,26 +317,36 @@ def output(self, _filename):
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
Expand All @@ -287,7 +356,6 @@ def output(self, _filename):
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
Expand Down
10 changes: 9 additions & 1 deletion slither/utils/standard_libraries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading