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

Echidna printer: improve constant function detection #437

Merged
merged 9 commits into from
Apr 17, 2020
2 changes: 1 addition & 1 deletion slither/core/declarations/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,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
133 changes: 116 additions & 17 deletions slither/printers/guidance/echidna.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@

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

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
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 +47,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 +115,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 +165,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 +181,37 @@ 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


class Echidna(AbstractPrinter):
ARGUMENT = 'echidna'
HELP = 'Export Echidna guiding information'
Expand Down Expand Up @@ -148,6 +240,11 @@ 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}

d = {'payable': payable,
'timestamp': timestamp,
'block_number': block_number,
Expand All @@ -156,7 +253,9 @@ 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}

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

Expand Down