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/unit/slithir/test_ssa_generation.py

1100 lines
35 KiB

2 years ago
# 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
2 years ago
from typing import Union, List, Optional, Dict, Callable
2 years ago
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
2 years ago
from slither.core.variables.local_variable import LocalVariable
2 years ago
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,
2 years ago
TemporaryVariableSSA,
2 years ago
)
# Directory of currently executing script. Will be used as basis for temporary file names.
2 years ago
SCRIPT_DIR = pathlib.Path(getsourcefile(lambda: 0)).parent # type:ignore
2 years ago
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
2 years ago
def have_ssa_if_ir(function: Function) -> None:
2 years ago
"""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
2 years ago
# pylint: disable=too-many-branches, too-many-locals
def ssa_basic_properties(function: Function) -> None:
2 years ago
"""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()
2 years ago
lvalue_assignments: Dict[str, int] = {}
2 years ago
for n in function.nodes:
for ir in n.irs:
2 years ago
if isinstance(ir, OperationWithLValue) and ir.lvalue:
2 years ago
name = ir.lvalue.name
2 years ago
if name is None:
continue
2 years ago
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)
2 years ago
ssa_lvalue = ssa.lvalue
if isinstance(ssa_lvalue, (StateIRVariable, LocalIRVariable)):
assert ssa_lvalue.index > 0
2 years ago
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
2 years ago
ssa_defs: Dict[str, int] = defaultdict(int)
2 years ago
for v in ssa_lvalues:
2 years ago
if v and v.name:
ssa_defs[v.name] += 1
2 years ago
2 years ago
for (k, count) in lvalue_assignments.items():
assert ssa_defs[k] >= count
2 years ago
# Helper 5/6
2 years ago
def check_property_5_and_6(
variables: List[LocalVariable], ssavars: List[LocalIRVariable]
) -> None:
2 years ago
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)
2 years ago
def ssa_phi_node_properties(f: Function) -> None:
2 years ago
"""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
2 years ago
def dominance_properties(f: Function) -> None:
2 years ago
"""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)
2 years ago
def phi_values_inserted(f: Function) -> None:
2 years ago
"""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
"""
2 years ago
def have_phi_for_var(
node: Node, var: Union[StateIRVariable, LocalIRVariable, TemporaryVariableSSA]
) -> bool:
2 years ago
"""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):
2 years ago
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))
],
):
2 years ago
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):
2 years ago
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)
2 years ago
@contextmanager
2 years ago
def select_solc_version(version: Optional[str]) -> None:
2 years ago
"""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()
2 years ago
def verify_properties_hold(source_code_or_slither: Union[str, Slither]) -> None:
2 years ago
"""Ensures that basic properties of SSA hold true"""
2 years ago
def verify_func(func: Function) -> None:
2 years ago
have_ssa_if_ir(func)
phi_values_inserted(func)
ssa_basic_properties(func)
ssa_phi_node_properties(func)
dominance_properties(func)
2 years ago
def verify(slither: Slither) -> None:
2 years ago
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:
2 years ago
slither: Slither
2 years ago
with slither_from_source(source_code_or_slither) as slither:
verify(slither)
2 years ago
def _dump_function(f: Function) -> None:
2 years ago
"""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("")
2 years ago
def _dump_functions(c: Contract) -> None:
2 years ago
"""Helper function to print functions and modifiers of a contract"""
for f in c.functions_and_modifiers:
_dump_function(f)
2 years ago
def get_filtered_ssa(f: Union[Function, Node], flt: Callable) -> List[Operation]:
2 years ago
"""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))
2 years ago
def test_multi_write() -> None:
2 years ago
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)
2 years ago
def test_single_branch_phi() -> None:
2 years ago
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)
2 years ago
def test_basic_phi() -> None:
2 years ago
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)
2 years ago
def test_basic_loop_phi() -> None:
2 years ago
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.xfail(strict=True, 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.xfail(strict=True, 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)
2 years ago
def test_ssa_inter_transactional() -> None:
2 years ago
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():
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():
"""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():
"""
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():
"""
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():
"""
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
2 years ago
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.xfail(strict=True, 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.", strict=True)
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.", strict=True)
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.xfail(strict=True, 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.xfail(strict=True, 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.xfail(strict=True, 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
def test_issue_1748():
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)