slither.utils.upgradeability
1from typing import Optional, Tuple, List 2 3from slither.analyses.data_dependency.data_dependency import get_dependencies 4from slither.core.cfg.node import Node, NodeType 5from slither.core.declarations import ( 6 Contract, 7 Function, 8) 9from slither.core.expressions import ( 10 Literal, 11 Identifier, 12 CallExpression, 13 AssignmentOperation, 14) 15from slither.core.solidity_types import ( 16 ElementaryType, 17) 18from slither.core.variables.local_variable import LocalVariable 19from slither.core.variables.state_variable import StateVariable 20from slither.core.variables.variable import Variable 21from slither.slithir.operations import ( 22 LowLevelCall, 23) 24from slither.tools.read_storage.read_storage import SlotInfo, SlitherReadStorage 25from slither.utils.encoding import encode_ir_for_upgradeability_compare 26 27 28class TaintedExternalContract: 29 def __init__(self, contract: "Contract") -> None: 30 self._contract: Contract = contract 31 self._tainted_functions: List[Function] = [] 32 self._tainted_variables: List[Variable] = [] 33 34 @property 35 def contract(self) -> Contract: 36 return self._contract 37 38 @property 39 def tainted_functions(self) -> List[Function]: 40 return self._tainted_functions 41 42 def add_tainted_function(self, f: Function): 43 self._tainted_functions.append(f) 44 45 @property 46 def tainted_variables(self) -> List[Variable]: 47 return self._tainted_variables 48 49 def add_tainted_variable(self, v: Variable): 50 self._tainted_variables.append(v) 51 52 53# pylint: disable=too-many-locals 54def compare( 55 v1: Contract, v2: Contract, include_external: bool = False 56) -> Tuple[ 57 List[Variable], 58 List[Variable], 59 List[Variable], 60 List[Function], 61 List[Function], 62 List[Function], 63]: 64 """ 65 Compares two versions of a contract. Most useful for upgradeable (logic) contracts, 66 but does not require that Contract.is_upgradeable returns true for either contract. 67 68 Args: 69 v1: Original version of (upgradeable) contract 70 v2: Updated version of (upgradeable) contract 71 include_external: Optional flag to enable cross-contract external taint analysis 72 73 Returns: 74 missing-vars-in-v2: list[Variable], 75 new-variables: list[Variable], 76 tainted-variables: list[Variable], 77 new-functions: list[Function], 78 modified-functions: list[Function], 79 tainted-functions: list[Function] 80 tainted-contracts: list[TaintedExternalContract] 81 """ 82 83 order_vars1 = v1.stored_state_variables_ordered 84 order_vars2 = v2.stored_state_variables_ordered 85 func_sigs1 = [function.solidity_signature for function in v1.functions] 86 func_sigs2 = [function.solidity_signature for function in v2.functions] 87 88 missing_vars_in_v2 = [] 89 new_variables = [] 90 tainted_variables = [] 91 new_functions = [] 92 modified_functions = [] 93 tainted_functions = [] 94 95 # Since this is not a detector, include any missing variables in the v2 contract 96 if len(order_vars2) < len(order_vars1): 97 missing_vars_in_v2.extend(get_missing_vars(v1, v2)) 98 99 # Find all new and modified functions in the v2 contract 100 new_modified_functions = [] 101 new_modified_function_vars = [] 102 for sig in func_sigs2: 103 function = v2.get_function_from_signature(sig) 104 orig_function = v1.get_function_from_signature(sig) 105 if sig not in func_sigs1: 106 new_modified_functions.append(function) 107 new_functions.append(function) 108 new_modified_function_vars += function.all_state_variables_written() 109 elif not function.is_constructor_variables and is_function_modified( 110 orig_function, function 111 ): 112 new_modified_functions.append(function) 113 modified_functions.append(function) 114 new_modified_function_vars += function.all_state_variables_written() 115 116 # Find all unmodified functions that call a modified function or read/write the 117 # same state variable(s) as a new/modified function, i.e., tainted functions 118 for function in v2.functions: 119 if ( 120 function in new_modified_functions 121 or function.is_constructor 122 or function.name.startswith("slither") 123 ): 124 continue 125 modified_calls = [ 126 func for func in new_modified_functions if func in function.internal_calls 127 ] 128 tainted_vars = [ 129 var 130 for var in set(new_modified_function_vars) 131 if var in function.all_state_variables_read() + function.all_state_variables_written() 132 and not var.is_constant 133 and not var.is_immutable 134 ] 135 if len(modified_calls) > 0 or len(tainted_vars) > 0: 136 tainted_functions.append(function) 137 138 # Find all new or tainted variables, i.e., variables that are written by a new/modified/tainted function 139 for var in order_vars2: 140 written_by = v2.get_functions_writing_to_variable(var) 141 if next((v for v in v1.state_variables_ordered if v.name == var.name), None) is None: 142 new_variables.append(var) 143 elif any(func in written_by for func in new_modified_functions + tainted_functions): 144 tainted_variables.append(var) 145 146 tainted_contracts = [] 147 if include_external: 148 # Find all external contracts and functions called by new/modified/tainted functions 149 tainted_contracts = tainted_external_contracts( 150 new_functions + modified_functions + tainted_functions 151 ) 152 153 return ( 154 missing_vars_in_v2, 155 new_variables, 156 tainted_variables, 157 new_functions, 158 modified_functions, 159 tainted_functions, 160 tainted_contracts, 161 ) 162 163 164def tainted_external_contracts(funcs: List[Function]) -> List[TaintedExternalContract]: 165 """ 166 Takes a list of functions from one contract, finds any calls in these to functions in external contracts, 167 and determines which variables and functions in the external contracts are tainted by these external calls. 168 Args: 169 funcs: a list of Function objects to search for external calls. 170 171 Returns: 172 TaintedExternalContract() ( 173 contract: Contract, 174 tainted_functions: List[TaintedFunction], 175 tainted_variables: List[TaintedVariable] 176 ) 177 """ 178 tainted_contracts: dict[str, TaintedExternalContract] = {} 179 tainted_list: list[TaintedExternalContract] = [] 180 181 for func in funcs: 182 for contract, target in func.all_high_level_calls(): 183 if contract.is_library: 184 # Not interested in library calls 185 continue 186 if contract.name not in tainted_contracts: 187 # A contract may be tainted by multiple function calls - only make one TaintedExternalContract object 188 tainted_contracts[contract.name] = TaintedExternalContract(contract) 189 if ( 190 isinstance(target, Function) 191 and target not in funcs 192 and target not in (f for f in tainted_contracts[contract.name].tainted_functions) 193 and not (target.is_constructor or target.is_fallback or target.is_receive) 194 ): 195 # Found a high-level call to a new tainted function 196 tainted_contracts[contract.name].add_tainted_function(target) 197 for var in target.all_state_variables_written(): 198 # Consider as tainted all variables written by the tainted function 199 if var not in (v for v in tainted_contracts[contract.name].tainted_variables): 200 tainted_contracts[contract.name].add_tainted_variable(var) 201 elif ( 202 isinstance(target, StateVariable) 203 and target not in (v for v in tainted_contracts[contract.name].tainted_variables) 204 and target.is_stored 205 ): 206 # Found a new high-level call to a public state variable getter 207 tainted_contracts[contract.name].add_tainted_variable(target) 208 for c in tainted_contracts.values(): 209 tainted_list.append(c) 210 contract = c.contract 211 variables = c.tainted_variables 212 for var in variables: 213 # For each tainted variable, consider as tainted any function that reads or writes to it 214 read_write = set( 215 contract.get_functions_reading_from_variable(var) 216 + contract.get_functions_writing_to_variable(var) 217 ) 218 for f in read_write: 219 if f not in tainted_contracts[contract.name].tainted_functions and not ( 220 f.is_constructor or f.is_fallback or f.is_receive 221 ): 222 c.add_tainted_function(f) 223 return tainted_list 224 225 226def tainted_inheriting_contracts( 227 tainted_contracts: List[TaintedExternalContract], contracts: List[Contract] = None 228) -> List[TaintedExternalContract]: 229 """ 230 Takes a list of TaintedExternalContract obtained from tainted_external_contracts, and finds any contracts which 231 inherit a tainted contract, as well as any functions that call tainted functions or read tainted variables in 232 the inherited contract. 233 Args: 234 tainted_contracts: the list obtained from `tainted_external_contracts` or `compare`. 235 contracts: (optional) the list of contracts to check for inheritance. If not provided, defaults to 236 `contract.compilation_unit.contracts` for each contract in tainted_contracts. 237 238 Returns: 239 An updated list of TaintedExternalContract, including all from the input list. 240 """ 241 for tainted in tainted_contracts: 242 contract = tainted.contract 243 check_contracts = contracts 244 if contracts is None: 245 check_contracts = contract.compilation_unit.contracts 246 # We are only interested in checking contracts that inherit a tainted contract 247 check_contracts = [ 248 c 249 for c in check_contracts 250 if c.name not in [t.contract.name for t in tainted_contracts] 251 and contract.name in [i.name for i in c.inheritance] 252 ] 253 for c in check_contracts: 254 new_taint = TaintedExternalContract(c) 255 for f in c.functions_declared: 256 # Search for functions that call an inherited tainted function or access an inherited tainted variable 257 internal_calls = [c for c in f.all_internal_calls() if isinstance(c, Function)] 258 if any( 259 call.canonical_name == t.canonical_name 260 for t in tainted.tainted_functions 261 for call in internal_calls 262 ) or any( 263 var.canonical_name == t.canonical_name 264 for t in tainted.tainted_variables 265 for var in f.all_state_variables_read() + f.all_state_variables_written() 266 ): 267 new_taint.add_tainted_function(f) 268 for f in new_taint.tainted_functions: 269 # For each newly found tainted function, consider as tainted any variable it writes to 270 for var in f.all_state_variables_written(): 271 if var not in ( 272 v for v in tainted.tainted_variables + new_taint.tainted_variables 273 ): 274 new_taint.add_tainted_variable(var) 275 for var in new_taint.tainted_variables: 276 # For each newly found tainted variable, consider as tainted any function that reads or writes to it 277 read_write = set( 278 contract.get_functions_reading_from_variable(var) 279 + contract.get_functions_writing_to_variable(var) 280 ) 281 for f in read_write: 282 if f not in ( 283 t for t in tainted.tainted_functions + new_taint.tainted_functions 284 ) and not (f.is_constructor or f.is_fallback or f.is_receive): 285 new_taint.add_tainted_function(f) 286 if len(new_taint.tainted_functions) > 0: 287 tainted_contracts.append(new_taint) 288 return tainted_contracts 289 290 291def get_missing_vars(v1: Contract, v2: Contract) -> List[StateVariable]: 292 """ 293 Gets all non-constant/immutable StateVariables that appear in v1 but not v2 294 Args: 295 v1: Contract version 1 296 v2: Contract version 2 297 298 Returns: 299 List of StateVariables from v1 missing in v2 300 """ 301 results = [] 302 order_vars1 = v1.stored_state_variables_ordered 303 order_vars2 = v2.stored_state_variables_ordered 304 if len(order_vars2) < len(order_vars1): 305 for variable in order_vars1: 306 if variable.name not in [v.name for v in order_vars2]: 307 results.append(variable) 308 return results 309 310 311def is_function_modified(f1: Function, f2: Function) -> bool: 312 """ 313 Compares two versions of a function, and returns True if the function has been modified. 314 First checks whether the functions' content hashes are equal to quickly rule out identical functions. 315 Walks the CFGs and compares IR operations if hashes differ to rule out false positives, i.e., from changed comments. 316 317 Args: 318 f1: Original version of the function 319 f2: New version of the function 320 321 Returns: 322 True if the functions differ, otherwise False 323 """ 324 # If the function content hashes are the same, no need to investigate the function further 325 if f1.source_mapping.content_hash == f2.source_mapping.content_hash: 326 return False 327 # If the hashes differ, it is possible a change in a name or in a comment could be the only difference 328 # So we need to resort to walking through the CFG and comparing the IR operations 329 queue_f1 = [f1.entry_point] 330 queue_f2 = [f2.entry_point] 331 visited = [] 332 while len(queue_f1) > 0 and len(queue_f2) > 0: 333 node_f1 = queue_f1.pop(0) 334 node_f2 = queue_f2.pop(0) 335 visited.extend([node_f1, node_f2]) 336 queue_f1.extend(son for son in node_f1.sons if son not in visited) 337 queue_f2.extend(son for son in node_f2.sons if son not in visited) 338 if len(node_f1.irs) != len(node_f2.irs): 339 return True 340 for i, ir in enumerate(node_f1.irs): 341 if encode_ir_for_upgradeability_compare(ir) != encode_ir_for_upgradeability_compare( 342 node_f2.irs[i] 343 ): 344 return True 345 return False 346 347 348def get_proxy_implementation_slot(proxy: Contract) -> Optional[SlotInfo]: 349 """ 350 Gets information about the storage slot where a proxy's implementation address is stored. 351 Args: 352 proxy: A Contract object (proxy.is_upgradeable_proxy should be true). 353 354 Returns: 355 (`SlotInfo`) | None : A dictionary of the slot information. 356 """ 357 358 delegate = get_proxy_implementation_var(proxy) 359 if isinstance(delegate, StateVariable): 360 if delegate.is_stored: 361 srs = SlitherReadStorage([proxy], 20) 362 return srs.get_storage_slot(delegate, proxy) 363 if delegate.is_constant and delegate.type.name == "bytes32": 364 return SlotInfo( 365 name=delegate.name, 366 type_string="address", 367 slot=int(delegate.expression.value, 16), 368 size=160, 369 offset=0, 370 ) 371 return None 372 373 374def get_proxy_implementation_var(proxy: Contract) -> Optional[Variable]: 375 """ 376 Gets the Variable that stores a proxy's implementation address. Uses data dependency to trace any LocalVariable 377 that is passed into a delegatecall as the target address back to its data source, ideally a StateVariable. 378 Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. 379 Args: 380 proxy: A Contract object (proxy.is_upgradeable_proxy should be true). 381 382 Returns: 383 (`Variable`) | None : The variable, ideally a StateVariable, which stores the proxy's implementation address. 384 """ 385 if not proxy.is_upgradeable_proxy or not proxy.fallback_function: 386 return None 387 388 delegate = find_delegate_in_fallback(proxy) 389 if isinstance(delegate, LocalVariable): 390 dependencies = get_dependencies(delegate, proxy) 391 try: 392 delegate = next(var for var in dependencies if isinstance(var, StateVariable)) 393 except StopIteration: 394 # TODO: Handle case where get_dependencies does not return any state variables. 395 return delegate 396 return delegate 397 398 399def find_delegate_in_fallback(proxy: Contract) -> Optional[Variable]: 400 """ 401 Searches a proxy's fallback function for a delegatecall, then extracts the Variable being passed in as the target. 402 Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. 403 Should typically be called by get_proxy_implementation_var(proxy). 404 Args: 405 proxy: A Contract object (should have a fallback function). 406 407 Returns: 408 (`Variable`) | None : The variable being passed as the destination argument in a delegatecall in the fallback. 409 """ 410 delegate: Optional[Variable] = None 411 fallback = proxy.fallback_function 412 for node in fallback.all_nodes(): 413 for ir in node.irs: 414 if isinstance(ir, LowLevelCall) and ir.function_name == "delegatecall": 415 delegate = ir.destination 416 if delegate is not None: 417 break 418 if ( 419 node.type == NodeType.ASSEMBLY 420 and isinstance(node.inline_asm, str) 421 and "delegatecall" in node.inline_asm 422 ): 423 delegate = extract_delegate_from_asm(proxy, node) 424 elif node.type == NodeType.EXPRESSION: 425 expression = node.expression 426 if isinstance(expression, AssignmentOperation): 427 expression = expression.expression_right 428 if ( 429 isinstance(expression, CallExpression) 430 and "delegatecall" in str(expression.called) 431 and len(expression.arguments) > 1 432 ): 433 dest = expression.arguments[1] 434 if isinstance(dest, CallExpression) and "sload" in str(dest.called): 435 dest = dest.arguments[0] 436 if isinstance(dest, Identifier): 437 delegate = dest.value 438 break 439 if ( 440 isinstance(dest, Literal) and len(dest.value) == 66 441 ): # 32 bytes = 64 chars + "0x" = 66 chars 442 # Storage slot is not declared as a constant, but rather is hardcoded in the assembly, 443 # so create a new StateVariable to represent it. 444 delegate = create_state_variable_from_slot(dest.value) 445 break 446 return delegate 447 448 449def extract_delegate_from_asm(contract: Contract, node: Node) -> Optional[Variable]: 450 """ 451 Finds a Variable with a name matching the argument passed into a delegatecall, when all we have is an Assembly node 452 with a block of code as one long string. Usually only the case for solc versions < 0.6.0. 453 Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. 454 Should typically be called by find_delegate_in_fallback(proxy). 455 Args: 456 contract: The parent Contract. 457 node: The Assembly Node (i.e., node.type == NodeType.ASSEMBLY) 458 459 Returns: 460 (`Variable`) | None : The variable being passed as the destination argument in a delegatecall in the fallback. 461 """ 462 asm_split = str(node.inline_asm).split("\n") 463 asm = next(line for line in asm_split if "delegatecall" in line) 464 params = asm.split("call(")[1].split(", ") 465 dest = params[1] 466 if dest.endswith(")") and not dest.startswith("sload("): 467 dest = params[2] 468 if dest.startswith("sload("): 469 dest = dest.replace(")", "(").split("(")[1] 470 if dest.startswith("0x"): 471 return create_state_variable_from_slot(dest) 472 if dest.isnumeric(): 473 slot_idx = int(dest) 474 return next( 475 ( 476 v 477 for v in contract.state_variables_ordered 478 if SlitherReadStorage.get_variable_info(contract, v)[0] == slot_idx 479 ), 480 None, 481 ) 482 for v in node.function.variables_read_or_written: 483 if v.name == dest: 484 if isinstance(v, LocalVariable) and v.expression is not None: 485 e = v.expression 486 if isinstance(e, Identifier) and isinstance(e.value, StateVariable): 487 v = e.value 488 # Fall through, return constant storage slot 489 if isinstance(v, StateVariable) and v.is_constant: 490 return v 491 if "_fallback_asm" in dest or "_slot" in dest: 492 dest = dest.split("_")[0] 493 return find_delegate_from_name(contract, dest, node.function) 494 495 496def find_delegate_from_name( 497 contract: Contract, dest: str, parent_func: Function 498) -> Optional[Variable]: 499 """ 500 Searches for a variable with a given name, starting with StateVariables declared in the contract, followed by 501 LocalVariables in the parent function, either declared in the function body or as parameters in the signature. 502 Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. 503 Args: 504 contract: The Contract object to search. 505 dest: The variable name to search for. 506 parent_func: The Function object to search. 507 508 Returns: 509 (`Variable`) | None : The variable with the matching name, if found 510 """ 511 for sv in contract.state_variables: 512 if sv.name == dest: 513 return sv 514 for lv in parent_func.local_variables: 515 if lv.name == dest: 516 return lv 517 for pv in parent_func.parameters + parent_func.returns: 518 if pv.name == dest: 519 return pv 520 if parent_func.contains_assembly: 521 for node in parent_func.all_nodes(): 522 if node.type == NodeType.ASSEMBLY and isinstance(node.inline_asm, str): 523 asm = next( 524 ( 525 s 526 for s in node.inline_asm.split("\n") 527 if f"{dest}:=sload(" in s.replace(" ", "") 528 ), 529 None, 530 ) 531 if asm: 532 slot = asm.split("sload(")[1].split(")")[0] 533 if slot.startswith("0x"): 534 return create_state_variable_from_slot(slot, name=dest) 535 try: 536 slot_idx = int(slot) 537 return next( 538 ( 539 v 540 for v in contract.state_variables_ordered 541 if SlitherReadStorage.get_variable_info(contract, v)[0] == slot_idx 542 ), 543 None, 544 ) 545 except TypeError: 546 continue 547 return None 548 549 550def create_state_variable_from_slot(slot: str, name: str = None) -> Optional[StateVariable]: 551 """ 552 Creates a new StateVariable object to wrap a hardcoded storage slot found in assembly. 553 Args: 554 slot: The storage slot hex string. 555 name: Optional name for the variable. The slot string is used if name is not provided. 556 557 Returns: 558 A newly created constant StateVariable of type bytes32, with the slot as the variable's expression and name, 559 if slot matches the length and prefix of a bytes32. Otherwise, returns None. 560 """ 561 if len(slot) == 66 and slot.startswith("0x"): # 32 bytes = 64 chars + "0x" = 66 chars 562 # Storage slot is not declared as a constant, but rather is hardcoded in the assembly, 563 # so create a new StateVariable to represent it. 564 v = StateVariable() 565 v.is_constant = True 566 v.expression = Literal(slot, ElementaryType("bytes32")) 567 if name is not None: 568 v.name = name 569 else: 570 v.name = slot 571 v.type = ElementaryType("bytes32") 572 return v 573 # This should probably also handle hashed strings, but for now return None 574 return None
29class TaintedExternalContract: 30 def __init__(self, contract: "Contract") -> None: 31 self._contract: Contract = contract 32 self._tainted_functions: List[Function] = [] 33 self._tainted_variables: List[Variable] = [] 34 35 @property 36 def contract(self) -> Contract: 37 return self._contract 38 39 @property 40 def tainted_functions(self) -> List[Function]: 41 return self._tainted_functions 42 43 def add_tainted_function(self, f: Function): 44 self._tainted_functions.append(f) 45 46 @property 47 def tainted_variables(self) -> List[Variable]: 48 return self._tainted_variables 49 50 def add_tainted_variable(self, v: Variable): 51 self._tainted_variables.append(v)
55def compare( 56 v1: Contract, v2: Contract, include_external: bool = False 57) -> Tuple[ 58 List[Variable], 59 List[Variable], 60 List[Variable], 61 List[Function], 62 List[Function], 63 List[Function], 64]: 65 """ 66 Compares two versions of a contract. Most useful for upgradeable (logic) contracts, 67 but does not require that Contract.is_upgradeable returns true for either contract. 68 69 Args: 70 v1: Original version of (upgradeable) contract 71 v2: Updated version of (upgradeable) contract 72 include_external: Optional flag to enable cross-contract external taint analysis 73 74 Returns: 75 missing-vars-in-v2: list[Variable], 76 new-variables: list[Variable], 77 tainted-variables: list[Variable], 78 new-functions: list[Function], 79 modified-functions: list[Function], 80 tainted-functions: list[Function] 81 tainted-contracts: list[TaintedExternalContract] 82 """ 83 84 order_vars1 = v1.stored_state_variables_ordered 85 order_vars2 = v2.stored_state_variables_ordered 86 func_sigs1 = [function.solidity_signature for function in v1.functions] 87 func_sigs2 = [function.solidity_signature for function in v2.functions] 88 89 missing_vars_in_v2 = [] 90 new_variables = [] 91 tainted_variables = [] 92 new_functions = [] 93 modified_functions = [] 94 tainted_functions = [] 95 96 # Since this is not a detector, include any missing variables in the v2 contract 97 if len(order_vars2) < len(order_vars1): 98 missing_vars_in_v2.extend(get_missing_vars(v1, v2)) 99 100 # Find all new and modified functions in the v2 contract 101 new_modified_functions = [] 102 new_modified_function_vars = [] 103 for sig in func_sigs2: 104 function = v2.get_function_from_signature(sig) 105 orig_function = v1.get_function_from_signature(sig) 106 if sig not in func_sigs1: 107 new_modified_functions.append(function) 108 new_functions.append(function) 109 new_modified_function_vars += function.all_state_variables_written() 110 elif not function.is_constructor_variables and is_function_modified( 111 orig_function, function 112 ): 113 new_modified_functions.append(function) 114 modified_functions.append(function) 115 new_modified_function_vars += function.all_state_variables_written() 116 117 # Find all unmodified functions that call a modified function or read/write the 118 # same state variable(s) as a new/modified function, i.e., tainted functions 119 for function in v2.functions: 120 if ( 121 function in new_modified_functions 122 or function.is_constructor 123 or function.name.startswith("slither") 124 ): 125 continue 126 modified_calls = [ 127 func for func in new_modified_functions if func in function.internal_calls 128 ] 129 tainted_vars = [ 130 var 131 for var in set(new_modified_function_vars) 132 if var in function.all_state_variables_read() + function.all_state_variables_written() 133 and not var.is_constant 134 and not var.is_immutable 135 ] 136 if len(modified_calls) > 0 or len(tainted_vars) > 0: 137 tainted_functions.append(function) 138 139 # Find all new or tainted variables, i.e., variables that are written by a new/modified/tainted function 140 for var in order_vars2: 141 written_by = v2.get_functions_writing_to_variable(var) 142 if next((v for v in v1.state_variables_ordered if v.name == var.name), None) is None: 143 new_variables.append(var) 144 elif any(func in written_by for func in new_modified_functions + tainted_functions): 145 tainted_variables.append(var) 146 147 tainted_contracts = [] 148 if include_external: 149 # Find all external contracts and functions called by new/modified/tainted functions 150 tainted_contracts = tainted_external_contracts( 151 new_functions + modified_functions + tainted_functions 152 ) 153 154 return ( 155 missing_vars_in_v2, 156 new_variables, 157 tainted_variables, 158 new_functions, 159 modified_functions, 160 tainted_functions, 161 tainted_contracts, 162 )
Compares two versions of a contract. Most useful for upgradeable (logic) contracts, but does not require that Contract.is_upgradeable returns true for either contract.
Args: v1: Original version of (upgradeable) contract v2: Updated version of (upgradeable) contract include_external: Optional flag to enable cross-contract external taint analysis
Returns: missing-vars-in-v2: list[Variable], new-variables: list[Variable], tainted-variables: list[Variable], new-functions: list[Function], modified-functions: list[Function], tainted-functions: list[Function] tainted-contracts: list[TaintedExternalContract]
165def tainted_external_contracts(funcs: List[Function]) -> List[TaintedExternalContract]: 166 """ 167 Takes a list of functions from one contract, finds any calls in these to functions in external contracts, 168 and determines which variables and functions in the external contracts are tainted by these external calls. 169 Args: 170 funcs: a list of Function objects to search for external calls. 171 172 Returns: 173 TaintedExternalContract() ( 174 contract: Contract, 175 tainted_functions: List[TaintedFunction], 176 tainted_variables: List[TaintedVariable] 177 ) 178 """ 179 tainted_contracts: dict[str, TaintedExternalContract] = {} 180 tainted_list: list[TaintedExternalContract] = [] 181 182 for func in funcs: 183 for contract, target in func.all_high_level_calls(): 184 if contract.is_library: 185 # Not interested in library calls 186 continue 187 if contract.name not in tainted_contracts: 188 # A contract may be tainted by multiple function calls - only make one TaintedExternalContract object 189 tainted_contracts[contract.name] = TaintedExternalContract(contract) 190 if ( 191 isinstance(target, Function) 192 and target not in funcs 193 and target not in (f for f in tainted_contracts[contract.name].tainted_functions) 194 and not (target.is_constructor or target.is_fallback or target.is_receive) 195 ): 196 # Found a high-level call to a new tainted function 197 tainted_contracts[contract.name].add_tainted_function(target) 198 for var in target.all_state_variables_written(): 199 # Consider as tainted all variables written by the tainted function 200 if var not in (v for v in tainted_contracts[contract.name].tainted_variables): 201 tainted_contracts[contract.name].add_tainted_variable(var) 202 elif ( 203 isinstance(target, StateVariable) 204 and target not in (v for v in tainted_contracts[contract.name].tainted_variables) 205 and target.is_stored 206 ): 207 # Found a new high-level call to a public state variable getter 208 tainted_contracts[contract.name].add_tainted_variable(target) 209 for c in tainted_contracts.values(): 210 tainted_list.append(c) 211 contract = c.contract 212 variables = c.tainted_variables 213 for var in variables: 214 # For each tainted variable, consider as tainted any function that reads or writes to it 215 read_write = set( 216 contract.get_functions_reading_from_variable(var) 217 + contract.get_functions_writing_to_variable(var) 218 ) 219 for f in read_write: 220 if f not in tainted_contracts[contract.name].tainted_functions and not ( 221 f.is_constructor or f.is_fallback or f.is_receive 222 ): 223 c.add_tainted_function(f) 224 return tainted_list
Takes a list of functions from one contract, finds any calls in these to functions in external contracts, and determines which variables and functions in the external contracts are tainted by these external calls. Args: funcs: a list of Function objects to search for external calls.
Returns: TaintedExternalContract() ( contract: Contract, tainted_functions: List[TaintedFunction], tainted_variables: List[TaintedVariable] )
227def tainted_inheriting_contracts( 228 tainted_contracts: List[TaintedExternalContract], contracts: List[Contract] = None 229) -> List[TaintedExternalContract]: 230 """ 231 Takes a list of TaintedExternalContract obtained from tainted_external_contracts, and finds any contracts which 232 inherit a tainted contract, as well as any functions that call tainted functions or read tainted variables in 233 the inherited contract. 234 Args: 235 tainted_contracts: the list obtained from `tainted_external_contracts` or `compare`. 236 contracts: (optional) the list of contracts to check for inheritance. If not provided, defaults to 237 `contract.compilation_unit.contracts` for each contract in tainted_contracts. 238 239 Returns: 240 An updated list of TaintedExternalContract, including all from the input list. 241 """ 242 for tainted in tainted_contracts: 243 contract = tainted.contract 244 check_contracts = contracts 245 if contracts is None: 246 check_contracts = contract.compilation_unit.contracts 247 # We are only interested in checking contracts that inherit a tainted contract 248 check_contracts = [ 249 c 250 for c in check_contracts 251 if c.name not in [t.contract.name for t in tainted_contracts] 252 and contract.name in [i.name for i in c.inheritance] 253 ] 254 for c in check_contracts: 255 new_taint = TaintedExternalContract(c) 256 for f in c.functions_declared: 257 # Search for functions that call an inherited tainted function or access an inherited tainted variable 258 internal_calls = [c for c in f.all_internal_calls() if isinstance(c, Function)] 259 if any( 260 call.canonical_name == t.canonical_name 261 for t in tainted.tainted_functions 262 for call in internal_calls 263 ) or any( 264 var.canonical_name == t.canonical_name 265 for t in tainted.tainted_variables 266 for var in f.all_state_variables_read() + f.all_state_variables_written() 267 ): 268 new_taint.add_tainted_function(f) 269 for f in new_taint.tainted_functions: 270 # For each newly found tainted function, consider as tainted any variable it writes to 271 for var in f.all_state_variables_written(): 272 if var not in ( 273 v for v in tainted.tainted_variables + new_taint.tainted_variables 274 ): 275 new_taint.add_tainted_variable(var) 276 for var in new_taint.tainted_variables: 277 # For each newly found tainted variable, consider as tainted any function that reads or writes to it 278 read_write = set( 279 contract.get_functions_reading_from_variable(var) 280 + contract.get_functions_writing_to_variable(var) 281 ) 282 for f in read_write: 283 if f not in ( 284 t for t in tainted.tainted_functions + new_taint.tainted_functions 285 ) and not (f.is_constructor or f.is_fallback or f.is_receive): 286 new_taint.add_tainted_function(f) 287 if len(new_taint.tainted_functions) > 0: 288 tainted_contracts.append(new_taint) 289 return tainted_contracts
Takes a list of TaintedExternalContract obtained from tainted_external_contracts, and finds any contracts which
inherit a tainted contract, as well as any functions that call tainted functions or read tainted variables in
the inherited contract.
Args:
tainted_contracts: the list obtained from tainted_external_contracts
or compare
.
contracts: (optional) the list of contracts to check for inheritance. If not provided, defaults to
contract.compilation_unit.contracts
for each contract in tainted_contracts.
Returns: An updated list of TaintedExternalContract, including all from the input list.
292def get_missing_vars(v1: Contract, v2: Contract) -> List[StateVariable]: 293 """ 294 Gets all non-constant/immutable StateVariables that appear in v1 but not v2 295 Args: 296 v1: Contract version 1 297 v2: Contract version 2 298 299 Returns: 300 List of StateVariables from v1 missing in v2 301 """ 302 results = [] 303 order_vars1 = v1.stored_state_variables_ordered 304 order_vars2 = v2.stored_state_variables_ordered 305 if len(order_vars2) < len(order_vars1): 306 for variable in order_vars1: 307 if variable.name not in [v.name for v in order_vars2]: 308 results.append(variable) 309 return results
Gets all non-constant/immutable StateVariables that appear in v1 but not v2 Args: v1: Contract version 1 v2: Contract version 2
Returns: List of StateVariables from v1 missing in v2
312def is_function_modified(f1: Function, f2: Function) -> bool: 313 """ 314 Compares two versions of a function, and returns True if the function has been modified. 315 First checks whether the functions' content hashes are equal to quickly rule out identical functions. 316 Walks the CFGs and compares IR operations if hashes differ to rule out false positives, i.e., from changed comments. 317 318 Args: 319 f1: Original version of the function 320 f2: New version of the function 321 322 Returns: 323 True if the functions differ, otherwise False 324 """ 325 # If the function content hashes are the same, no need to investigate the function further 326 if f1.source_mapping.content_hash == f2.source_mapping.content_hash: 327 return False 328 # If the hashes differ, it is possible a change in a name or in a comment could be the only difference 329 # So we need to resort to walking through the CFG and comparing the IR operations 330 queue_f1 = [f1.entry_point] 331 queue_f2 = [f2.entry_point] 332 visited = [] 333 while len(queue_f1) > 0 and len(queue_f2) > 0: 334 node_f1 = queue_f1.pop(0) 335 node_f2 = queue_f2.pop(0) 336 visited.extend([node_f1, node_f2]) 337 queue_f1.extend(son for son in node_f1.sons if son not in visited) 338 queue_f2.extend(son for son in node_f2.sons if son not in visited) 339 if len(node_f1.irs) != len(node_f2.irs): 340 return True 341 for i, ir in enumerate(node_f1.irs): 342 if encode_ir_for_upgradeability_compare(ir) != encode_ir_for_upgradeability_compare( 343 node_f2.irs[i] 344 ): 345 return True 346 return False
Compares two versions of a function, and returns True if the function has been modified. First checks whether the functions' content hashes are equal to quickly rule out identical functions. Walks the CFGs and compares IR operations if hashes differ to rule out false positives, i.e., from changed comments.
Args: f1: Original version of the function f2: New version of the function
Returns: True if the functions differ, otherwise False
349def get_proxy_implementation_slot(proxy: Contract) -> Optional[SlotInfo]: 350 """ 351 Gets information about the storage slot where a proxy's implementation address is stored. 352 Args: 353 proxy: A Contract object (proxy.is_upgradeable_proxy should be true). 354 355 Returns: 356 (`SlotInfo`) | None : A dictionary of the slot information. 357 """ 358 359 delegate = get_proxy_implementation_var(proxy) 360 if isinstance(delegate, StateVariable): 361 if delegate.is_stored: 362 srs = SlitherReadStorage([proxy], 20) 363 return srs.get_storage_slot(delegate, proxy) 364 if delegate.is_constant and delegate.type.name == "bytes32": 365 return SlotInfo( 366 name=delegate.name, 367 type_string="address", 368 slot=int(delegate.expression.value, 16), 369 size=160, 370 offset=0, 371 ) 372 return None
Gets information about the storage slot where a proxy's implementation address is stored. Args: proxy: A Contract object (proxy.is_upgradeable_proxy should be true).
Returns:
(SlotInfo
) | None : A dictionary of the slot information.
375def get_proxy_implementation_var(proxy: Contract) -> Optional[Variable]: 376 """ 377 Gets the Variable that stores a proxy's implementation address. Uses data dependency to trace any LocalVariable 378 that is passed into a delegatecall as the target address back to its data source, ideally a StateVariable. 379 Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. 380 Args: 381 proxy: A Contract object (proxy.is_upgradeable_proxy should be true). 382 383 Returns: 384 (`Variable`) | None : The variable, ideally a StateVariable, which stores the proxy's implementation address. 385 """ 386 if not proxy.is_upgradeable_proxy or not proxy.fallback_function: 387 return None 388 389 delegate = find_delegate_in_fallback(proxy) 390 if isinstance(delegate, LocalVariable): 391 dependencies = get_dependencies(delegate, proxy) 392 try: 393 delegate = next(var for var in dependencies if isinstance(var, StateVariable)) 394 except StopIteration: 395 # TODO: Handle case where get_dependencies does not return any state variables. 396 return delegate 397 return delegate
Gets the Variable that stores a proxy's implementation address. Uses data dependency to trace any LocalVariable
that is passed into a delegatecall as the target address back to its data source, ideally a StateVariable.
Can return a newly created StateVariable if an sload
from a hardcoded storage slot is found in assembly.
Args:
proxy: A Contract object (proxy.is_upgradeable_proxy should be true).
Returns:
(Variable
) | None : The variable, ideally a StateVariable, which stores the proxy's implementation address.
400def find_delegate_in_fallback(proxy: Contract) -> Optional[Variable]: 401 """ 402 Searches a proxy's fallback function for a delegatecall, then extracts the Variable being passed in as the target. 403 Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. 404 Should typically be called by get_proxy_implementation_var(proxy). 405 Args: 406 proxy: A Contract object (should have a fallback function). 407 408 Returns: 409 (`Variable`) | None : The variable being passed as the destination argument in a delegatecall in the fallback. 410 """ 411 delegate: Optional[Variable] = None 412 fallback = proxy.fallback_function 413 for node in fallback.all_nodes(): 414 for ir in node.irs: 415 if isinstance(ir, LowLevelCall) and ir.function_name == "delegatecall": 416 delegate = ir.destination 417 if delegate is not None: 418 break 419 if ( 420 node.type == NodeType.ASSEMBLY 421 and isinstance(node.inline_asm, str) 422 and "delegatecall" in node.inline_asm 423 ): 424 delegate = extract_delegate_from_asm(proxy, node) 425 elif node.type == NodeType.EXPRESSION: 426 expression = node.expression 427 if isinstance(expression, AssignmentOperation): 428 expression = expression.expression_right 429 if ( 430 isinstance(expression, CallExpression) 431 and "delegatecall" in str(expression.called) 432 and len(expression.arguments) > 1 433 ): 434 dest = expression.arguments[1] 435 if isinstance(dest, CallExpression) and "sload" in str(dest.called): 436 dest = dest.arguments[0] 437 if isinstance(dest, Identifier): 438 delegate = dest.value 439 break 440 if ( 441 isinstance(dest, Literal) and len(dest.value) == 66 442 ): # 32 bytes = 64 chars + "0x" = 66 chars 443 # Storage slot is not declared as a constant, but rather is hardcoded in the assembly, 444 # so create a new StateVariable to represent it. 445 delegate = create_state_variable_from_slot(dest.value) 446 break 447 return delegate
Searches a proxy's fallback function for a delegatecall, then extracts the Variable being passed in as the target.
Can return a newly created StateVariable if an sload
from a hardcoded storage slot is found in assembly.
Should typically be called by get_proxy_implementation_var(proxy).
Args:
proxy: A Contract object (should have a fallback function).
Returns:
(Variable
) | None : The variable being passed as the destination argument in a delegatecall in the fallback.
450def extract_delegate_from_asm(contract: Contract, node: Node) -> Optional[Variable]: 451 """ 452 Finds a Variable with a name matching the argument passed into a delegatecall, when all we have is an Assembly node 453 with a block of code as one long string. Usually only the case for solc versions < 0.6.0. 454 Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. 455 Should typically be called by find_delegate_in_fallback(proxy). 456 Args: 457 contract: The parent Contract. 458 node: The Assembly Node (i.e., node.type == NodeType.ASSEMBLY) 459 460 Returns: 461 (`Variable`) | None : The variable being passed as the destination argument in a delegatecall in the fallback. 462 """ 463 asm_split = str(node.inline_asm).split("\n") 464 asm = next(line for line in asm_split if "delegatecall" in line) 465 params = asm.split("call(")[1].split(", ") 466 dest = params[1] 467 if dest.endswith(")") and not dest.startswith("sload("): 468 dest = params[2] 469 if dest.startswith("sload("): 470 dest = dest.replace(")", "(").split("(")[1] 471 if dest.startswith("0x"): 472 return create_state_variable_from_slot(dest) 473 if dest.isnumeric(): 474 slot_idx = int(dest) 475 return next( 476 ( 477 v 478 for v in contract.state_variables_ordered 479 if SlitherReadStorage.get_variable_info(contract, v)[0] == slot_idx 480 ), 481 None, 482 ) 483 for v in node.function.variables_read_or_written: 484 if v.name == dest: 485 if isinstance(v, LocalVariable) and v.expression is not None: 486 e = v.expression 487 if isinstance(e, Identifier) and isinstance(e.value, StateVariable): 488 v = e.value 489 # Fall through, return constant storage slot 490 if isinstance(v, StateVariable) and v.is_constant: 491 return v 492 if "_fallback_asm" in dest or "_slot" in dest: 493 dest = dest.split("_")[0] 494 return find_delegate_from_name(contract, dest, node.function)
Finds a Variable with a name matching the argument passed into a delegatecall, when all we have is an Assembly node
with a block of code as one long string. Usually only the case for solc versions < 0.6.0.
Can return a newly created StateVariable if an sload
from a hardcoded storage slot is found in assembly.
Should typically be called by find_delegate_in_fallback(proxy).
Args:
contract: The parent Contract.
node: The Assembly Node (i.e., node.type == NodeType.ASSEMBLY)
Returns:
(Variable
) | None : The variable being passed as the destination argument in a delegatecall in the fallback.
497def find_delegate_from_name( 498 contract: Contract, dest: str, parent_func: Function 499) -> Optional[Variable]: 500 """ 501 Searches for a variable with a given name, starting with StateVariables declared in the contract, followed by 502 LocalVariables in the parent function, either declared in the function body or as parameters in the signature. 503 Can return a newly created StateVariable if an `sload` from a hardcoded storage slot is found in assembly. 504 Args: 505 contract: The Contract object to search. 506 dest: The variable name to search for. 507 parent_func: The Function object to search. 508 509 Returns: 510 (`Variable`) | None : The variable with the matching name, if found 511 """ 512 for sv in contract.state_variables: 513 if sv.name == dest: 514 return sv 515 for lv in parent_func.local_variables: 516 if lv.name == dest: 517 return lv 518 for pv in parent_func.parameters + parent_func.returns: 519 if pv.name == dest: 520 return pv 521 if parent_func.contains_assembly: 522 for node in parent_func.all_nodes(): 523 if node.type == NodeType.ASSEMBLY and isinstance(node.inline_asm, str): 524 asm = next( 525 ( 526 s 527 for s in node.inline_asm.split("\n") 528 if f"{dest}:=sload(" in s.replace(" ", "") 529 ), 530 None, 531 ) 532 if asm: 533 slot = asm.split("sload(")[1].split(")")[0] 534 if slot.startswith("0x"): 535 return create_state_variable_from_slot(slot, name=dest) 536 try: 537 slot_idx = int(slot) 538 return next( 539 ( 540 v 541 for v in contract.state_variables_ordered 542 if SlitherReadStorage.get_variable_info(contract, v)[0] == slot_idx 543 ), 544 None, 545 ) 546 except TypeError: 547 continue 548 return None
Searches for a variable with a given name, starting with StateVariables declared in the contract, followed by
LocalVariables in the parent function, either declared in the function body or as parameters in the signature.
Can return a newly created StateVariable if an sload
from a hardcoded storage slot is found in assembly.
Args:
contract: The Contract object to search.
dest: The variable name to search for.
parent_func: The Function object to search.
Returns:
(Variable
) | None : The variable with the matching name, if found
551def create_state_variable_from_slot(slot: str, name: str = None) -> Optional[StateVariable]: 552 """ 553 Creates a new StateVariable object to wrap a hardcoded storage slot found in assembly. 554 Args: 555 slot: The storage slot hex string. 556 name: Optional name for the variable. The slot string is used if name is not provided. 557 558 Returns: 559 A newly created constant StateVariable of type bytes32, with the slot as the variable's expression and name, 560 if slot matches the length and prefix of a bytes32. Otherwise, returns None. 561 """ 562 if len(slot) == 66 and slot.startswith("0x"): # 32 bytes = 64 chars + "0x" = 66 chars 563 # Storage slot is not declared as a constant, but rather is hardcoded in the assembly, 564 # so create a new StateVariable to represent it. 565 v = StateVariable() 566 v.is_constant = True 567 v.expression = Literal(slot, ElementaryType("bytes32")) 568 if name is not None: 569 v.name = name 570 else: 571 v.name = slot 572 v.type = ElementaryType("bytes32") 573 return v 574 # This should probably also handle hashed strings, but for now return None 575 return None
Creates a new StateVariable object to wrap a hardcoded storage slot found in assembly. Args: slot: The storage slot hex string. name: Optional name for the variable. The slot string is used if name is not provided.
Returns: A newly created constant StateVariable of type bytes32, with the slot as the variable's expression and name, if slot matches the length and prefix of a bytes32. Otherwise, returns None.