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
class TaintedExternalContract:
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)
TaintedExternalContract(contract: slither.core.declarations.contract.Contract)
30    def __init__(self, contract: "Contract") -> None:
31        self._contract: Contract = contract
32        self._tainted_functions: List[Function] = []
33        self._tainted_variables: List[Variable] = []
35    @property
36    def contract(self) -> Contract:
37        return self._contract
tainted_functions: List[slither.core.declarations.function.Function]
39    @property
40    def tainted_functions(self) -> List[Function]:
41        return self._tainted_functions
def add_tainted_function(self, f: slither.core.declarations.function.Function):
43    def add_tainted_function(self, f: Function):
44        self._tainted_functions.append(f)
tainted_variables: List[slither.core.variables.variable.Variable]
46    @property
47    def tainted_variables(self) -> List[Variable]:
48        return self._tainted_variables
def add_tainted_variable(self, v: slither.core.variables.variable.Variable):
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]

def tainted_external_contracts( funcs: List[slither.core.declarations.function.Function]) -> 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] )

def tainted_inheriting_contracts( tainted_contracts: List[TaintedExternalContract], contracts: List[slither.core.declarations.contract.Contract] = None) -> List[TaintedExternalContract]:
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

def is_function_modified( f1: slither.core.declarations.function.Function, f2: slither.core.declarations.function.Function) -> bool:
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

def get_proxy_implementation_slot( proxy: slither.core.declarations.contract.Contract) -> Union[slither.tools.read_storage.read_storage.SlotInfo, NoneType]:
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.

def get_proxy_implementation_var( proxy: slither.core.declarations.contract.Contract) -> Union[slither.core.variables.variable.Variable, NoneType]:
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.

def find_delegate_in_fallback( proxy: slither.core.declarations.contract.Contract) -> Union[slither.core.variables.variable.Variable, NoneType]:
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.

def extract_delegate_from_asm( contract: slither.core.declarations.contract.Contract, node: slither.core.cfg.node.Node) -> Union[slither.core.variables.variable.Variable, NoneType]:
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.

def find_delegate_from_name( contract: slither.core.declarations.contract.Contract, dest: str, parent_func: slither.core.declarations.function.Function) -> Union[slither.core.variables.variable.Variable, NoneType]:
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

def create_state_variable_from_slot( slot: str, name: str = None) -> Union[slither.core.variables.state_variable.StateVariable, NoneType]:
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.