1use super::ArbitrarySendErc20;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint},
5};
6use solar::{
7 ast,
8 interface::{Span, data_structures::Never, kw, sym},
9 sema::{
10 Gcx,
11 hir::{
12 self, ContractKind, ElementaryType, ExprKind, ItemId, LoopSource, Res, StmtKind,
13 TypeKind, Visit,
14 },
15 ty::{Ty, TyKind},
16 },
17};
18use std::{
19 cell::RefCell,
20 collections::{HashMap, HashSet},
21 ops::ControlFlow,
22 rc::Rc,
23};
24
25declare_forge_lint!(
26 ARBITRARY_SEND_ERC20,
27 Severity::High,
28 "arbitrary-send-erc20",
29 "`transferFrom` uses an arbitrary `from`; require it to equal `msg.sender` or `address(this)`"
30);
31
32declare_forge_lint!(
33 ARBITRARY_SEND_ERC20_PERMIT,
34 Severity::High,
35 "arbitrary-send-erc20-permit",
36 "`transferFrom` uses an arbitrary `from` after `permit`; a non-permit token (e.g. WETH) with a fallback can silently accept the permit and let anyone drain previously-approved tokens"
37);
38
39impl<'hir> LateLintPass<'hir> for ArbitrarySendErc20 {
40 fn check_function(
41 &mut self,
42 ctx: &LintContext,
43 gcx: Gcx<'hir>,
44 hir: &'hir hir::Hir<'hir>,
45 func: &'hir hir::Function<'hir>,
46 ) {
47 if !func.kind.is_function()
48 || matches!(
49 func.state_mutability,
50 ast::StateMutability::Pure | ast::StateMutability::View
51 )
52 {
53 return;
54 }
55 if func.contract.is_some_and(|cid| hir.contract(cid).kind == ContractKind::Library) {
58 return;
59 }
60 let Some(body) = func.body else { return };
61
62 let has_solady_lib = has_solady_safe_transfer_lib(hir);
63 if func.modifiers.iter().any(|m| modifier_prefix_always_exits(hir, m)) {
65 return;
66 }
67 let mut a = Analyzer::new(gcx, hir, has_solady_lib);
68 if let Some(cid) = func.contract {
72 seed_immutable_facts(gcx, hir, has_solady_lib, cid, &mut a);
73 }
74 seed_internal_callsite_facts(gcx, hir, has_solady_lib, func, &mut a);
75 for m in func.modifiers {
76 collect_modifier_safety(
77 gcx,
78 hir,
79 has_solady_lib,
80 m,
81 &mut a.safe_vars,
82 &mut a.self_vars,
83 );
84 }
85 for stmt in body.stmts {
86 let _ = a.visit_stmt(stmt);
87 if branch_always_exits(stmt) {
89 break;
90 }
91 }
92 for (span, lint) in a.hits {
93 ctx.emit(lint, span);
94 }
95 }
96}
97
98#[derive(Clone, Copy, PartialEq, Eq, Hash)]
101struct PermitRecord {
102 token: TokenKey,
103 owner: hir::VariableId,
104}
105
106#[derive(Clone, Copy, PartialEq, Eq, Hash)]
109enum TokenKey {
110 Var(hir::VariableId),
111 Field(hir::VariableId, solar::interface::Symbol),
112}
113
114impl TokenKey {
115 fn touches(&self, v: hir::VariableId) -> bool {
116 match self {
117 Self::Var(x) | Self::Field(x, _) => *x == v,
118 }
119 }
120}
121
122#[derive(Clone, Copy, Default)]
124struct AssignRhs {
125 is_safe: bool,
126 is_self: bool,
127 alias: Option<hir::VariableId>,
128 sum: Option<(hir::VariableId, hir::VariableId)>,
129}
130
131#[derive(Clone, Copy, PartialEq, Eq, Hash)]
133struct PendingRepayment {
134 receiver: hir::VariableId,
135 token: hir::VariableId,
136 amount: hir::VariableId,
137 fee: hir::VariableId,
138}
139
140struct Analyzer<'hir> {
141 gcx: Gcx<'hir>,
142 hir: &'hir hir::Hir<'hir>,
143 safe_vars: HashSet<hir::VariableId>,
147 self_vars: HashSet<hir::VariableId>,
150 permits: HashSet<PermitRecord>,
152 repayments: HashMap<PendingRepayment, u32>,
155 aliases: HashMap<hir::VariableId, hir::VariableId>,
157 sum_of: HashMap<hir::VariableId, (hir::VariableId, hir::VariableId)>,
159 has_solady_lib: bool,
161 hits: Vec<(Span, &'static SolLint)>,
162 written: HashSet<hir::VariableId>,
165}
166
167#[derive(Clone)]
168struct FlowState {
169 safe_vars: HashSet<hir::VariableId>,
170 self_vars: HashSet<hir::VariableId>,
171 permits: HashSet<PermitRecord>,
172 repayments: HashMap<PendingRepayment, u32>,
173 aliases: HashMap<hir::VariableId, hir::VariableId>,
174 sum_of: HashMap<hir::VariableId, (hir::VariableId, hir::VariableId)>,
175}
176
177#[derive(Default)]
178struct ProjectIndex {
179 function_ids_by_ptr: HashMap<usize, hir::FunctionId>,
180 internal_callsites: HashMap<hir::FunctionId, ParamCallsiteFacts>,
181}
182
183struct ParamCallsiteFacts {
184 seen: Vec<bool>,
185 all_safe: Vec<bool>,
186 all_self: Vec<bool>,
187 unknown: bool,
188}
189
190impl ParamCallsiteFacts {
191 fn new(len: usize) -> Self {
192 Self {
193 seen: vec![false; len],
194 all_safe: vec![true; len],
195 all_self: vec![true; len],
196 unknown: false,
197 }
198 }
199}
200
201impl FlowState {
202 fn empty() -> Self {
203 Self {
204 safe_vars: HashSet::new(),
205 self_vars: HashSet::new(),
206 permits: HashSet::new(),
207 repayments: HashMap::new(),
208 aliases: HashMap::new(),
209 sum_of: HashMap::new(),
210 }
211 }
212
213 fn intersection(a: &Self, b: &Self) -> Self {
214 Self {
215 safe_vars: a.safe_vars.intersection(&b.safe_vars).copied().collect(),
216 self_vars: a.self_vars.intersection(&b.self_vars).copied().collect(),
217 permits: a.permits.intersection(&b.permits).copied().collect(),
218 repayments: a
220 .repayments
221 .iter()
222 .filter_map(|(k, va)| b.repayments.get(k).map(|vb| (*k, *va.min(vb))))
223 .collect(),
224 aliases: a
225 .aliases
226 .iter()
227 .filter_map(|(k, v)| (b.aliases.get(k) == Some(v)).then_some((*k, *v)))
228 .collect(),
229 sum_of: a
230 .sum_of
231 .iter()
232 .filter_map(|(k, v)| (b.sum_of.get(k) == Some(v)).then_some((*k, *v)))
233 .collect(),
234 }
235 }
236
237 fn intersection_all(mut states: impl Iterator<Item = Self>) -> Self {
238 let mut out = states.next().unwrap_or_else(Self::empty);
239 for state in states {
240 out = Self::intersection(&out, &state);
241 }
242 out
243 }
244}
245
246const HELPER_DEPTH: u8 = 3;
248
249impl<'hir> Analyzer<'hir> {
250 fn new(gcx: Gcx<'hir>, hir: &'hir hir::Hir<'hir>, has_solady_lib: bool) -> Self {
251 Self {
252 gcx,
253 hir,
254 safe_vars: HashSet::new(),
255 self_vars: HashSet::new(),
256 permits: HashSet::new(),
257 repayments: HashMap::new(),
258 aliases: HashMap::new(),
259 sum_of: HashMap::new(),
260 has_solady_lib,
261 hits: Vec::new(),
262 written: HashSet::new(),
263 }
264 }
265
266 fn snapshot(&self) -> FlowState {
267 FlowState {
268 safe_vars: self.safe_vars.clone(),
269 self_vars: self.self_vars.clone(),
270 permits: self.permits.clone(),
271 repayments: self.repayments.clone(),
272 aliases: self.aliases.clone(),
273 sum_of: self.sum_of.clone(),
274 }
275 }
276
277 fn restore(&mut self, state: FlowState) {
278 self.safe_vars = state.safe_vars;
279 self.self_vars = state.self_vars;
280 self.permits = state.permits;
281 self.repayments = state.repayments;
282 self.aliases = state.aliases;
283 self.sum_of = state.sum_of;
284 }
285
286 fn canonical(&self, v: hir::VariableId) -> hir::VariableId {
288 let mut cur = v;
289 for _ in 0..8 {
290 match self.aliases.get(&cur).copied() {
291 Some(next) if next != cur => cur = next,
292 _ => break,
293 }
294 }
295 cur
296 }
297
298 fn is_safe(&self, expr: &hir::Expr<'_>) -> bool {
299 self.is_safe_inner(expr, HELPER_DEPTH)
300 }
301
302 fn is_safe_inner(&self, expr: &hir::Expr<'_>, depth: u8) -> bool {
303 match &expr.peel_parens().kind {
304 ExprKind::Member(base, ident) if ident.name == sym::sender => {
305 is_builtin(base, sym::msg)
306 }
307 ExprKind::Ident(_) if is_builtin(expr, sym::this) => true,
308 ExprKind::Ident(reses) => reses.iter().any(
309 |r| matches!(r, Res::Item(ItemId::Variable(vid)) if self.safe_vars.contains(vid)),
310 ),
311 ExprKind::Call(callee, args, _) if is_address_cast(callee) => {
312 args.exprs().next().is_some_and(|e| self.is_safe_inner(e, depth))
313 }
314 ExprKind::Payable(inner) => self.is_safe_inner(inner, depth),
315 ExprKind::Ternary(_, t, f) => {
316 self.is_safe_inner(t, depth) && self.is_safe_inner(f, depth)
317 }
318 ExprKind::Call(callee, args, _)
320 if depth > 0
321 && args.exprs().next().is_none()
322 && self.callee_returns_safe(callee, depth - 1) =>
323 {
324 true
325 }
326 _ => false,
327 }
328 }
329
330 fn callee_returns_safe(&self, callee: &hir::Expr<'_>, depth: u8) -> bool {
331 let ExprKind::Ident(reses) = &callee.peel_parens().kind else { return false };
332 reses.iter().any(|r| match r {
333 Res::Item(ItemId::Function(fid)) => {
334 let f = self.hir.function(*fid);
335 let Some(body) = f.body else { return false };
336 f.parameters.is_empty()
337 && body.stmts.len() == 1
338 && matches!(
339 &body.stmts[0].kind,
340 StmtKind::Return(Some(e)) if self.is_safe_inner(e, depth)
341 )
342 }
343 _ => false,
344 })
345 }
346
347 fn assign(&mut self, target: hir::VariableId, rhs: &hir::Expr<'_>) {
350 let eval = self.eval_rhs(Some(rhs));
351 self.written.insert(target);
352 if eval.is_safe {
353 self.safe_vars.insert(target);
354 } else {
355 self.safe_vars.remove(&target);
356 }
357 if eval.is_self {
358 self.self_vars.insert(target);
359 } else {
360 self.self_vars.remove(&target);
361 }
362 if let Some(alias) = eval.alias
363 && alias != target
364 {
365 self.aliases.insert(target, alias);
366 }
367 if let Some(sum) = eval.sum {
368 self.sum_of.insert(target, sum);
369 }
370 }
371
372 fn is_self_expr(&self, expr: &hir::Expr<'_>) -> bool {
376 self.is_self_expr_inner(expr, HELPER_DEPTH)
377 }
378
379 fn is_self_expr_inner(&self, expr: &hir::Expr<'_>, depth: u8) -> bool {
380 let expr = expr.peel_parens();
381 if is_address_self(expr) {
382 return true;
383 }
384 match &expr.kind {
385 ExprKind::Ident(reses) => reses.iter().any(
386 |r| matches!(r, Res::Item(ItemId::Variable(vid)) if self.self_vars.contains(vid)),
387 ),
388 ExprKind::Payable(inner) => self.is_self_expr_inner(inner, depth),
389 ExprKind::Call(callee, args, _) if is_address_cast(callee) => {
390 args.exprs().next().is_some_and(|e| self.is_self_expr_inner(e, depth))
391 }
392 ExprKind::Ternary(_, t, f) => {
393 self.is_self_expr_inner(t, depth) && self.is_self_expr_inner(f, depth)
394 }
395 ExprKind::Call(callee, args, _)
396 if depth > 0
397 && args.exprs().next().is_none()
398 && self.callee_returns_self(callee, depth - 1) =>
399 {
400 true
401 }
402 _ => false,
403 }
404 }
405
406 fn callee_returns_self(&self, callee: &hir::Expr<'_>, depth: u8) -> bool {
407 let ExprKind::Ident(reses) = &callee.peel_parens().kind else { return false };
408 reses.iter().any(|r| match r {
409 Res::Item(ItemId::Function(fid)) => {
410 let f = self.hir.function(*fid);
411 let Some(body) = f.body else { return false };
412 f.parameters.is_empty()
413 && body.stmts.len() == 1
414 && matches!(
415 &body.stmts[0].kind,
416 StmtKind::Return(Some(e)) if self.is_self_expr_inner(e, depth)
417 )
418 }
419 _ => false,
420 })
421 }
422
423 fn match_permit_call(&self, expr: &'hir hir::Expr<'hir>) -> Option<PermitRecord> {
427 let ExprKind::Call(callee, args, _) = &expr.kind else { return None };
428 let ExprKind::Member(recv, ident) = &callee.peel_parens().kind else { return None };
429 let name = ident.name.as_str();
430
431 if name == "permit"
432 && let Some(canonical) = canonical_args(
433 args.kind,
434 &[&["owner"], &["spender"], &["value"], &["deadline"], &["v"], &["r"], &["s"]],
435 )
436 && self.is_self_expr(canonical[1])
437 {
438 return Some(PermitRecord {
439 token: self.canonical_key(token_key(recv)?),
440 owner: self.canonical(underlying_var(canonical[0])?),
441 });
442 }
443
444 if name == "safePermit"
445 && let Some(canonical) = canonical_args(
446 args.kind,
447 &[
448 &["token"],
449 &["owner"],
450 &["spender"],
451 &["value"],
452 &["deadline"],
453 &["v"],
454 &["r"],
455 &["s"],
456 ],
457 )
458 && self.is_self_expr(canonical[2])
459 && let Some(cid) = receiver_contract_id(self.gcx, recv)
460 && self.hir.contract(cid).kind == ContractKind::Library
461 {
462 return Some(PermitRecord {
463 token: self.canonical_key(token_key(canonical[0])?),
464 owner: self.canonical(underlying_var(canonical[1])?),
465 });
466 }
467
468 None
469 }
470
471 fn kill_permits_for(&mut self, target: hir::VariableId) {
475 self.permits.retain(|p| !p.token.touches(target) && p.owner != target);
476 }
477
478 fn canonical_key(&self, k: TokenKey) -> TokenKey {
481 match k {
482 TokenKey::Var(v) => TokenKey::Var(self.canonical(v)),
483 TokenKey::Field(v, s) => TokenKey::Field(self.canonical(v), s),
484 }
485 }
486
487 fn invalidate(&mut self, v: hir::VariableId) {
489 self.safe_vars.remove(&v);
490 self.self_vars.remove(&v);
491 self.aliases.remove(&v);
492 self.sum_of.remove(&v);
493 self.kill_permits_for(v);
494 }
495
496 fn call_state_writes(&self, callee: &hir::Expr<'_>) -> Option<HashSet<hir::VariableId>> {
500 let fid = resolve_internal_fn(callee)?;
501 let f = self.hir.function(fid);
502 let body = f.body?;
503 if matches!(f.state_mutability, ast::StateMutability::Pure | ast::StateMutability::View) {
505 return Some(HashSet::new());
506 }
507 let mut writes = collect_state_writes(self.hir, body.stmts);
508 let mut nested = NestedCallCollector { hir: self.hir, out: HashSet::new() };
510 for s in body.stmts {
511 let _ = nested.visit_stmt(s);
512 }
513 for cid in nested.out {
514 let nf = self.hir.function(cid);
515 if let Some(nb) = nf.body {
516 writes.extend(collect_state_writes(self.hir, nb.stmts));
517 }
518 }
519 Some(writes)
520 }
521
522 fn permit_covers(&self, token: Option<TokenKey>, from: &hir::Expr<'_>) -> bool {
523 let (Some(token), Some(owner)) = (token, underlying_var(from)) else { return false };
524 self.permits.contains(&PermitRecord {
525 token: self.canonical_key(token),
526 owner: self.canonical(owner),
527 })
528 }
529
530 fn amount_matches(
532 &self,
533 amount_arg: &hir::Expr<'_>,
534 amount: hir::VariableId,
535 fee: hir::VariableId,
536 ) -> bool {
537 if is_amount_plus_fee(amount_arg, amount, fee) {
538 return true;
539 }
540 let Some(v) = underlying_var(amount_arg) else { return false };
541 matches!(self.sum_of.get(&v), Some((a, b))
542 if (*a == amount && *b == fee) || (*a == fee && *b == amount))
543 }
544
545 fn kill_repayments_for(&mut self, target: hir::VariableId) {
547 self.repayments.retain(|r, _| {
548 r.receiver != target && r.token != target && r.amount != target && r.fee != target
549 });
550 }
551
552 fn consume_repayment(
555 &mut self,
556 call_expr: &hir::Expr<'_>,
557 from: &hir::Expr<'_>,
558 token: Option<TokenKey>,
559 ) -> bool {
560 let Some(from_v) = underlying_var(from) else { return false };
561 let Some(TokenKey::Var(token_v)) = token else { return false };
564 let ExprKind::Call(_, args, _) = &call_expr.kind else { return false };
565 let (to_arg, amount_arg) = if let Some(a) =
567 canonical_args(args.kind, &[&["from"], &["to"], &["value", "amount"]])
568 {
569 (a[1], a[2])
570 } else if let Some(a) =
571 canonical_args(args.kind, &[&["token"], &["from"], &["to"], &["value", "amount"]])
572 {
573 (a[2], a[3])
574 } else {
575 return false;
576 };
577 if !self.is_self_expr(to_arg) {
578 return false;
579 }
580 let matched = self.repayments.keys().copied().find(|r| {
581 r.receiver == from_v
582 && r.token == token_v
583 && self.amount_matches(amount_arg, r.amount, r.fee)
584 });
585 if let Some(rep) = matched {
586 match self.repayments.get_mut(&rep) {
587 Some(count) if *count > 1 => *count -= 1,
588 _ => {
589 self.repayments.remove(&rep);
590 }
591 }
592 true
593 } else {
594 false
595 }
596 }
597
598 fn add_facts(&mut self, pred: &hir::Expr<'_>, negate: bool) {
601 match &pred.peel_parens().kind {
602 ExprKind::Binary(lhs, op, rhs) => {
603 let (eq, and, or) = if negate {
604 (ast::BinOpKind::Ne, ast::BinOpKind::Or, ast::BinOpKind::And)
605 } else {
606 (ast::BinOpKind::Eq, ast::BinOpKind::And, ast::BinOpKind::Or)
607 };
608 if op.kind == and {
609 self.add_facts(lhs, negate);
610 self.add_facts(rhs, negate);
611 } else if op.kind == or {
612 let baseline = self.snapshot();
614 self.add_facts(lhs, negate);
615 let after_lhs = self.snapshot();
616 self.restore(baseline);
617 self.add_facts(rhs, negate);
618 let after_rhs = self.snapshot();
619 self.restore(FlowState::intersection(&after_lhs, &after_rhs));
620 } else if op.kind == eq {
621 for (a, b) in [(lhs, rhs), (rhs, lhs)] {
622 if let Some(v) = underlying_var(b)
623 && self.is_safe_target(v)
624 {
625 if self.is_safe(a) {
626 self.safe_vars.insert(v);
627 }
628 if self.is_self_expr(a) {
630 self.self_vars.insert(v);
631 }
632 }
633 }
634 }
635 }
636 ExprKind::Unary(op, inner) if matches!(op.kind, ast::UnOpKind::Not) => {
637 self.add_facts(inner, !negate);
638 }
639 _ => {}
640 }
641 }
642
643 fn is_safe_target(&self, v: hir::VariableId) -> bool {
644 let var = self.hir.variable(v);
645 !var.kind.is_state() || var.is_immutable() || var.is_constant()
646 }
647
648 fn kill_field_permits(&mut self, lhs: &hir::Expr<'_>) {
651 let lhs = lhs.peel_parens();
652 if let ExprKind::Member(base, ident) = &lhs.kind
653 && let Some(base_v) = underlying_var(base)
654 {
655 let key = TokenKey::Field(self.canonical(base_v), ident.name);
656 self.permits.retain(|p| p.token != key);
657 }
658 }
659
660 fn handle_assign(&mut self, lhs: &hir::Expr<'_>, rhs: &hir::Expr<'_>) {
662 let lhs = lhs.peel_parens();
663 if let ExprKind::Tuple(lhs_elems) = &lhs.kind {
664 let rhs_elems = match &rhs.peel_parens().kind {
665 ExprKind::Tuple(r) => Some(*r),
666 _ => None,
667 };
668 type EvaluatedSlot<'a> = (Option<&'a hir::Expr<'a>>, AssignRhs);
670 let evaluated: Vec<EvaluatedSlot<'_>> = lhs_elems
671 .iter()
672 .enumerate()
673 .map(|(i, lhs_elem)| {
674 let lhs_expr = lhs_elem.as_deref();
675 let rhs_expr = rhs_elems.and_then(|r| r.get(i).copied()).flatten();
676 (lhs_expr, self.eval_rhs(rhs_expr))
677 })
678 .collect();
679 for (lhs_expr, eval) in evaluated {
680 if let Some(lhs_expr) = lhs_expr {
681 self.assign_one_flags(lhs_expr, eval);
682 }
683 }
684 } else {
685 self.assign_one(lhs, Some(rhs));
686 }
687 }
688
689 fn assign_one(&mut self, lhs: &hir::Expr<'_>, rhs: Option<&hir::Expr<'_>>) {
691 let eval = self.eval_rhs(rhs);
692 self.assign_one_flags(lhs, eval);
693 }
694
695 fn eval_rhs(&self, rhs: Option<&hir::Expr<'_>>) -> AssignRhs {
698 let Some(r) = rhs else { return AssignRhs::default() };
699 let alias = underlying_var(r).map(|v| self.canonical(v));
700 let sum = if let ExprKind::Binary(lhs, op, rhs_inner) = &r.peel_parens().kind
701 && matches!(op.kind, ast::BinOpKind::Add)
702 {
703 underlying_var(lhs).zip(underlying_var(rhs_inner))
704 } else {
705 None
706 };
707 AssignRhs { is_safe: self.is_safe(r), is_self: self.is_self_expr(r), alias, sum }
708 }
709
710 fn assign_one_flags(&mut self, lhs: &hir::Expr<'_>, eval: AssignRhs) {
712 let Some(target) = underlying_var(lhs) else { return };
713 self.written.insert(target);
714 self.kill_permits_for(target);
715 self.kill_repayments_for(target);
716 self.safe_vars.remove(&target);
717 self.self_vars.remove(&target);
718 self.aliases.remove(&target);
720 self.aliases.retain(|_, dst| *dst != target);
721 self.sum_of.remove(&target);
722 self.sum_of.retain(|_, (a, b)| *a != target && *b != target);
723 let var = self.hir.variable(target);
726 if var.kind.is_state() && !var.is_immutable() && !var.is_constant() {
727 return;
728 }
729 if eval.is_safe {
730 self.safe_vars.insert(target);
731 }
732 if eval.is_self {
733 self.self_vars.insert(target);
734 }
735 if let Some(alias) = eval.alias
736 && alias != target
737 {
738 self.aliases.insert(target, alias);
739 }
740 if let Some(sum) = eval.sum {
741 self.sum_of.insert(target, sum);
742 }
743 }
744
745 fn visit_isolated(&mut self, stmts: &'hir [hir::Stmt<'hir>]) {
748 let mut exits = vec![self.snapshot()];
749 if let Some(fallthrough) = self.visit_stmts_until_loop_exit(stmts, &mut exits) {
750 exits.push(fallthrough);
751 }
752 self.restore(FlowState::intersection_all(exits.into_iter()));
753 }
754
755 fn visit_stmts_until_loop_exit(
756 &mut self,
757 stmts: &'hir [hir::Stmt<'hir>],
758 exits: &mut Vec<FlowState>,
759 ) -> Option<FlowState> {
760 for stmt in stmts {
761 self.visit_stmt_until_loop_exit(stmt, exits)?;
762 }
763 Some(self.snapshot())
764 }
765
766 fn visit_stmt_until_loop_exit(
767 &mut self,
768 stmt: &'hir hir::Stmt<'hir>,
769 exits: &mut Vec<FlowState>,
770 ) -> Option<()> {
771 match &stmt.kind {
772 StmtKind::Break | StmtKind::Continue => {
773 exits.push(self.snapshot());
774 None
775 }
776 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
777 let state = self.visit_stmts_until_loop_exit(block.stmts, exits)?;
778 self.restore(state);
779 Some(())
780 }
781 StmtKind::If(cond, then, else_) => {
782 let _ = self.visit_expr(cond);
783 let baseline = self.snapshot();
784
785 self.add_facts(cond, false);
786 let then_fallthrough = self
787 .visit_stmt_until_loop_exit(then, exits)
788 .and_then(|_| (!branch_always_exits(then)).then(|| self.snapshot()));
789
790 self.restore(baseline);
791 self.add_facts(cond, true);
792 let else_fallthrough = match else_ {
793 Some(else_stmt) => self
794 .visit_stmt_until_loop_exit(else_stmt, exits)
795 .and_then(|_| (!branch_always_exits(else_stmt)).then(|| self.snapshot())),
796 None => Some(self.snapshot()),
797 };
798
799 match (then_fallthrough, else_fallthrough) {
800 (Some(then_state), Some(else_state)) => {
801 self.restore(FlowState::intersection(&then_state, &else_state));
802 Some(())
803 }
804 (Some(state), None) | (None, Some(state)) => {
805 self.restore(state);
806 Some(())
807 }
808 (None, None) => None,
809 }
810 }
811 StmtKind::Loop(..) => {
813 let _ = self.visit_stmt(stmt);
814 (!branch_always_exits(stmt)).then_some(())
815 }
816 _ => {
817 let _ = self.visit_stmt(stmt);
818 (!branch_always_exits(stmt)).then_some(())
819 }
820 }
821 }
822}
823
824impl<'hir> hir::Visit<'hir> for Analyzer<'hir> {
825 type BreakValue = Never;
826
827 fn hir(&self) -> &'hir hir::Hir<'hir> {
828 self.hir
829 }
830
831 fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
832 match &stmt.kind {
833 StmtKind::If(cond, then, else_) => {
836 let _ = self.visit_expr(cond);
837
838 let baseline = self.snapshot();
839 self.add_facts(cond, false);
840 let _ = self.visit_stmt(then);
841 let then_exits = branch_always_exits(then);
842 let after_then = self.snapshot();
843
844 self.restore(baseline);
846 self.add_facts(cond, true);
847 let else_exits = match else_ {
848 Some(e) => {
849 let _ = self.visit_stmt(e);
850 branch_always_exits(e)
851 }
852 None => false,
853 };
854 let after_else = self.snapshot();
855
856 let joined = match (then_exits, else_exits) {
857 (true, true) => FlowState {
860 safe_vars: after_then
861 .safe_vars
862 .union(&after_else.safe_vars)
863 .copied()
864 .collect(),
865 self_vars: after_then
866 .self_vars
867 .union(&after_else.self_vars)
868 .copied()
869 .collect(),
870 permits: after_then.permits.union(&after_else.permits).copied().collect(),
871 repayments: {
873 let mut m = after_then.repayments;
874 for (k, vb) in &after_else.repayments {
875 let entry = m.entry(*k).or_insert(0);
876 if *entry < *vb {
877 *entry = *vb;
878 }
879 }
880 m
881 },
882 aliases: {
883 let mut m = after_then.aliases;
884 for (k, v) in after_else.aliases {
885 m.entry(k).or_insert(v);
886 }
887 m
888 },
889 sum_of: {
890 let mut m = after_then.sum_of;
891 for (k, v) in after_else.sum_of {
892 m.entry(k).or_insert(v);
893 }
894 m
895 },
896 },
897 (true, false) => after_else,
898 (false, true) => after_then,
899 (false, false) => FlowState::intersection(&after_then, &after_else),
900 };
901 self.restore(joined);
902 return ControlFlow::Continue(());
903 }
904
905 StmtKind::Loop(block, source) => {
906 if matches!(source, LoopSource::DoWhile)
909 && !body_has_break_or_continue(do_while_user_stmts(block.stmts))
910 {
911 for s in block.stmts {
912 let _ = self.visit_stmt(s);
913 if branch_always_exits(s) {
914 break;
915 }
916 }
917 } else if matches!(source, LoopSource::DoWhile) {
918 let mut exits = vec![];
921 if let Some(fallthrough) =
922 self.visit_stmts_until_loop_exit(block.stmts, &mut exits)
923 {
924 exits.push(fallthrough);
925 }
926 if !exits.is_empty() {
927 self.restore(FlowState::intersection_all(exits.into_iter()));
928 }
929 } else {
930 self.visit_isolated(block.stmts);
931 }
932 return ControlFlow::Continue(());
933 }
934 StmtKind::Try(t) => {
935 let baseline = self.snapshot();
938 let _ = self.visit_expr(&t.expr);
939 let after_call = self.snapshot();
940 let mut post_clauses = Vec::with_capacity(t.clauses.len());
941 for (i, clause) in t.clauses.iter().enumerate() {
942 self.restore(if i == 0 { after_call.clone() } else { baseline.clone() });
943 for s in clause.block.stmts {
944 let _ = self.visit_stmt(s);
945 if branch_always_exits(s) {
946 break;
947 }
948 }
949 if !clause.block.stmts.iter().any(branch_always_exits) {
952 post_clauses.push(self.snapshot());
953 }
954 }
955 let joined = if post_clauses.is_empty() {
956 after_call
958 } else {
959 FlowState::intersection_all(post_clauses.into_iter())
960 };
961 self.restore(joined);
962 return ControlFlow::Continue(());
963 }
964
965 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
967 for s in block.stmts {
968 let _ = self.visit_stmt(s);
969 if branch_always_exits(s) {
970 break;
971 }
972 }
973 return ControlFlow::Continue(());
974 }
975
976 StmtKind::DeclSingle(vid) => {
979 if let Some(init) = self.hir.variable(*vid).initializer {
980 self.assign(*vid, init);
981 }
982 }
983
984 StmtKind::DeclMulti(vars, init) => {
986 if let ExprKind::Tuple(rhs) = &init.peel_parens().kind {
987 for (lhs, rhs) in vars.iter().zip(rhs.iter()) {
988 if let (Some(vid), Some(expr)) = (lhs, rhs) {
989 self.assign(*vid, expr);
990 }
991 }
992 }
993 }
994
995 _ => {}
996 }
997 self.walk_stmt(stmt)
998 }
999
1000 fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1001 if let ExprKind::Binary(lhs, op, rhs) = &expr.kind
1005 && matches!(op.kind, ast::BinOpKind::And | ast::BinOpKind::Or)
1006 {
1007 let _ = self.visit_expr(lhs);
1008 let negate = matches!(op.kind, ast::BinOpKind::Or);
1009 let skipped_rhs = self.snapshot();
1010 self.add_facts(lhs, negate);
1011 let result = self.visit_expr(rhs);
1012 let ran_rhs = self.snapshot();
1013 self.restore(FlowState::intersection(&skipped_rhs, &ran_rhs));
1014 return result;
1015 }
1016
1017 match &expr.kind {
1018 ExprKind::Call(callee, args, _) if is_require_or_assert(callee) => {
1019 let result = self.walk_expr(expr);
1022 if let Some(cond) = args.exprs().next() {
1023 self.add_facts(cond, false);
1024 }
1025 return result;
1026 }
1027 ExprKind::Call(callee, ..) => {
1028 if let Some(rep) = match_flash_loan_call(self.gcx, self.hir, expr) {
1030 *self.repayments.entry(rep).or_insert(0) += 1;
1031 } else if let Some(p) = self.match_permit_call(expr) {
1032 self.permits.insert(p);
1033 } else if let Some((from, token)) =
1034 match_sink(self.gcx, self.hir, self.has_solady_lib, expr)
1035 && !self.is_safe(from)
1036 && !self.consume_repayment(expr, from, token)
1037 {
1038 let lint = if self.permit_covers(token, from) {
1041 &ARBITRARY_SEND_ERC20_PERMIT
1042 } else {
1043 &ARBITRARY_SEND_ERC20
1044 };
1045 self.hits.push((expr.span, lint));
1046 } else if let Some(writes) = self.call_state_writes(callee) {
1047 let result = self.walk_expr(expr);
1050 for v in writes {
1051 self.invalidate(v);
1052 }
1053 return result;
1054 }
1055 }
1056 ExprKind::Assign(lhs, _, rhs) => {
1057 self.kill_field_permits(lhs);
1058 self.handle_assign(lhs, rhs);
1059 }
1060 ExprKind::Delete(target) => {
1062 self.kill_field_permits(target);
1063 self.assign_one(target.peel_parens(), None);
1064 }
1065 _ => {}
1066 }
1067 self.walk_expr(expr)
1068 }
1069}
1070
1071fn match_flash_loan_call<'hir>(
1075 gcx: Gcx<'hir>,
1076 hir: &hir::Hir<'hir>,
1077 expr: &hir::Expr<'hir>,
1078) -> Option<PendingRepayment> {
1079 let ExprKind::Call(callee, args, _) = &expr.kind else { return None };
1080 let ExprKind::Member(recv, ident) = &callee.peel_parens().kind else { return None };
1081 if ident.name.as_str() != "onFlashLoan" {
1082 return None;
1083 }
1084 let args =
1085 canonical_args(args.kind, &[&["initiator"], &["token"], &["amount"], &["fee"], &["data"]])?;
1086 let cid = receiver_contract_id(gcx, recv)?;
1087 if !contract_has_function(
1088 hir,
1089 cid,
1090 "onFlashLoan",
1091 &["address", "address", "uint256", "uint256", "bytes"],
1092 &["bytes32"],
1093 ) {
1094 return None;
1095 }
1096 Some(PendingRepayment {
1097 receiver: underlying_var(recv)?,
1098 token: underlying_var(args[1])?,
1099 amount: underlying_var(args[2])?,
1100 fee: underlying_var(args[3])?,
1101 })
1102}
1103
1104fn is_amount_plus_fee(expr: &hir::Expr<'_>, amount: hir::VariableId, fee: hir::VariableId) -> bool {
1106 let ExprKind::Binary(lhs, op, rhs) = &expr.peel_parens().kind else { return false };
1107 if !matches!(op.kind, ast::BinOpKind::Add) {
1108 return false;
1109 }
1110 let a = underlying_var(lhs);
1111 let b = underlying_var(rhs);
1112 (a == Some(amount) && b == Some(fee)) || (a == Some(fee) && b == Some(amount))
1113}
1114
1115fn match_sink<'hir>(
1124 gcx: Gcx<'hir>,
1125 hir: &'hir hir::Hir<'hir>,
1126 has_solady_lib: bool,
1127 expr: &'hir hir::Expr<'hir>,
1128) -> Option<(&'hir hir::Expr<'hir>, Option<TokenKey>)> {
1129 let ExprKind::Call(callee, args, _) = &expr.kind else { return None };
1130 let ExprKind::Member(recv, ident) = &callee.peel_parens().kind else { return None };
1131 let name = ident.name.as_str();
1132
1133 if (name == "transferFrom" || name == "safeTransferFrom")
1134 && let Some(canonical) =
1135 canonical_args(args.kind, &[&["from"], &["to"], &["value", "amount"]])
1136 {
1137 if let Some(cid) = receiver_contract_id(gcx, recv)
1139 && contract_has_function(
1140 hir,
1141 cid,
1142 "transferFrom",
1143 &["address", "address", "uint256"],
1144 &["bool"],
1145 )
1146 {
1147 return Some((canonical[0], token_key(recv)));
1148 }
1149 if name == "safeTransferFrom" && has_solady_lib && expr_is_address(gcx, recv) {
1153 return Some((canonical[0], token_key(recv)));
1154 }
1155 }
1156
1157 if name == "safeTransferFrom"
1158 && let Some(canonical) =
1159 canonical_args(args.kind, &[&["token"], &["from"], &["to"], &["value", "amount"]])
1160 && let Some(cid) = receiver_contract_id(gcx, recv)
1161 && hir.contract(cid).kind == ContractKind::Library
1162 && library_has_safe_transfer_from(hir, cid)
1163 {
1164 return Some((canonical[1], token_key(canonical[0])));
1165 }
1166
1167 None
1168}
1169
1170fn token_key(expr: &hir::Expr<'_>) -> Option<TokenKey> {
1174 if let Some(v) = underlying_var(expr) {
1175 return Some(TokenKey::Var(v));
1176 }
1177 let expr = expr.peel_parens();
1178 if let ExprKind::Member(base, ident) = &expr.kind
1179 && let Some(v) = underlying_var(base)
1180 {
1181 return Some(TokenKey::Field(v, ident.name));
1182 }
1183 None
1184}
1185
1186fn collect_modifier_safety<'hir>(
1190 gcx: Gcx<'hir>,
1191 hir: &'hir hir::Hir<'hir>,
1192 has_solady_lib: bool,
1193 invocation: &hir::Modifier<'hir>,
1194 out_safe: &mut HashSet<hir::VariableId>,
1195 out_self: &mut HashSet<hir::VariableId>,
1196) {
1197 let ItemId::Function(fid) = invocation.id else { return };
1198 let modifier = hir.function(fid);
1199 if !matches!(modifier.kind, hir::FunctionKind::Modifier) {
1200 return;
1201 }
1202 let Some(body) = modifier.body else { return };
1203
1204 if count_placeholders(body.stmts) != 1 {
1206 return;
1207 }
1208 let Some(placeholder_idx) =
1209 body.stmts.iter().position(|s| matches!(s.kind, StmtKind::Placeholder))
1210 else {
1211 return;
1212 };
1213
1214 let arg_map: Vec<(hir::VariableId, hir::VariableId)> = match invocation.args.kind {
1216 hir::CallArgsKind::Unnamed(exprs) => exprs
1217 .iter()
1218 .enumerate()
1219 .filter_map(|(i, arg)| Some((*modifier.parameters.get(i)?, underlying_var(arg)?)))
1220 .collect(),
1221 hir::CallArgsKind::Named(named) => named
1222 .iter()
1223 .filter_map(|na| {
1224 let mp = modifier.parameters.iter().find(|p| {
1225 hir.variable(**p).name.is_some_and(|n| n.as_str() == na.name.as_str())
1226 })?;
1227 Some((*mp, underlying_var(&na.value)?))
1228 })
1229 .collect(),
1230 };
1231 if arg_map.is_empty() {
1232 return;
1233 }
1234
1235 if contains_unanalysable(&body.stmts[..placeholder_idx]) {
1238 return;
1239 }
1240
1241 let mut a = Analyzer::new(gcx, hir, has_solady_lib);
1242 for stmt in &body.stmts[..placeholder_idx] {
1243 let _ = a.visit_stmt(stmt);
1244 }
1245 for (mp, caller) in arg_map {
1248 if !a.is_safe_target(caller) || a.written.contains(&mp) {
1249 continue;
1250 }
1251 if a.safe_vars.contains(&mp) {
1252 out_safe.insert(caller);
1253 }
1254 if a.self_vars.contains(&mp) {
1255 out_self.insert(caller);
1256 }
1257 }
1258}
1259
1260fn seed_immutable_facts<'hir>(
1263 gcx: Gcx<'hir>,
1264 hir: &'hir hir::Hir<'hir>,
1265 has_solady_lib: bool,
1266 cid: hir::ContractId,
1267 out: &mut Analyzer<'hir>,
1268) {
1269 for item in hir.contract_item_ids(cid) {
1270 if let Some(vid) = item.as_variable() {
1271 let v = hir.variable(vid);
1272 if v.kind.is_state()
1273 && (v.is_immutable() || v.is_constant())
1274 && let Some(init) = v.initializer
1275 {
1276 if out.is_safe(init) {
1277 out.safe_vars.insert(vid);
1278 }
1279 if out.is_self_expr(init) {
1280 out.self_vars.insert(vid);
1281 }
1282 }
1283 }
1284 }
1285 for item in hir.contract_item_ids(cid) {
1286 let Some(fid) = item.as_function() else { continue };
1287 let f = hir.function(fid);
1288 if !f.is_constructor() {
1289 continue;
1290 }
1291 let Some(body) = f.body else { continue };
1292 let mut ctor = Analyzer::new(gcx, hir, has_solady_lib);
1293 for stmt in body.stmts {
1294 let _ = ctor.visit_stmt(stmt);
1295 if branch_always_exits(stmt) {
1296 break;
1297 }
1298 }
1299 for v in &ctor.safe_vars {
1300 let var = hir.variable(*v);
1301 if var.kind.is_state() && (var.is_immutable() || var.is_constant()) {
1302 out.safe_vars.insert(*v);
1303 }
1304 }
1305 for v in &ctor.self_vars {
1306 let var = hir.variable(*v);
1307 if var.kind.is_state() && (var.is_immutable() || var.is_constant()) {
1308 out.self_vars.insert(*v);
1309 }
1310 }
1311 }
1312}
1313
1314fn seed_internal_callsite_facts<'hir>(
1315 gcx: Gcx<'hir>,
1316 hir: &'hir hir::Hir<'hir>,
1317 has_solady_lib: bool,
1318 func: &'hir hir::Function<'hir>,
1319 out: &mut Analyzer<'hir>,
1320) {
1321 if !is_internal_callsite_seed_candidate(func) {
1322 return;
1323 }
1324
1325 let index = project_index_for(gcx, hir, has_solady_lib);
1326 let ptr = std::ptr::from_ref::<hir::Function<'_>>(func) as usize;
1327 let Some(fid) = index.function_ids_by_ptr.get(&ptr).copied() else { return };
1328 let Some(facts) = index.internal_callsites.get(&fid) else { return };
1329 if facts.unknown {
1330 return;
1331 }
1332
1333 for (i, param) in func.parameters.iter().copied().enumerate() {
1334 if facts.seen.get(i).copied().unwrap_or(false)
1335 && facts.all_safe.get(i).copied().unwrap_or(false)
1336 {
1337 out.safe_vars.insert(param);
1338 if facts.all_self.get(i).copied().unwrap_or(false) {
1339 out.self_vars.insert(param);
1340 }
1341 }
1342 }
1343}
1344
1345const fn is_internal_callsite_seed_candidate(func: &hir::Function<'_>) -> bool {
1346 func.kind.is_function()
1347 && matches!(func.visibility, ast::Visibility::Private | ast::Visibility::Internal)
1348 && !func.parameters.is_empty()
1349}
1350
1351thread_local! {
1352 static PROJECT_INDEX: RefCell<Option<(usize, Rc<ProjectIndex>)>> = const { RefCell::new(None) };
1353}
1354
1355fn project_index_for<'hir>(
1356 gcx: Gcx<'hir>,
1357 hir: &'hir hir::Hir<'hir>,
1358 has_solady_lib: bool,
1359) -> Rc<ProjectIndex> {
1360 let key = std::ptr::from_ref::<hir::Hir<'_>>(hir) as usize;
1361 PROJECT_INDEX.with(|cell| {
1362 let mut slot = cell.borrow_mut();
1363 if let Some((cached_key, cached)) = slot.as_ref()
1364 && *cached_key == key
1365 {
1366 return cached.clone();
1367 }
1368 let fresh = Rc::new(build_project_index(gcx, hir, has_solady_lib));
1369 *slot = Some((key, fresh.clone()));
1370 fresh
1371 })
1372}
1373
1374fn build_project_index<'hir>(
1375 gcx: Gcx<'hir>,
1376 hir: &'hir hir::Hir<'hir>,
1377 has_solady_lib: bool,
1378) -> ProjectIndex {
1379 let mut index = ProjectIndex::default();
1380 for fid in hir.function_ids() {
1381 let func = hir.function(fid);
1382 index
1383 .function_ids_by_ptr
1384 .insert(std::ptr::from_ref::<hir::Function<'_>>(func) as usize, fid);
1385 }
1386
1387 let mut collector =
1388 InternalCallsiteCollector { gcx, hir, has_solady_lib, out: &mut index.internal_callsites };
1389 for fid in hir.function_ids() {
1390 let Some(body) = hir.function(fid).body else { continue };
1391 for stmt in body.stmts {
1392 let _ = collector.visit_stmt(stmt);
1393 }
1394 }
1395 index
1396}
1397
1398struct InternalCallsiteCollector<'a, 'hir> {
1399 gcx: Gcx<'hir>,
1400 hir: &'hir hir::Hir<'hir>,
1401 has_solady_lib: bool,
1402 out: &'a mut HashMap<hir::FunctionId, ParamCallsiteFacts>,
1403}
1404
1405impl<'hir> hir::Visit<'hir> for InternalCallsiteCollector<'_, 'hir> {
1406 type BreakValue = Never;
1407
1408 fn hir(&self) -> &'hir hir::Hir<'hir> {
1409 self.hir
1410 }
1411
1412 fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1413 if let ExprKind::Call(callee, args, _) = &expr.kind
1414 && let Some(fid) = resolve_internal_fn(callee)
1415 {
1416 self.record_call(fid, args);
1417 }
1418 self.walk_expr(expr)
1419 }
1420}
1421
1422impl<'hir> InternalCallsiteCollector<'_, 'hir> {
1423 fn record_call(&mut self, fid: hir::FunctionId, args: &'hir hir::CallArgs<'hir>) {
1424 let func = self.hir.function(fid);
1425 if !is_internal_callsite_seed_candidate(func) {
1426 return;
1427 }
1428
1429 let arity = func.parameters.len();
1430 let facts = self.out.entry(fid).or_insert_with(|| ParamCallsiteFacts::new(arity));
1431 if facts.unknown {
1432 return;
1433 }
1434 let Some(call_args) = internal_call_args(self.hir, func, args) else {
1435 facts.unknown = true;
1436 return;
1437 };
1438 if call_args.len() != arity || facts.seen.len() != arity {
1439 facts.unknown = true;
1440 return;
1441 }
1442
1443 let analyzer = Analyzer::new(self.gcx, self.hir, self.has_solady_lib);
1444 for (i, arg) in call_args.into_iter().enumerate() {
1445 facts.seen[i] = true;
1446 facts.all_safe[i] &= analyzer.is_safe(arg);
1447 facts.all_self[i] &= analyzer.is_self_expr(arg);
1448 }
1449 }
1450}
1451
1452fn internal_call_args<'hir>(
1453 hir: &'hir hir::Hir<'hir>,
1454 func: &'hir hir::Function<'hir>,
1455 args: &'hir hir::CallArgs<'hir>,
1456) -> Option<Vec<&'hir hir::Expr<'hir>>> {
1457 match args.kind {
1458 hir::CallArgsKind::Unnamed(exprs) => {
1459 (exprs.len() == func.parameters.len()).then(|| exprs.iter().collect())
1460 }
1461 hir::CallArgsKind::Named(named) => {
1462 if named.len() != func.parameters.len() {
1463 return None;
1464 }
1465 func.parameters
1466 .iter()
1467 .map(|param| {
1468 let name = hir.variable(*param).name?;
1469 named
1470 .iter()
1471 .find_map(|arg| (arg.name.as_str() == name.as_str()).then_some(&arg.value))
1472 })
1473 .collect()
1474 }
1475 }
1476}
1477
1478struct StateWriteCollector<'hir> {
1481 hir: &'hir hir::Hir<'hir>,
1482 out: HashSet<hir::VariableId>,
1483}
1484
1485impl<'hir> hir::Visit<'hir> for StateWriteCollector<'hir> {
1486 type BreakValue = Never;
1487
1488 fn hir(&self) -> &'hir hir::Hir<'hir> {
1489 self.hir
1490 }
1491
1492 fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1493 match &expr.kind {
1494 ExprKind::Assign(lhs, _, _) => self.add_target(lhs),
1495 ExprKind::Delete(e) => self.add_target(e),
1496 _ => {}
1497 }
1498 self.walk_expr(expr)
1499 }
1500}
1501
1502impl<'hir> StateWriteCollector<'hir> {
1503 fn add_target(&mut self, lhs: &hir::Expr<'_>) {
1504 let lhs = lhs.peel_parens();
1505 if let ExprKind::Tuple(elems) = &lhs.kind {
1506 for e in elems.iter().flatten() {
1507 self.add_target(e);
1508 }
1509 return;
1510 }
1511 if let Some(vid) = underlying_var(lhs) {
1512 let v = self.hir.variable(vid);
1513 if v.kind.is_state() {
1514 self.out.insert(vid);
1515 }
1516 }
1517 }
1518}
1519
1520fn collect_state_writes<'hir>(
1521 hir: &'hir hir::Hir<'hir>,
1522 stmts: &'hir [hir::Stmt<'hir>],
1523) -> HashSet<hir::VariableId> {
1524 let mut c = StateWriteCollector { hir, out: HashSet::new() };
1525 for s in stmts {
1526 let _ = c.visit_stmt(s);
1527 }
1528 c.out
1529}
1530
1531fn resolve_internal_fn(callee: &hir::Expr<'_>) -> Option<hir::FunctionId> {
1535 let callee = callee.peel_parens();
1536 let reses: &[Res] = match &callee.kind {
1537 ExprKind::Ident(reses) => reses,
1538 ExprKind::Member(recv, _) => match &recv.peel_parens().kind {
1539 ExprKind::Ident(reses) => reses,
1540 _ => return None,
1541 },
1542 _ => return None,
1543 };
1544 reses.iter().find_map(|r| match r {
1545 Res::Item(ItemId::Function(fid)) => Some(*fid),
1546 _ => None,
1547 })
1548}
1549
1550struct NestedCallCollector<'hir> {
1553 hir: &'hir hir::Hir<'hir>,
1554 out: HashSet<hir::FunctionId>,
1555}
1556
1557impl<'hir> hir::Visit<'hir> for NestedCallCollector<'hir> {
1558 type BreakValue = Never;
1559
1560 fn hir(&self) -> &'hir hir::Hir<'hir> {
1561 self.hir
1562 }
1563
1564 fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1565 if let ExprKind::Call(callee, ..) = &expr.kind
1566 && let Some(fid) = resolve_internal_fn(callee)
1567 {
1568 self.out.insert(fid);
1569 }
1570 self.walk_expr(expr)
1571 }
1572}
1573
1574fn modifier_prefix_always_exits(hir: &hir::Hir<'_>, invocation: &hir::Modifier<'_>) -> bool {
1577 let ItemId::Function(fid) = invocation.id else { return false };
1578 let modifier = hir.function(fid);
1579 if !matches!(modifier.kind, hir::FunctionKind::Modifier) {
1580 return false;
1581 }
1582 let Some(body) = modifier.body else { return false };
1583 if count_placeholders(body.stmts) != 1 {
1584 return false;
1585 }
1586 let Some(placeholder_idx) =
1587 body.stmts.iter().position(|s| matches!(s.kind, StmtKind::Placeholder))
1588 else {
1589 return false;
1590 };
1591 body.stmts[..placeholder_idx].iter().any(branch_always_exits)
1592}
1593
1594fn contains_unanalysable(stmts: &[hir::Stmt<'_>]) -> bool {
1597 fn in_stmt(s: &hir::Stmt<'_>) -> bool {
1598 match &s.kind {
1599 StmtKind::Err(_) => true,
1600 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => contains_unanalysable(b.stmts),
1601 StmtKind::If(_, t, e) => in_stmt(t) || e.as_ref().is_some_and(|s| in_stmt(s)),
1602 StmtKind::Loop(b, _) => contains_unanalysable(b.stmts),
1603 StmtKind::Try(t) => t.clauses.iter().any(|c| contains_unanalysable(c.block.stmts)),
1604 _ => false,
1605 }
1606 }
1607 stmts.iter().any(in_stmt)
1608}
1609
1610fn do_while_user_stmts<'a, 'hir>(stmts: &'a [hir::Stmt<'hir>]) -> &'a [hir::Stmt<'hir>] {
1612 match stmts.split_last() {
1613 Some((last, rest)) if is_loop_termination_if(last) => rest,
1614 _ => stmts,
1615 }
1616}
1617
1618fn is_loop_termination_if(stmt: &hir::Stmt<'_>) -> bool {
1619 let StmtKind::If(_, then_, else_) = &stmt.kind else { return false };
1620 is_break_stmt(then_) || else_.as_ref().is_some_and(|e| is_break_stmt(e))
1621}
1622
1623fn is_break_stmt(stmt: &hir::Stmt<'_>) -> bool {
1624 match &stmt.kind {
1625 StmtKind::Break => true,
1626 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => {
1627 b.stmts.len() == 1 && is_break_stmt(&b.stmts[0])
1628 }
1629 _ => false,
1630 }
1631}
1632
1633fn body_has_break_or_continue(stmts: &[hir::Stmt<'_>]) -> bool {
1635 fn in_stmt(stmt: &hir::Stmt<'_>) -> bool {
1636 match &stmt.kind {
1637 StmtKind::Break | StmtKind::Continue => true,
1638 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => body_has_break_or_continue(b.stmts),
1639 StmtKind::If(_, t, e) => in_stmt(t) || e.as_ref().is_some_and(|s| in_stmt(s)),
1640 StmtKind::Try(t) => t.clauses.iter().any(|c| body_has_break_or_continue(c.block.stmts)),
1641 StmtKind::Loop(..) => false,
1642 _ => false,
1643 }
1644 }
1645 stmts.iter().any(in_stmt)
1646}
1647
1648fn count_placeholders(stmts: &[hir::Stmt<'_>]) -> usize {
1649 fn count_in_stmt(stmt: &hir::Stmt<'_>) -> usize {
1650 match &stmt.kind {
1651 StmtKind::Placeholder => 1,
1652 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => count_placeholders(b.stmts),
1653 StmtKind::If(_, t, e) => count_in_stmt(t) + e.as_ref().map_or(0, |s| count_in_stmt(s)),
1654 StmtKind::Loop(b, _) => count_placeholders(b.stmts),
1655 StmtKind::Try(t) => t.clauses.iter().map(|c| count_placeholders(c.block.stmts)).sum(),
1656 _ => 0,
1657 }
1658 }
1659 stmts.iter().map(count_in_stmt).sum()
1660}
1661
1662fn underlying_var(expr: &hir::Expr<'_>) -> Option<hir::VariableId> {
1669 match &expr.peel_parens().kind {
1670 ExprKind::Ident(reses) => reses.iter().find_map(|r| match r {
1671 Res::Item(ItemId::Variable(vid)) => Some(*vid),
1672 _ => None,
1673 }),
1674 ExprKind::Call(callee, args, _) if is_cast_callee(callee) => {
1675 let mut exprs = args.exprs();
1678 let inner = exprs.next()?;
1679 if exprs.next().is_some() {
1680 return None;
1681 }
1682 underlying_var(inner)
1683 }
1684 ExprKind::Payable(inner) => underlying_var(inner),
1685 _ => None,
1686 }
1687}
1688
1689fn is_cast_callee(callee: &hir::Expr<'_>) -> bool {
1692 match &callee.peel_parens().kind {
1693 ExprKind::Type(_) => true,
1694 ExprKind::Ident(reses) => reses.iter().any(|r| matches!(r, Res::Item(ItemId::Contract(_)))),
1695 _ => false,
1696 }
1697}
1698
1699fn receiver_contract_id<'hir>(gcx: Gcx<'hir>, recv: &hir::Expr<'hir>) -> Option<hir::ContractId> {
1702 expr_contract_id(gcx, recv).or_else(|| direct_contract_id(recv))
1703}
1704
1705fn expr_contract_id<'hir>(gcx: Gcx<'hir>, expr: &hir::Expr<'hir>) -> Option<hir::ContractId> {
1706 expr_ty(gcx, expr).and_then(ty_contract_id)
1707}
1708
1709fn ty_contract_id(ty: Ty<'_>) -> Option<hir::ContractId> {
1710 match ty.peel_refs().kind {
1711 TyKind::Contract(id) => Some(id),
1712 TyKind::Type(ty) => ty_contract_id(ty),
1713 _ => None,
1714 }
1715}
1716
1717fn direct_contract_id(expr: &hir::Expr<'_>) -> Option<hir::ContractId> {
1718 match &expr.peel_parens().kind {
1719 ExprKind::Ident(reses) => reses.iter().find_map(|r| match r {
1720 Res::Item(ItemId::Contract(cid)) => Some(*cid),
1721 _ => None,
1722 }),
1723 ExprKind::Call(callee, ..) => direct_contract_id(callee),
1724 _ => None,
1725 }
1726}
1727
1728fn has_solady_safe_transfer_lib(hir: &hir::Hir<'_>) -> bool {
1730 hir.contracts_enumerated().any(|(cid, c)| {
1731 c.kind == ContractKind::Library
1732 && c.name.as_str() == "SafeTransferLib"
1733 && library_has_safe_transfer_from(hir, cid)
1734 })
1735}
1736
1737fn contract_has_function(
1738 hir: &hir::Hir<'_>,
1739 cid: hir::ContractId,
1740 name: &str,
1741 params: &[&str],
1742 returns: &[&str],
1743) -> bool {
1744 hir.contract_item_ids(cid).any(|item| {
1745 let Some(fid) = item.as_function() else { return false };
1746 let f = hir.function(fid);
1747 f.name.is_some_and(|n| n.name.as_str() == name)
1748 && f.parameters.len() == params.len()
1749 && f.returns.len() == returns.len()
1750 && f.parameters.iter().zip(params).all(|(id, abi)| is_elementary(hir, *id, abi))
1751 && f.returns.iter().zip(returns).all(|(id, abi)| is_elementary(hir, *id, abi))
1752 })
1753}
1754
1755fn library_has_safe_transfer_from(hir: &hir::Hir<'_>, cid: hir::ContractId) -> bool {
1759 hir.contract_item_ids(cid).any(|item| {
1760 let Some(fid) = item.as_function() else { return false };
1761 let f = hir.function(fid);
1762 if f.parameters.len() != 4 || f.name.is_none_or(|n| n.name.as_str() != "safeTransferFrom") {
1763 return false;
1764 }
1765 let token_ok = match hir.variable(f.parameters[0]).ty.kind {
1766 TypeKind::Elementary(ElementaryType::Address(_)) => true,
1767 TypeKind::Custom(ItemId::Contract(token_cid)) => contract_has_function(
1768 hir,
1769 token_cid,
1770 "transferFrom",
1771 &["address", "address", "uint256"],
1772 &["bool"],
1773 ),
1774 _ => false,
1775 };
1776 token_ok
1777 && is_address(hir, f.parameters[1])
1778 && is_address(hir, f.parameters[2])
1779 && is_elementary(hir, f.parameters[3], "uint256")
1780 })
1781}
1782
1783fn is_address(hir: &hir::Hir<'_>, id: hir::VariableId) -> bool {
1784 matches!(hir.variable(id).ty.kind, TypeKind::Elementary(ElementaryType::Address(_)))
1785}
1786
1787fn expr_is_address<'hir>(gcx: Gcx<'hir>, expr: &hir::Expr<'hir>) -> bool {
1789 expr_ty(gcx, expr).is_some_and(ty_is_address)
1790}
1791
1792fn expr_ty<'hir>(gcx: Gcx<'hir>, expr: &hir::Expr<'hir>) -> Option<Ty<'hir>> {
1793 gcx.type_of_expr(expr.peel_parens().id)
1794}
1795
1796fn ty_is_address(ty: Ty<'_>) -> bool {
1797 matches!(ty.peel_refs().kind, TyKind::Elementary(ElementaryType::Address(_)))
1798}
1799
1800fn is_elementary(hir: &hir::Hir<'_>, id: hir::VariableId, abi: &str) -> bool {
1801 matches!(&hir.variable(id).ty.kind, TypeKind::Elementary(ty) if ty.to_abi_str() == abi)
1802}
1803
1804fn canonical_args<'hir>(
1808 kind: hir::CallArgsKind<'hir>,
1809 aliases: &[&[&str]],
1810) -> Option<Vec<&'hir hir::Expr<'hir>>> {
1811 match kind {
1812 hir::CallArgsKind::Unnamed(exprs) => {
1813 (exprs.len() == aliases.len()).then(|| exprs.iter().collect())
1814 }
1815 hir::CallArgsKind::Named(named) => {
1816 if named.len() != aliases.len() {
1817 return None;
1818 }
1819 aliases
1820 .iter()
1821 .map(|accepted| {
1822 named.iter().find_map(|a| {
1823 accepted.iter().any(|n| a.name.as_str() == *n).then_some(&a.value)
1824 })
1825 })
1826 .collect()
1827 }
1828 }
1829}
1830
1831fn is_address_cast(callee: &hir::Expr<'_>) -> bool {
1832 matches!(
1833 &callee.peel_parens().kind,
1834 ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ElementaryType::Address(_)), .. })
1835 )
1836}
1837
1838fn is_require_or_assert(callee: &hir::Expr<'_>) -> bool {
1839 let ExprKind::Ident(reses) = &callee.kind else { return false };
1840 reses.iter().any(
1841 |r| matches!(r, Res::Builtin(b) if b.name() == sym::require || b.name() == sym::assert),
1842 )
1843}
1844
1845fn is_address_self(expr: &hir::Expr<'_>) -> bool {
1847 let expr = expr.peel_parens();
1848 if is_builtin(expr, sym::this) {
1849 return true;
1850 }
1851 if let ExprKind::Payable(inner) = &expr.kind {
1852 return is_address_self(inner);
1853 }
1854 matches!(&expr.kind, ExprKind::Call(callee, args, _) if is_address_cast(callee)
1855 && args.exprs().next().is_some_and(is_address_self))
1856}
1857
1858fn is_builtin(expr: &hir::Expr<'_>, name: solar::interface::Symbol) -> bool {
1859 let ExprKind::Ident(reses) = &expr.peel_parens().kind else { return false };
1860 reses.iter().any(|r| matches!(r, Res::Builtin(b) if b.name() == name))
1861}
1862
1863fn branch_always_exits(stmt: &hir::Stmt<'_>) -> bool {
1865 match &stmt.kind {
1866 StmtKind::Return(_) | StmtKind::Revert(_) => true,
1867 StmtKind::Expr(expr) => is_exit_call(expr),
1868 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => b.stmts.iter().any(branch_always_exits),
1870 StmtKind::If(_, t, Some(e)) => branch_always_exits(t) && branch_always_exits(e),
1871 StmtKind::Loop(block, LoopSource::DoWhile) => {
1874 let user = do_while_user_stmts(block.stmts);
1875 !body_has_break_or_continue(user) && user.iter().any(branch_always_exits)
1876 }
1877 StmtKind::Try(t) => t.clauses.iter().all(|c| c.block.stmts.iter().any(branch_always_exits)),
1880 _ => false,
1881 }
1882}
1883
1884fn is_exit_call(expr: &hir::Expr<'_>) -> bool {
1885 let ExprKind::Call(callee, args, _) = &expr.kind else { return false };
1886 if is_builtin(callee, kw::Revert) {
1887 return true;
1888 }
1889 if is_require_or_assert(callee)
1890 && let hir::CallArgsKind::Unnamed(unnamed) = args.kind
1891 && let Some(first) = unnamed.first()
1892 && matches!(
1893 &first.peel_parens().kind,
1894 ExprKind::Lit(lit) if matches!(lit.kind, ast::LitKind::Bool(false))
1895 )
1896 {
1897 return true;
1898 }
1899 false
1900}