1use super::MissingEventsAccessControl;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{
7 branch_always_exits, for_each_lhs_var, guard_vars, is_protected, is_sender_member,
8 is_zero_value, lhs_local_var, loop_stmts, referenced_item, underlying_var,
9 },
10 },
11};
12use solar::{
13 ast::{BinOpKind, ContractKind, DataLocation, StateMutability, Visibility},
14 interface::{Span, data_structures::Never},
15 sema::{
16 Gcx,
17 builtins::Builtin,
18 hir::{
19 self, EventId, Expr, ExprKind, FunctionId, ItemId, LoopSource, Stmt, StmtKind,
20 VariableId, Visit,
21 },
22 },
23};
24use std::{
25 collections::{HashMap, HashSet},
26 iter,
27 ops::ControlFlow,
28};
29
30declare_forge_lint!(
31 MISSING_EVENTS_ACCESS_CONTROL,
32 Severity::Low,
33 "missing-events-access-control",
34 "access control changes without an event"
35);
36
37impl<'gcx> LateLintPass<'gcx> for MissingEventsAccessControl {
38 fn check_contract(
39 &mut self,
40 ctx: &LintContext,
41 gcx: Gcx<'gcx>,
42 contract: &'gcx hir::Contract<'gcx>,
43 ) {
44 if !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract) {
45 return;
46 }
47
48 let functions: Vec<_> = contract.all_functions().collect();
50 let targets: HashSet<_> = functions.iter().flat_map(|&id| guard_vars(gcx, id)).collect();
51 if targets.is_empty() {
52 return;
53 }
54
55 for func_id in functions {
56 let func = gcx.hir.function(func_id);
57 let is_entry_point = func.kind.is_function()
58 && matches!(func.visibility, Visibility::Public | Visibility::External)
59 && !func.is_constructor()
60 && !func.is_special()
61 && !matches!(func.state_mutability, StateMutability::Pure | StateMutability::View);
62 if !is_entry_point || !is_protected(gcx, func_id) {
63 continue;
64 }
65
66 let guard_targets = guard_vars(gcx, func_id);
67 let mut analyzer = WriteAnalyzer {
68 gcx,
69 targets: &targets,
70 guard_targets: &guard_targets,
71 state: State {
72 taint: func
73 .parameters
74 .iter()
75 .map(|&p| (p, Sources::from([Source::Var(p)])))
76 .collect(),
77 ..Default::default()
78 },
79 call_stack: Vec::new(),
80 return_states: Vec::new(),
81 terminal_states: Vec::new(),
82 next_write_id: 0,
83 };
84 analyzer.analyze_function(func_id);
85
86 let mut emitted = HashSet::new();
87 for write in analyzer.state.writes {
88 if write.evented || !emitted.insert(write.var_id) {
89 continue;
90 }
91 let name = gcx
92 .hir
93 .variable(write.var_id)
94 .name
95 .map_or_else(|| "state variable".to_string(), |name| name.to_string());
96 ctx.emit_with_msg(
97 &MISSING_EVENTS_ACCESS_CONTROL,
98 write.span,
99 format!("`{name}` is changed without an event but is used for access control"),
100 );
101 }
102 }
103 }
104}
105
106fn for_each_lhs_index<'gcx>(expr: &'gcx Expr<'gcx>, f: &mut impl FnMut(&'gcx Expr<'gcx>)) {
108 match &expr.peel_parens().kind {
109 ExprKind::Index(base, index) => {
110 for_each_lhs_index(base, f);
111 if let Some(index) = index {
112 f(index);
113 }
114 }
115 ExprKind::Slice(base, start, end) => {
116 for_each_lhs_index(base, f);
117 for bound in start.iter().chain(end) {
118 f(bound);
119 }
120 }
121 ExprKind::Member(base, _) | ExprKind::Payable(base) | ExprKind::Unary(_, base) => {
122 for_each_lhs_index(base, f)
123 }
124 ExprKind::Tuple(exprs) => exprs.iter().flatten().for_each(|e| for_each_lhs_index(e, f)),
125 _ => {}
126 }
127}
128
129#[derive(Clone, Copy, PartialEq, Eq, Hash)]
133enum Source {
134 Var(VariableId),
135 Sender,
136}
137
138type Sources = HashSet<Source>;
139
140#[derive(Clone)]
141struct StateWrite {
142 id: usize,
143 var_id: VariableId,
144 span: Span,
145 sources: Sources,
146 fixed_clear: bool,
148 evented: bool,
149}
150
151#[derive(Clone, Default)]
152struct State {
153 taint: HashMap<VariableId, Sources>,
155 storage_aliases: HashMap<VariableId, VariableId>,
157 writes: Vec<StateWrite>,
158 emits: Vec<(EventId, Sources)>,
160}
161
162struct WriteAnalyzer<'a, 'gcx> {
164 gcx: Gcx<'gcx>,
165 targets: &'a HashSet<VariableId>,
166 guard_targets: &'a HashSet<VariableId>,
169 state: State,
170 call_stack: Vec<FunctionId>,
171 return_states: Vec<Vec<State>>,
173 terminal_states: Vec<Vec<State>>,
175 next_write_id: usize,
176}
177
178impl<'gcx> WriteAnalyzer<'_, 'gcx> {
179 fn analyze_function(&mut self, func_id: FunctionId) {
180 if self.call_stack.contains(&func_id) {
181 return;
182 }
183 let func = self.gcx.hir.function(func_id);
184 let Some(body) = func.body else { return };
185 self.call_stack.push(func_id);
186 self.return_states.push(Vec::new());
187 self.terminal_states.push(Vec::new());
188 for modifier in func.modifiers {
189 if let Some(modifier_id) = modifier.id.as_function() {
190 let _ = self.visit_call_args(&modifier.args);
191 self.analyze_call(modifier_id, |index| modifier.args.exprs().nth(index));
192 }
193 }
194 for stmt in body.stmts {
195 let _ = self.visit_stmt(stmt);
196 }
197 self.correlate_pending();
198 let mut exits = self.return_states.pop().unwrap();
199 let terminal_exits = self.terminal_states.pop().unwrap();
200 if !exits.is_empty() || !terminal_exits.is_empty() {
201 if !body.stmts.iter().any(|stmt| branch_always_exits(self.gcx, stmt)) {
202 exits.push(self.state.clone());
203 }
204 for write in &mut self.state.writes {
205 let mut matching = exits
207 .iter()
208 .chain(&terminal_exits)
209 .filter_map(|state| state.writes.iter().find(|other| other.id == write.id));
210 if let Some(first) = matching.next() {
211 write.evented = first.evented && matching.all(|other| other.evented);
212 }
213 }
214 self.state.emits.retain(|event| exits.iter().all(|state| state.emits.contains(event)));
215 }
216 if let Some(parent) = self.terminal_states.last_mut() {
217 parent.extend(terminal_exits);
218 }
219 self.call_stack.pop();
220 }
221
222 fn analyze_call(
225 &mut self,
226 callee_id: FunctionId,
227 mut argument: impl FnMut(usize) -> Option<&'gcx Expr<'gcx>>,
228 ) {
229 let params = self
230 .gcx
231 .hir
232 .function(callee_id)
233 .parameters
234 .iter()
235 .enumerate()
236 .filter_map(|(index, ¶m)| {
237 let sources = self.value_sources(argument(index)?);
238 (!sources.is_empty()).then_some((param, sources))
239 })
240 .collect();
241 let saved_taint = std::mem::replace(&mut self.state.taint, params);
242 let saved_aliases = std::mem::take(&mut self.state.storage_aliases);
243 self.analyze_function(callee_id);
244 self.state.taint = saved_taint;
245 self.state.storage_aliases = saved_aliases;
246 }
247
248 fn value_sources(&self, expr: &Expr<'_>) -> Sources {
250 let mut out = Sources::new();
251 let _ = expr.visit(&mut |e| {
252 if is_sender_member(self.gcx, e) {
253 out.insert(Source::Sender);
254 }
255 if let Some(var_id) = underlying_var(self.gcx, e) {
256 if self.gcx.hir.variable(var_id).kind.is_state() {
257 out.insert(Source::Var(var_id));
258 }
259 if let Some(sources) = self.state.taint.get(&var_id) {
260 out.extend(sources);
261 }
262 }
263 ControlFlow::<()>::Continue(())
264 });
265 out
266 }
267
268 fn lhs_state_vars(&self, lhs: &Expr<'_>) -> Vec<VariableId> {
270 let mut vars = Vec::new();
271 for_each_lhs_var(self.gcx, lhs, &mut |var_id| {
272 let root = if self.gcx.hir.variable(var_id).kind.is_state() {
273 Some(var_id)
274 } else {
275 self.state.storage_aliases.get(&var_id).copied()
276 };
277 if let Some(root) = root
278 && !vars.contains(&root)
279 {
280 vars.push(root);
281 }
282 });
283 vars
284 }
285
286 fn record_writes(&mut self, lhs: &Expr<'_>, sources: &Sources, fixed_clear: bool) {
287 for var_id in self.lhs_state_vars(lhs) {
288 if self.targets.contains(&var_id)
289 && (!sources.is_empty() || (fixed_clear && self.guard_targets.contains(&var_id)))
290 {
291 self.state.writes.push(StateWrite {
292 id: self.next_write_id,
293 var_id,
294 span: lhs.span,
295 sources: sources.clone(),
296 fixed_clear,
297 evented: false,
298 });
299 self.next_write_id += 1;
300 }
301 }
302 }
303
304 fn set_taint(&mut self, var_id: VariableId, sources: Sources) {
305 if sources.is_empty() {
306 self.state.taint.remove(&var_id);
307 } else {
308 self.state.taint.insert(var_id, sources);
309 }
310 }
311
312 fn set_local(&mut self, var_id: VariableId, sources: Sources, value: &Expr<'_>) {
314 self.set_taint(var_id, sources);
315 let root = (self.gcx.hir.variable(var_id).data_location == Some(DataLocation::Storage))
316 .then(|| self.lhs_state_vars(value).into_iter().next())
317 .flatten();
318 match root {
319 Some(root) => self.state.storage_aliases.insert(var_id, root),
320 None => self.state.storage_aliases.remove(&var_id),
321 };
322 }
323
324 fn record_emit(&mut self, expr: &Expr<'_>) {
328 let Some(event_id) = emitted_event_id(self.gcx, expr) else { return };
329 let event_sources = self.value_sources(expr);
330 self.state.emits.push((event_id, event_sources));
331 }
332
333 fn correlate_pending(&mut self) {
337 correlate_pending(self.gcx, &mut self.state);
338 }
339}
340
341impl<'gcx> Visit<'gcx> for WriteAnalyzer<'_, 'gcx> {
342 type BreakValue = Never;
343
344 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
345 &self.gcx.hir
346 }
347
348 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
349 match stmt.kind {
350 StmtKind::DeclSingle(var_id) => {
351 if let Some(init) = self.gcx.hir.variable(var_id).initializer {
352 self.visit_expr(init)?;
353 let sources = self.value_sources(init);
354 self.set_local(var_id, sources, init);
355 }
356 }
357 StmtKind::DeclMulti(vars, expr) => {
358 self.visit_expr(expr)?;
359 let sources = self.value_sources(expr);
360 for var_id in vars.iter().flatten() {
361 self.set_taint(*var_id, sources.clone());
362 }
363 }
364 StmtKind::If(cond, then_stmt, else_stmt) => {
365 self.visit_expr(cond)?;
366 let base = self.state.clone();
367 self.visit_stmt(then_stmt)?;
368 self.correlate_pending();
369 let then_state = std::mem::replace(&mut self.state, base.clone());
370 if let Some(else_stmt) = else_stmt {
371 self.visit_stmt(else_stmt)?;
372 self.correlate_pending();
373 }
374 let else_state = std::mem::take(&mut self.state);
375 self.state = merge_branches(
376 base,
377 then_state,
378 else_state,
379 branch_always_exits(self.gcx, then_stmt),
380 else_stmt.is_some_and(|expr| branch_always_exits(self.gcx, expr)),
381 );
382 }
383 StmtKind::Emit(expr) => {
384 self.visit_expr(expr)?;
385 self.record_emit(expr);
386 }
387 StmtKind::Return(expr) => {
388 if let Some(expr) = expr {
389 self.visit_expr(expr)?;
390 }
391 let mut state = self.state.clone();
392 correlate_pending(self.gcx, &mut state);
393 self.return_states.last_mut().unwrap().push(state);
394 }
395 StmtKind::Loop(block, source) => {
396 let n = self.state.writes.len();
402 let evented_before: Vec<bool> =
403 self.state.writes.iter().map(|w| w.evented).collect();
404 let emits_before = self.state.emits.clone();
405 if let LoopSource::For { update: Some(update) } = source
406 && let [stmt] = block.stmts
407 && let StmtKind::If(cond, body, Some(exit)) = stmt.kind
408 && matches!(exit.kind, StmtKind::Break)
409 {
410 self.visit_expr(cond)?;
414 let base = self.state.clone();
415 self.visit_stmt(body)?;
416 self.visit_stmt(update)?;
417 self.correlate_pending();
418 let body_state = std::mem::take(&mut self.state);
419 self.state = merge_branches(base.clone(), body_state, base, false, false);
420 } else {
421 for stmt in loop_stmts(block, source) {
422 self.visit_stmt(stmt)?;
423 }
424 }
425 self.correlate_pending();
426 for (write, was_evented) in self.state.writes[..n].iter_mut().zip(evented_before) {
427 write.evented = was_evented;
428 }
429 self.state.emits = emits_before;
430 }
431 StmtKind::Try(try_stmt) => {
432 self.visit_expr(&try_stmt.expr)?;
433 let base = self.state.clone();
438 let mut clause_states = Vec::with_capacity(try_stmt.clauses.len());
439 let mut continues = Vec::with_capacity(try_stmt.clauses.len());
440 for clause in try_stmt.clauses {
441 self.state = base.clone();
442 for stmt in clause.block.stmts {
443 self.visit_stmt(stmt)?;
444 }
445 self.correlate_pending();
446 clause_states.push(std::mem::take(&mut self.state));
447 continues.push(
448 !clause.block.stmts.iter().any(|stmt| branch_always_exits(self.gcx, stmt)),
449 );
450 }
451 self.state = merge_try_clauses(base, clause_states, continues);
452 }
453 _ => return self.walk_stmt(stmt),
454 }
455 ControlFlow::Continue(())
456 }
457
458 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
459 match &expr.kind {
460 ExprKind::Assign(lhs, op, rhs) => {
461 self.visit_expr(rhs)?;
462 self.visit_expr(lhs)?;
463 let mut sources = self.value_sources(rhs);
464 for_each_lhs_index(lhs, &mut |index| sources.extend(self.value_sources(index)));
465 if op.is_some() {
466 sources.extend(self.value_sources(lhs));
467 }
468 let local = lhs_local_var(self.gcx, lhs);
469 let rebinds_storage = op.is_none()
470 && local.is_some_and(|var_id| {
471 self.gcx.hir.variable(var_id).data_location == Some(DataLocation::Storage)
472 })
473 && self
474 .gcx
475 .type_of_expr(rhs.peel_parens().id)
476 .is_some_and(|ty| ty.loc() == Some(DataLocation::Storage));
477 if !rebinds_storage {
478 self.record_writes(lhs, &sources, is_zero_value(rhs));
479 }
480 if let Some(local) = local {
481 self.set_local(local, sources, rhs);
482 }
483 ControlFlow::Continue(())
484 }
485 ExprKind::Delete(inner) => {
486 let mut sources = Sources::new();
487 for_each_lhs_index(inner, &mut |index| sources.extend(self.value_sources(index)));
488 self.record_writes(inner, &sources, true);
489 self.walk_expr(expr)
490 }
491 ExprKind::Call(callee, ..) => {
492 self.walk_expr(expr)?;
493 if matches!(callee.peel_parens().kind, ExprKind::Ident(_))
494 && let Some(callee_id) = self.gcx.resolved_function(callee)
495 {
496 let gcx = self.gcx;
497 self.analyze_call(callee_id, |index| gcx.call_arg(expr, index));
498 }
499 if self.gcx.resolved_builtin(callee) == Some(Builtin::Selfdestruct) {
500 let mut state = self.state.clone();
501 correlate_pending(self.gcx, &mut state);
502 self.terminal_states.last_mut().unwrap().push(state);
503 }
504 ControlFlow::Continue(())
505 }
506 ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
507 self.visit_expr(lhs)?;
508 self.correlate_pending();
509 let base = self.state.clone();
510 self.visit_expr(rhs)?;
511 self.correlate_pending();
512 let rhs_state = std::mem::take(&mut self.state);
513 self.state = merge_branches(base.clone(), rhs_state, base, false, false);
515 ControlFlow::Continue(())
516 }
517 ExprKind::Ternary(cond, then_expr, else_expr) => {
518 self.visit_expr(cond)?;
519 self.correlate_pending();
520 let base = self.state.clone();
521 self.visit_expr(then_expr)?;
522 self.correlate_pending();
523 let then_state = std::mem::replace(&mut self.state, base.clone());
524 self.visit_expr(else_expr)?;
525 self.correlate_pending();
526 let else_state = std::mem::take(&mut self.state);
527 self.state = merge_branches(base, then_state, else_state, false, false);
528 ControlFlow::Continue(())
529 }
530 _ => self.walk_expr(expr),
531 }
532 }
533}
534
535fn merge_branches(
542 base: State,
543 then_state: State,
544 else_state: State,
545 then_exits: bool,
546 else_exits: bool,
547) -> State {
548 let emits = base.emits.clone();
549 let mut writes = base.writes;
550 for (i, write) in writes.iter_mut().enumerate() {
551 write.evented = then_state.writes[i].evented && else_state.writes[i].evented;
552 }
553 let n = writes.len();
554 writes.extend_from_slice(&then_state.writes[n..]);
555 writes.extend_from_slice(&else_state.writes[n..]);
556
557 let (taint, storage_aliases) = match (then_exits, else_exits) {
558 (true, true) => (base.taint, base.storage_aliases),
559 (true, false) => (else_state.taint, else_state.storage_aliases),
560 (false, true) => (then_state.taint, then_state.storage_aliases),
561 (false, false) => {
562 let mut taint = then_state.taint;
563 for (var_id, sources) in else_state.taint {
564 taint.entry(var_id).or_default().extend(sources);
565 }
566 let storage_aliases = then_state
567 .storage_aliases
568 .into_iter()
569 .filter(|(alias, root)| else_state.storage_aliases.get(alias) == Some(root))
570 .collect();
571 (taint, storage_aliases)
572 }
573 };
574 State { taint, storage_aliases, writes, emits }
575}
576
577fn merge_try_clauses(base: State, clause_states: Vec<State>, continues: Vec<bool>) -> State {
581 let n = base.writes.len();
582 let mut writes = base.writes;
583 for (i, write) in writes.iter_mut().enumerate() {
584 write.evented = clause_states.iter().all(|state| state.writes[i].evented);
585 }
586 for state in &clause_states {
587 writes.extend_from_slice(&state.writes[n..]);
588 }
589
590 let continuing = || clause_states.iter().zip(&continues).filter(|&(_, &c)| c).map(|(s, _)| s);
591
592 let (taint, storage_aliases) = match continuing().next() {
593 None => (base.taint, base.storage_aliases),
594 Some(first) => {
595 let mut taint = HashMap::new();
596 for state in continuing() {
597 for (&var_id, sources) in &state.taint {
598 taint
599 .entry(var_id)
600 .or_insert_with(Sources::new)
601 .extend(sources.iter().copied());
602 }
603 }
604 let mut storage_aliases = first.storage_aliases.clone();
605 for state in continuing().skip(1) {
606 storage_aliases
607 .retain(|alias, root| state.storage_aliases.get(alias) == Some(root));
608 }
609 (taint, storage_aliases)
610 }
611 };
612
613 State { taint, storage_aliases, writes, emits: base.emits }
614}
615
616fn emitted_event_id(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<EventId> {
617 let ExprKind::Call(callee, ..) = &expr.peel_parens().kind else { return None };
618 match referenced_item(gcx, callee)? {
619 ItemId::Event(event_id) => Some(event_id),
620 _ => None,
621 }
622}
623
624fn event_mentions_state_var(gcx: Gcx<'_>, event_id: EventId, var_id: VariableId) -> bool {
627 let Some(var_name) = gcx.hir.variable(var_id).name else { return false };
628 let var_name = normalize(var_name.as_str());
629 let mut keywords = vec![var_name.as_str()];
630 keywords.extend(var_name.strip_suffix('s').filter(|singular| !singular.is_empty()));
631 let roles = ["owner", "admin", "guardian", "manager", "role"];
632 keywords.extend(roles.into_iter().filter(|role| var_name.contains(role)));
633
634 let event = gcx.hir.event(event_id);
635 let param_names = event.parameters.iter().filter_map(|&p| gcx.hir.variable(p).name);
636 iter::once(event.name).chain(param_names).any(|name| {
637 let name = normalize(name.as_str());
638 keywords.iter().any(|keyword| !keyword.is_empty() && name.contains(keyword))
639 })
640}
641
642fn normalize(name: &str) -> String {
643 name.chars().filter(char::is_ascii_alphanumeric).map(|c| c.to_ascii_lowercase()).collect()
644}
645
646fn correlate_pending(gcx: Gcx<'_>, state: &mut State) {
647 for (event_id, event_sources) in &state.emits {
648 for write in &mut state.writes {
649 if !write.evented
650 && (write.fixed_clear || !write.sources.is_disjoint(event_sources))
651 && event_mentions_state_var(gcx, *event_id, write.var_id)
652 {
653 write.evented = true;
654 }
655 }
656 }
657}