1use super::LockedEther;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{
7 block_outcome, expr_is_address, is_address_self, is_builtin, is_contract_cast,
8 is_literal_zero, runtime_entry_points,
9 },
10 },
11};
12use solar::{
13 ast::{ContractKind, StateMutability},
14 interface::{Span, kw, sym},
15 sema::{
16 Gcx,
17 builtins::Builtin,
18 hir::{
19 self, Block, ContractId, ExprKind, FunctionId, ItemId, Res, StmtKind, TypeKind, Visit,
20 },
21 },
22};
23use std::{collections::HashSet, ops::ControlFlow};
24
25declare_forge_lint!(
26 LOCKED_ETHER,
27 Severity::Med,
28 "locked-ether",
29 "contract can receive ETH but has no mechanism to send it out"
30);
31
32impl<'gcx> LateLintPass<'gcx> for LockedEther {
33 fn check_nested_contract(
34 &mut self,
35 ctx: &LintContext,
36 gcx: Gcx<'gcx>,
37 contract_id: ContractId,
38 ) {
39 let contract = gcx.hir.contract(contract_id);
40 if !ctx.is_lint_enabled(LOCKED_ETHER.id)
42 || !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract)
43 || contract.linearization_failed()
44 {
45 return;
46 }
47
48 let receives = |fid: FunctionId| {
49 let func = gcx.hir.function(fid);
50 func.state_mutability == StateMutability::Payable
51 && !always_reverts(gcx, contract_id, func)
52 };
53 let entries = runtime_entry_points(gcx, contract_id);
56 if !entries.iter().any(|&fid| receives(fid)) && !contract.ctor.is_some_and(receives) {
57 return;
58 }
59
60 let mut visited = HashSet::new();
63 let mut checker = SendChecker { gcx, contract_id, worklist: entries };
64 while let Some(fid) = checker.worklist.pop() {
65 let func = gcx.hir.function(fid);
66 if !visited.insert(fid) || always_reverts(gcx, contract_id, func) {
68 continue;
69 }
70 for modifier in func.modifiers {
71 if checker.visit_call_args(&modifier.args).is_break() {
72 return;
73 }
74 checker.worklist.extend(gcx.resolve_modifier_target(contract_id, modifier));
75 }
76 if let Some(body) = func.body
77 && body.stmts.iter().any(|stmt| checker.visit_stmt(stmt).is_break())
78 {
79 return;
80 }
81 }
82
83 ctx.emit(&LOCKED_ETHER, contract.name.span);
84 }
85}
86
87fn always_reverts(gcx: Gcx<'_>, contract: ContractId, func: &hir::Function<'_>) -> bool {
90 let reverts = |stmts: &[hir::Stmt<'_>]| {
91 !block_outcome(gcx, Block { span: Span::DUMMY, stmts }).can_skip_placeholder()
92 };
93 func.body.is_some_and(|body| reverts(body.stmts))
94 || func.modifiers.iter().any(|m| {
95 let Some(body) =
96 gcx.resolve_modifier_target(contract, m).and_then(|id| gcx.hir.function(id).body)
97 else {
98 return false;
99 };
100 let is_placeholder = |s: &hir::Stmt<'_>| matches!(s.kind, StmtKind::Placeholder);
101 let Some(first) = body.stmts.iter().position(is_placeholder) else {
102 return reverts(body.stmts);
103 };
104 let last = body.stmts.iter().rposition(is_placeholder).unwrap();
105 reverts(&body.stmts[..first]) || reverts(&body.stmts[last + 1..])
106 })
107}
108
109struct SendChecker<'gcx> {
112 gcx: Gcx<'gcx>,
113 contract_id: ContractId,
115 worklist: Vec<FunctionId>,
116}
117
118impl<'gcx> Visit<'gcx> for SendChecker<'gcx> {
119 type BreakValue = ();
120
121 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
122 &self.gcx.hir
123 }
124
125 fn visit_stmt(&mut self, stmt: &'gcx hir::Stmt<'gcx>) -> ControlFlow<()> {
128 if matches!(stmt.kind, StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_))
129 {
130 return ControlFlow::Break(());
131 }
132 self.walk_stmt(stmt)
133 }
134
135 fn visit_expr(&mut self, expr: &'gcx hir::Expr<'gcx>) -> ControlFlow<()> {
136 if expr_sends_ether(self.gcx, expr) {
137 return ControlFlow::Break(());
138 }
139 if let ExprKind::Call(callee, ..) = &expr.kind {
140 match self.gcx.resolved_expr(callee) {
141 Some(Res::Item(ItemId::Function(fid))) => {
142 let direct = matches!(&callee.peel_parens().kind, ExprKind::Member(base, _)
145 if is_builtin(self.gcx, base, sym::super_) || is_contract_cast(self.gcx, base));
146 self.worklist.push(if direct {
147 fid
148 } else {
149 self.gcx.resolve_virtual_function(self.contract_id, fid)
150 });
151 }
152 Some(Res::Item(ItemId::Variable(id)))
154 if matches!(self.gcx.hir.variable(id).ty.kind, TypeKind::Function(_)) =>
155 {
156 return ControlFlow::Break(());
157 }
158 _ => {}
159 }
160 }
161 self.walk_expr(expr)
162 }
163}
164
165fn expr_sends_ether<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx hir::Expr<'gcx>) -> bool {
170 let ExprKind::Call(callee, args, opts) = &expr.kind else { return false };
171 let callee = callee.peel_parens();
172 let receiver = match &callee.kind {
173 ExprKind::Member(receiver, _) => Some(receiver),
174 _ => None,
175 };
176 if opts.is_some_and(|opts| {
177 opts.args.iter().any(|arg| arg.name.name == sym::value && !is_literal_zero(&arg.value))
178 }) && !receiver.is_some_and(|r| is_address_self(gcx, r))
179 {
180 return true;
181 }
182 match &callee.kind {
183 ExprKind::Member(receiver, member)
186 if expr_is_address(gcx, receiver) && !is_address_self(gcx, receiver) =>
187 {
188 match member.name {
189 sym::transfer | sym::send => {
191 args.len() == 1 && !args.exprs().next().is_some_and(is_literal_zero)
192 }
193 kw::Delegatecall | kw::Callcode => true,
194 kw::Call | kw::Staticcall => false,
195 _ => true,
198 }
199 }
200 ExprKind::Ident(_) if gcx.resolved_builtin(callee) == Some(Builtin::Selfdestruct) => {
201 !args.exprs().next().is_some_and(|expr| is_address_self(gcx, expr))
203 }
204 _ => false,
205 }
206}