1use super::ArbitrarySendEth;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{
7 DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT, HelperAnalysisCache, arg_for_param,
8 branch_always_exits, callee_no_arg_returns, function_no_arg_returns,
9 is_address_like_cast, is_address_self, is_builtin, is_contract_cast, is_literal_zero,
10 is_msg_sender, is_require_or_assert, is_sender_member, loop_stmts, loop_update,
11 modifier_prefix, referenced_item, tuple_elems, underlying_var, var_is_address_like,
12 },
13 },
14};
15use solar::{
16 ast::{BinOpKind, LitKind, StateMutability, UnOpKind},
17 interface::{Span, data_structures::Never, sym},
18 sema::{
19 Gcx,
20 builtins::Builtin,
21 hir::{
22 self, CallArgs, ContractId, ContractKind, ElementaryType, Expr, ExprKind, FunctionId,
23 Hir, ItemId, LoopSource, Modifier, Res, Stmt, StmtKind, TypeKind, Variable, VariableId,
24 Visit,
25 },
26 ty::TyKind,
27 },
28};
29use std::{collections::HashSet, ops::ControlFlow};
30
31declare_forge_lint!(
32 ARBITRARY_SEND_ETH,
33 Severity::High,
34 "arbitrary-send-eth",
35 "ETH is sent to a user-controlled destination; restrict the destination or the caller"
36);
37
38const HELPER_DEPTH: u8 = 3;
40
41const SELF_ALIAS_DEPTH: u8 = 8;
43
44const HELPER_CALL_DEPTH: usize = 4;
46
47impl<'gcx> LateLintPass<'gcx> for ArbitrarySendEth {
48 fn check_function(
49 &mut self,
50 ctx: &LintContext,
51 gcx: Gcx<'gcx>,
52 func: &'gcx hir::Function<'gcx>,
53 ) {
54 if matches!(func.state_mutability, StateMutability::Pure | StateMutability::View)
55 || func.is_constructor()
56 || func.contract.is_some_and(|cid| gcx.hir.contract(cid).kind == ContractKind::Library)
57 {
58 return;
59 }
60 let Some(body) = func.body else { return };
61
62 let mut args = Analyzer::new(gcx);
64 for arg in func.modifiers.iter().flat_map(|m| m.args.exprs()) {
65 let _ = args.visit_expr(arg);
66 }
67 for span in args.hits {
68 ctx.emit(&ARBITRARY_SEND_ETH, span);
69 }
70
71 let mut a = Analyzer::new(gcx);
72 for m in func.modifiers {
73 a.hoist_modifier_facts(m);
74 }
75 a.visit_stmts(body.stmts);
76 if !a.hits.is_empty() && !func.modifiers.iter().any(|m| a.guards.modifier_restricts(m)) {
77 for span in a.hits {
78 ctx.emit(&ARBITRARY_SEND_ETH, span);
79 }
80 }
81 }
82}
83
84#[derive(Clone, Default)]
86struct State {
87 safe_vars: HashSet<VariableId>,
89 caller_restricted: bool,
91}
92
93impl State {
94 fn meet(&self, other: &Self) -> Self {
95 Self {
96 safe_vars: self.safe_vars.intersection(&other.safe_vars).copied().collect(),
97 caller_restricted: self.caller_restricted && other.caller_restricted,
98 }
99 }
100}
101
102struct Analyzer<'gcx> {
103 gcx: Gcx<'gcx>,
104 guards: CallerGuards<'gcx>,
105 state: State,
106 loop_exits: Vec<Vec<State>>,
108 written: HashSet<VariableId>,
110 hits: Vec<Span>,
111}
112
113impl<'gcx> Analyzer<'gcx> {
114 fn new(gcx: Gcx<'gcx>) -> Self {
115 Self {
116 gcx,
117 guards: CallerGuards::new(gcx),
118 state: State::default(),
119 loop_exits: Vec::new(),
120 written: HashSet::new(),
121 hits: Vec::new(),
122 }
123 }
124
125 fn hoist_modifier_facts(&mut self, m: &'gcx Modifier<'gcx>) {
128 let ItemId::Function(fid) = m.id else { return };
129 let Some(prefix) = modifier_prefix(&self.gcx.hir, fid) else { return };
130 let modifier = self.gcx.hir.function(fid);
131 let mut a = Self::new(self.gcx);
132 for stmt in prefix {
133 a.stmt(stmt);
134 }
135 for ¶m in modifier.parameters {
136 if a.state.safe_vars.contains(¶m)
137 && !a.written.contains(¶m)
138 && let Some(caller) = arg_for_param(self.gcx, fid, param, &m.args)
139 .and_then(|expr| underlying_var(self.gcx, expr))
140 && self.is_safe_target(caller)
141 {
142 self.state.safe_vars.insert(caller);
143 }
144 }
145 }
146
147 fn is_safe(&self, expr: &'gcx Expr<'gcx>) -> bool {
151 self.is_safe_inner(expr, HELPER_DEPTH)
152 }
153
154 fn is_safe_inner(&self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
155 let expr = peel_casts(self.gcx, expr);
156 match &expr.kind {
157 ExprKind::Member(base, _) => {
158 is_sender_member(self.gcx, expr) || is_address_self(self.gcx, base)
159 }
160 ExprKind::Lit(_) => is_trusted_literal(expr),
161 ExprKind::Ident(_) => {
162 is_builtin(self.gcx, expr, sym::this)
163 || self.gcx.resolved_variable(expr).is_some_and(|v| self.is_safe_var(v))
164 }
165 ExprKind::Ternary(_, t, f) => {
166 self.is_safe_inner(t, depth) && self.is_safe_inner(f, depth)
167 }
168 ExprKind::Call(callee, args, _) => {
169 depth > 0
170 && args.exprs().next().is_none()
171 && callee_no_arg_returns(self.gcx, callee, |e| self.is_safe_inner(e, depth - 1))
172 }
173 _ => false,
174 }
175 }
176
177 fn is_safe_var(&self, v: VariableId) -> bool {
178 let var = self.gcx.hir.variable(v);
179 self.state.safe_vars.contains(&v)
180 || (var.kind.is_state() && (var.is_immutable() || var.is_constant()))
181 }
182
183 fn is_safe_target(&self, v: VariableId) -> bool {
186 let var = self.gcx.hir.variable(v);
187 !var.kind.is_state() || var.is_immutable() || var.is_constant()
188 }
189
190 fn assign_var(&mut self, target: VariableId, rhs: Option<&'gcx Expr<'gcx>>) {
192 self.written.insert(target);
193 self.state.safe_vars.remove(&target);
194 if !self.gcx.hir.variable(target).kind.is_state() && rhs.is_some_and(|r| self.is_safe(r)) {
195 self.state.safe_vars.insert(target);
196 }
197 }
198
199 fn assign_lhs(&mut self, lhs: &'gcx Expr<'gcx>, rhs: Option<&'gcx Expr<'gcx>>) {
201 if let Some(elems) = tuple_elems(lhs) {
202 let rhs = rhs.and_then(tuple_elems);
203 for (i, lhs) in elems.iter().enumerate() {
204 if let Some(lhs) = lhs {
205 self.assign_lhs(lhs, rhs.and_then(|r| r.get(i).copied().flatten()));
206 }
207 }
208 } else if let Some(v) = underlying_var(self.gcx, lhs) {
209 self.assign_var(v, rhs);
210 }
211 }
212
213 fn add_facts(&mut self, pred: &'gcx Expr<'gcx>, negate: bool) {
215 match &pred.peel_parens().kind {
216 ExprKind::Binary(lhs, op, rhs) => {
217 let (eq, and, or) = if negate {
218 (BinOpKind::Ne, BinOpKind::Or, BinOpKind::And)
219 } else {
220 (BinOpKind::Eq, BinOpKind::And, BinOpKind::Or)
221 };
222 if op.kind == and {
223 self.add_facts(lhs, negate);
224 self.add_facts(rhs, negate);
225 } else if op.kind == or {
226 let before = self.state.clone();
228 self.add_facts(lhs, negate);
229 let after_lhs = std::mem::replace(&mut self.state, before);
230 self.add_facts(rhs, negate);
231 self.state = after_lhs.meet(&self.state);
232 } else if op.kind == eq {
233 for (a, b) in [(lhs, rhs), (rhs, lhs)] {
234 if self.is_safe(a)
235 && let Some(v) = underlying_var(self.gcx, b)
236 && self.is_safe_target(v)
237 {
238 self.state.safe_vars.insert(v);
239 }
240 }
241 }
242 }
243 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
244 self.add_facts(inner, !negate);
245 }
246 _ => {}
247 }
248 }
249
250 fn note_guard(&mut self, cond: &'gcx Expr<'gcx>, holds: bool) {
252 self.add_facts(cond, !holds);
253 if self.guards.cond_restricts(cond, holds) {
254 self.state.caller_restricted = true;
255 }
256 }
257
258 fn visit_stmts(&mut self, stmts: &'gcx [Stmt<'gcx>]) -> bool {
261 stmts.iter().all(|s| self.stmt(s))
262 }
263
264 fn stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> bool {
266 match &stmt.kind {
267 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => return self.visit_stmts(b.stmts),
268 StmtKind::Break | StmtKind::Continue => {
269 let state = self.state.clone();
270 if let Some(exits) = self.loop_exits.last_mut() {
271 exits.push(state);
272 }
273 return false;
274 }
275 StmtKind::If(cond, then, else_) => {
276 let _ = self.visit_expr(cond);
277 let before = self.state.clone();
278 self.note_guard(cond, true);
279 let then_falls = self.stmt(then);
280 let after_then = std::mem::replace(&mut self.state, before);
281 self.note_guard(cond, false);
282 let else_falls = else_.is_none_or(|e| self.stmt(e));
283 match (then_falls, else_falls) {
284 (true, true) => self.state = after_then.meet(&self.state),
285 (true, false) => self.state = after_then,
286 _ => {}
287 }
288 return then_falls || else_falls;
289 }
290 StmtKind::Loop(block, source) => {
291 let baseline = (!matches!(source, LoopSource::DoWhile)).then(|| self.state.clone());
294 self.loop_exits.push(baseline.into_iter().collect());
295 if self.visit_stmts(block.stmts)
296 && loop_update(*source).is_none_or(|update| self.stmt(update))
297 {
298 let state = self.state.clone();
299 self.loop_exits.last_mut().expect("pushed above").push(state);
300 }
301 let exits = self.loop_exits.pop().expect("pushed above");
302 let falls = !exits.is_empty();
303 if let Some(joined) = exits.into_iter().reduce(|a, b| a.meet(&b)) {
304 self.state = joined;
305 }
306 return falls;
307 }
308 StmtKind::Try(t) => {
309 let _ = self.visit_expr(&t.expr);
310 let outer = self.state.clone();
311 let mut joined = None::<State>;
312 for clause in t.clauses {
313 self.state = outer.clone();
314 if self.visit_stmts(clause.block.stmts) {
315 joined = Some(
316 joined.map_or_else(|| self.state.clone(), |j| j.meet(&self.state)),
317 );
318 }
319 }
320 let falls = joined.is_some();
321 self.state = joined.unwrap_or(outer);
322 return falls;
323 }
324 StmtKind::DeclSingle(vid) => {
325 if let Some(init) = self.gcx.hir.variable(*vid).initializer {
326 self.assign_var(*vid, Some(init));
327 }
328 }
329 StmtKind::DeclMulti(vars, init) => {
330 for (vid, rhs) in vars.iter().zip(tuple_elems(init).into_iter().flatten()) {
331 if let (Some(vid), Some(rhs)) = (vid, rhs) {
332 self.assign_var(*vid, Some(rhs));
333 }
334 }
335 }
336 _ => {}
337 }
338 let _ = self.walk_stmt(stmt);
339 !branch_always_exits(self.gcx, stmt)
340 }
341}
342
343impl<'gcx> Visit<'gcx> for Analyzer<'gcx> {
344 type BreakValue = Never;
345
346 fn hir(&self) -> &'gcx Hir<'gcx> {
347 &self.gcx.hir
348 }
349
350 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
351 self.stmt(stmt);
352 ControlFlow::Continue(())
353 }
354
355 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
356 match &expr.kind {
357 ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
360 let _ = self.visit_expr(lhs);
361 let skipped = self.state.clone();
362 self.add_facts(lhs, op.kind == BinOpKind::Or);
363 let _ = self.visit_expr(rhs);
364 self.state = skipped.meet(&self.state);
365 }
366 ExprKind::Ternary(cond, t, f) => {
367 let _ = self.visit_expr(cond);
368 let before = self.state.clone();
369 self.add_facts(cond, false);
370 let _ = self.visit_expr(t);
371 let after_t = std::mem::replace(&mut self.state, before);
372 self.add_facts(cond, true);
373 let _ = self.visit_expr(f);
374 self.state = after_t.meet(&self.state);
375 }
376 ExprKind::Call(callee, args, _) if is_require_or_assert(self.gcx, callee) => {
377 let _ = self.walk_expr(expr);
379 if let Some(cond) = args.exprs().next() {
380 self.note_guard(cond, true);
381 }
382 }
383 ExprKind::Call(..) => {
384 if !self.state.caller_restricted
385 && let Some(dest) = match_sink(self.gcx, expr)
386 && !self.is_safe(dest)
387 {
388 self.hits.push(expr.span);
389 }
390 let _ = self.walk_expr(expr);
391 }
392 ExprKind::Assign(lhs, _, rhs) => {
393 self.assign_lhs(lhs, Some(rhs));
394 let _ = self.walk_expr(expr);
395 }
396 ExprKind::Delete(target) => {
397 self.assign_lhs(target, None);
398 let _ = self.walk_expr(expr);
399 }
400 _ => {
401 let _ = self.walk_expr(expr);
402 }
403 }
404 ControlFlow::Continue(())
405 }
406}
407
408fn match_sink<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> Option<&'gcx Expr<'gcx>> {
412 let ExprKind::Call(callee, args, opts) = &expr.kind else { return None };
413 let callee = callee.peel_parens();
414 if gcx.resolved_builtin(callee) == Some(Builtin::Selfdestruct) {
415 return args.exprs().next().filter(|dest| !is_address_self(gcx, dest));
416 }
417 if opts.is_some_and(|o| {
418 o.args.iter().any(|a| a.name.name == sym::value && !is_literal_zero(&a.value))
419 }) {
420 return match &callee.kind {
421 ExprKind::Member(recv, _) => (!is_address_self(gcx, recv)).then_some(recv),
422 _ => expr_is_function(gcx, callee).then_some(callee),
423 };
424 }
425 let ExprKind::Member(recv, member) = &callee.kind else { return None };
426 if matches!(
427 gcx.resolved_builtin(callee),
428 Some(Builtin::AddressPayableTransfer | Builtin::AddressPayableSend)
429 ) {
430 return (!is_address_self(gcx, recv) && !is_literal_zero(args.exprs().next()?))
431 .then_some(recv);
432 }
433 match_eth_library_call(gcx, expr, recv, member.name.as_str(), args)
434}
435
436fn match_eth_library_call<'gcx>(
439 gcx: Gcx<'gcx>,
440 call: &Expr<'gcx>,
441 recv: &'gcx Expr<'gcx>,
442 name: &str,
443 args: &'gcx CallArgs<'gcx>,
444) -> Option<&'gcx Expr<'gcx>> {
445 let (amount, arities): (Option<usize>, &[usize]) = match name {
447 "sendValue" | "safeTransferETH" | "safeMoveETH" => (Some(1), &[2]),
448 "forceSafeTransferETH" => (Some(1), &[2, 3]),
449 "trySafeTransferETH" => (Some(1), &[3]),
450 "functionCallWithValue" => (Some(2), &[3, 4]),
451 "safeTransferAllETH" => (None, &[1]),
452 "forceSafeTransferAllETH" => (None, &[1, 2]),
453 "trySafeTransferAllETH" => (None, &[2]),
454 _ => return None,
455 };
456 let using = gcx.resolved_call(call).is_some_and(|resolved| resolved.attached);
457 let is_lib = matches!(referenced_item(gcx, recv), Some(ItemId::Contract(cid))
458 if gcx.hir.contract(cid).kind == ContractKind::Library);
459 if (!using && !is_lib) || !arities.contains(&(args.len() + usize::from(using))) {
460 return None;
461 }
462 let dest = if using { recv } else { gcx.call_arg(call, 0)? };
463 if let Some(i) = amount
464 && is_literal_zero(gcx.call_arg(call, i - usize::from(using))?)
465 {
466 return None;
467 }
468 (!is_address_self(gcx, dest)).then_some(dest)
469}
470
471struct CallerGuards<'gcx> {
474 gcx: Gcx<'gcx>,
475 alias_cache: HelperAnalysisCache<(VariableId, u8), bool>,
476 stack: Vec<FunctionId>,
478}
479
480impl<'gcx> CallerGuards<'gcx> {
481 fn new(gcx: Gcx<'gcx>) -> Self {
482 Self {
483 gcx,
484 alias_cache: HelperAnalysisCache::new(DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT),
485 stack: Vec::new(),
486 }
487 }
488
489 fn modifier_restricts(&mut self, m: &Modifier<'_>) -> bool {
491 let ItemId::Function(fid) = m.id else { return false };
492 modifier_prefix(&self.gcx.hir, fid)
493 .is_some_and(|prefix| prefix.into_iter().any(|s| self.stmt_restricts(s)))
494 }
495
496 fn stmt_restricts(&mut self, stmt: &'gcx Stmt<'gcx>) -> bool {
497 match &stmt.kind {
498 StmtKind::Expr(e) => self.expr_restricts(e),
499 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
500 b.stmts.iter().any(|s| self.stmt_restricts(s))
501 }
502 StmtKind::If(cond, then, else_) => {
503 let then_exits = branch_always_exits(self.gcx, then);
504 let else_exits = else_.is_some_and(|e| branch_always_exits(self.gcx, e));
505 (then_exits != else_exits && self.cond_restricts(cond, else_exits))
508 || ((then_exits || self.stmt_restricts(then))
509 && (else_exits || else_.is_some_and(|e| self.stmt_restricts(e))))
510 }
511 _ => false,
512 }
513 }
514
515 fn expr_restricts(&mut self, expr: &'gcx Expr<'gcx>) -> bool {
518 let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return false };
519 if is_require_or_assert(self.gcx, callee) {
520 return args.exprs().next().is_some_and(|c| self.cond_restricts(c, true));
521 }
522 self.gcx
523 .resolved_function(callee)
524 .filter(|_| matches!(callee.peel_parens().kind, ExprKind::Ident(_)))
525 .is_some_and(|fid| {
526 if self.stack.contains(&fid) {
527 return false;
528 }
529 let Some(body) = self.gcx.hir.function(fid).body else { return false };
530 let mut stmts = body.stmts;
532 while let [rest @ .., last] = stmts
533 && matches!(last.kind, StmtKind::Return(None))
534 {
535 stmts = rest;
536 }
537 if stmts.iter().any(stmt_contains_return) {
538 return false;
539 }
540 self.stack.push(fid);
541 let restricts = stmts.iter().any(|s| self.stmt_restricts(s));
542 self.stack.pop();
543 restricts
544 })
545 }
546
547 fn cond_restricts(&mut self, cond: &'gcx Expr<'gcx>, holds: bool) -> bool {
549 match &cond.peel_parens().kind {
550 ExprKind::Binary(lhs, op, rhs) => {
551 let (eq, any, all) = if holds {
552 (BinOpKind::Eq, BinOpKind::And, BinOpKind::Or)
553 } else {
554 (BinOpKind::Ne, BinOpKind::Or, BinOpKind::And)
555 };
556 if op.kind == any {
557 self.cond_restricts(lhs, holds) || self.cond_restricts(rhs, holds)
558 } else if op.kind == all {
559 self.cond_restricts(lhs, holds) && self.cond_restricts(rhs, holds)
560 } else if op.kind == eq {
561 [(lhs, rhs), (rhs, lhs)].into_iter().any(|(a, b)| {
562 is_msg_sender_like(self.gcx, a, HELPER_DEPTH)
563 && self.is_trusted_principal(b, HELPER_DEPTH)
564 })
565 } else {
566 false
567 }
568 }
569 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
570 self.cond_restricts(inner, !holds)
571 }
572 _ => false,
573 }
574 }
575
576 fn is_trusted_principal(&mut self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
580 let expr = peel_casts(self.gcx, expr);
581 match &expr.kind {
582 ExprKind::Lit(_) => is_trusted_literal(expr),
583 ExprKind::Ident(_) => self.gcx.resolved_variable(expr).is_some_and(|v| {
584 self.gcx.hir.variable(v).kind.is_state()
585 && !self.state_var_aliases_self(v, SELF_ALIAS_DEPTH)
586 }),
587 ExprKind::Member(base, _) => self.is_trusted_principal(base, depth),
588 ExprKind::Index(base, idx) => {
589 self.is_trusted_principal(base, depth)
590 && idx.is_none_or(|i| index_is_static(self.gcx, i))
591 }
592 ExprKind::Call(callee, args, _) => {
593 depth > 0
594 && args.exprs().next().is_none()
595 && callee_no_arg_returns(self.gcx, callee, |e| {
596 self.is_trusted_principal(e, depth - 1)
597 })
598 }
599 _ => false,
600 }
601 }
602
603 fn state_var_aliases_self(&mut self, v: VariableId, depth: u8) -> bool {
606 let var = self.gcx.hir.variable(v);
607 if depth == 0 || !var.kind.is_state() {
608 return false;
609 }
610 let key = (v, depth);
611 if let Some(cached) = self.alias_cache.get(&key) {
612 return *cached;
613 }
614 if self.alias_cache.is_in_progress(&key) {
615 return false;
616 }
617 self.alias_cache.start(key);
618 let aliases = var
619 .initializer
620 .is_some_and(|init| self.rhs_carries_self(var, init, depth - 1, &HashSet::new()))
621 || var.contract.is_some_and(|cid| {
622 self.gcx.hir.contracts_enumerated().any(|(c, contract)| {
623 (c == cid || contract.linearized_bases.contains(&cid))
624 && self.contract_assigns_self(c, v, depth - 1)
625 })
626 });
627 self.alias_cache.finish(key, aliases);
628 aliases
629 }
630
631 fn rhs_carries_self(
634 &mut self,
635 target: &Variable<'_>,
636 rhs: &'gcx Expr<'gcx>,
637 depth: u8,
638 locals: &HashSet<VariableId>,
639 ) -> bool {
640 if var_is_address_like(target) {
641 self.expr_resolves_to_self(rhs, depth)
642 || lhs_root_var(self.gcx, rhs).is_some_and(|v| locals.contains(&v))
643 } else {
644 self.expr_may_contain_self(rhs, depth, locals)
645 }
646 }
647
648 fn expr_may_contain_self(
650 &mut self,
651 expr: &'gcx Expr<'gcx>,
652 depth: u8,
653 locals: &HashSet<VariableId>,
654 ) -> bool {
655 if self.expr_resolves_to_self(expr, depth)
656 || lhs_root_var(self.gcx, expr).is_some_and(|v| locals.contains(&v))
657 {
658 return true;
659 }
660 if depth == 0 {
661 return false;
662 }
663 let children: Vec<&'gcx Expr<'gcx>> = match &peel_casts(self.gcx, expr).kind {
664 ExprKind::Call(_, args, _) => args.exprs().collect(),
665 ExprKind::Ternary(_, t, f) => vec![t, f],
666 ExprKind::Tuple(elems) => elems.iter().copied().flatten().collect(),
667 ExprKind::Array(elems) => elems.iter().collect(),
668 _ => Vec::new(),
669 };
670 children.into_iter().any(|e| self.expr_may_contain_self(e, depth - 1, locals))
671 }
672
673 fn expr_resolves_to_self(&mut self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
675 let expr = peel_casts(self.gcx, expr);
676 if is_address_self(self.gcx, expr) {
677 return true;
678 }
679 if depth == 0 {
680 return false;
681 }
682 match &expr.kind {
683 ExprKind::Ident(_) | ExprKind::Member(..) | ExprKind::Index(..) => {
684 lhs_root_var(self.gcx, expr).is_some_and(|v| self.state_var_aliases_self(v, depth))
685 }
686 ExprKind::Call(callee, args, _) if args.exprs().next().is_none() => {
687 self.gcx.resolved_function(callee).is_some_and(|fid| {
688 function_no_arg_returns(self.gcx, fid, &mut |e| {
689 self.expr_resolves_to_self(e, depth - 1)
690 })
691 })
692 }
693 ExprKind::Call(callee, args, _) => identity_helper_arg(self.gcx, callee, args)
694 .is_some_and(|a| self.expr_resolves_to_self(a, depth - 1)),
695 ExprKind::Ternary(_, t, f) => {
696 self.expr_resolves_to_self(t, depth - 1) || self.expr_resolves_to_self(f, depth - 1)
697 }
698 ExprKind::Assign(_, _, rhs) => self.expr_resolves_to_self(rhs, depth - 1),
699 _ => false,
700 }
701 }
702
703 fn contract_assigns_self(&mut self, cid: ContractId, v: VariableId, depth: u8) -> bool {
705 self.gcx.hir.contract(cid).all_functions().any(|fid| {
706 let mut scan = SelfAssignScan {
707 guards: &mut *self,
708 target: v,
709 depth,
710 found: false,
711 stack: Vec::new(),
712 locals: HashSet::new(),
713 };
714 scan.scan_function(fid, None);
715 scan.found
716 })
717 }
718}
719
720struct SelfAssignScan<'a, 'gcx> {
723 guards: &'a mut CallerGuards<'gcx>,
724 target: VariableId,
725 depth: u8,
726 found: bool,
727 stack: Vec<FunctionId>,
728 locals: HashSet<VariableId>,
730}
731
732impl<'gcx> SelfAssignScan<'_, 'gcx> {
733 fn may_contain_self(&mut self, expr: &'gcx Expr<'gcx>) -> bool {
734 self.guards.expr_may_contain_self(expr, self.depth, &self.locals)
735 }
736
737 fn note_local(&mut self, v: VariableId, rhs: &'gcx Expr<'gcx>) {
738 if !self.guards.gcx.hir.variable(v).kind.is_state() && self.may_contain_self(rhs) {
739 self.locals.insert(v);
740 }
741 }
742
743 fn assign(&mut self, lhs: &'gcx Expr<'gcx>, rhs: &'gcx Expr<'gcx>) {
744 if let Some(elems) = tuple_elems(lhs) {
745 let rhs = tuple_elems(rhs);
746 for (i, lhs) in elems.iter().enumerate() {
747 if let Some(lhs) = lhs
748 && let Some(rhs) = rhs.and_then(|r| r.get(i).copied().flatten())
749 {
750 self.assign(lhs, rhs);
751 }
752 }
753 } else if let Some(v) = lhs_root_var(self.guards.gcx, lhs) {
754 if v == self.target {
755 let var = self.guards.gcx.hir.variable(v);
756 self.found |= self.guards.rhs_carries_self(var, rhs, self.depth, &self.locals);
757 } else {
758 self.note_local(v, rhs);
759 }
760 }
761 }
762
763 fn scan_function(&mut self, fid: FunctionId, args: Option<&'gcx CallArgs<'gcx>>) {
765 if self.found || self.stack.len() >= HELPER_CALL_DEPTH || self.stack.contains(&fid) {
766 return;
767 }
768 let f = self.guards.gcx.hir.function(fid);
769 let Some(body) = f.body else { return };
770 let saved = self.locals.clone();
771 for ¶m in f.parameters {
772 if let Some(arg) =
773 args.and_then(|args| arg_for_param(self.guards.gcx, fid, param, args))
774 && self.may_contain_self(arg)
775 {
776 self.locals.insert(param);
777 }
778 }
779 self.stack.push(fid);
780 for m in f.modifiers {
781 if let Some(invoked) = invoked_function(&self.guards.gcx.hir, m) {
782 self.scan_function(invoked, Some(&m.args));
783 }
784 }
785 for stmt in body.stmts {
786 let _ = self.visit_stmt(stmt);
787 }
788 self.stack.pop();
789 self.locals = saved;
790 }
791}
792
793impl<'gcx> Visit<'gcx> for SelfAssignScan<'_, 'gcx> {
794 type BreakValue = Never;
795
796 fn hir(&self) -> &'gcx Hir<'gcx> {
797 &self.guards.gcx.hir
798 }
799
800 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
801 if self.found {
802 return ControlFlow::Continue(());
803 }
804 match &stmt.kind {
805 StmtKind::DeclSingle(vid) => {
806 if let Some(init) = self.guards.gcx.hir.variable(*vid).initializer {
807 self.note_local(*vid, init);
808 }
809 }
810 StmtKind::DeclMulti(vars, init) => {
811 for (vid, rhs) in vars.iter().zip(tuple_elems(init).into_iter().flatten()) {
812 if let (Some(vid), Some(rhs)) = (vid, rhs) {
813 self.note_local(*vid, rhs);
814 }
815 }
816 }
817 _ => {}
818 }
819 self.walk_stmt(stmt)
820 }
821
822 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
823 if self.found {
824 return ControlFlow::Continue(());
825 }
826 match &expr.peel_parens().kind {
827 ExprKind::Assign(lhs, _, rhs) => self.assign(lhs, rhs),
828 ExprKind::Call(callee, args, _) => match &callee.peel_parens().kind {
829 ExprKind::Member(recv, member) => {
831 if member.name.as_str() == "push"
832 && lhs_root_var(self.guards.gcx, recv) == Some(self.target)
833 && expr_is_array_or_bytes(self.guards.gcx, recv)
834 && args.exprs().any(|a| self.may_contain_self(a))
835 {
836 self.found = true;
837 }
838 }
839 _ => {
840 if let Some(fid) = self.guards.gcx.resolved_function(callee) {
841 self.scan_function(fid, Some(args));
842 }
843 }
844 },
845 _ => {}
846 }
847 self.walk_expr(expr)
848 }
849}
850
851fn invoked_function(hir: &Hir<'_>, m: &Modifier<'_>) -> Option<FunctionId> {
853 match m.id {
854 ItemId::Function(fid) => Some(fid),
855 ItemId::Contract(cid) => hir.contract(cid).ctor,
856 _ => None,
857 }
858}
859
860fn identity_helper_arg<'gcx>(
862 gcx: Gcx<'gcx>,
863 callee: &'gcx Expr<'gcx>,
864 args: &'gcx CallArgs<'gcx>,
865) -> Option<&'gcx Expr<'gcx>> {
866 gcx.resolved_function(callee).and_then(|fid| {
867 let f = gcx.hir.function(fid);
868 let [stmt] = f.body?.stmts else { return None };
869 let StmtKind::Return(Some(ret)) = &stmt.kind else { return None };
870 let param = underlying_var(gcx, peel_casts(gcx, ret))?;
871 (f.parameters.len() == args.len() && f.returns.len() == 1 && f.parameters.contains(¶m))
872 .then(|| arg_for_param(gcx, fid, param, args))
873 .flatten()
874 })
875}
876
877fn lhs_root_var(gcx: Gcx<'_>, lhs: &Expr<'_>) -> Option<VariableId> {
879 match &lhs.peel_parens().kind {
880 ExprKind::Member(base, _) | ExprKind::Index(base, _) | ExprKind::Payable(base) => {
881 lhs_root_var(gcx, base)
882 }
883 ExprKind::Call(callee, args, _) if is_address_like_cast(gcx, callee) => {
884 args.exprs().next().and_then(|expr| lhs_root_var(gcx, expr))
885 }
886 _ => underlying_var(gcx, lhs),
887 }
888}
889
890fn index_is_static(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
893 expr.visit(&mut |e| match &e.kind {
894 ExprKind::Lit(_)
895 | ExprKind::Type(_)
896 | ExprKind::Payable(_)
897 | ExprKind::Unary(..)
898 | ExprKind::Binary(..)
899 | ExprKind::Member(..)
900 | ExprKind::Index(..)
901 | ExprKind::Ternary(..)
902 | ExprKind::Tuple([Some(_)]) => ControlFlow::Continue(()),
903 ExprKind::Ident(_)
904 if gcx.resolved_expr(e).is_some_and(|res| match res {
905 Res::Item(ItemId::Variable(v)) => gcx.hir.variable(v).kind.is_state(),
906 Res::Builtin(_) => false,
907 _ => true,
908 }) =>
909 {
910 ControlFlow::Continue(())
911 }
912 ExprKind::Call(callee, ..)
913 if matches!(callee.peel_parens().kind, ExprKind::Type(_))
914 || is_contract_cast(gcx, callee) =>
915 {
916 ControlFlow::Continue(())
917 }
918 _ => ControlFlow::Break(()),
919 })
920 .is_continue()
921}
922
923fn stmt_contains_return(stmt: &Stmt<'_>) -> bool {
925 match &stmt.kind {
926 StmtKind::Return(_) => true,
927 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
928 b.stmts.iter().any(stmt_contains_return)
929 }
930 StmtKind::Loop(b, source) => loop_stmts(*b, *source).any(stmt_contains_return),
931 StmtKind::If(_, t, e) => {
932 stmt_contains_return(t) || e.is_some_and(|e| stmt_contains_return(e))
933 }
934 StmtKind::Try(t) => {
935 t.clauses.iter().any(|c| c.block.stmts.iter().any(stmt_contains_return))
936 }
937 _ => false,
938 }
939}
940
941fn is_msg_sender_like<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
943 let expr = peel_casts(gcx, expr);
944 is_msg_sender(gcx, expr)
945 || matches!(&expr.kind, ExprKind::Call(callee, args, _)
946 if depth > 0
947 && args.exprs().next().is_none()
948 && callee_no_arg_returns(gcx, callee, |e| is_msg_sender_like(gcx, e, depth - 1)))
949}
950
951fn peel_casts<'a>(gcx: Gcx<'_>, expr: &'a Expr<'a>) -> &'a Expr<'a> {
953 let expr = expr.peel_parens();
954 match &expr.kind {
955 ExprKind::Payable(inner) => peel_casts(gcx, inner),
956 ExprKind::Call(callee, args, _)
957 if is_address_like_cast(gcx, callee) || is_numeric_cast(callee) =>
958 {
959 args.exprs().next().map_or(expr, |expr| peel_casts(gcx, expr))
960 }
961 _ => expr,
962 }
963}
964
965fn is_numeric_cast(callee: &Expr<'_>) -> bool {
967 matches!(
968 &callee.peel_parens().kind,
969 ExprKind::Type(hir::Type {
970 kind: TypeKind::Elementary(ElementaryType::UInt(_) | ElementaryType::Int(_)),
971 ..
972 })
973 )
974}
975
976fn is_trusted_literal(expr: &Expr<'_>) -> bool {
978 matches!(&expr.kind, ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Address(_)))
979 || is_literal_zero(expr)
980}
981
982fn expr_is_function<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> bool {
983 gcx.type_of_expr(expr.peel_parens().id)
984 .is_some_and(|ty| matches!(ty.peel_refs().kind, TyKind::Fn(_)))
985}
986
987fn expr_is_array_or_bytes<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> bool {
988 gcx.type_of_expr(expr.peel_parens().id).is_some_and(|ty| {
989 matches!(
990 ty.peel_refs().kind,
991 TyKind::Array(..) | TyKind::DynArray(_) | TyKind::Elementary(ElementaryType::Bytes)
992 )
993 })
994}