1use super::ArbitrarySendErc20;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{
7 arg_for_param, branch_always_exits, expr_is_address, is_address_like_cast,
8 is_address_self, is_address_type, is_elementary, is_msg_sender, is_require_or_assert,
9 loop_update, modifier_prefix, receiver_contract_id, state_lhs_vars, tuple_elems,
10 underlying_var,
11 },
12 },
13};
14use solar::{
15 ast::{BinOpKind, StateMutability, UnOpKind, Visibility},
16 interface::{Span, Symbol, data_structures::Never},
17 sema::{
18 Gcx,
19 hir::{
20 self, CallArgs, ContractId, ContractKind, Expr, ExprKind, FunctionId, FunctionKind,
21 Hir, ItemId, LoopSource, Modifier, Stmt, StmtKind, TypeKind, VariableId, Visit,
22 },
23 },
24};
25use std::{
26 cell::RefCell,
27 collections::{HashMap, HashSet},
28 hash::Hash,
29 ops::ControlFlow,
30 rc::Rc,
31};
32
33declare_forge_lint!(
34 ARBITRARY_SEND_ERC20,
35 Severity::High,
36 "arbitrary-send-erc20",
37 "`transferFrom` uses an arbitrary `from`; require it to equal `msg.sender` or `address(this)`"
38);
39
40declare_forge_lint!(
41 ARBITRARY_SEND_ERC20_PERMIT,
42 Severity::High,
43 "arbitrary-send-erc20-permit",
44 "`transferFrom` uses an arbitrary `from` after `permit`"
45);
46
47const HELPER_DEPTH: u8 = 3;
49
50impl<'gcx> LateLintPass<'gcx> for ArbitrarySendErc20 {
51 fn check_function(
52 &mut self,
53 ctx: &LintContext,
54 gcx: Gcx<'gcx>,
55 func: &'gcx hir::Function<'gcx>,
56 ) {
57 if matches!(func.state_mutability, StateMutability::Pure | StateMutability::View)
59 || func.is_constructor()
60 || func.contract.is_some_and(|cid| gcx.hir.contract(cid).kind == ContractKind::Library)
61 {
62 return;
63 }
64 let Some(body) = func.body else { return };
65 if func.modifiers.iter().any(|m| {
67 m.id.as_function()
68 .and_then(|fid| modifier_prefix(&gcx.hir, fid))
69 .is_some_and(|p| p.iter().any(|s| branch_always_exits(gcx, s)))
70 }) {
71 return;
72 }
73 let mut a = Analyzer::new(gcx);
74 if let Some(cid) = func.contract {
75 a.seed_immutable_facts(cid);
76 }
77 a.seed_callsite_facts(func);
78 for m in func.modifiers {
79 a.hoist_modifier_facts(m);
80 }
81 a.visit_stmts(body.stmts);
82 for (span, lint) in a.hits {
83 ctx.emit(lint, span);
84 }
85 }
86}
87
88#[derive(Clone, Copy, PartialEq, Eq, Hash)]
90enum TokenKey {
91 Var(VariableId),
92 Field(VariableId, Symbol),
93}
94
95impl TokenKey {
96 fn touches(self, v: VariableId) -> bool {
97 match self {
98 Self::Var(x) | Self::Field(x, _) => x == v,
99 }
100 }
101}
102
103#[derive(Clone, Copy, PartialEq, Eq, Hash)]
105struct PermitRecord {
106 token: TokenKey,
107 owner: VariableId,
108}
109
110#[derive(Clone, Copy, PartialEq, Eq, Hash)]
112struct PendingRepayment {
113 receiver: VariableId,
114 token: VariableId,
115 amount: VariableId,
116 fee: VariableId,
117}
118
119struct Sink<'gcx> {
121 from: &'gcx Expr<'gcx>,
122 to: &'gcx Expr<'gcx>,
123 amount: &'gcx Expr<'gcx>,
124 token: Option<TokenKey>,
125}
126
127#[derive(Clone, Copy, Default)]
129struct Rhs {
130 safe: bool,
131 is_self: bool,
132 alias: Option<VariableId>,
133 sum: Option<(VariableId, VariableId)>,
134}
135
136#[derive(Clone, Default)]
138struct State {
139 safe_vars: HashSet<VariableId>,
142 self_vars: HashSet<VariableId>,
144 permits: HashSet<PermitRecord>,
146 repayments: HashMap<PendingRepayment, u32>,
148 aliases: HashMap<VariableId, VariableId>,
150 sum_of: HashMap<VariableId, (VariableId, VariableId)>,
152}
153
154impl State {
155 fn meet(&self, other: &Self) -> Self {
156 Self {
157 safe_vars: self.safe_vars.intersection(&other.safe_vars).copied().collect(),
158 self_vars: self.self_vars.intersection(&other.self_vars).copied().collect(),
159 permits: self.permits.intersection(&other.permits).copied().collect(),
160 repayments: self
161 .repayments
162 .iter()
163 .filter_map(|(k, a)| other.repayments.get(k).map(|b| (*k, *a.min(b))))
164 .collect(),
165 aliases: common_entries(&self.aliases, &other.aliases),
166 sum_of: common_entries(&self.sum_of, &other.sum_of),
167 }
168 }
169}
170
171fn common_entries<K: Eq + Hash + Copy, V: PartialEq + Copy>(
172 a: &HashMap<K, V>,
173 b: &HashMap<K, V>,
174) -> HashMap<K, V> {
175 a.iter().filter(|(k, v)| b.get(k) == Some(v)).map(|(k, v)| (*k, *v)).collect()
176}
177
178struct Analyzer<'gcx> {
179 gcx: Gcx<'gcx>,
180 state: State,
182 loop_exits: Vec<Vec<State>>,
184 loop_repayment_floors: Vec<HashMap<PendingRepayment, u32>>,
192 written: HashSet<VariableId>,
194 hits: Vec<(Span, &'static SolLint)>,
195}
196
197impl<'gcx> Analyzer<'gcx> {
198 fn new(gcx: Gcx<'gcx>) -> Self {
199 Self {
200 gcx,
201 state: State::default(),
202 loop_exits: Vec::new(),
203 loop_repayment_floors: Vec::new(),
204 written: HashSet::new(),
205 hits: Vec::new(),
206 }
207 }
208
209 fn seed_immutable_facts(&mut self, cid: ContractId) {
212 for v in self.gcx.hir.contract(cid).variables() {
213 let var = self.gcx.hir.variable(v);
214 if (var.is_immutable() || var.is_constant())
215 && let Some(init) = var.initializer
216 {
217 if self.is_safe(init) {
218 self.state.safe_vars.insert(v);
219 }
220 if self.is_self_expr(init) {
221 self.state.self_vars.insert(v);
222 }
223 }
224 }
225 if let Some(ctor) = self.gcx.hir.contract(cid).ctor
226 && let Some(body) = self.gcx.hir.function(ctor).body
227 {
228 let mut a = Self::new(self.gcx);
229 a.visit_stmts(body.stmts);
230 let is_state = |v: &&VariableId| self.gcx.hir.variable(**v).kind.is_state();
231 self.state.safe_vars.extend(a.state.safe_vars.iter().filter(is_state));
232 self.state.self_vars.extend(a.state.self_vars.iter().filter(is_state));
233 }
234 }
235
236 fn seed_callsite_facts(&mut self, func: &'gcx hir::Function<'gcx>) {
239 if !is_internal_only(func) {
240 return;
241 }
242 let index = callsite_index(self.gcx);
243 let Some((fid, _)) =
244 self.gcx.hir.functions_enumerated().find(|(_, f)| std::ptr::eq(*f, func))
245 else {
246 return;
247 };
248 let Some(Some(facts)) = index.get(&fid) else { return };
249 for (¶m, &(safe, is_self)) in func.parameters.iter().zip(facts) {
250 if safe {
251 self.state.safe_vars.insert(param);
252 }
253 if is_self {
254 self.state.self_vars.insert(param);
255 }
256 }
257 }
258
259 fn hoist_modifier_facts(&mut self, m: &'gcx Modifier<'gcx>) {
262 let Some(fid) = m.id.as_function() else { return };
263 let Some(prefix) = modifier_prefix(&self.gcx.hir, fid) else { return };
264 let modifier = self.gcx.hir.function(fid);
265 let mut a = Self::new(self.gcx);
266 for stmt in prefix {
267 a.stmt(stmt);
268 }
269 for ¶m in modifier.parameters {
270 if !a.written.contains(¶m)
272 && let Some(caller) = arg_for_param(self.gcx, fid, param, &m.args)
273 .and_then(|expr| underlying_var(self.gcx, expr))
274 && self.is_safe_target(caller)
275 {
276 if a.state.safe_vars.contains(¶m) {
277 self.state.safe_vars.insert(caller);
278 }
279 if a.state.self_vars.contains(¶m) {
280 self.state.self_vars.insert(caller);
281 }
282 }
283 }
284 }
285
286 fn is_safe(&self, expr: &Expr<'_>) -> bool {
288 origin_matches(self.gcx, expr, HELPER_DEPTH, &self.state.safe_vars, |gcx, e| {
289 is_msg_sender(gcx, e) || is_address_self(gcx, e)
290 })
291 }
292
293 fn is_self_expr(&self, expr: &Expr<'_>) -> bool {
295 origin_matches(self.gcx, expr, HELPER_DEPTH, &self.state.self_vars, is_address_self)
296 }
297
298 fn is_safe_target(&self, v: VariableId) -> bool {
299 let var = self.gcx.hir.variable(v);
300 !var.kind.is_state() || var.is_immutable() || var.is_constant()
301 }
302
303 fn canonical(&self, v: VariableId) -> VariableId {
305 let mut cur = v;
306 for _ in 0..8 {
307 match self.state.aliases.get(&cur) {
308 Some(next) if *next != cur => cur = *next,
309 _ => break,
310 }
311 }
312 cur
313 }
314
315 fn canonical_key(&self, key: TokenKey) -> TokenKey {
316 match key {
317 TokenKey::Var(v) => TokenKey::Var(self.canonical(v)),
318 TokenKey::Field(v, name) => TokenKey::Field(self.canonical(v), name),
319 }
320 }
321
322 fn invalidate(&mut self, v: VariableId) {
324 let s = &mut self.state;
325 s.safe_vars.remove(&v);
326 s.self_vars.remove(&v);
327 s.aliases.retain(|k, dst| *k != v && *dst != v);
328 s.sum_of.retain(|k, (a, b)| *k != v && *a != v && *b != v);
329 s.permits.retain(|p| !p.token.touches(v) && p.owner != v);
330 s.repayments.retain(|r, _| ![r.receiver, r.token, r.amount, r.fee].contains(&v));
331 }
340
341 fn eval_rhs(&self, rhs: Option<&Expr<'_>>) -> Rhs {
342 let Some(rhs) = rhs else { return Rhs::default() };
343 Rhs {
344 safe: self.is_safe(rhs),
345 is_self: self.is_self_expr(rhs),
346 alias: underlying_var(self.gcx, rhs).map(|v| self.canonical(v)),
347 sum: sum_operands(self.gcx, rhs),
348 }
349 }
350
351 fn assign_var(&mut self, target: VariableId, rhs: Rhs) {
352 self.written.insert(target);
353 self.invalidate(target);
354 if !self.is_safe_target(target) {
355 return;
356 }
357 if rhs.safe {
358 self.state.safe_vars.insert(target);
359 }
360 if rhs.is_self {
361 self.state.self_vars.insert(target);
362 }
363 if let Some(alias) = rhs.alias
364 && alias != target
365 {
366 self.state.aliases.insert(target, alias);
367 }
368 if let Some(sum) = rhs.sum {
369 self.state.sum_of.insert(target, sum);
370 }
371 }
372
373 fn assign_lhs(&mut self, lhs: &Expr<'_>, rhs: Option<&Expr<'_>>) {
375 if let ExprKind::Member(base, ident) = &lhs.peel_parens().kind
377 && let Some(base) = underlying_var(self.gcx, base)
378 {
379 let key = TokenKey::Field(self.canonical(base), ident.name);
380 self.state.permits.retain(|p| p.token != key);
381 }
382 if let Some(elems) = tuple_elems(lhs) {
383 let rhs = rhs.and_then(tuple_elems);
384 let slots: Vec<_> = elems
386 .iter()
387 .enumerate()
388 .map(|(i, l)| (*l, self.eval_rhs(rhs.and_then(|r| r.get(i).copied().flatten()))))
389 .collect();
390 for (lhs, rhs) in slots {
391 if let Some(v) = lhs.and_then(|expr| underlying_var(self.gcx, expr)) {
392 self.assign_var(v, rhs);
393 }
394 }
395 } else if let Some(v) = underlying_var(self.gcx, lhs) {
396 let rhs = self.eval_rhs(rhs);
397 self.assign_var(v, rhs);
398 }
399 }
400
401 fn add_facts(&mut self, pred: &Expr<'_>, negate: bool) {
403 match &pred.peel_parens().kind {
404 ExprKind::Binary(lhs, op, rhs) => {
405 let (eq, and, or) = if negate {
406 (BinOpKind::Ne, BinOpKind::Or, BinOpKind::And)
407 } else {
408 (BinOpKind::Eq, BinOpKind::And, BinOpKind::Or)
409 };
410 if op.kind == and {
411 self.add_facts(lhs, negate);
412 self.add_facts(rhs, negate);
413 } else if op.kind == or {
414 let before = self.state.clone();
416 self.add_facts(lhs, negate);
417 let after_lhs = std::mem::replace(&mut self.state, before);
418 self.add_facts(rhs, negate);
419 self.state = after_lhs.meet(&self.state);
420 } else if op.kind == eq {
421 for (a, b) in [(lhs, rhs), (rhs, lhs)] {
422 if let Some(v) = underlying_var(self.gcx, b)
423 && self.is_safe_target(v)
424 {
425 if self.is_safe(a) {
426 self.state.safe_vars.insert(v);
427 }
428 if self.is_self_expr(a) {
429 self.state.self_vars.insert(v);
430 }
431 }
432 }
433 }
434 }
435 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
436 self.add_facts(inner, !negate);
437 }
438 _ => {}
439 }
440 }
441
442 fn match_permit_call(&self, expr: &Expr<'gcx>) -> Option<PermitRecord> {
445 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
446 let ExprKind::Member(recv, ident) = &callee.peel_parens().kind else { return None };
447 let (token, owner, spender) = match ident.name.as_str() {
448 "permit" => {
449 let a = canonical_args(self.gcx, expr, 7)?;
450 (*recv, a[0], a[1])
451 }
452 "safePermit"
453 if receiver_contract_id(self.gcx, recv).is_some_and(|cid| {
454 self.gcx.hir.contract(cid).kind == ContractKind::Library
455 }) =>
456 {
457 let a = canonical_args(self.gcx, expr, 8)?;
458 (a[0], a[1], a[2])
459 }
460 _ => return None,
461 };
462 if !self.is_self_expr(spender) {
463 return None;
464 }
465 Some(PermitRecord {
466 token: self.canonical_key(token_key(self.gcx, token)?),
467 owner: self.canonical(underlying_var(self.gcx, owner)?),
468 })
469 }
470
471 fn permit_covers(&self, sink: &Sink<'_>) -> bool {
472 let (Some(token), Some(owner)) = (sink.token, underlying_var(self.gcx, sink.from)) else {
473 return false;
474 };
475 self.state.permits.contains(&PermitRecord {
476 token: self.canonical_key(token),
477 owner: self.canonical(owner),
478 })
479 }
480
481 fn amount_matches(&self, expr: &Expr<'_>, amount: VariableId, fee: VariableId) -> bool {
483 let sum = sum_operands(self.gcx, expr).or_else(|| {
484 underlying_var(self.gcx, expr).and_then(|v| self.state.sum_of.get(&v).copied())
485 });
486 matches!(sum, Some(pair) if pair == (amount, fee) || pair == (fee, amount))
487 }
488
489 fn consume_repayment(&mut self, sink: &Sink<'_>) -> bool {
492 let (Some(from), Some(TokenKey::Var(token))) =
493 (underlying_var(self.gcx, sink.from), sink.token)
494 else {
495 return false;
496 };
497 if !self.is_self_expr(sink.to) {
498 return false;
499 }
500 let candidates: Vec<PendingRepayment> = self
501 .state
502 .repayments
503 .keys()
504 .copied()
505 .filter(|r| {
506 r.receiver == from
507 && r.token == token
508 && self.amount_matches(sink.amount, r.amount, r.fee)
509 })
510 .collect();
511 let floor = self.loop_repayment_floors.last();
516 let is_fresh = |r: &PendingRepayment, repayments: &HashMap<PendingRepayment, u32>| {
517 let count = repayments.get(r).copied().unwrap_or(0);
518 let floor = floor.and_then(|f| f.get(r)).copied().unwrap_or(0);
519 count > floor
520 };
521 let Some(rep) = candidates.into_iter().find(|r| is_fresh(r, &self.state.repayments)) else {
522 return false;
523 };
524 match self.state.repayments.get_mut(&rep) {
525 Some(count) if *count > 1 => *count -= 1,
526 _ => {
527 self.state.repayments.remove(&rep);
528 }
529 }
530 true
531 }
532
533 fn visit_stmts(&mut self, stmts: &'gcx [Stmt<'gcx>]) -> bool {
536 stmts.iter().all(|s| self.stmt(s))
537 }
538
539 fn stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> bool {
541 match &stmt.kind {
542 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => return self.visit_stmts(b.stmts),
543 StmtKind::Break | StmtKind::Continue => {
544 let state = self.state.clone();
545 if let Some(exits) = self.loop_exits.last_mut() {
546 exits.push(state);
547 }
548 return false;
549 }
550 StmtKind::If(cond, then, else_) => {
551 let _ = self.visit_expr(cond);
552 let before = self.state.clone();
553 self.add_facts(cond, false);
554 let then_falls = self.stmt(then);
555 let after_then = std::mem::replace(&mut self.state, before);
556 self.add_facts(cond, true);
557 let else_falls = else_.is_none_or(|e| self.stmt(e));
558 match (then_falls, else_falls) {
559 (true, true) => self.state = after_then.meet(&self.state),
560 (true, false) => self.state = after_then,
561 _ => {}
562 }
563 return then_falls || else_falls;
564 }
565 StmtKind::Loop(block, source) => {
566 let baseline = (!matches!(source, LoopSource::DoWhile)).then(|| self.state.clone());
569 self.loop_exits.push(baseline.into_iter().collect());
570 self.loop_repayment_floors.push(self.state.repayments.clone());
585 if self.visit_stmts(block.stmts)
586 && loop_update(*source).is_none_or(|update| self.stmt(update))
587 {
588 let state = self.state.clone();
589 self.loop_exits.last_mut().expect("pushed above").push(state);
590 }
591 self.loop_repayment_floors.pop().expect("pushed above");
592 let exits = self.loop_exits.pop().expect("pushed above");
593 let falls = !exits.is_empty();
594 if let Some(joined) = exits.into_iter().reduce(|a, b| a.meet(&b)) {
595 self.state = joined;
596 }
597 return falls;
598 }
599 StmtKind::Try(t) => {
600 let before = self.state.clone();
602 let _ = self.visit_expr(&t.expr);
603 let after_call = self.state.clone();
604 let mut joined = None::<State>;
605 for (i, clause) in t.clauses.iter().enumerate() {
606 self.state = if i == 0 { after_call.clone() } else { before.clone() };
607 if self.visit_stmts(clause.block.stmts) {
608 joined = Some(
609 joined.map_or_else(|| self.state.clone(), |j| j.meet(&self.state)),
610 );
611 }
612 }
613 let falls = joined.is_some();
614 self.state = joined.unwrap_or(after_call);
615 return falls;
616 }
617 StmtKind::DeclSingle(vid) => {
618 if let Some(init) = self.gcx.hir.variable(*vid).initializer {
619 let rhs = self.eval_rhs(Some(init));
620 self.assign_var(*vid, rhs);
621 }
622 }
623 StmtKind::DeclMulti(vars, init) => {
624 for (vid, rhs) in vars.iter().zip(tuple_elems(init).into_iter().flatten()) {
625 if let (Some(vid), Some(rhs)) = (vid, rhs) {
626 let rhs = self.eval_rhs(Some(rhs));
627 self.assign_var(*vid, rhs);
628 }
629 }
630 }
631 _ => {}
632 }
633 let _ = self.walk_stmt(stmt);
634 !branch_always_exits(self.gcx, stmt)
635 }
636}
637
638impl<'gcx> Visit<'gcx> for Analyzer<'gcx> {
639 type BreakValue = Never;
640
641 fn hir(&self) -> &'gcx Hir<'gcx> {
642 &self.gcx.hir
643 }
644
645 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
646 self.stmt(stmt);
647 ControlFlow::Continue(())
648 }
649
650 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
651 match &expr.kind {
652 ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
655 let _ = self.visit_expr(lhs);
656 let skipped = self.state.clone();
657 self.add_facts(lhs, op.kind == BinOpKind::Or);
658 let _ = self.visit_expr(rhs);
659 self.state = skipped.meet(&self.state);
660 }
661 ExprKind::Call(callee, args) if is_require_or_assert(self.gcx, callee) => {
662 let _ = self.walk_expr(expr);
664 if let Some(cond) = args.exprs().next() {
665 self.add_facts(cond, false);
666 }
667 }
668 ExprKind::Call(callee, ..) => {
669 if let Some(rep) = match_flash_loan_call(self.gcx, expr) {
670 *self.state.repayments.entry(rep).or_insert(0) += 1;
671 } else if let Some(permit) = self.match_permit_call(expr) {
672 self.state.permits.insert(permit);
673 } else if let Some(sink) = match_sink(self.gcx, expr)
674 && !self.is_safe(sink.from)
675 && !self.consume_repayment(&sink)
676 {
677 let lint = if self.permit_covers(&sink) {
680 &ARBITRARY_SEND_ERC20_PERMIT
681 } else {
682 &ARBITRARY_SEND_ERC20
683 };
684 self.hits.push((expr.span, lint));
685 }
686 let _ = self.walk_expr(expr);
689 if let Some(fid) = self.gcx.resolved_function(callee)
690 && matches!(callee.peel_parens().kind, ExprKind::Ident(_))
691 {
692 for v in state_writes(self.gcx, fid) {
693 self.invalidate(v);
694 }
695 }
696 }
697 ExprKind::Assign(lhs, _, rhs) => {
698 self.assign_lhs(lhs, Some(rhs));
699 let _ = self.walk_expr(expr);
700 }
701 ExprKind::Delete(target) => {
702 self.assign_lhs(target, None);
703 let _ = self.walk_expr(expr);
704 }
705 _ => {
706 let _ = self.walk_expr(expr);
707 }
708 }
709 ControlFlow::Continue(())
710 }
711}
712
713fn origin_matches(
716 gcx: Gcx<'_>,
717 expr: &Expr<'_>,
718 depth: u8,
719 vars: &HashSet<VariableId>,
720 base: fn(Gcx<'_>, &Expr<'_>) -> bool,
721) -> bool {
722 let expr = expr.peel_parens();
723 match &expr.kind {
724 ExprKind::Payable(inner) => return origin_matches(gcx, inner, depth, vars, base),
725 ExprKind::Call(callee, args) if is_address_like_cast(gcx, callee) => {
726 return args.exprs().next().is_some_and(|e| origin_matches(gcx, e, depth, vars, base));
727 }
728 _ => {}
729 }
730 base(gcx, expr)
731 || match &expr.kind {
732 ExprKind::Ident(_) => gcx.resolved_variable(expr).is_some_and(|v| vars.contains(&v)),
733 ExprKind::Ternary(_, t, f) => {
734 origin_matches(gcx, t, depth, vars, base)
735 && origin_matches(gcx, f, depth, vars, base)
736 }
737 ExprKind::Call(callee, args) if depth > 0 && args.exprs().next().is_none() => gcx
738 .resolved_function(callee)
739 .filter(|_| matches!(callee.peel_parens().kind, ExprKind::Ident(_)))
740 .is_some_and(|fid| {
741 let f = gcx.hir.function(fid);
742 f.parameters.is_empty()
743 && matches!(f.body.map(|b| b.stmts), Some([stmt])
744 if matches!(&stmt.kind, StmtKind::Return(Some(e))
745 if origin_matches(gcx, e, depth - 1, vars, base)))
746 }),
747 _ => false,
748 }
749}
750
751fn sum_operands(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<(VariableId, VariableId)> {
753 match &expr.peel_parens().kind {
754 ExprKind::Binary(lhs, op, rhs) if op.kind == BinOpKind::Add => {
755 underlying_var(gcx, lhs).zip(underlying_var(gcx, rhs))
756 }
757 _ => None,
758 }
759}
760
761fn token_key(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<TokenKey> {
763 if let Some(v) = underlying_var(gcx, expr) {
764 return Some(TokenKey::Var(v));
765 }
766 match &expr.peel_parens().kind {
767 ExprKind::Member(base, ident) => {
768 Some(TokenKey::Field(underlying_var(gcx, base)?, ident.name))
769 }
770 _ => None,
771 }
772}
773
774fn canonical_args<'gcx>(
776 gcx: Gcx<'gcx>,
777 expr: &Expr<'gcx>,
778 arity: usize,
779) -> Option<Vec<&'gcx Expr<'gcx>>> {
780 let ExprKind::Call(_, args) = &expr.kind else { return None };
781 if args.len() != arity {
782 return None;
783 }
784 (0..arity).map(|index| gcx.call_arg(expr, index)).collect()
785}
786
787fn match_flash_loan_call<'gcx>(gcx: Gcx<'gcx>, expr: &Expr<'gcx>) -> Option<PendingRepayment> {
790 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
791 let ExprKind::Member(recv, ident) = &callee.peel_parens().kind else { return None };
792 if ident.name.as_str() != "onFlashLoan" {
793 return None;
794 }
795 let a = canonical_args(gcx, expr, 5)?;
796 let cid = receiver_contract_id(gcx, recv)?;
797 if !interface_has_function(
798 gcx,
799 cid,
800 "onFlashLoan(address,address,uint256,uint256,bytes)",
801 &["address", "address", "uint256", "uint256", "bytes"],
802 &["bytes32"],
803 ) {
804 return None;
805 }
806 Some(PendingRepayment {
807 receiver: underlying_var(gcx, recv)?,
808 token: underlying_var(gcx, a[1])?,
809 amount: underlying_var(gcx, a[2])?,
810 fee: underlying_var(gcx, a[3])?,
811 })
812}
813
814fn match_sink<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> Option<Sink<'gcx>> {
819 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
820 let ExprKind::Member(recv, ident) = &callee.peel_parens().kind else { return None };
821 let name = ident.name.as_str();
822 if matches!(name, "transferFrom" | "safeTransferFrom")
823 && let Some(a) = canonical_args(gcx, expr, 3)
824 {
825 let erc20 = receiver_contract_id(gcx, recv).is_some_and(|cid| has_transfer_from(gcx, cid));
826 let attached = gcx.resolved_call(expr).is_some_and(|resolved| {
827 resolved.attached
828 && resolved
829 .res
830 .as_function()
831 .and_then(|fid| gcx.hir.function(fid).contract)
832 .is_some_and(|cid| library_has_safe_transfer_from(gcx, cid))
833 });
834 if erc20 || (name == "safeTransferFrom" && attached && expr_is_address(gcx, recv)) {
835 return Some(Sink { from: a[0], to: a[1], amount: a[2], token: token_key(gcx, recv) });
836 }
837 }
838 if name == "safeTransferFrom"
839 && let Some(a) = canonical_args(gcx, expr, 4)
840 && let Some(cid) = receiver_contract_id(gcx, recv)
841 && gcx.hir.contract(cid).kind == ContractKind::Library
842 && library_has_safe_transfer_from(gcx, cid)
843 {
844 return Some(Sink { from: a[1], to: a[2], amount: a[3], token: token_key(gcx, a[0]) });
845 }
846 None
847}
848
849fn state_writes<'gcx>(gcx: Gcx<'gcx>, fid: FunctionId) -> HashSet<VariableId> {
851 let mut w = StateWrites { gcx, out: HashSet::new(), callees: Vec::new() };
852 w.scan(fid);
853 for callee in std::mem::take(&mut w.callees) {
854 w.scan(callee);
855 }
856 w.out
857}
858
859struct StateWrites<'gcx> {
860 gcx: Gcx<'gcx>,
861 out: HashSet<VariableId>,
862 callees: Vec<FunctionId>,
863}
864
865impl StateWrites<'_> {
866 fn scan(&mut self, fid: FunctionId) {
867 if let Some(body) = self.gcx.hir.function(fid).body {
868 for stmt in body.stmts {
869 let _ = self.visit_stmt(stmt);
870 }
871 }
872 }
873}
874
875impl<'gcx> Visit<'gcx> for StateWrites<'gcx> {
876 type BreakValue = Never;
877
878 fn hir(&self) -> &'gcx Hir<'gcx> {
879 &self.gcx.hir
880 }
881
882 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
883 match &expr.kind {
884 ExprKind::Assign(lhs, ..) | ExprKind::Delete(lhs) => {
885 self.out.extend(state_lhs_vars(self.gcx, lhs));
886 }
887 ExprKind::Call(callee, ..)
888 if matches!(callee.peel_parens().kind, ExprKind::Ident(_)) =>
889 {
890 self.callees.extend(self.gcx.resolved_function(callee));
891 }
892 _ => {}
893 }
894 self.walk_expr(expr)
895 }
896}
897
898const fn is_internal_only(f: &hir::Function<'_>) -> bool {
901 !f.parameters.is_empty()
902 && (matches!(f.kind, FunctionKind::Modifier)
903 || (f.kind.is_function()
904 && matches!(f.visibility, Visibility::Private | Visibility::Internal)))
905}
906
907type CallsiteFacts = HashMap<FunctionId, Option<Vec<(bool, bool)>>>;
910
911thread_local! {
912 static CALLSITE_INDEX: RefCell<Option<(usize, Rc<CallsiteFacts>)>> = const { RefCell::new(None) };
913}
914
915fn callsite_index<'gcx>(gcx: Gcx<'gcx>) -> Rc<CallsiteFacts> {
917 let key = std::ptr::from_ref(&gcx.hir) as usize;
918 CALLSITE_INDEX.with(|cell| {
919 let mut slot = cell.borrow_mut();
920 if let Some((cached_key, index)) = &*slot
921 && *cached_key == key
922 {
923 return index.clone();
924 }
925 let mut c = CallsiteCollector { gcx, out: HashMap::new() };
926 for (_, func) in gcx.hir.functions_enumerated() {
927 for m in func.modifiers {
928 if let ItemId::Function(fid) = m.id {
929 c.record(fid, &m.args);
930 }
931 }
932 for stmt in func.body.map_or(&[][..], |b| b.stmts) {
933 let _ = c.visit_stmt(stmt);
934 }
935 }
936 let index = Rc::new(c.out);
937 *slot = Some((key, index.clone()));
938 index
939 })
940}
941
942struct CallsiteCollector<'gcx> {
943 gcx: Gcx<'gcx>,
944 out: CallsiteFacts,
945}
946
947impl<'gcx> CallsiteCollector<'gcx> {
948 fn record(&mut self, fid: FunctionId, args: &'gcx CallArgs<'gcx>) {
949 let f = self.gcx.hir.function(fid);
950 if !is_internal_only(f) {
951 return;
952 }
953 let call_args =
954 f.parameters.iter().map(|&p| arg_for_param(self.gcx, fid, p, args)).collect();
955 let entry =
956 self.out.entry(fid).or_insert_with(|| Some(vec![(true, true); f.parameters.len()]));
957 let (Some(facts), Some(call_args)) = (entry.as_mut(), call_args) else {
958 *entry = None;
959 return;
960 };
961 let none = HashSet::new();
962 for ((safe, is_self), arg) in facts.iter_mut().zip::<Vec<_>>(call_args) {
963 *safe &= origin_matches(self.gcx, arg, HELPER_DEPTH, &none, |gcx, e| {
964 is_msg_sender(gcx, e) || is_address_self(gcx, e)
965 });
966 *is_self &= origin_matches(self.gcx, arg, HELPER_DEPTH, &none, is_address_self);
967 }
968 }
969}
970
971impl<'gcx> Visit<'gcx> for CallsiteCollector<'gcx> {
972 type BreakValue = Never;
973
974 fn hir(&self) -> &'gcx Hir<'gcx> {
975 &self.gcx.hir
976 }
977
978 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
979 if let ExprKind::Call(callee, args) = &expr.kind
980 && matches!(callee.peel_parens().kind, ExprKind::Ident(_))
981 && let Some(fid) = self.gcx.resolved_function(callee)
982 {
983 self.record(fid, args);
984 }
985 self.walk_expr(expr)
986 }
987}
988
989fn has_transfer_from(gcx: Gcx<'_>, cid: ContractId) -> bool {
991 interface_has_function(
992 gcx,
993 cid,
994 "transferFrom(address,address,uint256)",
995 &["address", "address", "uint256"],
996 &["bool"],
997 )
998}
999
1000fn interface_has_function(
1001 gcx: Gcx<'_>,
1002 cid: ContractId,
1003 signature: &str,
1004 params: &[&str],
1005 returns: &[&str],
1006) -> bool {
1007 gcx.interface_functions(cid).all().iter().any(|function| {
1008 let f = gcx.hir.function(function.id);
1009 gcx.item_signature(function.id.into()) == signature
1010 && f.parameters.len() == params.len()
1011 && f.returns.len() == returns.len()
1012 && f.parameters.iter().zip(params).all(|(id, abi)| is_elementary(&gcx.hir, *id, abi))
1013 && f.returns.iter().zip(returns).all(|(id, abi)| is_elementary(&gcx.hir, *id, abi))
1014 })
1015}
1016
1017fn library_has_safe_transfer_from(gcx: Gcx<'_>, cid: ContractId) -> bool {
1021 let hir = &gcx.hir;
1022 hir.contract(cid).functions().any(|fid| {
1023 let f = hir.function(fid);
1024 let [token, from, to, amount] = f.parameters else { return false };
1025 let token_ok = match hir.variable(*token).ty.kind {
1026 TypeKind::Custom(ItemId::Contract(token_cid)) => has_transfer_from(gcx, token_cid),
1027 _ => is_address_type(hir, *token),
1028 };
1029 f.name.is_some_and(|n| n.name.as_str() == "safeTransferFrom")
1030 && token_ok
1031 && is_address_type(hir, *from)
1032 && is_address_type(hir, *to)
1033 && is_elementary(hir, *amount, "uint256")
1034 })
1035}