echidna.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import json
  2. from collections import defaultdict
  3. from typing import Dict, List, Set, Tuple, NamedTuple
  4. from slither.analyses.data_dependency.data_dependency import is_dependent
  5. from slither.core.cfg.node import Node
  6. from slither.core.declarations import Function
  7. from slither.core.declarations.solidity_variables import (
  8. SolidityVariableComposed,
  9. SolidityFunction,
  10. SolidityVariable,
  11. )
  12. from slither.core.expressions import NewContract
  13. from slither.core.slither_core import SlitherCore
  14. from slither.core.variables.state_variable import StateVariable
  15. from slither.core.variables.variable import Variable
  16. from slither.printers.abstract_printer import AbstractPrinter
  17. from slither.slithir.operations import (
  18. Member,
  19. Operation,
  20. SolidityCall,
  21. LowLevelCall,
  22. HighLevelCall,
  23. EventCall,
  24. Send,
  25. Transfer,
  26. InternalDynamicCall,
  27. InternalCall,
  28. TypeConversion,
  29. Balance,
  30. )
  31. from slither.slithir.operations.binary import Binary
  32. from slither.slithir.variables import Constant
  33. def _get_name(f: Function) -> str:
  34. if f.is_fallback or f.is_receive:
  35. return "()"
  36. return f.solidity_signature
  37. def _extract_payable(slither: SlitherCore) -> Dict[str, List[str]]:
  38. ret: Dict[str, List[str]] = {}
  39. for contract in slither.contracts:
  40. payable_functions = [_get_name(f) for f in contract.functions_entry_points if f.payable]
  41. if payable_functions:
  42. ret[contract.name] = payable_functions
  43. return ret
  44. def _extract_solidity_variable_usage(
  45. slither: SlitherCore, sol_var: SolidityVariable
  46. ) -> Dict[str, List[str]]:
  47. ret: Dict[str, List[str]] = {}
  48. for contract in slither.contracts:
  49. functions_using_sol_var = []
  50. for f in contract.functions_entry_points:
  51. for v in f.all_solidity_variables_read():
  52. if v == sol_var:
  53. functions_using_sol_var.append(_get_name(f))
  54. break
  55. if functions_using_sol_var:
  56. ret[contract.name] = functions_using_sol_var
  57. return ret
  58. def _is_constant(f: Function) -> bool: # pylint: disable=too-many-branches
  59. """
  60. Heuristic:
  61. - If view/pure with Solidity >= 0.4 -> Return true
  62. - If it contains assembly -> Return false (SlitherCore doesn't analyze asm)
  63. - Otherwise check for the rules from
  64. https://solidity.readthedocs.io/en/v0.5.0/contracts.html?highlight=pure#view-functions
  65. with an exception: internal dynamic call are not correctly handled, so we consider them as non-constant
  66. :param f:
  67. :return:
  68. """
  69. if f.view or f.pure:
  70. if f.contract.slither.crytic_compile and f.contract.slither.crytic_compile.compiler_version:
  71. if not f.contract.slither.crytic_compile.compiler_version.version.startswith("0.4"):
  72. return True
  73. if f.payable:
  74. return False
  75. if not f.is_implemented:
  76. return False
  77. if f.contains_assembly:
  78. return False
  79. if f.all_state_variables_written():
  80. return False
  81. for ir in f.all_slithir_operations():
  82. if isinstance(ir, InternalDynamicCall):
  83. return False
  84. if isinstance(ir, (EventCall, NewContract, LowLevelCall, Send, Transfer)):
  85. return False
  86. if isinstance(ir, SolidityCall) and ir.function in [
  87. SolidityFunction("selfdestruct(address)"),
  88. SolidityFunction("suicide(address)"),
  89. ]:
  90. return False
  91. if isinstance(ir, HighLevelCall):
  92. if isinstance(ir.function, Variable) or ir.function.view or ir.function.pure:
  93. # External call to constant functions are ensured to be constant only for solidity >= 0.5
  94. if (
  95. f.contract.slither.crytic_compile
  96. and f.contract.slither.crytic_compile.compiler_version
  97. ):
  98. if f.contract.slither.crytic_compile.compiler_version.version.startswith("0.4"):
  99. return False
  100. else:
  101. return False
  102. if isinstance(ir, InternalCall):
  103. # Storage write are not properly handled by all_state_variables_written
  104. if any(parameter.is_storage for parameter in ir.function.parameters):
  105. return False
  106. return True
  107. def _extract_constant_functions(slither: SlitherCore) -> Dict[str, List[str]]:
  108. ret: Dict[str, List[str]] = {}
  109. for contract in slither.contracts:
  110. cst_functions = [_get_name(f) for f in contract.functions_entry_points if _is_constant(f)]
  111. cst_functions += [
  112. v.function_name for v in contract.state_variables if v.visibility in ["public"]
  113. ]
  114. if cst_functions:
  115. ret[contract.name] = cst_functions
  116. return ret
  117. def _extract_assert(slither: SlitherCore) -> Dict[str, List[str]]:
  118. ret: Dict[str, List[str]] = {}
  119. for contract in slither.contracts:
  120. functions_using_assert = []
  121. for f in contract.functions_entry_points:
  122. for v in f.all_solidity_calls():
  123. if v == SolidityFunction("assert(bool)"):
  124. functions_using_assert.append(_get_name(f))
  125. break
  126. if functions_using_assert:
  127. ret[contract.name] = functions_using_assert
  128. return ret
  129. # Create a named tuple that is serialization in json
  130. def json_serializable(cls):
  131. # pylint: disable=unnecessary-comprehension
  132. # TODO: the next line is a quick workaround to prevent pylint from crashing
  133. # It can be removed once https://github.com/PyCQA/pylint/pull/3810 is merged
  134. my_super = super
  135. def as_dict(self):
  136. yield {
  137. name: value for name, value in zip(self._fields, iter(my_super(cls, self).__iter__()))
  138. }
  139. cls.__iter__ = as_dict
  140. return cls
  141. @json_serializable
  142. class ConstantValue(NamedTuple): # pylint: disable=inherit-non-class,too-few-public-methods
  143. # Here value should be Union[str, int, bool]
  144. # But the json lib in Echidna does not handle large integer in json
  145. # So we convert everything to string
  146. value: str
  147. type: str
  148. def _extract_constants_from_irs( # pylint: disable=too-many-branches,too-many-nested-blocks
  149. irs: List[Operation],
  150. all_cst_used: List[ConstantValue],
  151. all_cst_used_in_binary: Dict[str, List[ConstantValue]],
  152. context_explored: Set[Node],
  153. ):
  154. for ir in irs:
  155. if isinstance(ir, Binary):
  156. for r in ir.read:
  157. if isinstance(r, Constant):
  158. all_cst_used_in_binary[str(ir.type)].append(
  159. ConstantValue(str(r.value), str(r.type))
  160. )
  161. if isinstance(ir, TypeConversion):
  162. if isinstance(ir.variable, Constant):
  163. all_cst_used.append(ConstantValue(str(ir.variable.value), str(ir.type)))
  164. continue
  165. for r in ir.read:
  166. # Do not report struct_name in a.struct_name
  167. if isinstance(ir, Member):
  168. continue
  169. if isinstance(r, Constant):
  170. all_cst_used.append(ConstantValue(str(r.value), str(r.type)))
  171. if isinstance(r, StateVariable):
  172. if r.node_initialization:
  173. if r.node_initialization.irs:
  174. if r.node_initialization in context_explored:
  175. continue
  176. context_explored.add(r.node_initialization)
  177. _extract_constants_from_irs(
  178. r.node_initialization.irs,
  179. all_cst_used,
  180. all_cst_used_in_binary,
  181. context_explored,
  182. )
  183. def _extract_constants(
  184. slither: SlitherCore,
  185. ) -> Tuple[Dict[str, Dict[str, List]], Dict[str, Dict[str, Dict]]]:
  186. # contract -> function -> [ {"value": value, "type": type} ]
  187. ret_cst_used: Dict[str, Dict[str, List[ConstantValue]]] = defaultdict(dict)
  188. # contract -> function -> binary_operand -> [ {"value": value, "type": type ]
  189. ret_cst_used_in_binary: Dict[str, Dict[str, Dict[str, List[ConstantValue]]]] = defaultdict(dict)
  190. for contract in slither.contracts:
  191. for function in contract.functions_entry_points:
  192. all_cst_used: List = []
  193. all_cst_used_in_binary: Dict = defaultdict(list)
  194. context_explored = set()
  195. context_explored.add(function)
  196. _extract_constants_from_irs(
  197. function.all_slithir_operations(),
  198. all_cst_used,
  199. all_cst_used_in_binary,
  200. context_explored,
  201. )
  202. # Note: use list(set()) instead of set
  203. # As this is meant to be serialized in JSON, and JSON does not support set
  204. if all_cst_used:
  205. ret_cst_used[contract.name][_get_name(function)] = list(set(all_cst_used))
  206. if all_cst_used_in_binary:
  207. ret_cst_used_in_binary[contract.name][_get_name(function)] = {
  208. k: list(set(v)) for k, v in all_cst_used_in_binary.items()
  209. }
  210. return ret_cst_used, ret_cst_used_in_binary
  211. def _extract_function_relations(
  212. slither: SlitherCore,
  213. ) -> Dict[str, Dict[str, Dict[str, List[str]]]]:
  214. # contract -> function -> [functions]
  215. ret: Dict[str, Dict[str, Dict[str, List[str]]]] = defaultdict(dict)
  216. for contract in slither.contracts:
  217. ret[contract.name] = defaultdict(dict)
  218. written = {
  219. _get_name(function): function.all_state_variables_written()
  220. for function in contract.functions_entry_points
  221. }
  222. read = {
  223. _get_name(function): function.all_state_variables_read()
  224. for function in contract.functions_entry_points
  225. }
  226. for function in contract.functions_entry_points:
  227. ret[contract.name][_get_name(function)] = {
  228. "impacts": [],
  229. "is_impacted_by": [],
  230. }
  231. for candidate, varsWritten in written.items():
  232. if any((r in varsWritten for r in function.all_state_variables_read())):
  233. ret[contract.name][_get_name(function)]["is_impacted_by"].append(candidate)
  234. for candidate, varsRead in read.items():
  235. if any((r in varsRead for r in function.all_state_variables_written())):
  236. ret[contract.name][_get_name(function)]["impacts"].append(candidate)
  237. return ret
  238. def _have_external_calls(slither: SlitherCore) -> Dict[str, List[str]]:
  239. """
  240. Detect the functions with external calls
  241. :param slither:
  242. :return:
  243. """
  244. ret: Dict[str, List[str]] = defaultdict(list)
  245. for contract in slither.contracts:
  246. for function in contract.functions_entry_points:
  247. if function.all_high_level_calls() or function.all_low_level_calls():
  248. ret[contract.name].append(_get_name(function))
  249. if contract.name in ret:
  250. ret[contract.name] = list(set(ret[contract.name]))
  251. return ret
  252. def _use_balance(slither: SlitherCore) -> Dict[str, List[str]]:
  253. """
  254. Detect the functions with external calls
  255. :param slither:
  256. :return:
  257. """
  258. ret: Dict[str, List[str]] = defaultdict(list)
  259. for contract in slither.contracts:
  260. for function in contract.functions_entry_points:
  261. for ir in function.all_slithir_operations():
  262. if isinstance(ir, Balance):
  263. ret[contract.name].append(_get_name(function))
  264. if contract.name in ret:
  265. ret[contract.name] = list(set(ret[contract.name]))
  266. return ret
  267. def _call_a_parameter(slither: SlitherCore) -> Dict[str, List[Dict]]:
  268. """
  269. Detect the functions with external calls
  270. :param slither:
  271. :return:
  272. """
  273. # contract -> [ (function, idx, interface_called) ]
  274. ret: Dict[str, List[Dict]] = defaultdict(list)
  275. for contract in slither.contracts: # pylint: disable=too-many-nested-blocks
  276. for function in contract.functions_entry_points:
  277. for ir in function.all_slithir_operations():
  278. if isinstance(ir, HighLevelCall):
  279. for idx, parameter in enumerate(function.parameters):
  280. if is_dependent(ir.destination, parameter, function):
  281. ret[contract.name].append(
  282. {
  283. "function": _get_name(function),
  284. "parameter_idx": idx,
  285. "signature": _get_name(ir.function),
  286. }
  287. )
  288. if isinstance(ir, LowLevelCall):
  289. for idx, parameter in enumerate(function.parameters):
  290. if is_dependent(ir.destination, parameter, function):
  291. ret[contract.name].append(
  292. {
  293. "function": _get_name(function),
  294. "parameter_idx": idx,
  295. "signature": None,
  296. }
  297. )
  298. return ret
  299. class Echidna(AbstractPrinter):
  300. ARGUMENT = "echidna"
  301. HELP = "Export Echidna guiding information"
  302. WIKI = "https://github.com/trailofbits/slither/wiki/Printer-documentation#echidna"
  303. def output(self, filename): # pylint: disable=too-many-locals
  304. """
  305. Output the inheritance relation
  306. _filename is not used
  307. Args:
  308. _filename(string)
  309. """
  310. payable = _extract_payable(self.slither)
  311. timestamp = _extract_solidity_variable_usage(
  312. self.slither, SolidityVariableComposed("block.timestamp")
  313. )
  314. block_number = _extract_solidity_variable_usage(
  315. self.slither, SolidityVariableComposed("block.number")
  316. )
  317. msg_sender = _extract_solidity_variable_usage(
  318. self.slither, SolidityVariableComposed("msg.sender")
  319. )
  320. msg_gas = _extract_solidity_variable_usage(
  321. self.slither, SolidityVariableComposed("msg.gas")
  322. )
  323. assert_usage = _extract_assert(self.slither)
  324. cst_functions = _extract_constant_functions(self.slither)
  325. (cst_used, cst_used_in_binary) = _extract_constants(self.slither)
  326. functions_relations = _extract_function_relations(self.slither)
  327. constructors = {
  328. contract.name: contract.constructor.full_name
  329. for contract in self.slither.contracts
  330. if contract.constructor
  331. }
  332. external_calls = _have_external_calls(self.slither)
  333. call_parameters = _call_a_parameter(self.slither)
  334. use_balance = _use_balance(self.slither)
  335. d = {
  336. "payable": payable,
  337. "timestamp": timestamp,
  338. "block_number": block_number,
  339. "msg_sender": msg_sender,
  340. "msg_gas": msg_gas,
  341. "assert": assert_usage,
  342. "constant_functions": cst_functions,
  343. "constants_used": cst_used,
  344. "constants_used_in_binary": cst_used_in_binary,
  345. "functions_relations": functions_relations,
  346. "constructors": constructors,
  347. "have_external_calls": external_calls,
  348. "call_a_parameter": call_parameters,
  349. "use_balance": use_balance,
  350. }
  351. self.info(json.dumps(d, indent=4))
  352. res = self.generate_output(json.dumps(d, indent=4))
  353. return res