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 (callee, args, opts) = expr.as_call()?;
413 if gcx.resolved_builtin(callee) == Some(Builtin::Selfdestruct) {
414 return args.exprs().next().filter(|dest| !is_address_self(gcx, dest));
415 }
416 if opts.is_some_and(|o| {
417 o.args.iter().any(|a| a.name.name == sym::value && !is_literal_zero(&a.value))
418 }) {
419 return match &callee.kind {
420 ExprKind::Member(recv, _) => (!is_address_self(gcx, recv)).then_some(recv),
421 _ => expr_is_function(gcx, callee).then_some(callee),
422 };
423 }
424 let ExprKind::Member(recv, member) = &callee.kind else { return None };
425 if matches!(
426 gcx.resolved_builtin(callee),
427 Some(Builtin::AddressPayableTransfer | Builtin::AddressPayableSend)
428 ) {
429 return (!is_address_self(gcx, recv) && !is_literal_zero(args.exprs().next()?))
430 .then_some(recv);
431 }
432 match_eth_library_call(gcx, expr, recv, member.name.as_str(), args)
433}
434
435fn match_eth_library_call<'gcx>(
438 gcx: Gcx<'gcx>,
439 call: &Expr<'gcx>,
440 recv: &'gcx Expr<'gcx>,
441 name: &str,
442 args: &'gcx CallArgs<'gcx>,
443) -> Option<&'gcx Expr<'gcx>> {
444 let (amount, arities): (Option<usize>, &[usize]) = match name {
446 "sendValue" | "safeTransferETH" | "safeMoveETH" => (Some(1), &[2]),
447 "forceSafeTransferETH" => (Some(1), &[2, 3]),
448 "trySafeTransferETH" => (Some(1), &[3]),
449 "functionCallWithValue" => (Some(2), &[3, 4]),
450 "safeTransferAllETH" => (None, &[1]),
451 "forceSafeTransferAllETH" => (None, &[1, 2]),
452 "trySafeTransferAllETH" => (None, &[2]),
453 _ => return None,
454 };
455 let using = gcx.resolved_call(call).is_some_and(|resolved| resolved.attached);
456 let is_lib = matches!(referenced_item(gcx, recv), Some(ItemId::Contract(cid))
457 if gcx.hir.contract(cid).kind == ContractKind::Library);
458 if (!using && !is_lib) || !arities.contains(&(args.len() + usize::from(using))) {
459 return None;
460 }
461 let dest = if using { recv } else { gcx.call_arg(call, 0)? };
462 if let Some(i) = amount
463 && is_literal_zero(gcx.call_arg(call, i - usize::from(using))?)
464 {
465 return None;
466 }
467 (!is_address_self(gcx, dest)).then_some(dest)
468}
469
470struct CallerGuards<'gcx> {
473 gcx: Gcx<'gcx>,
474 alias_cache: HelperAnalysisCache<(VariableId, u8), bool>,
475 stack: Vec<FunctionId>,
477}
478
479impl<'gcx> CallerGuards<'gcx> {
480 fn new(gcx: Gcx<'gcx>) -> Self {
481 Self {
482 gcx,
483 alias_cache: HelperAnalysisCache::new(DEFAULT_HELPER_ANALYSIS_CACHE_LIMIT),
484 stack: Vec::new(),
485 }
486 }
487
488 fn modifier_restricts(&mut self, m: &Modifier<'_>) -> bool {
490 let ItemId::Function(fid) = m.id else { return false };
491 modifier_prefix(&self.gcx.hir, fid)
492 .is_some_and(|prefix| prefix.into_iter().any(|s| self.stmt_restricts(s)))
493 }
494
495 fn stmt_restricts(&mut self, stmt: &'gcx Stmt<'gcx>) -> bool {
496 match &stmt.kind {
497 StmtKind::Expr(e) => self.expr_restricts(e),
498 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
499 b.stmts.iter().any(|s| self.stmt_restricts(s))
500 }
501 StmtKind::If(cond, then, else_) => {
502 let then_exits = branch_always_exits(self.gcx, then);
503 let else_exits = else_.is_some_and(|e| branch_always_exits(self.gcx, e));
504 (then_exits != else_exits && self.cond_restricts(cond, else_exits))
507 || ((then_exits || self.stmt_restricts(then))
508 && (else_exits || else_.is_some_and(|e| self.stmt_restricts(e))))
509 }
510 _ => false,
511 }
512 }
513
514 fn expr_restricts(&mut self, expr: &'gcx Expr<'gcx>) -> bool {
517 let ExprKind::Call(callee, args) = &expr.peel_parens().kind else { return false };
518 if is_require_or_assert(self.gcx, callee) {
519 return args.exprs().next().is_some_and(|c| self.cond_restricts(c, true));
520 }
521 self.gcx
522 .resolved_function(callee)
523 .filter(|_| matches!(callee.peel_parens().kind, ExprKind::Ident(_)))
524 .is_some_and(|fid| {
525 if self.stack.contains(&fid) {
526 return false;
527 }
528 let Some(body) = self.gcx.hir.function(fid).body else { return false };
529 let mut stmts = body.stmts;
531 while let [rest @ .., last] = stmts
532 && matches!(last.kind, StmtKind::Return(None))
533 {
534 stmts = rest;
535 }
536 if stmts.iter().any(stmt_contains_return) {
537 return false;
538 }
539 self.stack.push(fid);
540 let restricts = stmts.iter().any(|s| self.stmt_restricts(s));
541 self.stack.pop();
542 restricts
543 })
544 }
545
546 fn cond_restricts(&mut self, cond: &'gcx Expr<'gcx>, holds: bool) -> bool {
548 match &cond.peel_parens().kind {
549 ExprKind::Binary(lhs, op, rhs) => {
550 let (eq, any, all) = if holds {
551 (BinOpKind::Eq, BinOpKind::And, BinOpKind::Or)
552 } else {
553 (BinOpKind::Ne, BinOpKind::Or, BinOpKind::And)
554 };
555 if op.kind == any {
556 self.cond_restricts(lhs, holds) || self.cond_restricts(rhs, holds)
557 } else if op.kind == all {
558 self.cond_restricts(lhs, holds) && self.cond_restricts(rhs, holds)
559 } else if op.kind == eq {
560 [(lhs, rhs), (rhs, lhs)].into_iter().any(|(a, b)| {
561 is_msg_sender_like(self.gcx, a, HELPER_DEPTH)
562 && self.is_trusted_principal(b, HELPER_DEPTH)
563 })
564 } else {
565 false
566 }
567 }
568 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
569 self.cond_restricts(inner, !holds)
570 }
571 _ => false,
572 }
573 }
574
575 fn is_trusted_principal(&mut self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
579 let expr = peel_casts(self.gcx, expr);
580 match &expr.kind {
581 ExprKind::Lit(_) => is_trusted_literal(expr),
582 ExprKind::Ident(_) => self.gcx.resolved_variable(expr).is_some_and(|v| {
583 self.gcx.hir.variable(v).kind.is_state()
584 && !self.state_var_aliases_self(v, SELF_ALIAS_DEPTH)
585 }),
586 ExprKind::Member(base, _) => self.is_trusted_principal(base, depth),
587 ExprKind::Index(base, idx) => {
588 self.is_trusted_principal(base, depth)
589 && idx.is_none_or(|i| index_is_static(self.gcx, i))
590 }
591 ExprKind::Call(callee, args) => {
592 depth > 0
593 && args.exprs().next().is_none()
594 && callee_no_arg_returns(self.gcx, callee, |e| {
595 self.is_trusted_principal(e, depth - 1)
596 })
597 }
598 _ => false,
599 }
600 }
601
602 fn state_var_aliases_self(&mut self, v: VariableId, depth: u8) -> bool {
605 let var = self.gcx.hir.variable(v);
606 if depth == 0 || !var.kind.is_state() {
607 return false;
608 }
609 let key = (v, depth);
610 if let Some(cached) = self.alias_cache.get(&key) {
611 return *cached;
612 }
613 if self.alias_cache.is_in_progress(&key) {
614 return false;
615 }
616 self.alias_cache.start(key);
617 let aliases = var
618 .initializer
619 .is_some_and(|init| self.rhs_carries_self(var, init, depth - 1, &HashSet::new()))
620 || var.contract.is_some_and(|cid| {
621 self.gcx.hir.contracts_enumerated().any(|(c, contract)| {
622 (c == cid || contract.linearized_bases.contains(&cid))
623 && self.contract_assigns_self(c, v, depth - 1)
624 })
625 });
626 self.alias_cache.finish(key, aliases);
627 aliases
628 }
629
630 fn rhs_carries_self(
633 &mut self,
634 target: &Variable<'_>,
635 rhs: &'gcx Expr<'gcx>,
636 depth: u8,
637 locals: &HashSet<VariableId>,
638 ) -> bool {
639 if var_is_address_like(target) {
640 self.expr_resolves_to_self(rhs, depth)
641 || lhs_root_var(self.gcx, rhs).is_some_and(|v| locals.contains(&v))
642 } else {
643 self.expr_may_contain_self(rhs, depth, locals)
644 }
645 }
646
647 fn expr_may_contain_self(
649 &mut self,
650 expr: &'gcx Expr<'gcx>,
651 depth: u8,
652 locals: &HashSet<VariableId>,
653 ) -> bool {
654 if self.expr_resolves_to_self(expr, depth)
655 || lhs_root_var(self.gcx, expr).is_some_and(|v| locals.contains(&v))
656 {
657 return true;
658 }
659 if depth == 0 {
660 return false;
661 }
662 let children: Vec<&'gcx Expr<'gcx>> = match &peel_casts(self.gcx, expr).kind {
663 ExprKind::Call(_, args) => args.exprs().collect(),
664 ExprKind::Ternary(_, t, f) => vec![t, f],
665 ExprKind::Tuple(elems) => elems.iter().copied().flatten().collect(),
666 ExprKind::Array(elems) => elems.iter().collect(),
667 _ => Vec::new(),
668 };
669 children.into_iter().any(|e| self.expr_may_contain_self(e, depth - 1, locals))
670 }
671
672 fn expr_resolves_to_self(&mut self, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
674 let expr = peel_casts(self.gcx, expr);
675 if is_address_self(self.gcx, expr) {
676 return true;
677 }
678 if depth == 0 {
679 return false;
680 }
681 match &expr.kind {
682 ExprKind::Ident(_) | ExprKind::Member(..) | ExprKind::Index(..) => {
683 lhs_root_var(self.gcx, expr).is_some_and(|v| self.state_var_aliases_self(v, depth))
684 }
685 ExprKind::Call(callee, args) if args.exprs().next().is_none() => {
686 self.gcx.resolved_function(callee).is_some_and(|fid| {
687 function_no_arg_returns(self.gcx, fid, &mut |e| {
688 self.expr_resolves_to_self(e, depth - 1)
689 })
690 })
691 }
692 ExprKind::Call(callee, args) => identity_helper_arg(self.gcx, callee, args)
693 .is_some_and(|a| self.expr_resolves_to_self(a, depth - 1)),
694 ExprKind::Ternary(_, t, f) => {
695 self.expr_resolves_to_self(t, depth - 1) || self.expr_resolves_to_self(f, depth - 1)
696 }
697 ExprKind::Assign(_, _, rhs) => self.expr_resolves_to_self(rhs, depth - 1),
698 _ => false,
699 }
700 }
701
702 fn contract_assigns_self(&mut self, cid: ContractId, v: VariableId, depth: u8) -> bool {
704 self.gcx.hir.contract(cid).all_functions().any(|fid| {
705 let mut scan = SelfAssignScan {
706 guards: &mut *self,
707 target: v,
708 depth,
709 found: false,
710 stack: Vec::new(),
711 locals: HashSet::new(),
712 };
713 scan.scan_function(fid, None);
714 scan.found
715 })
716 }
717}
718
719struct SelfAssignScan<'a, 'gcx> {
722 guards: &'a mut CallerGuards<'gcx>,
723 target: VariableId,
724 depth: u8,
725 found: bool,
726 stack: Vec<FunctionId>,
727 locals: HashSet<VariableId>,
729}
730
731impl<'gcx> SelfAssignScan<'_, 'gcx> {
732 fn may_contain_self(&mut self, expr: &'gcx Expr<'gcx>) -> bool {
733 self.guards.expr_may_contain_self(expr, self.depth, &self.locals)
734 }
735
736 fn note_local(&mut self, v: VariableId, rhs: &'gcx Expr<'gcx>) {
737 if !self.guards.gcx.hir.variable(v).kind.is_state() && self.may_contain_self(rhs) {
738 self.locals.insert(v);
739 }
740 }
741
742 fn assign(&mut self, lhs: &'gcx Expr<'gcx>, rhs: &'gcx Expr<'gcx>) {
743 if let Some(elems) = tuple_elems(lhs) {
744 let rhs = tuple_elems(rhs);
745 for (i, lhs) in elems.iter().enumerate() {
746 if let Some(lhs) = lhs
747 && let Some(rhs) = rhs.and_then(|r| r.get(i).copied().flatten())
748 {
749 self.assign(lhs, rhs);
750 }
751 }
752 } else if let Some(v) = lhs_root_var(self.guards.gcx, lhs) {
753 if v == self.target {
754 let var = self.guards.gcx.hir.variable(v);
755 self.found |= self.guards.rhs_carries_self(var, rhs, self.depth, &self.locals);
756 } else {
757 self.note_local(v, rhs);
758 }
759 }
760 }
761
762 fn scan_function(&mut self, fid: FunctionId, args: Option<&'gcx CallArgs<'gcx>>) {
764 if self.found || self.stack.len() >= HELPER_CALL_DEPTH || self.stack.contains(&fid) {
765 return;
766 }
767 let f = self.guards.gcx.hir.function(fid);
768 let Some(body) = f.body else { return };
769 let saved = self.locals.clone();
770 for ¶m in f.parameters {
771 if let Some(arg) =
772 args.and_then(|args| arg_for_param(self.guards.gcx, fid, param, args))
773 && self.may_contain_self(arg)
774 {
775 self.locals.insert(param);
776 }
777 }
778 self.stack.push(fid);
779 for m in f.modifiers {
780 if let Some(invoked) = invoked_function(&self.guards.gcx.hir, m) {
781 self.scan_function(invoked, Some(&m.args));
782 }
783 }
784 for stmt in body.stmts {
785 let _ = self.visit_stmt(stmt);
786 }
787 self.stack.pop();
788 self.locals = saved;
789 }
790}
791
792impl<'gcx> Visit<'gcx> for SelfAssignScan<'_, 'gcx> {
793 type BreakValue = Never;
794
795 fn hir(&self) -> &'gcx Hir<'gcx> {
796 &self.guards.gcx.hir
797 }
798
799 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
800 if self.found {
801 return ControlFlow::Continue(());
802 }
803 match &stmt.kind {
804 StmtKind::DeclSingle(vid) => {
805 if let Some(init) = self.guards.gcx.hir.variable(*vid).initializer {
806 self.note_local(*vid, init);
807 }
808 }
809 StmtKind::DeclMulti(vars, init) => {
810 for (vid, rhs) in vars.iter().zip(tuple_elems(init).into_iter().flatten()) {
811 if let (Some(vid), Some(rhs)) = (vid, rhs) {
812 self.note_local(*vid, rhs);
813 }
814 }
815 }
816 _ => {}
817 }
818 self.walk_stmt(stmt)
819 }
820
821 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
822 if self.found {
823 return ControlFlow::Continue(());
824 }
825 match &expr.peel_parens().kind {
826 ExprKind::Assign(lhs, _, rhs) => self.assign(lhs, rhs),
827 ExprKind::Call(callee, args) => match &callee.peel_parens().kind {
828 ExprKind::Member(recv, member) => {
830 if member.name.as_str() == "push"
831 && lhs_root_var(self.guards.gcx, recv) == Some(self.target)
832 && expr_is_array_or_bytes(self.guards.gcx, recv)
833 && args.exprs().any(|a| self.may_contain_self(a))
834 {
835 self.found = true;
836 }
837 }
838 _ => {
839 if let Some(fid) = self.guards.gcx.resolved_function(callee) {
840 self.scan_function(fid, Some(args));
841 }
842 }
843 },
844 _ => {}
845 }
846 self.walk_expr(expr)
847 }
848}
849
850fn invoked_function(hir: &Hir<'_>, m: &Modifier<'_>) -> Option<FunctionId> {
852 match m.id {
853 ItemId::Function(fid) => Some(fid),
854 ItemId::Contract(cid) => hir.contract(cid).ctor,
855 _ => None,
856 }
857}
858
859fn identity_helper_arg<'gcx>(
861 gcx: Gcx<'gcx>,
862 callee: &'gcx Expr<'gcx>,
863 args: &'gcx CallArgs<'gcx>,
864) -> Option<&'gcx Expr<'gcx>> {
865 gcx.resolved_function(callee).and_then(|fid| {
866 let f = gcx.hir.function(fid);
867 let [stmt] = f.body?.stmts else { return None };
868 let StmtKind::Return(Some(ret)) = &stmt.kind else { return None };
869 let param = underlying_var(gcx, peel_casts(gcx, ret))?;
870 (f.parameters.len() == args.len() && f.returns.len() == 1 && f.parameters.contains(¶m))
871 .then(|| arg_for_param(gcx, fid, param, args))
872 .flatten()
873 })
874}
875
876fn lhs_root_var(gcx: Gcx<'_>, lhs: &Expr<'_>) -> Option<VariableId> {
878 match &lhs.peel_parens().kind {
879 ExprKind::Member(base, _) | ExprKind::Index(base, _) | ExprKind::Payable(base) => {
880 lhs_root_var(gcx, base)
881 }
882 ExprKind::Call(callee, args) if is_address_like_cast(gcx, callee) => {
883 args.exprs().next().and_then(|expr| lhs_root_var(gcx, expr))
884 }
885 _ => underlying_var(gcx, lhs),
886 }
887}
888
889fn index_is_static(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
892 expr.visit(&mut |e| match &e.kind {
893 ExprKind::Lit(_)
894 | ExprKind::Type(_)
895 | ExprKind::Payable(_)
896 | ExprKind::Unary(..)
897 | ExprKind::Binary(..)
898 | ExprKind::Member(..)
899 | ExprKind::Index(..)
900 | ExprKind::Ternary(..)
901 | ExprKind::Tuple([Some(_)]) => ControlFlow::Continue(()),
902 ExprKind::Ident(_)
903 if gcx.resolved_expr(e).is_some_and(|res| match res {
904 Res::Item(ItemId::Variable(v)) => gcx.hir.variable(v).kind.is_state(),
905 Res::Builtin(_) => false,
906 _ => true,
907 }) =>
908 {
909 ControlFlow::Continue(())
910 }
911 ExprKind::Call(callee, ..)
912 if matches!(callee.peel_parens().kind, ExprKind::Type(_))
913 || is_contract_cast(gcx, callee) =>
914 {
915 ControlFlow::Continue(())
916 }
917 _ => ControlFlow::Break(()),
918 })
919 .is_continue()
920}
921
922fn stmt_contains_return(stmt: &Stmt<'_>) -> bool {
924 match &stmt.kind {
925 StmtKind::Return(_) => true,
926 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
927 b.stmts.iter().any(stmt_contains_return)
928 }
929 StmtKind::Loop(b, source) => loop_stmts(*b, *source).any(stmt_contains_return),
930 StmtKind::If(_, t, e) => {
931 stmt_contains_return(t) || e.is_some_and(|e| stmt_contains_return(e))
932 }
933 StmtKind::Try(t) => {
934 t.clauses.iter().any(|c| c.block.stmts.iter().any(stmt_contains_return))
935 }
936 _ => false,
937 }
938}
939
940fn is_msg_sender_like<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>, depth: u8) -> bool {
942 let expr = peel_casts(gcx, expr);
943 is_msg_sender(gcx, expr)
944 || matches!(&expr.kind, ExprKind::Call(callee, args)
945 if depth > 0
946 && args.exprs().next().is_none()
947 && callee_no_arg_returns(gcx, callee, |e| is_msg_sender_like(gcx, e, depth - 1)))
948}
949
950fn peel_casts<'a>(gcx: Gcx<'_>, expr: &'a Expr<'a>) -> &'a Expr<'a> {
952 let expr = expr.peel_parens();
953 match &expr.kind {
954 ExprKind::Payable(inner) => peel_casts(gcx, inner),
955 ExprKind::Call(callee, args)
956 if is_address_like_cast(gcx, callee) || is_numeric_cast(callee) =>
957 {
958 args.exprs().next().map_or(expr, |expr| peel_casts(gcx, expr))
959 }
960 _ => expr,
961 }
962}
963
964fn is_numeric_cast(callee: &Expr<'_>) -> bool {
966 matches!(
967 &callee.peel_parens().kind,
968 ExprKind::Type(hir::Type {
969 kind: TypeKind::Elementary(ElementaryType::UInt(_) | ElementaryType::Int(_)),
970 ..
971 })
972 )
973}
974
975fn is_trusted_literal(expr: &Expr<'_>) -> bool {
977 matches!(&expr.kind, ExprKind::Lit(lit) if matches!(lit.kind, LitKind::Address(_)))
978 || is_literal_zero(expr)
979}
980
981fn expr_is_function<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> bool {
982 gcx.type_of_expr(expr.peel_parens().id)
983 .is_some_and(|ty| matches!(ty.peel_refs().kind, TyKind::Fn(_)))
984}
985
986fn expr_is_array_or_bytes<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> bool {
987 gcx.type_of_expr(expr.peel_parens().id).is_some_and(|ty| {
988 matches!(
989 ty.peel_refs().kind,
990 TyKind::Array(..) | TyKind::DynArray(_) | TyKind::Elementary(ElementaryType::Bytes)
991 )
992 })
993}