1use super::MissingEventsArithmetic;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::primitives::{branch_always_exits, is_require_or_assert},
7 },
8};
9use solar::{
10 ast::{ContractKind, StateMutability, Visibility},
11 interface::{Span, kw, sym},
12 sema::hir::{
13 self, BinOpKind, ElementaryType, ExprKind, FunctionId, ItemId, Res, StmtKind, TypeKind,
14 UnOpKind, VariableId,
15 },
16};
17use std::collections::{HashMap, HashSet};
18
19declare_forge_lint!(
20 MISSING_EVENTS_ARITHMETIC,
21 Severity::Low,
22 "missing-events-arithmetic",
23 "critical arithmetic state changes should emit events"
24);
25
26impl<'hir> LateLintPass<'hir> for MissingEventsArithmetic {
27 fn check_contract(
28 &mut self,
29 ctx: &LintContext,
30 _gcx: solar::sema::Gcx<'hir>,
31 hir: &'hir hir::Hir<'hir>,
32 contract: &'hir hir::Contract<'hir>,
33 ) {
34 if contract.kind != ContractKind::Contract {
35 return;
36 }
37
38 let candidate_vars: HashSet<_> =
39 contract.variables().filter(|&var_id| is_candidate_state_var(hir, var_id)).collect();
40 if candidate_vars.is_empty() {
41 return;
42 }
43
44 let mut protected_funcs = HashSet::new();
45 let mut protected_entry_points = Vec::new();
46 for func_id in contract.all_functions() {
47 let func = hir.function(func_id);
48 if !is_external_function(func) || !is_protected(hir, func_id, func) {
49 continue;
50 }
51
52 protected_funcs.insert(func_id);
53 if !matches!(func.state_mutability, StateMutability::Pure | StateMutability::View) {
54 protected_entry_points.push(func_id);
55 }
56 }
57 if protected_entry_points.is_empty() {
58 return;
59 }
60
61 let arithmetic_vars =
62 vars_used_in_unprotected_arithmetic(hir, contract, &candidate_vars, &protected_funcs);
63 if arithmetic_vars.is_empty() {
64 return;
65 }
66
67 for func_id in protected_entry_points {
68 let mut analyzer = WriteAnalyzer::new(hir, &arithmetic_vars);
69 let writes = analyzer.analyze_entry_point(func_id);
70 let mut emitted = HashSet::new();
71
72 for write in writes {
73 if !emitted.insert(write.var_id) {
74 continue;
75 }
76
77 let name = hir
78 .variable(write.var_id)
79 .name
80 .map(|name| name.as_str().to_string())
81 .unwrap_or_else(|| "state variable".to_string());
82 ctx.emit_with_msg(
83 &MISSING_EVENTS_ARITHMETIC,
84 write.span,
85 format!("`{name}` is changed without an event but is used in arithmetic"),
86 );
87 }
88 }
89 }
90}
91
92fn is_candidate_state_var(hir: &hir::Hir<'_>, var_id: VariableId) -> bool {
93 let var = hir.variable(var_id);
94 var.kind.is_state()
95 && !var.is_constant()
96 && !var.is_immutable()
97 && matches!(
98 var.ty.kind,
99 TypeKind::Elementary(ElementaryType::Int(_) | ElementaryType::UInt(_))
100 )
101}
102
103fn is_external_function(func: &hir::Function<'_>) -> bool {
104 func.kind.is_function()
105 && matches!(func.visibility, Visibility::Public | Visibility::External)
106 && !func.is_constructor()
107 && !func.is_special()
108}
109
110fn vars_used_in_unprotected_arithmetic<'hir>(
111 hir: &'hir hir::Hir<'hir>,
112 contract: &hir::Contract<'hir>,
113 candidate_vars: &HashSet<VariableId>,
114 protected_funcs: &HashSet<FunctionId>,
115) -> HashSet<VariableId> {
116 let mut used = HashSet::new();
117
118 for func_id in contract.all_functions() {
119 let func = hir.function(func_id);
120 if !is_external_function(func) || protected_funcs.contains(&func_id) {
121 continue;
122 }
123
124 let mut analyzer = ArithmeticUseAnalyzer::new(hir, candidate_vars);
125 used.extend(analyzer.analyze_entry_point(func_id));
126 }
127
128 used
129}
130
131#[derive(Clone, Copy, Debug)]
132struct StateWrite {
133 var_id: VariableId,
134 span: Span,
135}
136
137#[derive(Clone, Default)]
138struct WriteState {
139 taint: HashMap<VariableId, HashSet<VariableId>>,
140 dynamic_taint: HashSet<VariableId>,
141 pending_writes: Vec<StateWrite>,
142}
143
144impl WriteState {
145 fn record_write(&mut self, write: StateWrite) {
146 self.pending_writes.push(write);
147 }
148
149 fn record_event(&mut self) {
150 self.pending_writes.clear();
151 }
152}
153
154#[derive(Default)]
155struct WriteFlow {
156 fallthrough: Vec<WriteState>,
157 returned: Vec<WriteState>,
158}
159
160impl WriteFlow {
161 fn fallthrough(state: WriteState) -> Self {
162 Self { fallthrough: vec![state], returned: Vec::new() }
163 }
164
165 fn returned(state: WriteState) -> Self {
166 Self { fallthrough: Vec::new(), returned: vec![state] }
167 }
168}
169
170struct WriteAnalyzer<'a, 'hir> {
171 hir: &'hir hir::Hir<'hir>,
172 targets: &'a HashSet<VariableId>,
173 call_stack: Vec<FunctionId>,
174}
175
176impl<'a, 'hir> WriteAnalyzer<'a, 'hir> {
177 const fn new(hir: &'hir hir::Hir<'hir>, targets: &'a HashSet<VariableId>) -> Self {
178 Self { hir, targets, call_stack: Vec::new() }
179 }
180
181 fn analyze_entry_point(&mut self, func_id: FunctionId) -> Vec<StateWrite> {
182 let mut state = WriteState::default();
183 let func = self.hir.function(func_id);
184 for ¶m in func.parameters {
185 state.taint.insert(param, HashSet::from([param]));
186 }
187 let modifier_ids: Vec<_> =
188 func.modifiers.iter().filter_map(|modifier| modifier.id.as_function()).collect();
189
190 let flow = self.analyze_function(func_id, state);
191 let flow = self.analyze_modifier_suffixes(&modifier_ids, flow);
192 flow.fallthrough
193 .iter()
194 .chain(&flow.returned)
195 .flat_map(|state| state.pending_writes.iter().copied())
196 .collect()
197 }
198
199 fn analyze_function(&mut self, func_id: FunctionId, state: WriteState) -> WriteFlow {
200 if self.call_stack.contains(&func_id) {
201 return WriteFlow::fallthrough(state);
202 }
203
204 let func = self.hir.function(func_id);
205 let Some(body) = func.body else {
206 return WriteFlow::fallthrough(state);
207 };
208
209 self.call_stack.push(func_id);
210 let flow = self.analyze_stmts(body.stmts, vec![state]);
211 self.call_stack.pop();
212 flow
213 }
214
215 fn analyze_modifier_suffixes(
216 &mut self,
217 modifier_ids: &[FunctionId],
218 mut flow: WriteFlow,
219 ) -> WriteFlow {
220 for &modifier_id in modifier_ids.iter().rev() {
221 flow = self.analyze_modifier_suffix(modifier_id, flow);
222 }
223 flow
224 }
225
226 fn analyze_modifier_suffix(&mut self, modifier_id: FunctionId, flow: WriteFlow) -> WriteFlow {
227 let modifier = self.hir.function(modifier_id);
228 let Some(body) = modifier.body else { return flow };
229 let Some(placeholder_pos) =
230 body.stmts.iter().position(|stmt| matches!(stmt.kind, StmtKind::Placeholder))
231 else {
232 return flow;
233 };
234 let suffix = &body.stmts[placeholder_pos + 1..];
235 if suffix.is_empty() {
236 return flow;
237 }
238
239 let fallthrough_flow = self.analyze_stmts(suffix, flow.fallthrough);
240 let returned_flow = self.analyze_stmts(suffix, flow.returned);
241
242 let mut returned = fallthrough_flow.returned;
243 returned.extend(returned_flow.fallthrough);
244 returned.extend(returned_flow.returned);
245 WriteFlow { fallthrough: fallthrough_flow.fallthrough, returned }
246 }
247
248 fn analyze_stmts(
249 &mut self,
250 stmts: &'hir [hir::Stmt<'hir>],
251 mut states: Vec<WriteState>,
252 ) -> WriteFlow {
253 let mut returned = Vec::new();
254
255 for stmt in stmts {
256 let mut next_states = Vec::new();
257 for state in states {
258 let flow = self.analyze_stmt(stmt, state);
259 next_states.extend(flow.fallthrough);
260 returned.extend(flow.returned);
261 }
262 states = merge_write_states(next_states);
263 if states.is_empty() {
264 break;
265 }
266 }
267
268 WriteFlow { fallthrough: states, returned }
269 }
270
271 fn analyze_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>, mut state: WriteState) -> WriteFlow {
272 match stmt.kind {
273 StmtKind::DeclSingle(var_id) => {
274 let var = self.hir.variable(var_id);
275 if let Some(init) = var.initializer
276 && !var.kind.is_state()
277 {
278 self.analyze_expr(init, &mut state);
279 let sources = self.taint_sources(&state, init);
280 let is_dynamic = self.expr_has_dynamic_value(&state, init);
281 self.set_local_taint(&mut state, var_id, sources, is_dynamic);
282 }
283 WriteFlow::fallthrough(state)
284 }
285 StmtKind::DeclMulti(vars, expr) => {
286 self.analyze_expr(expr, &mut state);
287 let sources = self.taint_sources(&state, expr);
288 let is_dynamic = self.expr_has_dynamic_value(&state, expr);
289 for var_id in vars.iter().flatten().copied() {
290 if !self.hir.variable(var_id).kind.is_state() {
291 self.set_local_taint(&mut state, var_id, sources.clone(), is_dynamic);
292 }
293 }
294 WriteFlow::fallthrough(state)
295 }
296 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
297 self.analyze_stmts(block.stmts, vec![state])
298 }
299 StmtKind::If(cond, then_stmt, else_stmt) => {
300 self.analyze_expr(cond, &mut state);
301
302 let then_flow = self.analyze_stmt(then_stmt, state.clone());
303
304 let else_flow = if let Some(else_stmt) = else_stmt {
305 self.analyze_stmt(else_stmt, state)
306 } else {
307 WriteFlow::fallthrough(state)
308 };
309
310 let mut returned = then_flow.returned;
311 returned.extend(else_flow.returned);
312 let mut fallthrough = then_flow.fallthrough;
313 fallthrough.extend(else_flow.fallthrough);
314 WriteFlow { fallthrough: merge_write_states(fallthrough), returned }
315 }
316 StmtKind::Try(try_stmt) => {
317 self.analyze_expr(&try_stmt.expr, &mut state);
318 let mut fallthrough = Vec::new();
319 let mut returned = Vec::new();
320 for clause in try_stmt.clauses {
321 let flow = self.analyze_stmts(clause.block.stmts, vec![state.clone()]);
322 fallthrough.extend(flow.fallthrough);
323 returned.extend(flow.returned);
324 }
325 WriteFlow { fallthrough: merge_write_states(fallthrough), returned }
326 }
327 StmtKind::Expr(expr) => {
328 self.analyze_expr(expr, &mut state);
329 WriteFlow::fallthrough(state)
330 }
331 StmtKind::Revert(expr) => {
332 self.analyze_expr(expr, &mut state);
333 WriteFlow::default()
334 }
335 StmtKind::Emit(expr) => {
336 self.analyze_expr(expr, &mut state);
337 state.record_event();
338 WriteFlow::fallthrough(state)
339 }
340 StmtKind::Return(expr) => {
341 if let Some(expr) = expr {
342 self.analyze_expr(expr, &mut state);
343 }
344 WriteFlow::returned(state)
345 }
346 StmtKind::Break
347 | StmtKind::Continue
348 | StmtKind::Placeholder
349 | StmtKind::AssemblyBlock(_)
350 | StmtKind::Switch(_)
351 | StmtKind::Err(_) => WriteFlow::fallthrough(state),
352 }
353 }
354
355 fn analyze_expr(&mut self, expr: &'hir hir::Expr<'hir>, state: &mut WriteState) {
356 match &expr.kind {
357 ExprKind::Assign(lhs, op, rhs) => {
358 self.analyze_expr(rhs, state);
359
360 let sources = self.taint_sources(state, rhs);
361 let is_dynamic = self.expr_has_dynamic_value(state, rhs);
362 let is_arithmetic_assignment = op.is_some_and(|op| is_arithmetic_op(op.kind));
363 for var_id in state_lhs_vars(self.hir, lhs) {
364 if self.targets.contains(&var_id) && (is_arithmetic_assignment || is_dynamic) {
365 state.record_write(StateWrite { var_id, span: lhs.span });
366 }
367 }
368
369 if let Some(local) = lhs_local_var(self.hir, lhs) {
370 self.set_local_taint(state, local, sources, is_dynamic);
371 } else {
372 self.analyze_lhs_indices(lhs, state);
373 }
374 }
375 ExprKind::Call(callee, args, opts) => {
376 self.analyze_expr(callee, state);
377 if let Some(opts) = opts {
378 for opt in opts.args {
379 self.analyze_expr(&opt.value, state);
380 }
381 }
382 for arg in args.exprs() {
383 self.analyze_expr(arg, state);
384 }
385
386 for callee_id in resolved_function_ids(callee) {
387 self.analyze_internal_call(callee_id, args, state);
388 }
389 }
390 ExprKind::Binary(lhs, _, rhs) => {
391 self.analyze_expr(lhs, state);
392 self.analyze_expr(rhs, state);
393 }
394 ExprKind::Unary(op, inner) if is_inc_dec_op(op.kind) => {
395 for var_id in state_lhs_vars(self.hir, inner) {
396 if self.targets.contains(&var_id) {
397 state.record_write(StateWrite { var_id, span: inner.span });
398 }
399 }
400 self.analyze_lhs_indices(inner, state);
401 }
402 ExprKind::Unary(_, inner)
403 | ExprKind::Delete(inner)
404 | ExprKind::Member(inner, _)
405 | ExprKind::Payable(inner) => self.analyze_expr(inner, state),
406 ExprKind::Index(base, index) => {
407 self.analyze_expr(base, state);
408 if let Some(index) = index {
409 self.analyze_expr(index, state);
410 }
411 }
412 ExprKind::Slice(base, start, end) => {
413 self.analyze_expr(base, state);
414 if let Some(start) = start {
415 self.analyze_expr(start, state);
416 }
417 if let Some(end) = end {
418 self.analyze_expr(end, state);
419 }
420 }
421 ExprKind::Ternary(cond, true_expr, false_expr) => {
422 self.analyze_expr(cond, state);
423
424 let mut true_state = state.clone();
425 self.analyze_expr(true_expr, &mut true_state);
426
427 let mut false_state = state.clone();
428 self.analyze_expr(false_expr, &mut false_state);
429
430 if let Some(merged) = merge_write_states(vec![true_state, false_state]).pop() {
431 *state = merged;
432 }
433 }
434 ExprKind::Array(exprs) => {
435 for expr in *exprs {
436 self.analyze_expr(expr, state);
437 }
438 }
439 ExprKind::Tuple(exprs) => {
440 for expr in exprs.iter().copied().flatten() {
441 self.analyze_expr(expr, state);
442 }
443 }
444 ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
445 ExprKind::Ident(_) | ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
446 }
447 }
448
449 fn analyze_internal_call(
450 &mut self,
451 callee_id: FunctionId,
452 args: &hir::CallArgs<'hir>,
453 state: &mut WriteState,
454 ) {
455 if self.call_stack.contains(&callee_id) {
456 return;
457 }
458
459 let callee = self.hir.function(callee_id);
460 let Some(_) = callee.body else { return };
461
462 let saved_state = state.clone();
463 let mut callee_state = state.clone();
464 callee_state.taint.clear();
465 callee_state.dynamic_taint.clear();
466 for (param, arg) in callee.parameters.iter().copied().zip(args.exprs()) {
467 let sources = self.taint_sources(&saved_state, arg);
468 let is_dynamic = self.expr_has_dynamic_value(&saved_state, arg);
469 self.set_local_taint(&mut callee_state, param, sources, is_dynamic);
470 }
471
472 let flow = self.analyze_function(callee_id, callee_state);
473 let mut states = flow.fallthrough;
474 states.extend(flow.returned);
475 if let Some(mut merged) = merge_write_states(states).pop() {
476 merged.taint = saved_state.taint;
477 merged.dynamic_taint = saved_state.dynamic_taint;
478 *state = merged;
479 }
480 }
481
482 fn analyze_lhs_indices(&mut self, expr: &'hir hir::Expr<'hir>, state: &mut WriteState) {
483 match &expr.kind {
484 ExprKind::Index(base, index) => {
485 self.analyze_lhs_indices(base, state);
486 if let Some(index) = index {
487 self.analyze_expr(index, state);
488 }
489 }
490 ExprKind::Slice(base, start, end) => {
491 self.analyze_lhs_indices(base, state);
492 if let Some(start) = start {
493 self.analyze_expr(start, state);
494 }
495 if let Some(end) = end {
496 self.analyze_expr(end, state);
497 }
498 }
499 ExprKind::Member(base, _) | ExprKind::Payable(base) => {
500 self.analyze_lhs_indices(base, state);
501 }
502 ExprKind::Tuple(exprs) => {
503 for expr in exprs.iter().copied().flatten() {
504 self.analyze_lhs_indices(expr, state);
505 }
506 }
507 _ => {}
508 }
509 }
510
511 fn taint_sources(&self, state: &WriteState, expr: &hir::Expr<'_>) -> HashSet<VariableId> {
512 collect_write_taint_sources(self.hir, &state.taint, expr)
513 }
514
515 fn expr_has_dynamic_value(&self, state: &WriteState, expr: &hir::Expr<'_>) -> bool {
516 expr_has_dynamic_value(self.hir, &state.taint, &state.dynamic_taint, expr)
517 }
518
519 fn set_local_taint(
520 &mut self,
521 state: &mut WriteState,
522 var_id: VariableId,
523 sources: HashSet<VariableId>,
524 is_dynamic: bool,
525 ) {
526 if sources.is_empty() {
527 state.taint.remove(&var_id);
528 } else {
529 state.taint.insert(var_id, sources);
530 }
531 if is_dynamic {
532 state.dynamic_taint.insert(var_id);
533 } else {
534 state.dynamic_taint.remove(&var_id);
535 }
536 }
537}
538
539fn merge_write_states(mut states: Vec<WriteState>) -> Vec<WriteState> {
540 let Some(mut merged) = states.pop() else {
541 return Vec::new();
542 };
543
544 for state in states {
545 merged.taint = merge_taint(&merged.taint, &state.taint);
546 merged.dynamic_taint.extend(state.dynamic_taint);
547 merged.pending_writes.extend(state.pending_writes);
548 }
549
550 vec![merged]
551}
552
553fn merge_taint(
554 lhs: &HashMap<VariableId, HashSet<VariableId>>,
555 rhs: &HashMap<VariableId, HashSet<VariableId>>,
556) -> HashMap<VariableId, HashSet<VariableId>> {
557 let mut merged = lhs.clone();
558 for (&var_id, sources) in rhs {
559 merged.entry(var_id).or_default().extend(sources.iter().copied());
560 }
561 merged
562}
563
564fn set_taint_entry(
565 taint: &mut HashMap<VariableId, HashSet<VariableId>>,
566 var_id: VariableId,
567 sources: HashSet<VariableId>,
568) {
569 if sources.is_empty() {
570 taint.remove(&var_id);
571 } else {
572 taint.insert(var_id, sources);
573 }
574}
575
576struct ArithmeticUseAnalyzer<'a, 'hir> {
577 hir: &'hir hir::Hir<'hir>,
578 targets: &'a HashSet<VariableId>,
579 taint: HashMap<VariableId, HashSet<VariableId>>,
580 used: HashSet<VariableId>,
581 call_stack: Vec<FunctionId>,
582}
583
584impl<'a, 'hir> ArithmeticUseAnalyzer<'a, 'hir> {
585 fn new(hir: &'hir hir::Hir<'hir>, targets: &'a HashSet<VariableId>) -> Self {
586 Self { hir, targets, taint: HashMap::new(), used: HashSet::new(), call_stack: Vec::new() }
587 }
588
589 fn analyze_entry_point(&mut self, func_id: FunctionId) -> HashSet<VariableId> {
590 self.taint.clear();
591 self.analyze_function(func_id);
592 std::mem::take(&mut self.used)
593 }
594
595 fn analyze_function(&mut self, func_id: FunctionId) {
596 if self.call_stack.contains(&func_id) {
597 return;
598 }
599
600 let func = self.hir.function(func_id);
601 let Some(body) = func.body else { return };
602
603 self.call_stack.push(func_id);
604 for stmt in body.stmts {
605 self.analyze_stmt(stmt);
606 }
607 self.call_stack.pop();
608 }
609
610 fn analyze_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) {
611 match stmt.kind {
612 StmtKind::DeclSingle(var_id) => {
613 let var = self.hir.variable(var_id);
614 if let Some(init) = var.initializer
615 && !var.kind.is_state()
616 {
617 self.analyze_expr(init);
618 let sources = self.taint_sources(init);
619 self.set_local_taint(var_id, sources);
620 }
621 }
622 StmtKind::DeclMulti(vars, expr) => {
623 self.analyze_expr(expr);
624 let sources = self.taint_sources(expr);
625 for var_id in vars.iter().flatten().copied() {
626 if !self.hir.variable(var_id).kind.is_state() {
627 self.set_local_taint(var_id, sources.clone());
628 }
629 }
630 }
631 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
632 for stmt in block.stmts {
633 self.analyze_stmt(stmt);
634 }
635 }
636 StmtKind::If(cond, then_stmt, else_stmt) => {
637 self.analyze_expr(cond);
638 self.analyze_stmt(then_stmt);
639 if let Some(else_stmt) = else_stmt {
640 self.analyze_stmt(else_stmt);
641 }
642 }
643 StmtKind::Try(try_stmt) => {
644 self.analyze_expr(&try_stmt.expr);
645 for clause in try_stmt.clauses {
646 for stmt in clause.block.stmts {
647 self.analyze_stmt(stmt);
648 }
649 }
650 }
651 StmtKind::Expr(expr) | StmtKind::Emit(expr) | StmtKind::Revert(expr) => {
652 self.analyze_expr(expr);
653 }
654 StmtKind::Return(expr) => {
655 if let Some(expr) = expr {
656 self.analyze_expr(expr);
657 }
658 }
659 StmtKind::Break
660 | StmtKind::Continue
661 | StmtKind::Placeholder
662 | StmtKind::AssemblyBlock(_)
663 | StmtKind::Switch(_)
664 | StmtKind::Err(_) => {}
665 }
666 }
667
668 fn analyze_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
669 match &expr.kind {
670 ExprKind::Assign(lhs, _, rhs) => {
671 self.analyze_expr(rhs);
672 if let Some(local) = lhs_local_var(self.hir, lhs) {
673 let sources = self.taint_sources(rhs);
674 self.set_local_taint(local, sources);
675 } else {
676 self.analyze_lhs_indices(lhs);
677 }
678 }
679 ExprKind::Binary(lhs, op, rhs) => {
680 if is_arithmetic_op(op.kind) {
681 let lhs_sources = self.taint_sources(lhs);
682 let rhs_sources = self.taint_sources(rhs);
683 self.used.extend(lhs_sources);
684 self.used.extend(rhs_sources);
685 }
686 self.analyze_expr(lhs);
687 self.analyze_expr(rhs);
688 }
689 ExprKind::Call(callee, args, opts) => {
690 self.analyze_expr(callee);
691 if let Some(opts) = opts {
692 for opt in opts.args {
693 self.analyze_expr(&opt.value);
694 }
695 }
696 for arg in args.exprs() {
697 self.analyze_expr(arg);
698 }
699
700 for callee_id in resolved_function_ids(callee) {
701 self.analyze_internal_call(callee_id, args);
702 }
703 }
704 ExprKind::Unary(_, inner)
705 | ExprKind::Delete(inner)
706 | ExprKind::Member(inner, _)
707 | ExprKind::Payable(inner) => self.analyze_expr(inner),
708 ExprKind::Index(base, index) => {
709 self.analyze_expr(base);
710 if let Some(index) = index {
711 self.analyze_expr(index);
712 }
713 }
714 ExprKind::Slice(base, start, end) => {
715 self.analyze_expr(base);
716 if let Some(start) = start {
717 self.analyze_expr(start);
718 }
719 if let Some(end) = end {
720 self.analyze_expr(end);
721 }
722 }
723 ExprKind::Ternary(cond, true_expr, false_expr) => {
724 self.analyze_expr(cond);
725 self.analyze_expr(true_expr);
726 self.analyze_expr(false_expr);
727 }
728 ExprKind::Array(exprs) => {
729 for expr in *exprs {
730 self.analyze_expr(expr);
731 }
732 }
733 ExprKind::Tuple(exprs) => {
734 for expr in exprs.iter().copied().flatten() {
735 self.analyze_expr(expr);
736 }
737 }
738 ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
739 ExprKind::Ident(_) | ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
740 }
741 }
742
743 fn analyze_internal_call(&mut self, callee_id: FunctionId, args: &hir::CallArgs<'hir>) {
744 if self.call_stack.contains(&callee_id) {
745 return;
746 }
747
748 let callee = self.hir.function(callee_id);
749 let Some(_) = callee.body else { return };
750
751 let saved_taint = std::mem::take(&mut self.taint);
752 for (param, arg) in callee.parameters.iter().copied().zip(args.exprs()) {
753 let sources = collect_state_sources(self.hir, self.targets, &saved_taint, arg);
754 if !sources.is_empty() {
755 self.taint.insert(param, sources);
756 }
757 }
758
759 self.analyze_function(callee_id);
760 self.taint = saved_taint;
761 }
762
763 fn analyze_lhs_indices(&mut self, expr: &'hir hir::Expr<'hir>) {
764 match &expr.kind {
765 ExprKind::Index(base, index) => {
766 self.analyze_lhs_indices(base);
767 if let Some(index) = index {
768 self.analyze_expr(index);
769 }
770 }
771 ExprKind::Slice(base, start, end) => {
772 self.analyze_lhs_indices(base);
773 if let Some(start) = start {
774 self.analyze_expr(start);
775 }
776 if let Some(end) = end {
777 self.analyze_expr(end);
778 }
779 }
780 ExprKind::Member(base, _) | ExprKind::Payable(base) => self.analyze_lhs_indices(base),
781 ExprKind::Tuple(exprs) => {
782 for expr in exprs.iter().copied().flatten() {
783 self.analyze_lhs_indices(expr);
784 }
785 }
786 _ => {}
787 }
788 }
789
790 fn taint_sources(&self, expr: &hir::Expr<'_>) -> HashSet<VariableId> {
791 let mut sources = collect_state_sources(self.hir, self.targets, &self.taint, expr);
792 self.collect_call_return_sources(expr, &mut sources);
793 sources
794 }
795
796 fn collect_call_return_sources(&self, expr: &hir::Expr<'_>, out: &mut HashSet<VariableId>) {
797 match &expr.peel_parens().kind {
798 ExprKind::Call(callee, args, opts) => {
799 self.collect_call_return_sources(callee, out);
800 if let Some(opts) = opts {
801 for opt in opts.args {
802 self.collect_call_return_sources(&opt.value, out);
803 }
804 }
805 for arg in args.exprs() {
806 self.collect_call_return_sources(arg, out);
807 }
808 for callee_id in resolved_function_ids(callee) {
809 self.collect_function_return_sources(callee_id, args, out);
810 }
811 }
812 ExprKind::Assign(_, _, rhs) => self.collect_call_return_sources(rhs, out),
813 ExprKind::Binary(lhs, _, rhs) => {
814 self.collect_call_return_sources(lhs, out);
815 self.collect_call_return_sources(rhs, out);
816 }
817 ExprKind::Unary(_, inner)
818 | ExprKind::Delete(inner)
819 | ExprKind::Member(inner, _)
820 | ExprKind::Payable(inner) => self.collect_call_return_sources(inner, out),
821 ExprKind::Index(base, index) => {
822 self.collect_call_return_sources(base, out);
823 if let Some(index) = index {
824 self.collect_call_return_sources(index, out);
825 }
826 }
827 ExprKind::Slice(base, start, end) => {
828 self.collect_call_return_sources(base, out);
829 if let Some(start) = start {
830 self.collect_call_return_sources(start, out);
831 }
832 if let Some(end) = end {
833 self.collect_call_return_sources(end, out);
834 }
835 }
836 ExprKind::Ternary(cond, true_expr, false_expr) => {
837 self.collect_call_return_sources(cond, out);
838 self.collect_call_return_sources(true_expr, out);
839 self.collect_call_return_sources(false_expr, out);
840 }
841 ExprKind::Array(exprs) => {
842 for expr in *exprs {
843 self.collect_call_return_sources(expr, out);
844 }
845 }
846 ExprKind::Tuple(exprs) => {
847 for expr in exprs.iter().copied().flatten() {
848 self.collect_call_return_sources(expr, out);
849 }
850 }
851 ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
852 ExprKind::Ident(_) | ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
853 }
854 }
855
856 fn collect_function_return_sources(
857 &self,
858 callee_id: FunctionId,
859 args: &hir::CallArgs<'_>,
860 out: &mut HashSet<VariableId>,
861 ) {
862 if self.call_stack.contains(&callee_id) {
863 return;
864 }
865
866 let callee = self.hir.function(callee_id);
867 let Some(body) = callee.body else { return };
868
869 let mut taint = HashMap::new();
870 for (param, arg) in callee.parameters.iter().copied().zip(args.exprs()) {
871 let sources = collect_state_sources(self.hir, self.targets, &self.taint, arg);
872 if !sources.is_empty() {
873 taint.insert(param, sources);
874 }
875 }
876
877 for stmt in body.stmts {
878 self.collect_stmt_return_sources(stmt, &mut taint, out);
879 }
880 }
881
882 fn collect_stmt_return_sources(
883 &self,
884 stmt: &hir::Stmt<'_>,
885 taint: &mut HashMap<VariableId, HashSet<VariableId>>,
886 out: &mut HashSet<VariableId>,
887 ) {
888 match stmt.kind {
889 StmtKind::DeclSingle(var_id) => {
890 let var = self.hir.variable(var_id);
891 if let Some(init) = var.initializer
892 && !var.kind.is_state()
893 {
894 let sources = collect_state_sources(self.hir, self.targets, taint, init);
895 set_taint_entry(taint, var_id, sources);
896 }
897 }
898 StmtKind::DeclMulti(vars, expr) => {
899 let sources = collect_state_sources(self.hir, self.targets, taint, expr);
900 for var_id in vars.iter().flatten().copied() {
901 if !self.hir.variable(var_id).kind.is_state() {
902 set_taint_entry(taint, var_id, sources.clone());
903 }
904 }
905 }
906 StmtKind::Return(Some(expr)) => {
907 out.extend(collect_state_sources(self.hir, self.targets, taint, expr));
908 }
909 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
910 for stmt in block.stmts {
911 self.collect_stmt_return_sources(stmt, taint, out);
912 }
913 }
914 StmtKind::If(_, then_stmt, else_stmt) => {
915 let mut then_taint = taint.clone();
916 self.collect_stmt_return_sources(then_stmt, &mut then_taint, out);
917 if let Some(else_stmt) = else_stmt {
918 let mut else_taint = taint.clone();
919 self.collect_stmt_return_sources(else_stmt, &mut else_taint, out);
920 *taint = merge_taint(&then_taint, &else_taint);
921 } else {
922 *taint = merge_taint(taint, &then_taint);
923 }
924 }
925 StmtKind::Try(try_stmt) => {
926 for clause in try_stmt.clauses {
927 let mut clause_taint = taint.clone();
928 for stmt in clause.block.stmts {
929 self.collect_stmt_return_sources(stmt, &mut clause_taint, out);
930 }
931 *taint = merge_taint(taint, &clause_taint);
932 }
933 }
934 StmtKind::Expr(_)
935 | StmtKind::Emit(_)
936 | StmtKind::Revert(_)
937 | StmtKind::Return(None)
938 | StmtKind::Break
939 | StmtKind::Continue
940 | StmtKind::Placeholder
941 | StmtKind::AssemblyBlock(_)
942 | StmtKind::Switch(_)
943 | StmtKind::Err(_) => {}
944 }
945 }
946
947 fn set_local_taint(&mut self, var_id: VariableId, sources: HashSet<VariableId>) {
948 if sources.is_empty() {
949 self.taint.remove(&var_id);
950 } else {
951 self.taint.insert(var_id, sources);
952 }
953 }
954}
955
956fn collect_write_taint_sources(
957 hir: &hir::Hir<'_>,
958 taint: &HashMap<VariableId, HashSet<VariableId>>,
959 expr: &hir::Expr<'_>,
960) -> HashSet<VariableId> {
961 let mut out = HashSet::new();
962 collect_write_taint_sources_into(hir, taint, expr, &mut out);
963 out
964}
965
966fn collect_write_taint_sources_into(
967 hir: &hir::Hir<'_>,
968 taint: &HashMap<VariableId, HashSet<VariableId>>,
969 expr: &hir::Expr<'_>,
970 out: &mut HashSet<VariableId>,
971) {
972 match &expr.peel_parens().kind {
973 ExprKind::Ident(reses) => {
974 for res in *reses {
975 if let Res::Item(ItemId::Variable(var_id)) = res {
976 let var = hir.variable(*var_id);
977 if var.kind.is_state() && !var.is_constant() && !var.is_immutable() {
978 out.insert(*var_id);
979 }
980 if let Some(sources) = taint.get(var_id) {
981 out.extend(sources.iter().copied());
982 }
983 }
984 }
985 }
986 ExprKind::Assign(_, _, rhs) => collect_write_taint_sources_into(hir, taint, rhs, out),
987 ExprKind::Binary(lhs, _, rhs) => {
988 collect_write_taint_sources_into(hir, taint, lhs, out);
989 collect_write_taint_sources_into(hir, taint, rhs, out);
990 }
991 ExprKind::Unary(_, inner)
992 | ExprKind::Delete(inner)
993 | ExprKind::Member(inner, _)
994 | ExprKind::Payable(inner) => collect_write_taint_sources_into(hir, taint, inner, out),
995 ExprKind::Index(base, index) => {
996 collect_write_taint_sources_into(hir, taint, base, out);
997 if let Some(index) = index {
998 collect_write_taint_sources_into(hir, taint, index, out);
999 }
1000 }
1001 ExprKind::Slice(base, start, end) => {
1002 collect_write_taint_sources_into(hir, taint, base, out);
1003 if let Some(start) = start {
1004 collect_write_taint_sources_into(hir, taint, start, out);
1005 }
1006 if let Some(end) = end {
1007 collect_write_taint_sources_into(hir, taint, end, out);
1008 }
1009 }
1010 ExprKind::Call(callee, args, opts) => {
1011 collect_write_taint_sources_into(hir, taint, callee, out);
1012 if let Some(opts) = opts {
1013 for opt in opts.args {
1014 collect_write_taint_sources_into(hir, taint, &opt.value, out);
1015 }
1016 }
1017 for arg in args.exprs() {
1018 collect_write_taint_sources_into(hir, taint, arg, out);
1019 }
1020 }
1021 ExprKind::Ternary(cond, true_expr, false_expr) => {
1022 collect_write_taint_sources_into(hir, taint, cond, out);
1023 collect_write_taint_sources_into(hir, taint, true_expr, out);
1024 collect_write_taint_sources_into(hir, taint, false_expr, out);
1025 }
1026 ExprKind::Array(exprs) => {
1027 for expr in *exprs {
1028 collect_write_taint_sources_into(hir, taint, expr, out);
1029 }
1030 }
1031 ExprKind::Tuple(exprs) => {
1032 for expr in exprs.iter().copied().flatten() {
1033 collect_write_taint_sources_into(hir, taint, expr, out);
1034 }
1035 }
1036 ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
1037 ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
1038 }
1039}
1040
1041fn expr_has_dynamic_value(
1042 hir: &hir::Hir<'_>,
1043 taint: &HashMap<VariableId, HashSet<VariableId>>,
1044 dynamic_taint: &HashSet<VariableId>,
1045 expr: &hir::Expr<'_>,
1046) -> bool {
1047 if !collect_write_taint_sources(hir, taint, expr).is_empty() {
1048 return true;
1049 }
1050
1051 match &expr.peel_parens().kind {
1052 ExprKind::Ident(reses) => reses.iter().any(|res| {
1053 let Res::Item(ItemId::Variable(var_id)) = res else { return false };
1054 dynamic_taint.contains(var_id)
1055 }),
1056 ExprKind::Call(..) => true,
1057 ExprKind::Member(_, _) if is_dynamic_builtin_member(expr) => true,
1058 ExprKind::Assign(_, _, rhs) => expr_has_dynamic_value(hir, taint, dynamic_taint, rhs),
1059 ExprKind::Binary(lhs, _, rhs) => {
1060 expr_has_dynamic_value(hir, taint, dynamic_taint, lhs)
1061 || expr_has_dynamic_value(hir, taint, dynamic_taint, rhs)
1062 }
1063 ExprKind::Unary(_, inner)
1064 | ExprKind::Delete(inner)
1065 | ExprKind::Member(inner, _)
1066 | ExprKind::Payable(inner) => expr_has_dynamic_value(hir, taint, dynamic_taint, inner),
1067 ExprKind::Index(base, index) => {
1068 expr_has_dynamic_value(hir, taint, dynamic_taint, base)
1069 || index
1070 .is_some_and(|index| expr_has_dynamic_value(hir, taint, dynamic_taint, index))
1071 }
1072 ExprKind::Slice(base, start, end) => {
1073 expr_has_dynamic_value(hir, taint, dynamic_taint, base)
1074 || start
1075 .is_some_and(|start| expr_has_dynamic_value(hir, taint, dynamic_taint, start))
1076 || end.is_some_and(|end| expr_has_dynamic_value(hir, taint, dynamic_taint, end))
1077 }
1078 ExprKind::Ternary(cond, true_expr, false_expr) => {
1079 expr_has_dynamic_value(hir, taint, dynamic_taint, cond)
1080 || expr_has_dynamic_value(hir, taint, dynamic_taint, true_expr)
1081 || expr_has_dynamic_value(hir, taint, dynamic_taint, false_expr)
1082 }
1083 ExprKind::Array(exprs) => {
1084 exprs.iter().any(|expr| expr_has_dynamic_value(hir, taint, dynamic_taint, expr))
1085 }
1086 ExprKind::Tuple(exprs) => exprs
1087 .iter()
1088 .copied()
1089 .flatten()
1090 .any(|expr| expr_has_dynamic_value(hir, taint, dynamic_taint, expr)),
1091 ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => false,
1092 ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => false,
1093 }
1094}
1095
1096fn is_dynamic_builtin_member(expr: &hir::Expr<'_>) -> bool {
1097 let ExprKind::Member(base, _) = &expr.peel_parens().kind else { return false };
1098 let ExprKind::Ident(reses) = &base.peel_parens().kind else { return false };
1099 reses.iter().any(|res| {
1100 let Res::Builtin(builtin) = res else { return false };
1101 matches!(builtin.name(), sym::block | sym::msg | sym::tx)
1102 })
1103}
1104
1105fn collect_state_sources(
1106 hir: &hir::Hir<'_>,
1107 targets: &HashSet<VariableId>,
1108 taint: &HashMap<VariableId, HashSet<VariableId>>,
1109 expr: &hir::Expr<'_>,
1110) -> HashSet<VariableId> {
1111 let mut out = HashSet::new();
1112 collect_state_sources_into(hir, targets, taint, expr, &mut out);
1113 out
1114}
1115
1116fn collect_state_sources_into(
1117 hir: &hir::Hir<'_>,
1118 targets: &HashSet<VariableId>,
1119 taint: &HashMap<VariableId, HashSet<VariableId>>,
1120 expr: &hir::Expr<'_>,
1121 out: &mut HashSet<VariableId>,
1122) {
1123 match &expr.peel_parens().kind {
1124 ExprKind::Ident(reses) => {
1125 for res in *reses {
1126 if let Res::Item(ItemId::Variable(var_id)) = res {
1127 if targets.contains(var_id) && hir.variable(*var_id).kind.is_state() {
1128 out.insert(*var_id);
1129 }
1130 if let Some(sources) = taint.get(var_id) {
1131 out.extend(
1132 sources.iter().copied().filter(|source| targets.contains(source)),
1133 );
1134 }
1135 }
1136 }
1137 }
1138 ExprKind::Assign(_, _, rhs) => collect_state_sources_into(hir, targets, taint, rhs, out),
1139 ExprKind::Binary(lhs, _, rhs) => {
1140 collect_state_sources_into(hir, targets, taint, lhs, out);
1141 collect_state_sources_into(hir, targets, taint, rhs, out);
1142 }
1143 ExprKind::Unary(_, inner)
1144 | ExprKind::Delete(inner)
1145 | ExprKind::Member(inner, _)
1146 | ExprKind::Payable(inner) => collect_state_sources_into(hir, targets, taint, inner, out),
1147 ExprKind::Index(base, index) => {
1148 collect_state_sources_into(hir, targets, taint, base, out);
1149 if let Some(index) = index {
1150 collect_state_sources_into(hir, targets, taint, index, out);
1151 }
1152 }
1153 ExprKind::Slice(base, start, end) => {
1154 collect_state_sources_into(hir, targets, taint, base, out);
1155 if let Some(start) = start {
1156 collect_state_sources_into(hir, targets, taint, start, out);
1157 }
1158 if let Some(end) = end {
1159 collect_state_sources_into(hir, targets, taint, end, out);
1160 }
1161 }
1162 ExprKind::Call(callee, args, opts) => {
1163 collect_state_sources_into(hir, targets, taint, callee, out);
1164 if let Some(opts) = opts {
1165 for opt in opts.args {
1166 collect_state_sources_into(hir, targets, taint, &opt.value, out);
1167 }
1168 }
1169 for arg in args.exprs() {
1170 collect_state_sources_into(hir, targets, taint, arg, out);
1171 }
1172 }
1173 ExprKind::Ternary(cond, true_expr, false_expr) => {
1174 collect_state_sources_into(hir, targets, taint, cond, out);
1175 collect_state_sources_into(hir, targets, taint, true_expr, out);
1176 collect_state_sources_into(hir, targets, taint, false_expr, out);
1177 }
1178 ExprKind::Array(exprs) => {
1179 for expr in *exprs {
1180 collect_state_sources_into(hir, targets, taint, expr, out);
1181 }
1182 }
1183 ExprKind::Tuple(exprs) => {
1184 for expr in exprs.iter().copied().flatten() {
1185 collect_state_sources_into(hir, targets, taint, expr, out);
1186 }
1187 }
1188 ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => {}
1189 ExprKind::Lit(_) | ExprKind::YulMember(..) | ExprKind::Err(_) => {}
1190 }
1191}
1192
1193fn lhs_local_var(hir: &hir::Hir<'_>, lhs: &hir::Expr<'_>) -> Option<VariableId> {
1194 if let ExprKind::Ident(reses) = &lhs.peel_parens().kind {
1195 for res in *reses {
1196 if let Res::Item(ItemId::Variable(var_id)) = res
1197 && !hir.variable(*var_id).kind.is_state()
1198 {
1199 return Some(*var_id);
1200 }
1201 }
1202 }
1203 None
1204}
1205
1206fn state_lhs_vars(hir: &hir::Hir<'_>, lhs: &hir::Expr<'_>) -> Vec<VariableId> {
1207 let mut vars = Vec::new();
1208 collect_state_lhs_vars(hir, lhs, &mut vars);
1209 vars
1210}
1211
1212fn collect_state_lhs_vars(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>, vars: &mut Vec<VariableId>) {
1213 match &expr.peel_parens().kind {
1214 ExprKind::Ident(reses) => {
1215 for res in *reses {
1216 if let Res::Item(ItemId::Variable(var_id)) = res
1217 && hir.variable(*var_id).kind.is_state()
1218 && !vars.contains(var_id)
1219 {
1220 vars.push(*var_id);
1221 }
1222 }
1223 }
1224 ExprKind::Index(base, _) | ExprKind::Slice(base, ..) => {
1225 collect_state_lhs_vars(hir, base, vars);
1226 }
1227 ExprKind::Member(base, _)
1228 | ExprKind::Payable(base)
1229 | ExprKind::Unary(_, base)
1230 | ExprKind::Delete(base) => collect_state_lhs_vars(hir, base, vars),
1231 ExprKind::Tuple(exprs) => {
1232 for expr in exprs.iter().copied().flatten() {
1233 collect_state_lhs_vars(hir, expr, vars);
1234 }
1235 }
1236 _ => {}
1237 }
1238}
1239
1240const fn is_arithmetic_op(kind: BinOpKind) -> bool {
1241 matches!(
1242 kind,
1243 BinOpKind::Add
1244 | BinOpKind::Sub
1245 | BinOpKind::Mul
1246 | BinOpKind::Div
1247 | BinOpKind::Rem
1248 | BinOpKind::Pow
1249 )
1250}
1251
1252const fn is_inc_dec_op(kind: UnOpKind) -> bool {
1253 matches!(kind, UnOpKind::PreInc | UnOpKind::PostInc | UnOpKind::PreDec | UnOpKind::PostDec)
1254}
1255
1256fn is_protected(hir: &hir::Hir<'_>, func_id: FunctionId, func: &hir::Function<'_>) -> bool {
1257 for modifier in func.modifiers {
1258 if let Some(modifier_id) = modifier.id.as_function()
1259 && modifier_has_access_control(hir, modifier_id)
1260 {
1261 return true;
1262 }
1263 }
1264
1265 function_has_access_guard(hir, func_id, &mut HashSet::new())
1266}
1267
1268fn modifier_has_access_control(hir: &hir::Hir<'_>, modifier_id: FunctionId) -> bool {
1269 let modifier = hir.function(modifier_id);
1270 if let Some(body) = modifier.body {
1271 for stmt in body.stmts {
1272 if stmt_is_access_guard(hir, stmt, &mut HashSet::new()) {
1273 return true;
1274 }
1275 }
1276 return false;
1277 }
1278
1279 modifier.name.is_some_and(|name| name_looks_like_access_control(name.as_str()))
1280}
1281
1282fn function_has_access_guard(
1283 hir: &hir::Hir<'_>,
1284 func_id: FunctionId,
1285 seen: &mut HashSet<FunctionId>,
1286) -> bool {
1287 if !seen.insert(func_id) {
1288 return false;
1289 }
1290
1291 let func = hir.function(func_id);
1292 let Some(body) = func.body else { return false };
1293
1294 for stmt in body.stmts {
1295 if stmt_is_access_guard(hir, stmt, seen) {
1296 return true;
1297 }
1298 }
1299 false
1300}
1301
1302fn stmt_is_access_guard(
1303 hir: &hir::Hir<'_>,
1304 stmt: &hir::Stmt<'_>,
1305 seen: &mut HashSet<FunctionId>,
1306) -> bool {
1307 match stmt.kind {
1308 StmtKind::If(cond, then_stmt, else_stmt) => {
1309 (expr_is_unauthorized_access_check(hir, cond) && branch_always_exits(then_stmt))
1310 || (expr_is_authorized_access_check(hir, cond)
1311 && else_stmt.is_some_and(branch_always_exits))
1312 }
1313 StmtKind::Expr(expr) => expr_has_access_guard(hir, expr, seen),
1314 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
1315 block.stmts.iter().any(|stmt| stmt_is_access_guard(hir, stmt, seen))
1316 }
1317 StmtKind::Try(_)
1318 | StmtKind::Return(Some(_))
1319 | StmtKind::Emit(_)
1320 | StmtKind::Revert(_)
1321 | StmtKind::DeclSingle(_)
1322 | StmtKind::DeclMulti(_, _) => false,
1323 StmtKind::Return(None)
1324 | StmtKind::Break
1325 | StmtKind::Continue
1326 | StmtKind::Placeholder
1327 | StmtKind::AssemblyBlock(_)
1328 | StmtKind::Switch(_)
1329 | StmtKind::Err(_) => false,
1330 }
1331}
1332
1333fn expr_has_access_guard(
1334 hir: &hir::Hir<'_>,
1335 expr: &hir::Expr<'_>,
1336 seen: &mut HashSet<FunctionId>,
1337) -> bool {
1338 match &expr.peel_parens().kind {
1339 ExprKind::Call(callee, args, _) if is_require_or_assert(callee) => {
1340 args.exprs().next().is_some_and(|cond| expr_is_authorized_access_check(hir, cond))
1341 }
1342 ExprKind::Call(callee, _, _) => {
1343 for callee_id in resolved_function_ids(callee) {
1344 let func = hir.function(callee_id);
1345 let name_only_guard = func.body.is_none()
1346 && func.returns.is_empty()
1347 && func.name.is_some_and(|name| name_looks_like_access_control(name.as_str()));
1348 if name_only_guard || function_has_access_guard(hir, callee_id, seen) {
1349 return true;
1350 }
1351 }
1352 false
1353 }
1354 _ => false,
1355 }
1356}
1357
1358#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1359enum AccessCheckPolarity {
1360 Authorized,
1361 Unauthorized,
1362}
1363
1364fn expr_is_authorized_access_check(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
1365 expr_access_check_polarity(hir, expr)
1366 .is_some_and(|polarity| matches!(polarity, AccessCheckPolarity::Authorized))
1367}
1368
1369fn expr_is_unauthorized_access_check(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
1370 expr_access_check_polarity(hir, expr)
1371 .is_some_and(|polarity| matches!(polarity, AccessCheckPolarity::Unauthorized))
1372}
1373
1374fn expr_access_check_polarity(
1375 hir: &hir::Hir<'_>,
1376 expr: &hir::Expr<'_>,
1377) -> Option<AccessCheckPolarity> {
1378 match &expr.peel_parens().kind {
1379 ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
1380 Some(match expr_access_check_polarity(hir, inner)? {
1381 AccessCheckPolarity::Authorized => AccessCheckPolarity::Unauthorized,
1382 AccessCheckPolarity::Unauthorized => AccessCheckPolarity::Authorized,
1383 })
1384 }
1385 ExprKind::Binary(lhs, op, rhs) if op.kind == BinOpKind::And => {
1386 let lhs = expr_access_check_polarity(hir, lhs);
1387 let rhs = expr_access_check_polarity(hir, rhs);
1388 if matches!(lhs, Some(AccessCheckPolarity::Authorized))
1389 || matches!(rhs, Some(AccessCheckPolarity::Authorized))
1390 {
1391 Some(AccessCheckPolarity::Authorized)
1392 } else if matches!(lhs, Some(AccessCheckPolarity::Unauthorized))
1393 && matches!(rhs, Some(AccessCheckPolarity::Unauthorized))
1394 {
1395 Some(AccessCheckPolarity::Unauthorized)
1396 } else {
1397 None
1398 }
1399 }
1400 ExprKind::Binary(lhs, op, rhs) if op.kind == BinOpKind::Or => {
1401 let lhs = expr_access_check_polarity(hir, lhs);
1402 let rhs = expr_access_check_polarity(hir, rhs);
1403 if matches!(lhs, Some(AccessCheckPolarity::Authorized))
1404 && matches!(rhs, Some(AccessCheckPolarity::Authorized))
1405 {
1406 Some(AccessCheckPolarity::Authorized)
1407 } else if matches!(lhs, Some(AccessCheckPolarity::Unauthorized))
1408 || matches!(rhs, Some(AccessCheckPolarity::Unauthorized))
1409 {
1410 Some(AccessCheckPolarity::Unauthorized)
1411 } else {
1412 None
1413 }
1414 }
1415 ExprKind::Binary(lhs, op, rhs)
1416 if matches!(op.kind, BinOpKind::Eq | BinOpKind::Ne)
1417 && expr_compares_sender_to_authority(hir, lhs, rhs) =>
1418 {
1419 Some(if op.kind == BinOpKind::Eq {
1420 AccessCheckPolarity::Authorized
1421 } else {
1422 AccessCheckPolarity::Unauthorized
1423 })
1424 }
1425 _ if expr_looks_like_access_check(hir, expr) => Some(AccessCheckPolarity::Authorized),
1426 _ => None,
1427 }
1428}
1429
1430fn expr_compares_sender_to_authority(
1431 hir: &hir::Hir<'_>,
1432 lhs: &hir::Expr<'_>,
1433 rhs: &hir::Expr<'_>,
1434) -> bool {
1435 let mut seen = HashSet::new();
1436 (expr_reads_sender(hir, lhs, &mut seen)
1437 && (expr_reads_state_variable(hir, rhs) || expr_calls_non_sender_user_function(hir, rhs)))
1438 || {
1439 let mut seen = HashSet::new();
1440 expr_reads_sender(hir, rhs, &mut seen)
1441 && (expr_reads_state_variable(hir, lhs)
1442 || expr_calls_non_sender_user_function(hir, lhs))
1443 }
1444}
1445
1446fn expr_looks_like_access_check(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
1447 expr_reads_sender(hir, expr, &mut HashSet::new())
1448 && (expr_reads_state_variable(hir, expr) || expr_calls_non_sender_user_function(hir, expr))
1449}
1450
1451fn expr_reads_state_variable(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
1452 match &expr.peel_parens().kind {
1453 ExprKind::Ident(reses) => reses.iter().any(|res| {
1454 let Res::Item(ItemId::Variable(var_id)) = res else { return false };
1455 hir.variable(*var_id).kind.is_state()
1456 }),
1457 ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
1458 expr_reads_state_variable(hir, lhs) || expr_reads_state_variable(hir, rhs)
1459 }
1460 ExprKind::Unary(_, inner)
1461 | ExprKind::Delete(inner)
1462 | ExprKind::Member(inner, _)
1463 | ExprKind::Payable(inner) => expr_reads_state_variable(hir, inner),
1464 ExprKind::Index(base, index) => {
1465 expr_reads_state_variable(hir, base)
1466 || index.is_some_and(|index| expr_reads_state_variable(hir, index))
1467 }
1468 ExprKind::Slice(base, start, end) => {
1469 expr_reads_state_variable(hir, base)
1470 || start.is_some_and(|start| expr_reads_state_variable(hir, start))
1471 || end.is_some_and(|end| expr_reads_state_variable(hir, end))
1472 }
1473 ExprKind::Call(callee, args, opts) => {
1474 expr_reads_state_variable(hir, callee)
1475 || opts.is_some_and(|opts| {
1476 opts.args.iter().any(|opt| expr_reads_state_variable(hir, &opt.value))
1477 })
1478 || args.exprs().any(|arg| expr_reads_state_variable(hir, arg))
1479 }
1480 ExprKind::Ternary(cond, true_expr, false_expr) => {
1481 expr_reads_state_variable(hir, cond)
1482 || expr_reads_state_variable(hir, true_expr)
1483 || expr_reads_state_variable(hir, false_expr)
1484 }
1485 ExprKind::Array(exprs) => exprs.iter().any(|expr| expr_reads_state_variable(hir, expr)),
1486 ExprKind::Tuple(exprs) => {
1487 exprs.iter().copied().flatten().any(|expr| expr_reads_state_variable(hir, expr))
1488 }
1489 _ => false,
1490 }
1491}
1492
1493fn expr_calls_non_sender_user_function(hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
1494 match &expr.peel_parens().kind {
1495 ExprKind::Call(callee, args, opts) => {
1496 resolved_function_ids(callee).any(|func_id| {
1497 hir.function(func_id)
1498 .name
1499 .is_some_and(|name| !name_looks_like_sender_accessor(name.as_str()))
1500 }) || expr_calls_non_sender_user_function(hir, callee)
1501 || opts.is_some_and(|opts| {
1502 opts.args.iter().any(|opt| expr_calls_non_sender_user_function(hir, &opt.value))
1503 })
1504 || args.exprs().any(|arg| expr_calls_non_sender_user_function(hir, arg))
1505 }
1506 ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
1507 expr_calls_non_sender_user_function(hir, lhs)
1508 || expr_calls_non_sender_user_function(hir, rhs)
1509 }
1510 ExprKind::Unary(_, inner)
1511 | ExprKind::Delete(inner)
1512 | ExprKind::Member(inner, _)
1513 | ExprKind::Payable(inner) => expr_calls_non_sender_user_function(hir, inner),
1514 ExprKind::Index(base, index) => {
1515 expr_calls_non_sender_user_function(hir, base)
1516 || index.is_some_and(|index| expr_calls_non_sender_user_function(hir, index))
1517 }
1518 ExprKind::Slice(base, start, end) => {
1519 expr_calls_non_sender_user_function(hir, base)
1520 || start.is_some_and(|start| expr_calls_non_sender_user_function(hir, start))
1521 || end.is_some_and(|end| expr_calls_non_sender_user_function(hir, end))
1522 }
1523 ExprKind::Ternary(cond, true_expr, false_expr) => {
1524 expr_calls_non_sender_user_function(hir, cond)
1525 || expr_calls_non_sender_user_function(hir, true_expr)
1526 || expr_calls_non_sender_user_function(hir, false_expr)
1527 }
1528 ExprKind::Array(exprs) => {
1529 exprs.iter().any(|expr| expr_calls_non_sender_user_function(hir, expr))
1530 }
1531 ExprKind::Tuple(exprs) => exprs
1532 .iter()
1533 .copied()
1534 .flatten()
1535 .any(|expr| expr_calls_non_sender_user_function(hir, expr)),
1536 _ => false,
1537 }
1538}
1539
1540fn expr_reads_sender(
1541 hir: &hir::Hir<'_>,
1542 expr: &hir::Expr<'_>,
1543 seen: &mut HashSet<FunctionId>,
1544) -> bool {
1545 if is_sender_member(expr) {
1546 return true;
1547 }
1548
1549 match &expr.peel_parens().kind {
1550 ExprKind::Call(callee, args, opts) => {
1551 for callee_id in resolved_function_ids(callee) {
1552 if function_reads_sender(hir, callee_id, seen) {
1553 return true;
1554 }
1555 }
1556
1557 expr_reads_sender(hir, callee, seen)
1558 || opts.is_some_and(|opts| {
1559 opts.args.iter().any(|opt| expr_reads_sender(hir, &opt.value, seen))
1560 })
1561 || args.exprs().any(|arg| expr_reads_sender(hir, arg, seen))
1562 }
1563 ExprKind::Binary(lhs, _, rhs) => {
1564 expr_reads_sender(hir, lhs, seen) || expr_reads_sender(hir, rhs, seen)
1565 }
1566 ExprKind::Unary(_, inner)
1567 | ExprKind::Delete(inner)
1568 | ExprKind::Member(inner, _)
1569 | ExprKind::Payable(inner) => expr_reads_sender(hir, inner, seen),
1570 ExprKind::Index(base, index) => {
1571 expr_reads_sender(hir, base, seen)
1572 || index.is_some_and(|index| expr_reads_sender(hir, index, seen))
1573 }
1574 ExprKind::Slice(base, start, end) => {
1575 expr_reads_sender(hir, base, seen)
1576 || start.is_some_and(|start| expr_reads_sender(hir, start, seen))
1577 || end.is_some_and(|end| expr_reads_sender(hir, end, seen))
1578 }
1579 ExprKind::Ternary(cond, true_expr, false_expr) => {
1580 expr_reads_sender(hir, cond, seen)
1581 || expr_reads_sender(hir, true_expr, seen)
1582 || expr_reads_sender(hir, false_expr, seen)
1583 }
1584 ExprKind::Array(exprs) => exprs.iter().any(|expr| expr_reads_sender(hir, expr, seen)),
1585 ExprKind::Tuple(exprs) => {
1586 exprs.iter().copied().flatten().any(|expr| expr_reads_sender(hir, expr, seen))
1587 }
1588 ExprKind::Assign(_, _, rhs) => expr_reads_sender(hir, rhs, seen),
1589 _ => false,
1590 }
1591}
1592
1593fn function_reads_sender(
1594 hir: &hir::Hir<'_>,
1595 func_id: FunctionId,
1596 seen: &mut HashSet<FunctionId>,
1597) -> bool {
1598 if !seen.insert(func_id) {
1599 return false;
1600 }
1601
1602 let func = hir.function(func_id);
1603 let Some(body) = func.body else { return false };
1604 body.stmts.iter().any(|stmt| stmt_reads_sender(hir, stmt, seen))
1605}
1606
1607fn stmt_reads_sender(
1608 hir: &hir::Hir<'_>,
1609 stmt: &hir::Stmt<'_>,
1610 seen: &mut HashSet<FunctionId>,
1611) -> bool {
1612 match stmt.kind {
1613 StmtKind::DeclSingle(var_id) => {
1614 hir.variable(var_id).initializer.is_some_and(|init| expr_reads_sender(hir, init, seen))
1615 }
1616 StmtKind::DeclMulti(_, expr)
1617 | StmtKind::Expr(expr)
1618 | StmtKind::Emit(expr)
1619 | StmtKind::Revert(expr) => expr_reads_sender(hir, expr, seen),
1620 StmtKind::Return(Some(expr)) => expr_reads_sender(hir, expr, seen),
1621 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
1622 block.stmts.iter().any(|stmt| stmt_reads_sender(hir, stmt, seen))
1623 }
1624 StmtKind::If(cond, then_stmt, else_stmt) => {
1625 expr_reads_sender(hir, cond, seen)
1626 || stmt_reads_sender(hir, then_stmt, seen)
1627 || else_stmt.is_some_and(|stmt| stmt_reads_sender(hir, stmt, seen))
1628 }
1629 StmtKind::Try(try_stmt) => {
1630 expr_reads_sender(hir, &try_stmt.expr, seen)
1631 || try_stmt.clauses.iter().any(|clause| {
1632 clause.block.stmts.iter().any(|stmt| stmt_reads_sender(hir, stmt, seen))
1633 })
1634 }
1635 StmtKind::Return(None)
1636 | StmtKind::Break
1637 | StmtKind::Continue
1638 | StmtKind::Placeholder
1639 | StmtKind::AssemblyBlock(_)
1640 | StmtKind::Switch(_)
1641 | StmtKind::Err(_) => false,
1642 }
1643}
1644
1645fn is_sender_member(expr: &hir::Expr<'_>) -> bool {
1646 let ExprKind::Member(base, member) = &expr.peel_parens().kind else { return false };
1647 let ExprKind::Ident(reses) = &base.peel_parens().kind else { return false };
1648
1649 reses.iter().any(|res| {
1650 let Res::Builtin(builtin) = res else { return false };
1651 matches!((builtin.name(), member.name), (sym::msg, sym::sender) | (sym::tx, kw::Origin))
1652 })
1653}
1654
1655fn name_looks_like_access_control(name: &str) -> bool {
1656 let lower = name.to_ascii_lowercase();
1657 lower == "auth"
1658 || lower == "requiresauth"
1659 || lower == "restricted"
1660 || lower.starts_with("onlyowner")
1661 || lower.starts_with("onlyrole")
1662 || lower.starts_with("checkowner")
1663 || lower.starts_with("_checkowner")
1664 || lower.starts_with("checkrole")
1665 || lower.starts_with("_checkrole")
1666}
1667
1668fn name_looks_like_sender_accessor(name: &str) -> bool {
1669 let lower = name.to_ascii_lowercase();
1670 lower == "_msgsender" || lower == "msgsender" || lower == "sender"
1671}
1672
1673fn resolved_function_ids<'hir>(
1674 callee: &'hir hir::Expr<'hir>,
1675) -> impl Iterator<Item = FunctionId> + 'hir {
1676 let reses = match &callee.peel_parens().kind {
1677 ExprKind::Ident(reses) => *reses,
1678 _ => &[],
1679 };
1680 reses.iter().filter_map(|res| match res {
1681 Res::Item(ItemId::Function(func_id)) => Some(*func_id),
1682 _ => None,
1683 })
1684}