Skip to content

Commit

Permalink
Merge pull request #437 from crytic/dev-echidna-printer-improvements
Browse files Browse the repository at this point in the history
Echidna printer: improve constant function detection
  • Loading branch information
montyly authored Apr 17, 2020
2 parents 8f74407 + fc6b593 commit ce2199a
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 18 deletions.
2 changes: 1 addition & 1 deletion slither/core/declarations/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ def solidity_calls(self):
"""
list(SolidityFunction): List of Soldity calls
"""
return list(self._internal_calls)
return list(self._solidity_calls)

@property
def high_level_calls(self):
Expand Down
206 changes: 189 additions & 17 deletions slither/printers/guidance/echidna.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@

import json
from collections import defaultdict
from typing import Dict, List, Set, Tuple
from typing import Dict, List, Set, Tuple, Union, NamedTuple

from slither.analyses.data_dependency.data_dependency import is_dependent
from slither.core.cfg.node import Node
from slither.core.declarations import Function
from slither.core.declarations.solidity_variables import SolidityVariableComposed, SolidityFunction, SolidityVariable
from slither.core.expressions import NewContract
from slither.core.slither_core import Slither
from slither.core.variables.variable import Variable
from slither.core.variables.state_variable import StateVariable
from slither.printers.abstract_printer import AbstractPrinter
from slither.core.declarations.solidity_variables import SolidityVariableComposed, SolidityFunction, SolidityVariable
from slither.slithir.operations import Member, Operation
from slither.slithir.operations import Member, Operation, SolidityCall, LowLevelCall, HighLevelCall, EventCall, Send, \
Transfer, InternalDynamicCall, InternalCall, TypeConversion, Balance
from slither.slithir.operations.binary import Binary, BinaryType
from slither.core.variables.state_variable import StateVariable
from slither.slithir.variables import Constant


Expand Down Expand Up @@ -46,10 +48,54 @@ def _extract_solidity_variable_usage(slither: Slither, sol_var: SolidityVariable
return ret


def _is_constant(f: Function) -> bool:
"""
Heuristic:
- If view/pure with Solidity >= 0.4 -> Return true
- If it contains assembly -> Return false (Slither doesn't analyze asm)
- Otherwise check for the rules from
https://solidity.readthedocs.io/en/v0.5.0/contracts.html?highlight=pure#view-functions
with an exception: internal dynamic call are not correctly handled, so we consider them as non-constant
:param f:
:return:
"""
if f.view or f.pure:
if not f.contract.slither.crytic_compile.compiler_version.version.startswith('0.4'):
return True
if f.payable:
return False
if not f.is_implemented:
return False
if f.contains_assembly:
return False
if f.all_state_variables_written():
return False
for ir in f.all_slithir_operations():
if isinstance(ir, InternalDynamicCall):
return False
if isinstance(ir, (EventCall, NewContract, LowLevelCall, Send, Transfer)):
return False
if isinstance(ir, SolidityCall) and ir.function in [SolidityFunction('selfdestruct(address)'),
SolidityFunction('suicide(address)')]:
return False
if isinstance(ir, HighLevelCall):
if ir.function.view or ir.function.pure:
# External call to constant functions are ensured to be constant only for solidity >= 0.5
if f.contract.slither.crytic_compile.compiler_version.version.startswith('0.4'):
return False
else:
return False
if isinstance(ir, InternalCall):
# Storage write are not properly handled by all_state_variables_written
if any(parameter.is_storage for parameter in ir.function.parameters):
return False
return True


def _extract_constant_functions(slither: Slither) -> Dict[str, List[str]]:
ret: Dict[str, List[str]] = {}
for contract in slither.contracts:
cst_functions = [_get_name(f) for f in contract.functions_entry_points if f.view or f.pure]
cst_functions = [_get_name(f) for f in contract.functions_entry_points if _is_constant(f)]
cst_functions += [v.function_name for v in contract.state_variables if v.visibility in ['public']]
if cst_functions:
ret[contract.name] = cst_functions
Expand All @@ -70,21 +116,42 @@ def _extract_assert(slither: Slither) -> Dict[str, List[str]]:
return ret


# Create a named tuple that is serialization in json
def json_serializable(cls):
def as_dict(self):
yield {name: value for name, value in zip(
self._fields,
iter(super(cls, self).__iter__()))}

cls.__iter__ = as_dict
return cls


@json_serializable
class ConstantValue(NamedTuple):
value: Union[str, int, bool]
type: str


def _extract_constants_from_irs(irs: List[Operation],
all_cst_used: List,
all_cst_used_in_binary: Dict,
all_cst_used: List[ConstantValue],
all_cst_used_in_binary: Dict[str, List[ConstantValue]],
context_explored: Set[Node]):
for ir in irs:
if isinstance(ir, Binary):
for r in ir.read:
if isinstance(r, Constant):
all_cst_used_in_binary[BinaryType.str(ir.type)].append(r.value)
all_cst_used_in_binary[BinaryType.str(ir.type)].append(ConstantValue(r.value, str(r.type)))
if isinstance(ir, TypeConversion):
if isinstance(ir.variable, Constant):
all_cst_used.append(ConstantValue(ir.variable.value, str(ir.type)))
continue
for r in ir.read:
# Do not report struct_name in a.struct_name
if isinstance(ir, Member):
continue
if isinstance(r, Constant):
all_cst_used.append(r.value)
all_cst_used.append(ConstantValue(r.value, str(r.type)))
if isinstance(r, StateVariable):
if r.node_initialization:
if r.node_initialization.irs:
Expand All @@ -99,12 +166,14 @@ def _extract_constants_from_irs(irs: List[Operation],


def _extract_constants(slither: Slither) -> Tuple[Dict[str, Dict[str, List]], Dict[str, Dict[str, Dict]]]:
ret_cst_used: Dict[str, Dict[str, List]] = defaultdict(dict)
ret_cst_used_in_binary: Dict[str, Dict[str, Dict]] = defaultdict(dict)
# contract -> function -> [ {"value": value, "type": type} ]
ret_cst_used: Dict[str, Dict[str, List[ConstantValue]]] = defaultdict(dict)
# contract -> function -> binary_operand -> [ {"value": value, "type": type ]
ret_cst_used_in_binary: Dict[str, Dict[str, Dict[str, List[ConstantValue]]]] = defaultdict(dict)
for contract in slither.contracts:
for function in contract.functions_entry_points:
all_cst_used = []
all_cst_used_in_binary = defaultdict(list)
all_cst_used: List = []
all_cst_used_in_binary: Dict = defaultdict(list)

context_explored = set()
context_explored.add(function)
Expand All @@ -113,13 +182,100 @@ def _extract_constants(slither: Slither) -> Tuple[Dict[str, Dict[str, List]], Di
all_cst_used_in_binary,
context_explored)

# Note: use list(set()) instead of set
# As this is meant to be serialized in JSON, and JSON does not support set
if all_cst_used:
ret_cst_used[contract.name][function.full_name] = all_cst_used
ret_cst_used[contract.name][_get_name(function)] = list(set(all_cst_used))
if all_cst_used_in_binary:
ret_cst_used_in_binary[contract.name][function.full_name] = all_cst_used_in_binary
ret_cst_used_in_binary[contract.name][_get_name(function)] = {k: list(set(v)) for k, v in
all_cst_used_in_binary.items()}
return ret_cst_used, ret_cst_used_in_binary


def _extract_function_relations(slither: Slither) -> Dict[str, Dict[str, Dict[str, List[str]]]]:
# contract -> function -> [functions]
ret: Dict[str, Dict[str, Dict[str, List[str]]]] = defaultdict(dict)
for contract in slither.contracts:
ret[contract.name] = defaultdict(dict)
written = {_get_name(function): function.all_state_variables_written()
for function in contract.functions_entry_points}
read = {_get_name(function): function.all_state_variables_read()
for function in contract.functions_entry_points}
for function in contract.functions_entry_points:
ret[contract.name][_get_name(function)] = {"impacts": [],
"is_impacted_by": []}
for candidate, varsWritten in written.items():
if any((r in varsWritten for r in function.all_state_variables_read())):
ret[contract.name][_get_name(function)]["is_impacted_by"].append(candidate)
for candidate, varsRead in read.items():
if any((r in varsRead for r in function.all_state_variables_written())):
ret[contract.name][_get_name(function)]["impacts"].append(candidate)
return ret


def _have_external_calls(slither: Slither) -> Dict[str, List[str]]:
"""
Detect the functions with external calls
:param slither:
:return:
"""
ret: Dict[str, List[str]] = defaultdict(list)
for contract in slither.contracts:
for function in contract.functions_entry_points:
if function.all_high_level_calls() or function.all_low_level_calls():
ret[contract.name].append(_get_name(function))
if contract.name in ret:
ret[contract.name] = list(set(ret[contract.name]))
return ret


def _use_balance(slither: Slither) -> Dict[str, List[str]]:
"""
Detect the functions with external calls
:param slither:
:return:
"""
ret: Dict[str, List[str]] = defaultdict(list)
for contract in slither.contracts:
for function in contract.functions_entry_points:
for ir in function.all_slithir_operations():
if isinstance(ir, Balance):
ret[contract.name].append(_get_name(function))
if contract.name in ret:
ret[contract.name] = list(set(ret[contract.name]))
return ret


def _call_a_parameter(slither: Slither) -> Dict[str, List[Dict]]:
"""
Detect the functions with external calls
:param slither:
:return:
"""
# contract -> [ (function, idx, interface_called) ]
ret: Dict[str, List[Dict]] = defaultdict(list)
for contract in slither.contracts:
for function in contract.functions_entry_points:
for ir in function.all_slithir_operations():
if isinstance(ir, HighLevelCall):
for idx, parameter in enumerate(function.parameters):
if is_dependent(ir.destination, parameter, function):
ret[contract.name].append({
"function": _get_name(function),
"parameter_idx": idx,
"signature": _get_name(ir.function)
})
if isinstance(ir, LowLevelCall):
for idx, parameter in enumerate(function.parameters):
if is_dependent(ir.destination, parameter, function):
ret[contract.name].append({
"function": _get_name(function),
"parameter_idx": idx,
"signature": None
})
return ret


class Echidna(AbstractPrinter):
ARGUMENT = 'echidna'
HELP = 'Export Echidna guiding information'
Expand Down Expand Up @@ -148,6 +304,17 @@ def output(self, filename):
cst_functions = _extract_constant_functions(self.slither)
(cst_used, cst_used_in_binary) = _extract_constants(self.slither)

functions_relations = _extract_function_relations(self.slither)

constructors = {contract.name: contract.constructor.full_name
for contract in self.slither.contracts if contract.constructor}

external_calls = _have_external_calls(self.slither)

call_parameters = _call_a_parameter(self.slither)

use_balance = _use_balance(self.slither)

d = {'payable': payable,
'timestamp': timestamp,
'block_number': block_number,
Expand All @@ -156,7 +323,12 @@ def output(self, filename):
'assert': assert_usage,
'constant_functions': cst_functions,
'constants_used': cst_used,
'constants_used_in_binary': cst_used_in_binary}
'constants_used_in_binary': cst_used_in_binary,
'functions_relations': functions_relations,
'constructors': constructors,
'have_external_calls': external_calls,
'call_a_parameter': call_parameters,
'use_balance': use_balance}

self.info(json.dumps(d, indent=4))

Expand Down

0 comments on commit ce2199a

Please sign in to comment.