mirror of https://github.com/crytic/slither
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.
1080 lines
35 KiB
1080 lines
35 KiB
# # pylint: disable=too-many-lines
|
|
import pathlib
|
|
from collections import defaultdict
|
|
from argparse import ArgumentTypeError
|
|
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.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,
|
|
)
|
|
|
|
from slither.core.solidity_types import ArrayType
|
|
|
|
# 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<v;i++) {
|
|
if (val > 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<v;i++) {
|
|
if (val > 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():
|
|
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"
|
|
|