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