Skip to content

Commit 1562b34

Browse files
committed
feat: add balance-reliance detector for unsafe address.balance usage
Adds a new detector that identifies potentially unsafe uses of address.balance in smart contracts: 1. Strict equality comparisons (== or !=) - vulnerable to ETH forcing via selfdestruct or pre-deployment balance manipulation 2. Assignment to state variables - leads to stale data and incorrect assumptions about current balances The detector uses data dependency analysis to catch indirect usage patterns where balance values flow through local variables. Fixes #2778
1 parent d9794fd commit 1562b34

File tree

6 files changed

+324
-0
lines changed

6 files changed

+324
-0
lines changed

slither/detectors/all_detectors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
from .statements.write_after_write import WriteAfterWrite
8585
from .statements.msg_value_in_loop import MsgValueInLoop
8686
from .statements.msg_value_in_nonpayable import MsgValueInNonPayable
87+
from .statements.balance_reliance import BalanceReliance
8788
from .statements.delegatecall_in_loop import DelegatecallInLoop
8889
from .functions.protected_variable import ProtectedVariables
8990
from .functions.permit_domain_signature_collision import DomainSeparatorCollision
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
"""
2+
Detector for potentially unsafe uses of address.balance.
3+
4+
Detects:
5+
1. Strict equality comparisons (== or !=) with address.balance
6+
2. Assignment of address.balance to state variables
7+
8+
These patterns are problematic because:
9+
- Attackers can forcibly send ETH using selfdestruct
10+
- Pre-calculated contract addresses can receive ETH before deployment
11+
- Balance can change between transactions unpredictably
12+
13+
Related to issue #2778.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
from slither.analyses.data_dependency.data_dependency import is_dependent_ssa
19+
from slither.core.cfg.node import Node
20+
from slither.core.declarations import Function
21+
from slither.core.declarations.contract import Contract
22+
from slither.core.declarations.function_contract import FunctionContract
23+
from slither.core.declarations.function_top_level import FunctionTopLevel
24+
from slither.core.declarations.solidity_variables import SolidityFunction
25+
from slither.core.variables.state_variable import StateVariable
26+
from slither.detectors.abstract_detector import (
27+
AbstractDetector,
28+
DetectorClassification,
29+
DETECTOR_INFO,
30+
)
31+
from slither.slithir.operations import (
32+
Assignment,
33+
Binary,
34+
BinaryType,
35+
SolidityCall,
36+
)
37+
from slither.slithir.variables.temporary_ssa import TemporaryVariableSSA
38+
from slither.slithir.variables.local_variable import LocalIRVariable
39+
from slither.utils.output import Output
40+
41+
42+
class BalanceReliance(AbstractDetector):
43+
"""
44+
Detects potentially unsafe uses of address.balance:
45+
1. Strict equality comparisons (== or !=)
46+
2. Assignment to state variables
47+
"""
48+
49+
ARGUMENT = "balance-reliance"
50+
HELP = "Dangerous reliance on address.balance"
51+
IMPACT = DetectorClassification.LOW
52+
CONFIDENCE = DetectorClassification.MEDIUM
53+
54+
WIKI = "https://github.com/crytic/slither/wiki/Detector-Documentation#balance-reliance"
55+
WIKI_TITLE = "Dangerous reliance on address.balance"
56+
57+
WIKI_DESCRIPTION = """
58+
Detects potentially unsafe uses of `address.balance`:
59+
1. Strict equality comparisons (`==` or `!=`) - An attacker can forcibly send ETH using `selfdestruct`, breaking equality assumptions.
60+
2. Assignment to state variables - Storing balance values leads to stale data and incorrect assumptions."""
61+
62+
WIKI_EXPLOIT_SCENARIO = """
63+
```solidity
64+
contract Crowdsale {
65+
uint256 public savedBalance;
66+
67+
function fund_reached() public returns(bool) {
68+
// BAD: strict equality can be manipulated
69+
return address(this).balance == 100 ether;
70+
}
71+
72+
function saveBalance() public {
73+
// BAD: balance can change, making stored value stale
74+
savedBalance = address(this).balance;
75+
}
76+
}
77+
```
78+
An attacker can use `selfdestruct` to forcibly send ETH to the contract, making `fund_reached()` return false even after 100 ETH is collected. Similarly, `savedBalance` becomes stale immediately after being set."""
79+
80+
WIKI_RECOMMENDATION = """
81+
Use range checks instead of strict equality for balance comparisons:
82+
```solidity
83+
// GOOD: use >= for minimum balance checks
84+
require(address(this).balance >= 100 ether, "Insufficient balance");
85+
86+
// GOOD: use range checks
87+
require(address(this).balance >= minAmount && address(this).balance <= maxAmount);
88+
```
89+
Avoid storing balance in state variables. If needed, recalculate on each use."""
90+
91+
# Only applicable to Solidity
92+
LANGUAGE = "solidity"
93+
94+
def _find_balance_taints(
95+
self, functions: list[FunctionContract]
96+
) -> list[LocalIRVariable | TemporaryVariableSSA]:
97+
"""
98+
Find all variables that hold address.balance values.
99+
"""
100+
taints = []
101+
for func in functions:
102+
for node in func.nodes:
103+
for ir in node.irs_ssa:
104+
if isinstance(ir, SolidityCall) and ir.function == SolidityFunction(
105+
"balance(address)"
106+
):
107+
taints.append(ir.lvalue)
108+
return taints
109+
110+
def _is_tainted(
111+
self,
112+
var,
113+
taints: list[LocalIRVariable | TemporaryVariableSSA],
114+
contract: Contract,
115+
) -> bool:
116+
"""Check if a variable is tainted by address.balance."""
117+
for taint in taints:
118+
if is_dependent_ssa(var, taint, contract):
119+
return True
120+
return False
121+
122+
def _detect_strict_equality(
123+
self,
124+
functions: list[FunctionContract],
125+
taints: list[LocalIRVariable | TemporaryVariableSSA],
126+
contract: Contract,
127+
) -> list[tuple[FunctionContract, Node, str]]:
128+
"""
129+
Detect strict equality comparisons (== or !=) with balance-tainted values.
130+
"""
131+
results = []
132+
for func in functions:
133+
if isinstance(func, FunctionTopLevel):
134+
continue
135+
for node in func.nodes:
136+
for ir in node.irs_ssa:
137+
if isinstance(ir, Binary) and ir.type in (
138+
BinaryType.EQUAL,
139+
BinaryType.NOT_EQUAL,
140+
):
141+
# Check if either operand is tainted by balance
142+
left_tainted = self._is_tainted(ir.variable_left, taints, contract)
143+
right_tainted = self._is_tainted(ir.variable_right, taints, contract)
144+
if left_tainted or right_tainted:
145+
op = "==" if ir.type == BinaryType.EQUAL else "!="
146+
results.append((func, node, f"strict equality ({op})"))
147+
return results
148+
149+
def _detect_state_assignment(
150+
self,
151+
functions: list[FunctionContract],
152+
taints: list[LocalIRVariable | TemporaryVariableSSA],
153+
contract: Contract,
154+
) -> list[tuple[FunctionContract, Node, str]]:
155+
"""
156+
Detect assignment of balance-tainted values to state variables.
157+
"""
158+
results = []
159+
for func in functions:
160+
if isinstance(func, FunctionTopLevel):
161+
continue
162+
for node in func.nodes:
163+
for ir in node.irs_ssa:
164+
if isinstance(ir, Assignment):
165+
# Check if assigning to a state variable
166+
if isinstance(ir.lvalue, StateVariable) or (
167+
hasattr(ir.lvalue, "non_ssa_version")
168+
and isinstance(ir.lvalue.non_ssa_version, StateVariable)
169+
):
170+
# Check if the value being assigned is tainted
171+
if self._is_tainted(ir.rvalue, taints, contract):
172+
results.append((func, node, "state variable assignment"))
173+
return results
174+
175+
def _detect(self) -> list[Output]:
176+
results = []
177+
178+
for contract in self.compilation_unit.contracts_derived:
179+
functions = contract.all_functions_called + contract.modifiers
180+
181+
# Find all balance taints
182+
taints = self._find_balance_taints(functions)
183+
if not taints:
184+
continue
185+
186+
# Detect strict equality comparisons
187+
equality_issues = self._detect_strict_equality(functions, taints, contract)
188+
189+
# Detect state variable assignments
190+
assignment_issues = self._detect_state_assignment(functions, taints, contract)
191+
192+
# Combine and report
193+
all_issues = equality_issues + assignment_issues
194+
195+
for func, node, issue_type in all_issues:
196+
info: DETECTOR_INFO = [
197+
func,
198+
f" uses address.balance in {issue_type}:\n",
199+
"\t- ",
200+
node,
201+
"\n",
202+
]
203+
results.append(self.generate_result(info))
204+
205+
return results
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
BalanceRelianceTest.checkExactBalance() (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#9-11) uses address.balance in strict equality (==):
2+
- address(this).balance == 1000000000000000000 (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#10)
3+
4+
BalanceRelianceTest.checkNotBalance() (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#14-16) uses address.balance in strict equality (!=):
5+
- address(this).balance != 0 (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#15)
6+
7+
BalanceRelianceTest.indirectCheck(uint256) (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#19-22) uses address.balance in strict equality (==):
8+
- currentBal == expected (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#21)
9+
10+
BalanceRelianceTest.indirectSave() (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#30-33) uses address.balance in state variable assignment:
11+
- savedBalance = bal (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#32)
12+
13+
BalanceRelianceTest.requireExact() (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#36-38) uses address.balance in strict equality (==):
14+
- require(bool,string)(address(this).balance == 100000000000000000000,Must be exactly 100 ETH) (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#37)
15+
16+
BalanceRelianceTest.saveCurrentBalance() (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#25-27) uses address.balance in state variable assignment:
17+
- savedBalance = address(this).balance (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#26)
18+
19+
ExternalBalanceTest.checkTargetBalance() (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#76-78) uses address.balance in strict equality (==):
20+
- target.balance == 1000000000000000000 (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#77)
21+
22+
ExternalBalanceTest.saveTargetBalance() (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#81-83) uses address.balance in state variable assignment:
23+
- externalBalance = target.balance (tests/e2e/detectors/test_data/balance-reliance/0.8.0/balance_reliance.sol#82)
24+
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
contract BalanceRelianceTest {
5+
uint256 public savedBalance;
6+
uint256 public threshold;
7+
8+
// BAD: Strict equality check with address.balance
9+
function checkExactBalance() public view returns (bool) {
10+
return address(this).balance == 1 ether;
11+
}
12+
13+
// BAD: Strict inequality check with address.balance
14+
function checkNotBalance() public view returns (bool) {
15+
return address(this).balance != 0;
16+
}
17+
18+
// BAD: Indirect strict equality (via local variable)
19+
function indirectCheck(uint256 expected) public view returns (bool) {
20+
uint256 currentBal = address(this).balance;
21+
return currentBal == expected;
22+
}
23+
24+
// BAD: Saving balance to state variable
25+
function saveCurrentBalance() public {
26+
savedBalance = address(this).balance;
27+
}
28+
29+
// BAD: Indirect state assignment
30+
function indirectSave() public {
31+
uint256 bal = address(this).balance;
32+
savedBalance = bal;
33+
}
34+
35+
// BAD: require with strict equality
36+
function requireExact() public view {
37+
require(address(this).balance == 100 ether, "Must be exactly 100 ETH");
38+
}
39+
40+
// GOOD: Greater than or equal comparison
41+
function checkMinBalance() public view returns (bool) {
42+
return address(this).balance >= 1 ether;
43+
}
44+
45+
// GOOD: Less than comparison
46+
function checkMaxBalance() public view returns (bool) {
47+
return address(this).balance < 100 ether;
48+
}
49+
50+
// GOOD: Range check (uses >= and <=, not == or !=)
51+
function checkBalanceRange() public view returns (bool) {
52+
return address(this).balance >= 1 ether && address(this).balance <= 10 ether;
53+
}
54+
55+
// GOOD: Using balance in arithmetic (not strict equality)
56+
function addToBalance() public view returns (uint256) {
57+
return address(this).balance + 1 ether;
58+
}
59+
60+
// GOOD: Assigning to local variable only (not state)
61+
function getBalance() public view returns (uint256) {
62+
uint256 bal = address(this).balance;
63+
return bal;
64+
}
65+
}
66+
67+
contract ExternalBalanceTest {
68+
address public target;
69+
uint256 public externalBalance;
70+
71+
constructor(address _target) {
72+
target = _target;
73+
}
74+
75+
// BAD: Strict equality on external address balance
76+
function checkTargetBalance() public view returns (bool) {
77+
return target.balance == 1 ether;
78+
}
79+
80+
// BAD: Saving external balance to state
81+
function saveTargetBalance() public {
82+
externalBalance = target.balance;
83+
}
84+
85+
// GOOD: Greater than check on external balance
86+
function checkTargetMinBalance() public view returns (bool) {
87+
return target.balance >= 1 ether;
88+
}
89+
}
Binary file not shown.

tests/e2e/detectors/test_detectors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,6 +1492,11 @@ def id_test(test_item: Test):
14921492
"msg_value_in_nonpayable.sol",
14931493
"0.8.0",
14941494
),
1495+
Test(
1496+
all_detectors.BalanceReliance,
1497+
"balance_reliance.sol",
1498+
"0.8.0",
1499+
),
14951500
Test(
14961501
all_detectors.DelegatecallInLoop,
14971502
"delegatecall_loop.sol",

0 commit comments

Comments
 (0)