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