Skip to content

Commit 6873b8f

Browse files
authored
Merge pull request #2696 from crytic/dev-detectors-add-callstack
Add calls stack information to detectors
2 parents aa322d9 + 8726486 commit 6873b8f

File tree

22 files changed

+164
-67
lines changed

22 files changed

+164
-67
lines changed

Diff for: slither/detectors/statements/calls_in_loop.py

+29-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Optional
1+
from typing import List, Optional, Tuple
22
from slither.core.cfg.node import NodeType, Node
33
from slither.detectors.abstract_detector import (
44
AbstractDetector,
@@ -16,18 +16,24 @@
1616
InternalCall,
1717
)
1818

19+
Result = List[Tuple[Node, List[str]]]
1920

20-
def detect_call_in_loop(contract: Contract) -> List[Node]:
21-
ret: List[Node] = []
21+
22+
def detect_call_in_loop(contract: Contract) -> Result:
23+
ret: Result = []
2224
for f in contract.functions_entry_points:
2325
if f.is_implemented:
24-
call_in_loop(f.entry_point, 0, [], ret)
26+
call_in_loop(f.entry_point, 0, [], [], ret)
2527

2628
return ret
2729

2830

2931
def call_in_loop(
30-
node: Optional[Node], in_loop_counter: int, visited: List[Node], ret: List[Node]
32+
node: Optional[Node],
33+
in_loop_counter: int,
34+
visited: List[Node],
35+
calls_stack: List[str],
36+
ret: Result,
3137
) -> None:
3238
if node is None:
3339
return
@@ -41,18 +47,19 @@ def call_in_loop(
4147
elif node.type == NodeType.ENDLOOP:
4248
in_loop_counter -= 1
4349

44-
if in_loop_counter > 0:
45-
for ir in node.all_slithir_operations():
46-
if isinstance(ir, (LowLevelCall, HighLevelCall, Send, Transfer)):
47-
if isinstance(ir, LibraryCall):
48-
continue
49-
ret.append(ir.node)
50-
if isinstance(ir, (InternalCall)):
51-
assert ir.function
52-
call_in_loop(ir.function.entry_point, in_loop_counter, visited, ret)
50+
for ir in node.irs:
51+
if isinstance(ir, (LowLevelCall, HighLevelCall, Send, Transfer)) and in_loop_counter > 0:
52+
if isinstance(ir, LibraryCall):
53+
continue
54+
ret.append((ir.node, calls_stack.copy()))
55+
if isinstance(ir, (InternalCall)):
56+
assert ir.function
57+
calls_stack.append(node.function.canonical_name)
58+
call_in_loop(ir.function.entry_point, in_loop_counter, visited, calls_stack, ret)
59+
calls_stack.pop()
5360

5461
for son in node.sons:
55-
call_in_loop(son, in_loop_counter, visited, ret)
62+
call_in_loop(son, in_loop_counter, visited, calls_stack, ret)
5663

5764

5865
class MultipleCallsInLoop(AbstractDetector):
@@ -96,10 +103,16 @@ def _detect(self) -> List[Output]:
96103
results: List[Output] = []
97104
for c in self.compilation_unit.contracts_derived:
98105
values = detect_call_in_loop(c)
99-
for node in values:
106+
for node, calls_stack in values:
100107
func = node.function
101108

102109
info: DETECTOR_INFO = [func, " has external calls inside a loop: ", node, "\n"]
110+
111+
if len(calls_stack) > 0:
112+
info.append("\tCalls stack containing the loop:\n")
113+
for call in calls_stack:
114+
info.extend(["\t\t", call, "\n"])
115+
103116
res = self.generate_result(info)
104117
results.append(res)
105118

Diff for: slither/detectors/statements/costly_operations_in_loop.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Optional
1+
from typing import List, Optional, Tuple
22
from slither.core.cfg.node import NodeType, Node
33
from slither.detectors.abstract_detector import (
44
AbstractDetector,
@@ -10,18 +10,24 @@
1010
from slither.slithir.operations import InternalCall, OperationWithLValue
1111
from slither.core.variables.state_variable import StateVariable
1212

13+
Result = List[Tuple[Node, List[str]]]
1314

14-
def detect_costly_operations_in_loop(contract: Contract) -> List[Node]:
15-
ret: List[Node] = []
15+
16+
def detect_costly_operations_in_loop(contract: Contract) -> Result:
17+
ret: Result = []
1618
for f in contract.functions_entry_points:
1719
if f.is_implemented:
18-
costly_operations_in_loop(f.entry_point, 0, [], ret)
20+
costly_operations_in_loop(f.entry_point, 0, [], [], ret)
1921

2022
return ret
2123

2224

2325
def costly_operations_in_loop(
24-
node: Optional[Node], in_loop_counter: int, visited: List[Node], ret: List[Node]
26+
node: Optional[Node],
27+
in_loop_counter: int,
28+
visited: List[Node],
29+
calls_stack: List[str],
30+
ret: Result,
2531
) -> None:
2632

2733
if node is None:
@@ -38,16 +44,20 @@ def costly_operations_in_loop(
3844
in_loop_counter -= 1
3945

4046
if in_loop_counter > 0:
41-
for ir in node.all_slithir_operations():
47+
for ir in node.irs:
4248
# Ignore Array/Mapping/Struct types for now
4349
if isinstance(ir, OperationWithLValue) and isinstance(ir.lvalue, StateVariable):
44-
ret.append(ir.node)
50+
ret.append((ir.node, calls_stack.copy()))
4551
break
4652
if isinstance(ir, (InternalCall)) and ir.function:
47-
costly_operations_in_loop(ir.function.entry_point, in_loop_counter, visited, ret)
53+
calls_stack.append(node.function.canonical_name)
54+
costly_operations_in_loop(
55+
ir.function.entry_point, in_loop_counter, visited, calls_stack, ret
56+
)
57+
calls_stack.pop()
4858

4959
for son in node.sons:
50-
costly_operations_in_loop(son, in_loop_counter, visited, ret)
60+
costly_operations_in_loop(son, in_loop_counter, visited, calls_stack, ret)
5161

5262

5363
class CostlyOperationsInLoop(AbstractDetector):
@@ -100,10 +110,16 @@ def _detect(self) -> List[Output]:
100110
results: List[Output] = []
101111
for c in self.compilation_unit.contracts_derived:
102112
values = detect_costly_operations_in_loop(c)
103-
for node in values:
113+
for node, calls_stack in values:
104114
func = node.function
105115
info: DETECTOR_INFO = [func, " has costly operations inside a loop:\n"]
106116
info += ["\t- ", node, "\n"]
117+
118+
if len(calls_stack) > 0:
119+
info.append("\tCalls stack containing the loop:\n")
120+
for call in calls_stack:
121+
info.extend(["\t\t", call, "\n"])
122+
107123
res = self.generate_result(info)
108124
results.append(res)
109125

Diff for: slither/detectors/statements/delegatecall_in_loop.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Optional
1+
from typing import List, Optional, Tuple
22
from slither.core.cfg.node import NodeType, Node
33
from slither.detectors.abstract_detector import (
44
AbstractDetector,
@@ -9,17 +9,23 @@
99
from slither.core.declarations import Contract
1010
from slither.utils.output import Output
1111

12+
Result = List[Tuple[Node, List[str]]]
1213

13-
def detect_delegatecall_in_loop(contract: Contract) -> List[Node]:
14-
results: List[Node] = []
14+
15+
def detect_delegatecall_in_loop(contract: Contract) -> Result:
16+
results: Result = []
1517
for f in contract.functions_entry_points:
1618
if f.is_implemented and f.payable:
17-
delegatecall_in_loop(f.entry_point, 0, [], results)
19+
delegatecall_in_loop(f.entry_point, 0, [], [], results)
1820
return results
1921

2022

2123
def delegatecall_in_loop(
22-
node: Optional[Node], in_loop_counter: int, visited: List[Node], results: List[Node]
24+
node: Optional[Node],
25+
in_loop_counter: int,
26+
visited: List[Node],
27+
calls_stack: List[str],
28+
results: Result,
2329
) -> None:
2430

2531
if node is None:
@@ -35,18 +41,22 @@ def delegatecall_in_loop(
3541
elif node.type == NodeType.ENDLOOP:
3642
in_loop_counter -= 1
3743

38-
for ir in node.all_slithir_operations():
44+
for ir in node.irs:
3945
if (
4046
in_loop_counter > 0
4147
and isinstance(ir, (LowLevelCall))
4248
and ir.function_name == "delegatecall"
4349
):
44-
results.append(ir.node)
50+
results.append((ir.node, calls_stack.copy()))
4551
if isinstance(ir, (InternalCall)) and ir.function:
46-
delegatecall_in_loop(ir.function.entry_point, in_loop_counter, visited, results)
52+
calls_stack.append(node.function.canonical_name)
53+
delegatecall_in_loop(
54+
ir.function.entry_point, in_loop_counter, visited, calls_stack, results
55+
)
56+
calls_stack.pop()
4757

4858
for son in node.sons:
49-
delegatecall_in_loop(son, in_loop_counter, visited, results)
59+
delegatecall_in_loop(son, in_loop_counter, visited, calls_stack, results)
5060

5161

5262
class DelegatecallInLoop(AbstractDetector):
@@ -95,7 +105,7 @@ def _detect(self) -> List[Output]:
95105
results: List[Output] = []
96106
for c in self.compilation_unit.contracts_derived:
97107
values = detect_delegatecall_in_loop(c)
98-
for node in values:
108+
for node, calls_stack in values:
99109
func = node.function
100110

101111
info: DETECTOR_INFO = [
@@ -104,6 +114,12 @@ def _detect(self) -> List[Output]:
104114
node,
105115
"\n",
106116
]
117+
118+
if len(calls_stack) > 0:
119+
info.append("\tCalls stack containing the loop:\n")
120+
for call in calls_stack:
121+
info.extend(["\t\t", call, "\n"])
122+
107123
res = self.generate_result(info)
108124
results.append(res)
109125

Diff for: slither/detectors/statements/msg_value_in_loop.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Optional
1+
from typing import List, Optional, Tuple
22
from slither.core.cfg.node import NodeType, Node
33
from slither.detectors.abstract_detector import (
44
AbstractDetector,
@@ -12,17 +12,23 @@
1212
from slither.core.variables import Variable
1313
from slither.core.expressions.literal import Literal
1414

15+
Result = List[Tuple[Node, List[str]]]
1516

16-
def detect_msg_value_in_loop(contract: Contract) -> List[Node]:
17-
results: List[Node] = []
17+
18+
def detect_msg_value_in_loop(contract: Contract) -> Result:
19+
results: Result = []
1820
for f in contract.functions_entry_points:
1921
if f.is_implemented and f.payable:
20-
msg_value_in_loop(f.entry_point, 0, [], results)
22+
msg_value_in_loop(f.entry_point, 0, [], [], results)
2123
return results
2224

2325

2426
def msg_value_in_loop(
25-
node: Optional[Node], in_loop_counter: int, visited: List[Node], results: List[Node]
27+
node: Optional[Node],
28+
in_loop_counter: int,
29+
visited: List[Node],
30+
calls_stack: List[str],
31+
results: Result,
2632
) -> None:
2733

2834
if node is None:
@@ -38,7 +44,7 @@ def msg_value_in_loop(
3844
elif node.type == NodeType.ENDLOOP:
3945
in_loop_counter -= 1
4046

41-
for ir in node.all_slithir_operations():
47+
for ir in node.irs:
4248
if in_loop_counter > 0 and SolidityVariableComposed("msg.value") in ir.read:
4349
# If we find a conditional expression with msg.value and is compared to 0 we don't report it
4450
if ir.node.is_conditional() and SolidityVariableComposed("msg.value") in ir.read:
@@ -55,12 +61,16 @@ def msg_value_in_loop(
5561
and str(compared_to.expression.value) == "0"
5662
):
5763
continue
58-
results.append(ir.node)
64+
results.append((ir.node, calls_stack.copy()))
5965
if isinstance(ir, (InternalCall)):
60-
msg_value_in_loop(ir.function.entry_point, in_loop_counter, visited, results)
66+
calls_stack.append(node.function.canonical_name)
67+
msg_value_in_loop(
68+
ir.function.entry_point, in_loop_counter, visited, calls_stack, results
69+
)
70+
calls_stack.pop()
6171

6272
for son in node.sons:
63-
msg_value_in_loop(son, in_loop_counter, visited, results)
73+
msg_value_in_loop(son, in_loop_counter, visited, calls_stack, results)
6474

6575

6676
class MsgValueInLoop(AbstractDetector):
@@ -105,10 +115,16 @@ def _detect(self) -> List[Output]:
105115
results: List[Output] = []
106116
for c in self.compilation_unit.contracts_derived:
107117
values = detect_msg_value_in_loop(c)
108-
for node in values:
118+
for node, calls_stack in values:
109119
func = node.function
110120

111121
info: DETECTOR_INFO = [func, " use msg.value in a loop: ", node, "\n"]
122+
123+
if len(calls_stack) > 0:
124+
info.append("\tCalls stack containing the loop:\n")
125+
for call in calls_stack:
126+
info.extend(["\t\t", call, "\n"])
127+
112128
res = self.generate_result(info)
113129
results.append(res)
114130

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
CostlyOperationsInLoopBase.bad_base() (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#5-9) has costly operations inside a loop:
22
- state_variable_base ++ (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#7)
33

4-
CostlyOperationsInLoop.bad3_internal() (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#41-43) has costly operations inside a loop:
5-
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#42)
6-
74
CostlyOperationsInLoop.bad2() (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#26-33) has costly operations inside a loop:
85
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#31)
96

7+
CostlyOperationsInLoop.bad3_internal() (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#41-43) has costly operations inside a loop:
8+
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#42)
9+
Calls stack containing the loop:
10+
CostlyOperationsInLoop.bad3()
11+
1012
CostlyOperationsInLoop.bad() (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#20-24) has costly operations inside a loop:
1113
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.4.25/multiple_costly_operations_in_loop.sol#22)
1214

Diff for: tests/e2e/detectors/snapshots/detectors__detector_CostlyOperationsInLoop_0_5_16_multiple_costly_operations_in_loop_sol__0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ CostlyOperationsInLoop.bad2() (tests/e2e/detectors/test_data/costly-loop/0.5.16/
66

77
CostlyOperationsInLoop.bad3_internal() (tests/e2e/detectors/test_data/costly-loop/0.5.16/multiple_costly_operations_in_loop.sol#41-43) has costly operations inside a loop:
88
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.5.16/multiple_costly_operations_in_loop.sol#42)
9+
Calls stack containing the loop:
10+
CostlyOperationsInLoop.bad3()
911

1012
CostlyOperationsInLoop.bad() (tests/e2e/detectors/test_data/costly-loop/0.5.16/multiple_costly_operations_in_loop.sol#20-24) has costly operations inside a loop:
1113
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.5.16/multiple_costly_operations_in_loop.sol#22)

Diff for: tests/e2e/detectors/snapshots/detectors__detector_CostlyOperationsInLoop_0_6_11_multiple_costly_operations_in_loop_sol__0.txt

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ CostlyOperationsInLoop.bad() (tests/e2e/detectors/test_data/costly-loop/0.6.11/m
44
CostlyOperationsInLoop.bad2() (tests/e2e/detectors/test_data/costly-loop/0.6.11/multiple_costly_operations_in_loop.sol#26-33) has costly operations inside a loop:
55
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.6.11/multiple_costly_operations_in_loop.sol#31)
66

7-
CostlyOperationsInLoop.bad3_internal() (tests/e2e/detectors/test_data/costly-loop/0.6.11/multiple_costly_operations_in_loop.sol#41-43) has costly operations inside a loop:
8-
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.6.11/multiple_costly_operations_in_loop.sol#42)
9-
107
CostlyOperationsInLoopBase.bad_base() (tests/e2e/detectors/test_data/costly-loop/0.6.11/multiple_costly_operations_in_loop.sol#5-9) has costly operations inside a loop:
118
- state_variable_base ++ (tests/e2e/detectors/test_data/costly-loop/0.6.11/multiple_costly_operations_in_loop.sol#7)
129

10+
CostlyOperationsInLoop.bad3_internal() (tests/e2e/detectors/test_data/costly-loop/0.6.11/multiple_costly_operations_in_loop.sol#41-43) has costly operations inside a loop:
11+
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.6.11/multiple_costly_operations_in_loop.sol#42)
12+
Calls stack containing the loop:
13+
CostlyOperationsInLoop.bad3()
14+
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
CostlyOperationsInLoop.bad3_internal() (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#41-43) has costly operations inside a loop:
2-
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#42)
3-
41
CostlyOperationsInLoopBase.bad_base() (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#5-9) has costly operations inside a loop:
52
- state_variable_base ++ (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#7)
63

74
CostlyOperationsInLoop.bad2() (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#26-33) has costly operations inside a loop:
85
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#31)
96

7+
CostlyOperationsInLoop.bad3_internal() (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#41-43) has costly operations inside a loop:
8+
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#42)
9+
Calls stack containing the loop:
10+
CostlyOperationsInLoop.bad3()
11+
1012
CostlyOperationsInLoop.bad() (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#20-24) has costly operations inside a loop:
1113
- state_variable ++ (tests/e2e/detectors/test_data/costly-loop/0.7.6/multiple_costly_operations_in_loop.sol#22)
1214

Diff for: tests/e2e/detectors/snapshots/detectors__detector_DelegatecallInLoop_0_4_25_delegatecall_loop_sol__0.txt

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
C.bad2_internal(address[]) (tests/e2e/detectors/test_data/delegatecall-loop/0.4.25/delegatecall_loop.sol#19-23) has delegatecall inside a loop in a payable function: address(this).delegatecall(abi.encodeWithSignature(addBalance(address),receivers[i])) (tests/e2e/detectors/test_data/delegatecall-loop/0.4.25/delegatecall_loop.sol#21)
2+
Calls stack containing the loop:
3+
C.bad2(address[])
4+
15
C.bad(address[]) (tests/e2e/detectors/test_data/delegatecall-loop/0.4.25/delegatecall_loop.sol#5-9) has delegatecall inside a loop in a payable function: address(this).delegatecall(abi.encodeWithSignature(addBalance(address),receivers[i])) (tests/e2e/detectors/test_data/delegatecall-loop/0.4.25/delegatecall_loop.sol#7)
26

37
C.bad3(address[]) (tests/e2e/detectors/test_data/delegatecall-loop/0.4.25/delegatecall_loop.sol#25-31) has delegatecall inside a loop in a payable function: address(this).delegatecall(abi.encodeWithSignature(addBalance(address),receivers[i])) (tests/e2e/detectors/test_data/delegatecall-loop/0.4.25/delegatecall_loop.sol#28)
48

5-
C.bad2_internal(address[]) (tests/e2e/detectors/test_data/delegatecall-loop/0.4.25/delegatecall_loop.sol#19-23) has delegatecall inside a loop in a payable function: address(this).delegatecall(abi.encodeWithSignature(addBalance(address),receivers[i])) (tests/e2e/detectors/test_data/delegatecall-loop/0.4.25/delegatecall_loop.sol#21)
6-

0 commit comments

Comments
 (0)