# # pylint: disable=too-many-lines import pathlib from argparse import ArgumentTypeError from collections import defaultdict from inspect import getsourcefile from typing import Union, List, Dict, Callable import pytest from solc_select.solc_select import valid_version as solc_valid_version from slither import Slither from slither.core.cfg.node import Node, NodeType from slither.core.declarations import Function, Contract from slither.core.solidity_types import ArrayType from slither.core.variables.local_variable import LocalVariable from slither.core.variables.state_variable import StateVariable from slither.slithir.operations import ( OperationWithLValue, Phi, Assignment, HighLevelCall, Return, Operation, Binary, BinaryType, InternalCall, Index, InitArray, ) from slither.slithir.utils.ssa import is_used_later from slither.slithir.variables import ( Constant, ReferenceVariable, LocalIRVariable, StateIRVariable, TemporaryVariableSSA, ) # Directory of currently executing script. Will be used as basis for temporary file names. SCRIPT_DIR = pathlib.Path(getsourcefile(lambda: 0)).parent # type:ignore def valid_version(ver: str) -> bool: """Wrapper function to check if the solc-version is valid The solc_select function raises and exception but for checks below, only a bool is needed. """ try: solc_valid_version(ver) return True except ArgumentTypeError: return False def have_ssa_if_ir(function: Function) -> None: """Verifies that all nodes in a function that have IR also have SSA IR""" for n in function.nodes: if n.irs: assert n.irs_ssa # pylint: disable=too-many-branches, too-many-locals def ssa_basic_properties(function: Function) -> None: """Verifies that basic properties of ssa holds 1. Every name is defined only once 2. A l-value is never index zero - there is always a zero-value available for each var 3. Every r-value is at least defined at some point 4. The number of ssa defs is >= the number of assignments to var 5. Function parameters SSA are stored in function.parameters_ssa - if function parameter is_storage it refers to a fake variable 6. Function returns SSA are stored in function.returns_ssa - if function return is_storage it refers to a fake variable """ ssa_lvalues = set() ssa_rvalues = set() lvalue_assignments: Dict[str, int] = {} for n in function.nodes: for ir in n.irs: if isinstance(ir, OperationWithLValue) and ir.lvalue: name = ir.lvalue.name if name is None: continue if name in lvalue_assignments: lvalue_assignments[name] += 1 else: lvalue_assignments[name] = 1 for ssa in n.irs_ssa: if isinstance(ssa, OperationWithLValue): # 1 assert ssa.lvalue not in ssa_lvalues ssa_lvalues.add(ssa.lvalue) # 2 (if Local/State Var) ssa_lvalue = ssa.lvalue if isinstance(ssa_lvalue, (StateIRVariable, LocalIRVariable)): assert ssa_lvalue.index > 0 for rvalue in filter( lambda x: not isinstance(x, (StateIRVariable, Constant)), ssa.read ): ssa_rvalues.add(rvalue) # 3 # Each var can have one non-defined value, the value initially held. Typically, # var_0, i_0, state_0 or similar. undef_vars = set() for rvalue in ssa_rvalues: if rvalue not in ssa_lvalues: assert rvalue.non_ssa_version not in undef_vars undef_vars.add(rvalue.non_ssa_version) # 4 ssa_defs: Dict[str, int] = defaultdict(int) for v in ssa_lvalues: if v and v.name: ssa_defs[v.name] += 1 for (k, count) in lvalue_assignments.items(): assert ssa_defs[k] >= count # Helper 5/6 def check_property_5_and_6( variables: List[LocalVariable], ssavars: List[LocalIRVariable] ) -> None: for var in filter(lambda x: x.name, variables): ssa_vars = [x for x in ssavars if x.non_ssa_version == var] assert len(ssa_vars) == 1 ssa_var = ssa_vars[0] assert var.is_storage == ssa_var.is_storage if ssa_var.is_storage: assert len(ssa_var.refers_to) == 1 assert ssa_var.refers_to[0].location == "reference_to_storage" # 5 check_property_5_and_6(function.parameters, function.parameters_ssa) # 6 check_property_5_and_6(function.returns, function.returns_ssa) def ssa_phi_node_properties(f: Function) -> None: """Every phi-function should have as many args as predecessors This does not apply if the phi-node refers to state variables, they make use os special phi-nodes for tracking potential values a state variable can have """ for node in f.nodes: for ssa in node.irs_ssa: if isinstance(ssa, Phi): n = len(ssa.read) if not isinstance(ssa.lvalue, StateIRVariable): assert len(node.fathers) == n # TODO (hbrodin): This should probably go into another file, not specific to SSA def dominance_properties(f: Function) -> None: """Verifies properties related to dominators holds 1. Every node have an immediate dominator except entry_node which have none 2. From every node immediate dominator there is a path via its successors to the node """ def find_path(from_node: Node, to: Node) -> bool: visited = set() worklist = list(from_node.sons) while worklist: first, *worklist = worklist if first == to: return True visited.add(first) for successor in first.sons: if successor not in visited: worklist.append(successor) return False for node in f.nodes: if node is f.entry_point: assert node.immediate_dominator is None else: assert node.immediate_dominator is not None assert find_path(node.immediate_dominator, node) def phi_values_inserted(f: Function) -> None: """Verifies that phi-values are inserted at the right places For every node that has a dominance frontier, any def (including phi) should be a phi function in its dominance frontier """ def have_phi_for_var( node: Node, var: Union[StateIRVariable, LocalIRVariable, TemporaryVariableSSA] ) -> bool: """Checks if a node has a phi-instruction for var The ssa version would ideally be checked, but then more data flow analysis would be needed, for cases where a new def for var is introduced before reaching DF """ non_ssa = var.non_ssa_version for ssa in node.irs_ssa: if isinstance(ssa, Phi): if non_ssa in map( lambda ssa_var: ssa_var.non_ssa_version, [ r for r in ssa.read if isinstance(r, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA)) ], ): return True return False for node in filter(lambda n: n.dominance_frontier, f.nodes): for df in node.dominance_frontier: for ssa in node.irs_ssa: if isinstance(ssa, OperationWithLValue): ssa_lvalue = ssa.lvalue if isinstance( ssa_lvalue, (StateIRVariable, LocalIRVariable, TemporaryVariableSSA) ) and is_used_later(node, ssa_lvalue): assert have_phi_for_var(df, ssa_lvalue) def verify_properties_hold(slither: Slither) -> None: """Ensures that basic properties of SSA hold true""" def verify_func(func: Function) -> None: have_ssa_if_ir(func) phi_values_inserted(func) ssa_basic_properties(func) ssa_phi_node_properties(func) dominance_properties(func) def verify(slither: Slither) -> None: for cu in slither.compilation_units: for func in cu.functions_and_modifiers: _dump_function(func) verify_func(func) for contract in cu.contracts: for f in contract.functions: if f.is_constructor or f.is_constructor_variables: _dump_function(f) verify_func(f) assert isinstance(slither, Slither) verify(slither) def _dump_function(f: Function) -> None: """Helper function to print nodes/ssa ir for a function or modifier""" print(f"---- {f.name} ----") for n in f.nodes: print(n) for ir in n.irs_ssa: print(f"\t{ir}") print("") def _dump_functions(c: Contract) -> None: """Helper function to print functions and modifiers of a contract""" for f in c.functions_and_modifiers: _dump_function(f) def get_filtered_ssa(f: Union[Function, Node], flt: Callable) -> List[Operation]: """Returns a list of all ssanodes filtered by filter for all nodes in function f""" if isinstance(f, Function): return [ssanode for node in f.nodes for ssanode in node.irs_ssa if flt(ssanode)] assert isinstance(f, Node) return [ssanode for ssanode in f.irs_ssa if flt(ssanode)] def get_ssa_of_type(f: Union[Function, Node], ssatype) -> List[Operation]: """Returns a list of all ssanodes of a specific type for all nodes in function f""" return get_filtered_ssa(f, lambda ssanode: isinstance(ssanode, ssatype)) def test_multi_write(slither_from_source) -> None: source = """ pragma solidity ^0.8.11; contract Test { function multi_write(uint val) external pure returns(uint) { val = 1; val = 2; val = 3; } }""" with slither_from_source(source) as slither: verify_properties_hold(slither) def test_single_branch_phi(slither_from_source) -> None: source = """ pragma solidity ^0.8.11; contract Test { function single_branch_phi(uint val) external pure returns(uint) { if (val == 3) { val = 9; } return val; } } """ with slither_from_source(source) as slither: verify_properties_hold(slither) def test_basic_phi(slither_from_source) -> None: source = """ pragma solidity ^0.8.11; contract Test { function basic_phi(uint val) external pure returns(uint) { if (val == 3) { val = 9; } else { val = 1; } return val; } } """ with slither_from_source(source) as slither: verify_properties_hold(slither) def test_basic_loop_phi(slither_from_source) -> None: source = """ pragma solidity ^0.8.11; contract Test { function basic_loop_phi(uint val) external pure returns(uint) { for (uint i=0;i<128;i++) { val = val + 1; } return val; } } """ with slither_from_source(source) as slither: verify_properties_hold(slither) @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") def test_phi_propagation_loop(slither_from_source): source = """ pragma solidity ^0.8.11; contract Test { function looping(uint v) external pure returns(uint) { uint val = 0; for (uint i=0;i i) { val = i; } else { val = 3; } } return val; } } """ with slither_from_source(source) as slither: verify_properties_hold(slither) @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") def test_free_function_properties(slither_from_source): source = """ pragma solidity ^0.8.11; function free_looping(uint v) returns(uint) { uint val = 0; for (uint i=0;i i) { val = i; } else { val = 3; } } return val; } contract Test {} """ with slither_from_source(source) as slither: verify_properties_hold(slither) def test_ssa_inter_transactional(slither_from_source) -> None: source = """ pragma solidity ^0.8.11; contract A { uint my_var_A; uint my_var_B; function direct_set(uint i) public { my_var_A = i; } function direct_set_plus_one(uint i) public { my_var_A = i + 1; } function indirect_set() public { my_var_B = my_var_A; } } """ with slither_from_source(source) as slither: c = slither.contracts[0] variables = c.variables_as_dict funcs = c.available_functions_as_dict() direct_set = funcs["direct_set(uint256)"] # Skip entry point and go straight to assignment ir assign1 = direct_set.nodes[1].irs_ssa[0] assert isinstance(assign1, Assignment) assign2 = direct_set.nodes[1].irs_ssa[0] assert isinstance(assign2, Assignment) indirect_set = funcs["indirect_set()"] phi = indirect_set.entry_point.irs_ssa[0] assert isinstance(phi, Phi) # phi rvalues come from 1, initial value of my_var_a and 2, assignment in direct_set assert len(phi.rvalues) == 3 assert all(x.non_ssa_version == variables["my_var_A"] for x in phi.rvalues) assert assign1.lvalue in phi.rvalues assert assign2.lvalue in phi.rvalues @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") def test_ssa_phi_callbacks(slither_from_source): source = """ pragma solidity ^0.8.11; contract A { uint my_var_A; uint my_var_B; function direct_set(uint i) public { my_var_A = i; } function use_a() public { // Expect a phi-node here my_var_B = my_var_A; B b = new B(); my_var_A = 3; b.do_stuff(); // Expect a phi-node here my_var_B = my_var_A; } } contract B { function do_stuff() public returns (uint) { // This could be calling back into A } } """ with slither_from_source(source) as slither: c = slither.get_contract_from_name("A")[0] _dump_functions(c) f = [x for x in c.functions if x.name == "use_a"][0] var_a = [x for x in c.variables if x.name == "my_var_A"][0] entry_phi = [ x for x in f.entry_point.irs_ssa if isinstance(x, Phi) and x.lvalue.non_ssa_version == var_a ][0] # The four potential sources are: # 1. initial value # 2. my_var_A = i; # 3. my_var_A = 3; # 4. phi-value after call to b.do_stuff(), which could be reentrant. assert len(entry_phi.rvalues) == 4 # Locate the first high-level call (should be b.do_stuff()) call_node = [x for y in f.nodes for x in y.irs_ssa if isinstance(x, HighLevelCall)][0] n = call_node.node # Get phi-node after call after_call_phi = n.irs_ssa[n.irs_ssa.index(call_node) + 1] # The two sources for this phi node is # 1. my_var_A = i; # 2. my_var_A = 3; assert isinstance(after_call_phi, Phi) assert len(after_call_phi.rvalues) == 2 @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") def test_storage_refers_to(slither_from_source): """Test the storage aspects of the SSA IR When declaring a var as being storage, start tracking what storage it refers_to. When a phi-node is created, ensure refers_to is propagated to the phi-node. Assignments also propagate refers_to. Whenever a ReferenceVariable is the destination of an assignment (e.g. s.v = 10) below, create additional versions of the variables it refers to record that a a write was made. In the current implementation, this is referenced by phis. """ source = """ contract A{ struct St{ int v; } St state0; St state1; function f() public{ St storage s = state0; if(true){ s = state1; } s.v = 10; } } """ with slither_from_source(source) as slither: c = slither.contracts[0] f = c.functions[0] phinodes = get_ssa_of_type(f, Phi) # Expect 2 in entrypoint (state0/state1 initial values), 1 at 'ENDIF' and two related to the # ReferenceVariable write s.v = 10. assert len(phinodes) == 5 # Assign s to state0, s to state1, s.v to 10 assigns = get_ssa_of_type(f, Assignment) assert len(assigns) == 3 # The IR variables have is_storage assert all(x.lvalue.is_storage for x in assigns if isinstance(x, LocalIRVariable)) # s.v ReferenceVariable points to one of the phi vars... ref0 = [x.lvalue for x in assigns if isinstance(x.lvalue, ReferenceVariable)][0] sphis = [x for x in phinodes if x.lvalue == ref0.points_to] assert len(sphis) == 1 sphi = sphis[0] # ...and that phi refers to the two entry phi-values entryphi = [x for x in phinodes if x.lvalue in sphi.lvalue.refers_to] assert len(entryphi) == 2 # The remaining two phis are the ones recording that write through ReferenceVariable occured for ephi in entryphi: phinodes.remove(ephi) phinodes.remove(sphi) assert len(phinodes) == 2 # And they are recorded in one of the entry phis assert phinodes[0].lvalue in entryphi[0].rvalues or entryphi[1].rvalues assert phinodes[1].lvalue in entryphi[0].rvalues or entryphi[1].rvalues @pytest.mark.skipif( not valid_version("0.4.0"), reason="Solidity version 0.4.0 not available on this platform" ) def test_initial_version_exists_for_locals(slither_from_source): """ In solidity you can write statements such as uint a = a + 1, this test ensures that can be handled for local variables. """ src = """ contract C { function func() internal { uint a = a + 1; } } """ with slither_from_source(src, "0.4.0") as slither: verify_properties_hold(slither) c = slither.contracts[0] f = c.functions[0] addition = get_ssa_of_type(f, Binary)[0] assert addition.type == BinaryType.ADDITION assert isinstance(addition.variable_right, Constant) a_0 = addition.variable_left assert a_0.index == 0 assert a_0.name == "a" assignment = get_ssa_of_type(f, Assignment)[0] a_1 = assignment.lvalue assert a_1.index == 1 assert a_1.name == "a" assert assignment.rvalue == addition.lvalue assert a_0.non_ssa_version == a_1.non_ssa_version @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") @pytest.mark.skipif( not valid_version("0.4.0"), reason="Solidity version 0.4.0 not available on this platform" ) def test_initial_version_exists_for_state_variables(slither_from_source): """ In solidity you can write statements such as uint a = a + 1, this test ensures that can be handled for state variables. """ src = """ contract C { uint a = a + 1; } """ with slither_from_source(src, "0.4.0") as slither: verify_properties_hold(slither) c = slither.contracts[0] f = c.functions[0] # There will be one artificial ctor function for the state vars addition = get_ssa_of_type(f, Binary)[0] assert addition.type == BinaryType.ADDITION assert isinstance(addition.variable_right, Constant) a_0 = addition.variable_left assert isinstance(a_0, StateIRVariable) assert a_0.name == "a" assignment = get_ssa_of_type(f, Assignment)[0] a_1 = assignment.lvalue assert isinstance(a_1, StateIRVariable) assert a_1.index == a_0.index + 1 assert a_1.name == "a" assert assignment.rvalue == addition.lvalue assert a_0.non_ssa_version == a_1.non_ssa_version assert isinstance(a_0.non_ssa_version, StateVariable) # No conditional/other function interaction so no phis assert len(get_ssa_of_type(f, Phi)) == 0 @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") def test_initial_version_exists_for_state_variables_function_assign(slither_from_source): """ In solidity you can write statements such as uint a = a + 1, this test ensures that can be handled for local variables. """ # TODO (hbrodin): Could be a detector that a is not used in f src = """ contract C { uint a = f(); function f() internal returns(uint) { return a; } } """ with slither_from_source(src) as slither: verify_properties_hold(slither) c = slither.contracts[0] f, ctor = c.functions if f.is_constructor_variables: f, ctor = ctor, f # ctor should have a single call to f that assigns to a # temporary variable, that is then assigned to a call = get_ssa_of_type(ctor, InternalCall)[0] assert call.node.function == f assign = get_ssa_of_type(ctor, Assignment)[0] assert assign.rvalue == call.lvalue assert isinstance(assign.lvalue, StateIRVariable) assert assign.lvalue.name == "a" # f should have a phi node on entry of a0, a1 and should return # a2 phi = get_ssa_of_type(f, Phi)[0] assert len(phi.rvalues) == 2 assert assign.lvalue in phi.rvalues @pytest.mark.skipif( not valid_version("0.4.0"), reason="Solidity version 0.4.0 not available on this platform" ) def test_return_local_before_assign(slither_from_source): src = """ // this require solidity < 0.5 // a variable can be returned before declared. Ensure it can be // handled by Slither. contract A { function local(bool my_bool) internal returns(uint){ if(my_bool){ return a_local; } uint a_local = 10; } } """ with slither_from_source(src, "0.4.0") as slither: f = slither.contracts[0].functions[0] ret = get_ssa_of_type(f, Return)[0] assert len(ret.values) == 1 assert ret.values[0].index == 0 assign = get_ssa_of_type(f, Assignment)[0] assert assign.lvalue.index == 1 assert assign.lvalue.non_ssa_version == ret.values[0].non_ssa_version @pytest.mark.skipif( not valid_version("0.5.0"), reason="Solidity version 0.5.0 not available on this platform" ) def test_shadow_local(slither_from_source): src = """ contract A { // this require solidity 0.5 function shadowing_local() internal{ uint local = 0; { uint local = 1; { uint local = 2; } } } } """ with slither_from_source(src, "0.5.0") as slither: _dump_functions(slither.contracts[0]) f = slither.contracts[0].functions[0] # Ensure all assignments are to a variable of index 1 # not using the same IR var. assert all(map(lambda x: x.lvalue.index == 1, get_ssa_of_type(f, Assignment))) @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") def test_multiple_named_args_returns(slither_from_source): """Verifies that named arguments and return values have correct versions Each arg/ret have an initial version, version 0, and is written once and should then have version 1. """ src = """ contract A { function multi(uint arg1, uint arg2) internal returns (uint ret1, uint ret2) { arg1 = arg1 + 1; arg2 = arg2 + 2; ret1 = arg1 + 3; ret2 = arg2 + 4; } }""" with slither_from_source(src) as slither: verify_properties_hold(slither) f = slither.contracts[0].functions[0] # Ensure all LocalIRVariables (not TemporaryVariables) have index 1 assert all( map( lambda x: x.lvalue.index == 1 or not isinstance(x.lvalue, LocalIRVariable), get_ssa_of_type(f, OperationWithLValue), ) ) @pytest.mark.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) def test_memory_array(slither_from_source): src = """ contract MemArray { struct A { uint val1; uint val2; } function test_array() internal { A[] memory a= new A[](4); // Create REF_0 -> a_1[2] accept_array_entry(a[2]); // Create REF_1 -> a_1[3] accept_array_entry(a[3]); A memory alocal; accept_array_entry(alocal); } // val_1 = ϕ(val_0, REF_0, REF_1, alocal_1) // val_0 is an unknown external value function accept_array_entry(A memory val) public returns (uint) { uint zero = 0; b(zero); // Create REF_2 -> val_1.val1 return b(val.val1); } function b(uint arg) public returns (uint){ // arg_1 = ϕ(arg_0, zero_1, REF_2) return arg + 1; } }""" with slither_from_source(src) as slither: c = slither.contracts[0] ftest_array, faccept, fb = c.functions # Locate REF_0/REF_1/alocal (they are all args to the call) accept_args = [x.arguments[0] for x in get_ssa_of_type(ftest_array, InternalCall)] # Check entrypoint of accept_array_entry, it should contain a phi-node # of expected rvalues [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) for arg in accept_args: assert arg in phi_entry_accept.rvalues # NOTE(hbrodin): There should be an additional val_0 in the phi-node. # That additional val_0 indicates an external caller of this function. assert len(phi_entry_accept.rvalues) == len(accept_args) + 1 # Args used to invoke b b_args = [x.arguments[0] for x in get_ssa_of_type(faccept, InternalCall)] # Check entrypoint of B, it should contain a phi-node of expected # rvalues [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) for arg in b_args: assert arg in phi_entry_b.rvalues # NOTE(hbrodin): There should be an additional arg_0 (see comment about phi_entry_accept). assert len(phi_entry_b.rvalues) == len(b_args) + 1 @pytest.mark.xfail(reason="Tests for wanted state of SSA IR, not current.", strict=True) def test_storage_array(slither_from_source): src = """ contract StorageArray { struct A { uint val1; uint val2; } // NOTE(hbrodin): a is never written, should only become a_0. Same for astorage (astorage_0). Phi-nodes at entry // should only add new versions of a state variable if it is actually written. A[] a; A astorage; function test_array() internal { accept_array_entry(a[2]); accept_array_entry(a[3]); accept_array_entry(astorage); } function accept_array_entry(A storage val) internal returns (uint) { // val is either a[2], a[3] or astorage_0. Ideally this could be identified. uint five = 5; // NOTE(hbrodin): If the following line is enabled, there would ideally be a phi-node representing writes // to either a or astorage. //val.val2 = 4; b(five); return b(val.val1); } function b(uint value) public returns (uint){ // Expect a phi-node at the entrypoint // value_1 = ϕ(value_0, five_0, REF_x), where REF_x is the reference to val.val1 in accept_array_entry. return value + 1; } }""" with slither_from_source(src) as slither: c = slither.contracts[0] _dump_functions(c) ftest, faccept, fb = c.functions # None of a/astorage is written so expect that there are no phi-nodes at entrypoint. assert len(get_ssa_of_type(ftest.entry_point, Phi)) == 0 # Expect all references to start from index 0 (no writes) assert all(x.variable_left.index == 0 for x in get_ssa_of_type(ftest, Index)) [phi_entry_accept] = get_ssa_of_type(faccept.entry_point, Phi) assert len(phi_entry_accept.rvalues) == 3 # See comment in b above [phi_entry_b] = get_ssa_of_type(fb.entry_point, Phi) assert len(phi_entry_b.rvalues) == 3 # See comment in b above @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") def test_issue_468(slither_from_source): """ Ensure issue 468 is corrected as per https://github.com/crytic/slither/issues/468#issuecomment-620974151 The one difference is that we allow the phi-function at entry of f to hold exit state which contains init state and state from branch, which is a bit redundant. This could be further simplified. """ source = """ contract State { int state = 0; function f(int a) public returns (int) { // phi-node here for state if (a < 1) { state += 1; } // phi-node here for state return state; } } """ with slither_from_source(source) as slither: c = slither.get_contract_from_name("State")[0] f = [x for x in c.functions if x.name == "f"][0] # Check that there is an entry point phi values for each later value # plus one additional which is the initial value entry_ssa = f.entry_point.irs_ssa assert len(entry_ssa) == 1 phi_entry = entry_ssa[0] assert isinstance(phi_entry, Phi) # Find the second phi function endif_node = [x for x in f.nodes if x.type == NodeType.ENDIF][0] assert len(endif_node.irs_ssa) == 1 phi_endif = endif_node.irs_ssa[0] assert isinstance(phi_endif, Phi) # Ensure second phi-function contains init-phi and one additional assert len(phi_endif.rvalues) == 2 assert phi_entry.lvalue in phi_endif.rvalues # Find return-statement and ensure it returns the phi_endif return_node = [x for x in f.nodes if x.type == NodeType.RETURN][0] assert len(return_node.irs_ssa) == 1 ret = return_node.irs_ssa[0] assert len(ret.values) == 1 assert phi_endif.lvalue in ret.values # Ensure that the phi_endif (which is the end-state for function as well) is in the entry_phi assert phi_endif.lvalue in phi_entry.rvalues @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") def test_issue_434(slither_from_source): source = """ contract Contract { int public a; function f() public { g(); a += 1; } function e() public { a -= 1; } function g() public { e(); } } """ with slither_from_source(source) as slither: c = slither.get_contract_from_name("Contract")[0] e = [x for x in c.functions if x.name == "e"][0] f = [x for x in c.functions if x.name == "f"][0] g = [x for x in c.functions if x.name == "g"][0] # Ensure there is a phi-node at the beginning of f and e phi_entry_e = get_ssa_of_type(e.entry_point, Phi)[0] phi_entry_f = get_ssa_of_type(f.entry_point, Phi)[0] # But not at g assert len(get_ssa_of_type(g, Phi)) == 0 # Ensure that the final states of f and e are in the entry-states add_f = get_filtered_ssa( f, lambda x: isinstance(x, Binary) and x.type == BinaryType.ADDITION )[0] sub_e = get_filtered_ssa( e, lambda x: isinstance(x, Binary) and x.type == BinaryType.SUBTRACTION )[0] assert add_f.lvalue in phi_entry_f.rvalues assert add_f.lvalue in phi_entry_e.rvalues assert sub_e.lvalue in phi_entry_f.rvalues assert sub_e.lvalue in phi_entry_e.rvalues # Ensure there is a phi-node after call to g call = get_ssa_of_type(f, InternalCall)[0] idx = call.node.irs_ssa.index(call) aftercall_phi = call.node.irs_ssa[idx + 1] assert isinstance(aftercall_phi, Phi) # Ensure that phi node ^ is used in the addition afterwards assert aftercall_phi.lvalue in (add_f.variable_left, add_f.variable_right) @pytest.mark.xfail(strict=True, reason="Fails in current slither version. Fix in #1102.") def test_issue_473(slither_from_source): source = """ contract Contract { function f() public returns (int) { int a = 1; if (a > 0) { a = 2; } if (a == 3) { a = 6; } return a; } } """ with slither_from_source(source) as slither: c = slither.get_contract_from_name("Contract")[0] f = c.functions[0] phis = get_ssa_of_type(f, Phi) return_value = get_ssa_of_type(f, Return)[0] # There shall be two phi functions assert len(phis) == 2 first_phi = phis[0] second_phi = phis[1] # The second phi is the one being returned, if it's the first swap them (iteration order) if first_phi.lvalue in return_value.values: first_phi, second_phi = second_phi, first_phi # First phi is for [a=1 or a=2] assert len(first_phi.rvalues) == 2 # second is for [a=6 or first phi] assert first_phi.lvalue in second_phi.rvalues assert len(second_phi.rvalues) == 2 # return is for second phi assert len(return_value.values) == 1 assert second_phi.lvalue in return_value.values def test_issue_1748(slither_from_source): source = """ contract Contract { uint[] arr; function foo(uint i) public { arr = [1]; } } """ with slither_from_source(source) as slither: c = slither.get_contract_from_name("Contract")[0] f = c.functions[0] operations = f.slithir_operations assign_op = operations[0] assert isinstance(assign_op, InitArray) def test_issue_1776(slither_from_source): source = """ contract Contract { function foo() public returns (uint) { uint[5][10][] memory arr = new uint[5][10][](2); return 0; } } """ with slither_from_source(source) as slither: c = slither.get_contract_from_name("Contract")[0] f = c.functions[0] operations = f.slithir_operations new_op = operations[0] lvalue = new_op.lvalue lvalue_type = lvalue.type assert isinstance(lvalue_type, ArrayType) assert lvalue_type.is_dynamic lvalue_type1 = lvalue_type.type assert isinstance(lvalue_type1, ArrayType) assert not lvalue_type1.is_dynamic assert lvalue_type1.length_value.value == "10" lvalue_type2 = lvalue_type1.type assert isinstance(lvalue_type2, ArrayType) assert not lvalue_type2.is_dynamic assert lvalue_type2.length_value.value == "5" def test_issue_1846_ternary_in_if(slither_from_source): source = """ contract Contract { function foo(uint x) public returns (uint y) { if (x > 0) { y = x > 1 ? 2 : 3; } else { y = 4; } } } """ with slither_from_source(source) as slither: c = slither.get_contract_from_name("Contract")[0] f = c.functions[0] node = f.nodes[1] assert node.type == NodeType.IF assert node.son_true.type == NodeType.IF assert node.son_false.type == NodeType.EXPRESSION def test_issue_1846_ternary_in_ternary(slither_from_source): source = """ contract Contract { function foo(uint x) public returns (uint y) { y = x > 0 ? x > 1 ? 2 : 3 : 4; } } """ with slither_from_source(source) as slither: c = slither.get_contract_from_name("Contract")[0] f = c.functions[0] node = f.nodes[1] assert node.type == NodeType.IF assert node.son_true.type == NodeType.IF assert node.son_false.type == NodeType.EXPRESSION