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 written: HashSet<VariableId>,
186 hits: Vec<(Span, &'static SolLint)>,
187}
188
189impl<'gcx> Analyzer<'gcx> {
190 fn new(gcx: Gcx<'gcx>) -> Self {
191 Self {
192 gcx,
193 state: State::default(),
194 loop_exits: Vec::new(),
195 written: HashSet::new(),
196 hits: Vec::new(),
197 }
198 }
199
200 fn seed_immutable_facts(&mut self, cid: ContractId) {
203 for v in self.gcx.hir.contract(cid).variables() {
204 let var = self.gcx.hir.variable(v);
205 if (var.is_immutable() || var.is_constant())
206 && let Some(init) = var.initializer
207 {
208 if self.is_safe(init) {
209 self.state.safe_vars.insert(v);
210 }
211 if self.is_self_expr(init) {
212 self.state.self_vars.insert(v);
213 }
214 }
215 }
216 if let Some(ctor) = self.gcx.hir.contract(cid).ctor
217 && let Some(body) = self.gcx.hir.function(ctor).body
218 {
219 let mut a = Self::new(self.gcx);
220 a.visit_stmts(body.stmts);
221 let is_state = |v: &&VariableId| self.gcx.hir.variable(**v).kind.is_state();
222 self.state.safe_vars.extend(a.state.safe_vars.iter().filter(is_state));
223 self.state.self_vars.extend(a.state.self_vars.iter().filter(is_state));
224 }
225 }
226
227 fn seed_callsite_facts(&mut self, func: &'gcx hir::Function<'gcx>) {
230 if !is_internal_only(func) {
231 return;
232 }
233 let index = callsite_index(self.gcx);
234 let Some((fid, _)) =
235 self.gcx.hir.functions_enumerated().find(|(_, f)| std::ptr::eq(*f, func))
236 else {
237 return;
238 };
239 let Some(Some(facts)) = index.get(&fid) else { return };
240 for (¶m, &(safe, is_self)) in func.parameters.iter().zip(facts) {
241 if safe {
242 self.state.safe_vars.insert(param);
243 }
244 if is_self {
245 self.state.self_vars.insert(param);
246 }
247 }
248 }
249
250 fn hoist_modifier_facts(&mut self, m: &'gcx Modifier<'gcx>) {
253 let Some(fid) = m.id.as_function() else { return };
254 let Some(prefix) = modifier_prefix(&self.gcx.hir, fid) else { return };
255 let modifier = self.gcx.hir.function(fid);
256 let mut a = Self::new(self.gcx);
257 for stmt in prefix {
258 a.stmt(stmt);
259 }
260 for ¶m in modifier.parameters {
261 if !a.written.contains(¶m)
263 && let Some(caller) = arg_for_param(self.gcx, fid, param, &m.args)
264 .and_then(|expr| underlying_var(self.gcx, expr))
265 && self.is_safe_target(caller)
266 {
267 if a.state.safe_vars.contains(¶m) {
268 self.state.safe_vars.insert(caller);
269 }
270 if a.state.self_vars.contains(¶m) {
271 self.state.self_vars.insert(caller);
272 }
273 }
274 }
275 }
276
277 fn is_safe(&self, expr: &Expr<'_>) -> bool {
279 origin_matches(self.gcx, expr, HELPER_DEPTH, &self.state.safe_vars, |gcx, e| {
280 is_msg_sender(gcx, e) || is_address_self(gcx, e)
281 })
282 }
283
284 fn is_self_expr(&self, expr: &Expr<'_>) -> bool {
286 origin_matches(self.gcx, expr, HELPER_DEPTH, &self.state.self_vars, is_address_self)
287 }
288
289 fn is_safe_target(&self, v: VariableId) -> bool {
290 let var = self.gcx.hir.variable(v);
291 !var.kind.is_state() || var.is_immutable() || var.is_constant()
292 }
293
294 fn canonical(&self, v: VariableId) -> VariableId {
296 let mut cur = v;
297 for _ in 0..8 {
298 match self.state.aliases.get(&cur) {
299 Some(next) if *next != cur => cur = *next,
300 _ => break,
301 }
302 }
303 cur
304 }
305
306 fn canonical_key(&self, key: TokenKey) -> TokenKey {
307 match key {
308 TokenKey::Var(v) => TokenKey::Var(self.canonical(v)),
309 TokenKey::Field(v, name) => TokenKey::Field(self.canonical(v), name),
310 }
311 }
312
313 fn invalidate(&mut self, v: VariableId) {
315 let s = &mut self.state;
316 s.safe_vars.remove(&v);
317 s.self_vars.remove(&v);
318 s.aliases.retain(|k, dst| *k != v && *dst != v);
319 s.sum_of.retain(|k, (a, b)| *k != v && *a != v && *b != v);
320 s.permits.retain(|p| !p.token.touches(v) && p.owner != v);
321 s.repayments.retain(|r, _| ![r.receiver, r.token, r.amount, r.fee].contains(&v));
322 }
323
324 fn eval_rhs(&self, rhs: Option<&Expr<'_>>) -> Rhs {
325 let Some(rhs) = rhs else { return Rhs::default() };
326 Rhs {
327 safe: self.is_safe(rhs),
328 is_self: self.is_self_expr(rhs),
329 alias: underlying_var(self.gcx, rhs).map(|v| self.canonical(v)),
330 sum: sum_operands(self.gcx, rhs),
331 }
332 }
333
334 fn assign_var(&mut self, target: VariableId, rhs: Rhs) {
335 self.written.insert(target);
336 self.invalidate(target);
337 if !self.is_safe_target(target) {
338 return;
339 }
340 if rhs.safe {
341 self.state.safe_vars.insert(target);
342 }
343 if rhs.is_self {
344 self.state.self_vars.insert(target);
345 }
346 if let Some(alias) = rhs.alias
347 && alias != target
348 {
349 self.state.aliases.insert(target, alias);
350 }
351 if let Some(sum) = rhs.sum {
352 self.state.sum_of.insert(target, sum);
353 }
354 }
355
356 fn assign_lhs(&mut self, lhs: &Expr<'_>, rhs: Option<&Expr<'_>>) {
358 if let ExprKind::Member(base, ident) = &lhs.peel_parens().kind
360 && let Some(base) = underlying_var(self.gcx, base)
361 {
362 let key = TokenKey::Field(self.canonical(base), ident.name);
363 self.state.permits.retain(|p| p.token != key);
364 }
365 if let Some(elems) = tuple_elems(lhs) {
366 let rhs = rhs.and_then(tuple_elems);
367 let slots: Vec<_> = elems
369 .iter()
370 .enumerate()
371 .map(|(i, l)| (*l, self.eval_rhs(rhs.and_then(|r| r.get(i).copied().flatten()))))
372 .collect();
373 for (lhs, rhs) in slots {
374 if let Some(v) = lhs.and_then(|expr| underlying_var(self.gcx, expr)) {
375 self.assign_var(v, rhs);
376 }
377 }
378 } else if let Some(v) = underlying_var(self.gcx, lhs) {
379 let rhs = self.eval_rhs(rhs);
380 self.assign_var(v, rhs);
381 }
382 }
383
384 fn add_facts(&mut self, pred: &Expr<'_>, negate: bool) {
386 match &pred.peel_parens().kind {
387 ExprKind::Binary(lhs, op, rhs) => {
388 let (eq, and, or) = if negate {
389 (BinOpKind::Ne, BinOpKind::Or, BinOpKind::And)
390 } else {
391 (BinOpKind::Eq, BinOpKind::And, BinOpKind::Or)
392 };
393 if op.kind == and {
394 self.add_facts(lhs, negate);
395 self.add_facts(rhs, negate);
396 } else if op.kind == or {
397 let before = self.state.clone();
399 self.add_facts(lhs, negate);
400 let after_lhs = std::mem::replace(&mut self.state, before);
401 self.add_facts(rhs, negate);
402 self.state = after_lhs.meet(&self.state);
403 } else if op.kind == eq {
404 for (a, b) in [(lhs, rhs), (rhs, lhs)] {
405 if let Some(v) = underlying_var(self.gcx, b)
406 && self.is_safe_target(v)
407 {
408 if self.is_safe(a) {
409 self.state.safe_vars.insert(v);
410 }
411 if self.is_self_expr(a) {
412 self.state.self_vars.insert(v);
413 }
414 }
415 }
416 }
417 }
418 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
419 self.add_facts(inner, !negate);
420 }
421 _ => {}
422 }
423 }
424
425 fn match_permit_call(&self, expr: &Expr<'gcx>) -> Option<PermitRecord> {
428 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
429 let ExprKind::Member(recv, ident) = &callee.peel_parens().kind else { return None };
430 let (token, owner, spender) = match ident.name.as_str() {
431 "permit" => {
432 let a = canonical_args(self.gcx, expr, 7)?;
433 (*recv, a[0], a[1])
434 }
435 "safePermit"
436 if receiver_contract_id(self.gcx, recv).is_some_and(|cid| {
437 self.gcx.hir.contract(cid).kind == ContractKind::Library
438 }) =>
439 {
440 let a = canonical_args(self.gcx, expr, 8)?;
441 (a[0], a[1], a[2])
442 }
443 _ => return None,
444 };
445 if !self.is_self_expr(spender) {
446 return None;
447 }
448 Some(PermitRecord {
449 token: self.canonical_key(token_key(self.gcx, token)?),
450 owner: self.canonical(underlying_var(self.gcx, owner)?),
451 })
452 }
453
454 fn permit_covers(&self, sink: &Sink<'_>) -> bool {
455 let (Some(token), Some(owner)) = (sink.token, underlying_var(self.gcx, sink.from)) else {
456 return false;
457 };
458 self.state.permits.contains(&PermitRecord {
459 token: self.canonical_key(token),
460 owner: self.canonical(owner),
461 })
462 }
463
464 fn amount_matches(&self, expr: &Expr<'_>, amount: VariableId, fee: VariableId) -> bool {
466 let sum = sum_operands(self.gcx, expr).or_else(|| {
467 underlying_var(self.gcx, expr).and_then(|v| self.state.sum_of.get(&v).copied())
468 });
469 matches!(sum, Some(pair) if pair == (amount, fee) || pair == (fee, amount))
470 }
471
472 fn consume_repayment(&mut self, sink: &Sink<'_>) -> bool {
475 let (Some(from), Some(TokenKey::Var(token))) =
476 (underlying_var(self.gcx, sink.from), sink.token)
477 else {
478 return false;
479 };
480 if !self.is_self_expr(sink.to) {
481 return false;
482 }
483 let Some(rep) = self.state.repayments.keys().copied().find(|r| {
484 r.receiver == from
485 && r.token == token
486 && self.amount_matches(sink.amount, r.amount, r.fee)
487 }) else {
488 return false;
489 };
490 match self.state.repayments.get_mut(&rep) {
491 Some(count) if *count > 1 => *count -= 1,
492 _ => {
493 self.state.repayments.remove(&rep);
494 }
495 }
496 true
497 }
498
499 fn visit_stmts(&mut self, stmts: &'gcx [Stmt<'gcx>]) -> bool {
502 stmts.iter().all(|s| self.stmt(s))
503 }
504
505 fn stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> bool {
507 match &stmt.kind {
508 StmtKind::Block(b) | StmtKind::UncheckedBlock(b) => return self.visit_stmts(b.stmts),
509 StmtKind::Break | StmtKind::Continue => {
510 let state = self.state.clone();
511 if let Some(exits) = self.loop_exits.last_mut() {
512 exits.push(state);
513 }
514 return false;
515 }
516 StmtKind::If(cond, then, else_) => {
517 let _ = self.visit_expr(cond);
518 let before = self.state.clone();
519 self.add_facts(cond, false);
520 let then_falls = self.stmt(then);
521 let after_then = std::mem::replace(&mut self.state, before);
522 self.add_facts(cond, true);
523 let else_falls = else_.is_none_or(|e| self.stmt(e));
524 match (then_falls, else_falls) {
525 (true, true) => self.state = after_then.meet(&self.state),
526 (true, false) => self.state = after_then,
527 _ => {}
528 }
529 return then_falls || else_falls;
530 }
531 StmtKind::Loop(block, source) => {
532 let baseline = (!matches!(source, LoopSource::DoWhile)).then(|| self.state.clone());
535 self.loop_exits.push(baseline.into_iter().collect());
536 if self.visit_stmts(block.stmts)
537 && loop_update(*source).is_none_or(|update| self.stmt(update))
538 {
539 let state = self.state.clone();
540 self.loop_exits.last_mut().expect("pushed above").push(state);
541 }
542 let exits = self.loop_exits.pop().expect("pushed above");
543 let falls = !exits.is_empty();
544 if let Some(joined) = exits.into_iter().reduce(|a, b| a.meet(&b)) {
545 self.state = joined;
546 }
547 return falls;
548 }
549 StmtKind::Try(t) => {
550 let before = self.state.clone();
552 let _ = self.visit_expr(&t.expr);
553 let after_call = self.state.clone();
554 let mut joined = None::<State>;
555 for (i, clause) in t.clauses.iter().enumerate() {
556 self.state = if i == 0 { after_call.clone() } else { before.clone() };
557 if self.visit_stmts(clause.block.stmts) {
558 joined = Some(
559 joined.map_or_else(|| self.state.clone(), |j| j.meet(&self.state)),
560 );
561 }
562 }
563 let falls = joined.is_some();
564 self.state = joined.unwrap_or(after_call);
565 return falls;
566 }
567 StmtKind::DeclSingle(vid) => {
568 if let Some(init) = self.gcx.hir.variable(*vid).initializer {
569 let rhs = self.eval_rhs(Some(init));
570 self.assign_var(*vid, rhs);
571 }
572 }
573 StmtKind::DeclMulti(vars, init) => {
574 for (vid, rhs) in vars.iter().zip(tuple_elems(init).into_iter().flatten()) {
575 if let (Some(vid), Some(rhs)) = (vid, rhs) {
576 let rhs = self.eval_rhs(Some(rhs));
577 self.assign_var(*vid, rhs);
578 }
579 }
580 }
581 _ => {}
582 }
583 let _ = self.walk_stmt(stmt);
584 !branch_always_exits(self.gcx, stmt)
585 }
586}
587
588impl<'gcx> Visit<'gcx> for Analyzer<'gcx> {
589 type BreakValue = Never;
590
591 fn hir(&self) -> &'gcx Hir<'gcx> {
592 &self.gcx.hir
593 }
594
595 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
596 self.stmt(stmt);
597 ControlFlow::Continue(())
598 }
599
600 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
601 match &expr.kind {
602 ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
605 let _ = self.visit_expr(lhs);
606 let skipped = self.state.clone();
607 self.add_facts(lhs, op.kind == BinOpKind::Or);
608 let _ = self.visit_expr(rhs);
609 self.state = skipped.meet(&self.state);
610 }
611 ExprKind::Call(callee, args, _) if is_require_or_assert(self.gcx, callee) => {
612 let _ = self.walk_expr(expr);
614 if let Some(cond) = args.exprs().next() {
615 self.add_facts(cond, false);
616 }
617 }
618 ExprKind::Call(callee, ..) => {
619 if let Some(rep) = match_flash_loan_call(self.gcx, expr) {
620 *self.state.repayments.entry(rep).or_insert(0) += 1;
621 } else if let Some(permit) = self.match_permit_call(expr) {
622 self.state.permits.insert(permit);
623 } else if let Some(sink) = match_sink(self.gcx, expr)
624 && !self.is_safe(sink.from)
625 && !self.consume_repayment(&sink)
626 {
627 let lint = if self.permit_covers(&sink) {
630 &ARBITRARY_SEND_ERC20_PERMIT
631 } else {
632 &ARBITRARY_SEND_ERC20
633 };
634 self.hits.push((expr.span, lint));
635 }
636 let _ = self.walk_expr(expr);
639 if let Some(fid) = self.gcx.resolved_function(callee)
640 && matches!(callee.peel_parens().kind, ExprKind::Ident(_))
641 {
642 for v in state_writes(self.gcx, fid) {
643 self.invalidate(v);
644 }
645 }
646 }
647 ExprKind::Assign(lhs, _, rhs) => {
648 self.assign_lhs(lhs, Some(rhs));
649 let _ = self.walk_expr(expr);
650 }
651 ExprKind::Delete(target) => {
652 self.assign_lhs(target, None);
653 let _ = self.walk_expr(expr);
654 }
655 _ => {
656 let _ = self.walk_expr(expr);
657 }
658 }
659 ControlFlow::Continue(())
660 }
661}
662
663fn origin_matches(
666 gcx: Gcx<'_>,
667 expr: &Expr<'_>,
668 depth: u8,
669 vars: &HashSet<VariableId>,
670 base: fn(Gcx<'_>, &Expr<'_>) -> bool,
671) -> bool {
672 let expr = expr.peel_parens();
673 match &expr.kind {
674 ExprKind::Payable(inner) => return origin_matches(gcx, inner, depth, vars, base),
675 ExprKind::Call(callee, args, _) if is_address_like_cast(gcx, callee) => {
676 return args.exprs().next().is_some_and(|e| origin_matches(gcx, e, depth, vars, base));
677 }
678 _ => {}
679 }
680 base(gcx, expr)
681 || match &expr.kind {
682 ExprKind::Ident(_) => gcx.resolved_variable(expr).is_some_and(|v| vars.contains(&v)),
683 ExprKind::Ternary(_, t, f) => {
684 origin_matches(gcx, t, depth, vars, base)
685 && origin_matches(gcx, f, depth, vars, base)
686 }
687 ExprKind::Call(callee, args, _) if depth > 0 && args.exprs().next().is_none() => gcx
688 .resolved_function(callee)
689 .filter(|_| matches!(callee.peel_parens().kind, ExprKind::Ident(_)))
690 .is_some_and(|fid| {
691 let f = gcx.hir.function(fid);
692 f.parameters.is_empty()
693 && matches!(f.body.map(|b| b.stmts), Some([stmt])
694 if matches!(&stmt.kind, StmtKind::Return(Some(e))
695 if origin_matches(gcx, e, depth - 1, vars, base)))
696 }),
697 _ => false,
698 }
699}
700
701fn sum_operands(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<(VariableId, VariableId)> {
703 match &expr.peel_parens().kind {
704 ExprKind::Binary(lhs, op, rhs) if op.kind == BinOpKind::Add => {
705 underlying_var(gcx, lhs).zip(underlying_var(gcx, rhs))
706 }
707 _ => None,
708 }
709}
710
711fn token_key(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<TokenKey> {
713 if let Some(v) = underlying_var(gcx, expr) {
714 return Some(TokenKey::Var(v));
715 }
716 match &expr.peel_parens().kind {
717 ExprKind::Member(base, ident) => {
718 Some(TokenKey::Field(underlying_var(gcx, base)?, ident.name))
719 }
720 _ => None,
721 }
722}
723
724fn canonical_args<'gcx>(
726 gcx: Gcx<'gcx>,
727 expr: &Expr<'gcx>,
728 arity: usize,
729) -> Option<Vec<&'gcx Expr<'gcx>>> {
730 let ExprKind::Call(_, args, _) = &expr.kind else { return None };
731 if args.len() != arity {
732 return None;
733 }
734 (0..arity).map(|index| gcx.call_arg(expr, index)).collect()
735}
736
737fn match_flash_loan_call<'gcx>(gcx: Gcx<'gcx>, expr: &Expr<'gcx>) -> Option<PendingRepayment> {
740 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
741 let ExprKind::Member(recv, ident) = &callee.peel_parens().kind else { return None };
742 if ident.name.as_str() != "onFlashLoan" {
743 return None;
744 }
745 let a = canonical_args(gcx, expr, 5)?;
746 let cid = receiver_contract_id(gcx, recv)?;
747 if !interface_has_function(
748 gcx,
749 cid,
750 "onFlashLoan(address,address,uint256,uint256,bytes)",
751 &["address", "address", "uint256", "uint256", "bytes"],
752 &["bytes32"],
753 ) {
754 return None;
755 }
756 Some(PendingRepayment {
757 receiver: underlying_var(gcx, recv)?,
758 token: underlying_var(gcx, a[1])?,
759 amount: underlying_var(gcx, a[2])?,
760 fee: underlying_var(gcx, a[3])?,
761 })
762}
763
764fn match_sink<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> Option<Sink<'gcx>> {
769 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
770 let ExprKind::Member(recv, ident) = &callee.peel_parens().kind else { return None };
771 let name = ident.name.as_str();
772 if matches!(name, "transferFrom" | "safeTransferFrom")
773 && let Some(a) = canonical_args(gcx, expr, 3)
774 {
775 let erc20 = receiver_contract_id(gcx, recv).is_some_and(|cid| has_transfer_from(gcx, cid));
776 let attached = gcx.resolved_call(expr).is_some_and(|resolved| {
777 resolved.attached
778 && resolved
779 .res
780 .as_function()
781 .and_then(|fid| gcx.hir.function(fid).contract)
782 .is_some_and(|cid| library_has_safe_transfer_from(gcx, cid))
783 });
784 if erc20 || (name == "safeTransferFrom" && attached && expr_is_address(gcx, recv)) {
785 return Some(Sink { from: a[0], to: a[1], amount: a[2], token: token_key(gcx, recv) });
786 }
787 }
788 if name == "safeTransferFrom"
789 && let Some(a) = canonical_args(gcx, expr, 4)
790 && let Some(cid) = receiver_contract_id(gcx, recv)
791 && gcx.hir.contract(cid).kind == ContractKind::Library
792 && library_has_safe_transfer_from(gcx, cid)
793 {
794 return Some(Sink { from: a[1], to: a[2], amount: a[3], token: token_key(gcx, a[0]) });
795 }
796 None
797}
798
799fn state_writes<'gcx>(gcx: Gcx<'gcx>, fid: FunctionId) -> HashSet<VariableId> {
801 let mut w = StateWrites { gcx, out: HashSet::new(), callees: Vec::new() };
802 w.scan(fid);
803 for callee in std::mem::take(&mut w.callees) {
804 w.scan(callee);
805 }
806 w.out
807}
808
809struct StateWrites<'gcx> {
810 gcx: Gcx<'gcx>,
811 out: HashSet<VariableId>,
812 callees: Vec<FunctionId>,
813}
814
815impl StateWrites<'_> {
816 fn scan(&mut self, fid: FunctionId) {
817 if let Some(body) = self.gcx.hir.function(fid).body {
818 for stmt in body.stmts {
819 let _ = self.visit_stmt(stmt);
820 }
821 }
822 }
823}
824
825impl<'gcx> Visit<'gcx> for StateWrites<'gcx> {
826 type BreakValue = Never;
827
828 fn hir(&self) -> &'gcx Hir<'gcx> {
829 &self.gcx.hir
830 }
831
832 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
833 match &expr.kind {
834 ExprKind::Assign(lhs, ..) | ExprKind::Delete(lhs) => {
835 self.out.extend(state_lhs_vars(self.gcx, lhs));
836 }
837 ExprKind::Call(callee, ..)
838 if matches!(callee.peel_parens().kind, ExprKind::Ident(_)) =>
839 {
840 self.callees.extend(self.gcx.resolved_function(callee));
841 }
842 _ => {}
843 }
844 self.walk_expr(expr)
845 }
846}
847
848const fn is_internal_only(f: &hir::Function<'_>) -> bool {
851 !f.parameters.is_empty()
852 && (matches!(f.kind, FunctionKind::Modifier)
853 || (f.kind.is_function()
854 && matches!(f.visibility, Visibility::Private | Visibility::Internal)))
855}
856
857type CallsiteFacts = HashMap<FunctionId, Option<Vec<(bool, bool)>>>;
860
861thread_local! {
862 static CALLSITE_INDEX: RefCell<Option<(usize, Rc<CallsiteFacts>)>> = const { RefCell::new(None) };
863}
864
865fn callsite_index<'gcx>(gcx: Gcx<'gcx>) -> Rc<CallsiteFacts> {
867 let key = std::ptr::from_ref(&gcx.hir) as usize;
868 CALLSITE_INDEX.with(|cell| {
869 let mut slot = cell.borrow_mut();
870 if let Some((cached_key, index)) = &*slot
871 && *cached_key == key
872 {
873 return index.clone();
874 }
875 let mut c = CallsiteCollector { gcx, out: HashMap::new() };
876 for (_, func) in gcx.hir.functions_enumerated() {
877 for m in func.modifiers {
878 if let ItemId::Function(fid) = m.id {
879 c.record(fid, &m.args);
880 }
881 }
882 for stmt in func.body.map_or(&[][..], |b| b.stmts) {
883 let _ = c.visit_stmt(stmt);
884 }
885 }
886 let index = Rc::new(c.out);
887 *slot = Some((key, index.clone()));
888 index
889 })
890}
891
892struct CallsiteCollector<'gcx> {
893 gcx: Gcx<'gcx>,
894 out: CallsiteFacts,
895}
896
897impl<'gcx> CallsiteCollector<'gcx> {
898 fn record(&mut self, fid: FunctionId, args: &'gcx CallArgs<'gcx>) {
899 let f = self.gcx.hir.function(fid);
900 if !is_internal_only(f) {
901 return;
902 }
903 let call_args =
904 f.parameters.iter().map(|&p| arg_for_param(self.gcx, fid, p, args)).collect();
905 let entry =
906 self.out.entry(fid).or_insert_with(|| Some(vec![(true, true); f.parameters.len()]));
907 let (Some(facts), Some(call_args)) = (entry.as_mut(), call_args) else {
908 *entry = None;
909 return;
910 };
911 let none = HashSet::new();
912 for ((safe, is_self), arg) in facts.iter_mut().zip::<Vec<_>>(call_args) {
913 *safe &= origin_matches(self.gcx, arg, HELPER_DEPTH, &none, |gcx, e| {
914 is_msg_sender(gcx, e) || is_address_self(gcx, e)
915 });
916 *is_self &= origin_matches(self.gcx, arg, HELPER_DEPTH, &none, is_address_self);
917 }
918 }
919}
920
921impl<'gcx> Visit<'gcx> for CallsiteCollector<'gcx> {
922 type BreakValue = Never;
923
924 fn hir(&self) -> &'gcx Hir<'gcx> {
925 &self.gcx.hir
926 }
927
928 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
929 if let ExprKind::Call(callee, args, _) = &expr.kind
930 && matches!(callee.peel_parens().kind, ExprKind::Ident(_))
931 && let Some(fid) = self.gcx.resolved_function(callee)
932 {
933 self.record(fid, args);
934 }
935 self.walk_expr(expr)
936 }
937}
938
939fn has_transfer_from(gcx: Gcx<'_>, cid: ContractId) -> bool {
941 interface_has_function(
942 gcx,
943 cid,
944 "transferFrom(address,address,uint256)",
945 &["address", "address", "uint256"],
946 &["bool"],
947 )
948}
949
950fn interface_has_function(
951 gcx: Gcx<'_>,
952 cid: ContractId,
953 signature: &str,
954 params: &[&str],
955 returns: &[&str],
956) -> bool {
957 gcx.interface_functions(cid).all().iter().any(|function| {
958 let f = gcx.hir.function(function.id);
959 gcx.item_signature(function.id.into()) == signature
960 && f.parameters.len() == params.len()
961 && f.returns.len() == returns.len()
962 && f.parameters.iter().zip(params).all(|(id, abi)| is_elementary(&gcx.hir, *id, abi))
963 && f.returns.iter().zip(returns).all(|(id, abi)| is_elementary(&gcx.hir, *id, abi))
964 })
965}
966
967fn library_has_safe_transfer_from(gcx: Gcx<'_>, cid: ContractId) -> bool {
971 let hir = &gcx.hir;
972 hir.contract(cid).functions().any(|fid| {
973 let f = hir.function(fid);
974 let [token, from, to, amount] = f.parameters else { return false };
975 let token_ok = match hir.variable(*token).ty.kind {
976 TypeKind::Custom(ItemId::Contract(token_cid)) => has_transfer_from(gcx, token_cid),
977 _ => is_address_type(hir, *token),
978 };
979 f.name.is_some_and(|n| n.name.as_str() == "safeTransferFrom")
980 && token_ok
981 && is_address_type(hir, *from)
982 && is_address_type(hir, *to)
983 && is_elementary(hir, *amount, "uint256")
984 })
985}