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