Static Analyzer for Solidity
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
slither/tests/test_ssa_generation.py

1062 lines
34 KiB

# pylint: disable=too-many-lines
import os
import pathlib
from argparse import ArgumentTypeError
from collections import defaultdict
from contextlib import contextmanager
from inspect import getsourcefile
from tempfile import NamedTemporaryFile
from typing import Union, List, Optional
import pytest
from solc_select import solc_select
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.variables.state_variable import StateVariable
from slither.slithir.operations import (
OperationWithLValue,
Phi,
Assignment,
HighLevelCall,
Return,
Operation,
Binary,
BinaryType,
InternalCall,
Index,
)
from slither.slithir.utils.ssa import is_used_later
from slither.slithir.variables import (
Constant,
ReferenceVariable,
LocalIRVariable,
StateIRVariable,
)
# Directory of currently executing script. Will be used as basis for temporary file names.
SCRIPT_DIR = pathlib.Path(getsourcefile(lambda: 0)).parent
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):
"""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
def ssa_basic_properties(function: Function):
"""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 = {}
for n in function.nodes:
for ir in n.irs:
if isinstance(ir, OperationWithLValue):
name = ir.lvalue.name
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)
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 = defaultdict(int)
for v in ssa_lvalues:
ssa_defs[v.name] += 1
for (k, n) in lvalue_assignments.items():
assert ssa_defs[k] >= n
# Helper 5/6
def check_property_5_and_6(variables, ssavars):
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):
"""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):
"""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):
"""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):
"""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, ssa.read):
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):
if is_used_later(node, ssa.lvalue):
assert have_phi_for_var(df, ssa.lvalue)
@contextmanager
def select_solc_version(version: Optional[str]):
"""Selects solc version to use for running tests.
If no version is provided, latest is used."""
# If no solc_version selected just use the latest avail
if not version:
# This sorts the versions numerically
vers = sorted(
map(
lambda x: (int(x[0]), int(x[1]), int(x[2])),
map(lambda x: x.split(".", 3), solc_select.installed_versions()),
)
)
ver = list(vers)[-1]
version = ".".join(map(str, ver))
env = dict(os.environ)
env_restore = dict(env)
env["SOLC_VERSION"] = version
os.environ.clear()
os.environ.update(env)
yield version
os.environ.clear()
os.environ.update(env_restore)
@contextmanager
def slither_from_source(source_code: str, solc_version: Optional[str] = None):
"""Yields a Slither instance using source_code string and solc_version
Creates a temporary file and changes the solc-version temporary to solc_version.
"""
fname = ""
try:
with NamedTemporaryFile(dir=SCRIPT_DIR, mode="w", suffix=".sol", delete=False) as f:
fname = f.name
f.write(source_code)
with select_solc_version(solc_version):
yield Slither(fname)
finally:
pathlib.Path(fname).unlink()
def verify_properties_hold(source_code_or_slither: Union[str, Slither]):
"""Ensures that basic properties of SSA hold true"""
def verify_func(func: Function):
have_ssa_if_ir(func)
phi_values_inserted(func)
ssa_basic_properties(func)
ssa_phi_node_properties(func)
dominance_properties(func)
def verify(slither):
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)
if isinstance(source_code_or_slither, Slither):
verify(source_code_or_slither)
else:
with slither_from_source(source_code_or_slither) as slither:
verify(slither)
def _dump_function(f: Function):
"""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):
"""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) -> 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():
contract = """
pragma solidity ^0.8.11;
contract Test {
function multi_write(uint val) external pure returns(uint) {
val = 1;
val = 2;
val = 3;
}
}"""
verify_properties_hold(contract)
def test_single_branch_phi():
contract = """
pragma solidity ^0.8.11;
contract Test {
function single_branch_phi(uint val) external pure returns(uint) {
if (val == 3) {
val = 9;
}
return val;
}
}
"""
verify_properties_hold(contract)
def test_basic_phi():
contract = """
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;
}
}
"""
verify_properties_hold(contract)
def test_basic_loop_phi():
contract = """
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;
}
}
"""
verify_properties_hold(contract)
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_phi_propagation_loop():
contract = """
pragma solidity ^0.8.11;
contract Test {
function looping(uint v) external pure returns(uint) {
uint val = 0;
for (uint i=0;i<v;i++) {
if (val > i) {
val = i;
} else {
val = 3;
}
}
return val;
}
}
"""
verify_properties_hold(contract)
@pytest.mark.skip(reason="Fails in current slither version. Fix in #1102.")
def test_free_function_properties():
contract = """
pragma solidity ^0.8.11;
function free_looping(uint v) returns(uint) {
uint val = 0;
for (uint i=0;i<v;i++) {
if (val > i) {
val = i;
} else {
val = 3;
}
}
return val;
}
contract Test {}
"""
verify_properties_hold(contract)
def test_ssa_inter_transactional():
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.skip(reason="Fails in current slither version. Fix in #1102.")
def test_ssa_phi_callbacks():
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.skip(reason="Fails in current slither version. Fix in #1102.")
def test_storage_refers_to():
"""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.skip(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_locals():
"""
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.skip(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():
"""
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.skip(reason="Fails in current slither version. Fix in #1102.")
def test_initial_version_exists_for_state_variables_function_assign():
"""
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.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():
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():
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.skip(reason="Fails in current slither version. Fix in #1102.")
def test_multiple_named_args_returns():
"""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.")
def test_memory_array():
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.")
def test_storage_array():
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.skip(reason="Fails in current slither version. Fix in #1102.")
def test_issue_468():
"""
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.skip(reason="Fails in current slither version. Fix in #1102.")
def test_issue_434():
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.skip(reason="Fails in current slither version. Fix in #1102.")
def test_issue_473():
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