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
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.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]

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

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

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

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

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

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

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

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]:
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

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