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, referenced_item, underlying_var,
9 },
10 },
11};
12use solar::{
13 ast::{ContractKind, DataLocation, StateMutability, Visibility},
14 interface::{Span, data_structures::Never},
15 sema::{
16 Gcx,
17 hir::{
18 self, EventId, Expr, ExprKind, FunctionId, ItemId, Stmt, StmtKind, VariableId, Visit,
19 },
20 },
21};
22use std::{
23 collections::{HashMap, HashSet},
24 iter,
25 ops::ControlFlow,
26};
27
28declare_forge_lint!(
29 MISSING_EVENTS_ACCESS_CONTROL,
30 Severity::Low,
31 "missing-events-access-control",
32 "access control changes without an event"
33);
34
35impl<'gcx> LateLintPass<'gcx> for MissingEventsAccessControl {
36 fn check_contract(
37 &mut self,
38 ctx: &LintContext,
39 gcx: Gcx<'gcx>,
40 contract: &'gcx hir::Contract<'gcx>,
41 ) {
42 if !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract) {
43 return;
44 }
45
46 let functions: Vec<_> = contract.all_functions().collect();
48 let targets: HashSet<_> = functions.iter().flat_map(|&id| guard_vars(gcx, id)).collect();
49 if targets.is_empty() {
50 return;
51 }
52
53 for func_id in functions {
54 let func = gcx.hir.function(func_id);
55 let is_entry_point = func.kind.is_function()
56 && matches!(func.visibility, Visibility::Public | Visibility::External)
57 && !func.is_constructor()
58 && !func.is_special()
59 && !matches!(func.state_mutability, StateMutability::Pure | StateMutability::View);
60 if !is_entry_point || !is_protected(gcx, func_id) {
61 continue;
62 }
63
64 let guard_targets = guard_vars(gcx, func_id);
65 let mut analyzer = WriteAnalyzer {
66 gcx,
67 targets: &targets,
68 guard_targets: &guard_targets,
69 state: State {
70 taint: func
71 .parameters
72 .iter()
73 .map(|&p| (p, Sources::from([Source::Var(p)])))
74 .collect(),
75 ..Default::default()
76 },
77 call_stack: Vec::new(),
78 };
79 analyzer.analyze_function(func_id);
80
81 let mut emitted = HashSet::new();
82 for write in analyzer.state.writes {
83 if write.evented || !emitted.insert(write.var_id) {
84 continue;
85 }
86 let name = gcx
87 .hir
88 .variable(write.var_id)
89 .name
90 .map_or_else(|| "state variable".to_string(), |name| name.to_string());
91 ctx.emit_with_msg(
92 &MISSING_EVENTS_ACCESS_CONTROL,
93 write.span,
94 format!("`{name}` is changed without an event but is used for access control"),
95 );
96 }
97 }
98 }
99}
100
101fn for_each_lhs_index<'gcx>(expr: &'gcx Expr<'gcx>, f: &mut impl FnMut(&'gcx Expr<'gcx>)) {
103 match &expr.peel_parens().kind {
104 ExprKind::Index(base, index) => {
105 for_each_lhs_index(base, f);
106 if let Some(index) = index {
107 f(index);
108 }
109 }
110 ExprKind::Slice(base, start, end) => {
111 for_each_lhs_index(base, f);
112 for bound in start.iter().chain(end) {
113 f(bound);
114 }
115 }
116 ExprKind::Member(base, _) | ExprKind::Payable(base) | ExprKind::Unary(_, base) => {
117 for_each_lhs_index(base, f)
118 }
119 ExprKind::Tuple(exprs) => exprs.iter().flatten().for_each(|e| for_each_lhs_index(e, f)),
120 _ => {}
121 }
122}
123
124#[derive(Clone, Copy, PartialEq, Eq, Hash)]
128enum Source {
129 Var(VariableId),
130 Sender,
131}
132
133type Sources = HashSet<Source>;
134
135#[derive(Clone)]
136struct StateWrite {
137 var_id: VariableId,
138 span: Span,
139 sources: Sources,
140 fixed_clear: bool,
142 evented: bool,
143}
144
145#[derive(Clone, Default)]
146struct State {
147 taint: HashMap<VariableId, Sources>,
149 storage_aliases: HashMap<VariableId, VariableId>,
151 writes: Vec<StateWrite>,
152}
153
154struct WriteAnalyzer<'a, 'gcx> {
156 gcx: Gcx<'gcx>,
157 targets: &'a HashSet<VariableId>,
158 guard_targets: &'a HashSet<VariableId>,
161 state: State,
162 call_stack: Vec<FunctionId>,
163}
164
165impl<'gcx> WriteAnalyzer<'_, 'gcx> {
166 fn analyze_function(&mut self, func_id: FunctionId) {
167 if self.call_stack.contains(&func_id) {
168 return;
169 }
170 let func = self.gcx.hir.function(func_id);
171 let Some(body) = func.body else { return };
172 self.call_stack.push(func_id);
173 for modifier in func.modifiers {
174 if let Some(modifier_id) = modifier.id.as_function() {
175 let _ = self.visit_call_args(&modifier.args);
176 self.analyze_call(modifier_id, |index| modifier.args.exprs().nth(index));
177 }
178 }
179 for stmt in body.stmts {
180 let _ = self.visit_stmt(stmt);
181 }
182 self.call_stack.pop();
183 }
184
185 fn analyze_call(
188 &mut self,
189 callee_id: FunctionId,
190 mut argument: impl FnMut(usize) -> Option<&'gcx Expr<'gcx>>,
191 ) {
192 let params = self
193 .gcx
194 .hir
195 .function(callee_id)
196 .parameters
197 .iter()
198 .enumerate()
199 .filter_map(|(index, ¶m)| {
200 let sources = self.value_sources(argument(index)?);
201 (!sources.is_empty()).then_some((param, sources))
202 })
203 .collect();
204 let saved_taint = std::mem::replace(&mut self.state.taint, params);
205 let saved_aliases = std::mem::take(&mut self.state.storage_aliases);
206 self.analyze_function(callee_id);
207 self.state.taint = saved_taint;
208 self.state.storage_aliases = saved_aliases;
209 }
210
211 fn value_sources(&self, expr: &Expr<'_>) -> Sources {
213 let mut out = Sources::new();
214 let _ = expr.visit(&mut |e| {
215 if is_sender_member(self.gcx, e) {
216 out.insert(Source::Sender);
217 }
218 if let Some(var_id) = underlying_var(self.gcx, e) {
219 if self.gcx.hir.variable(var_id).kind.is_state() {
220 out.insert(Source::Var(var_id));
221 }
222 if let Some(sources) = self.state.taint.get(&var_id) {
223 out.extend(sources);
224 }
225 }
226 ControlFlow::<()>::Continue(())
227 });
228 out
229 }
230
231 fn lhs_state_vars(&self, lhs: &Expr<'_>) -> Vec<VariableId> {
233 let mut vars = Vec::new();
234 for_each_lhs_var(self.gcx, lhs, &mut |var_id| {
235 let root = if self.gcx.hir.variable(var_id).kind.is_state() {
236 Some(var_id)
237 } else {
238 self.state.storage_aliases.get(&var_id).copied()
239 };
240 if let Some(root) = root
241 && !vars.contains(&root)
242 {
243 vars.push(root);
244 }
245 });
246 vars
247 }
248
249 fn record_writes(&mut self, lhs: &Expr<'_>, sources: &Sources, fixed_clear: bool) {
250 for var_id in self.lhs_state_vars(lhs) {
251 if self.targets.contains(&var_id)
252 && (!sources.is_empty() || (fixed_clear && self.guard_targets.contains(&var_id)))
253 {
254 self.state.writes.push(StateWrite {
255 var_id,
256 span: lhs.span,
257 sources: sources.clone(),
258 fixed_clear,
259 evented: false,
260 });
261 }
262 }
263 }
264
265 fn set_taint(&mut self, var_id: VariableId, sources: Sources) {
266 if sources.is_empty() {
267 self.state.taint.remove(&var_id);
268 } else {
269 self.state.taint.insert(var_id, sources);
270 }
271 }
272
273 fn set_local(&mut self, var_id: VariableId, sources: Sources, value: &Expr<'_>) {
275 self.set_taint(var_id, sources);
276 let root = (self.gcx.hir.variable(var_id).data_location == Some(DataLocation::Storage))
277 .then(|| self.lhs_state_vars(value).into_iter().next())
278 .flatten();
279 match root {
280 Some(root) => self.state.storage_aliases.insert(var_id, root),
281 None => self.state.storage_aliases.remove(&var_id),
282 };
283 }
284
285 fn mark_event(&mut self, expr: &Expr<'_>) {
288 let Some(event_id) = emitted_event_id(self.gcx, expr) else { return };
289 let event_sources = self.value_sources(expr);
290 for write in &mut self.state.writes {
291 if !write.evented
292 && (write.fixed_clear || !write.sources.is_disjoint(&event_sources))
293 && event_mentions_state_var(self.gcx, event_id, write.var_id)
294 {
295 write.evented = true;
296 }
297 }
298 }
299}
300
301impl<'gcx> Visit<'gcx> for WriteAnalyzer<'_, 'gcx> {
302 type BreakValue = Never;
303
304 fn hir(&self) -> &'gcx hir::Hir<'gcx> {
305 &self.gcx.hir
306 }
307
308 fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Never> {
309 match stmt.kind {
310 StmtKind::DeclSingle(var_id) => {
311 if let Some(init) = self.gcx.hir.variable(var_id).initializer {
312 self.visit_expr(init)?;
313 let sources = self.value_sources(init);
314 self.set_local(var_id, sources, init);
315 }
316 }
317 StmtKind::DeclMulti(vars, expr) => {
318 self.visit_expr(expr)?;
319 let sources = self.value_sources(expr);
320 for var_id in vars.iter().flatten() {
321 self.set_taint(*var_id, sources.clone());
322 }
323 }
324 StmtKind::If(cond, then_stmt, else_stmt) => {
325 self.visit_expr(cond)?;
326 let base = self.state.clone();
327 self.visit_stmt(then_stmt)?;
328 let then_state = std::mem::replace(&mut self.state, base.clone());
329 if let Some(else_stmt) = else_stmt {
330 self.visit_stmt(else_stmt)?;
331 }
332 let else_state = std::mem::take(&mut self.state);
333 self.state = merge_branches(
334 base,
335 then_state,
336 else_state,
337 branch_always_exits(self.gcx, then_stmt),
338 else_stmt.is_some_and(|expr| branch_always_exits(self.gcx, expr)),
339 );
340 }
341 StmtKind::Emit(expr) => {
342 self.visit_expr(expr)?;
343 self.mark_event(expr);
344 }
345 _ => return self.walk_stmt(stmt),
346 }
347 ControlFlow::Continue(())
348 }
349
350 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
351 match &expr.kind {
352 ExprKind::Assign(lhs, op, rhs) => {
353 self.visit_expr(rhs)?;
354 self.visit_expr(lhs)?;
355 let mut sources = self.value_sources(rhs);
356 for_each_lhs_index(lhs, &mut |index| sources.extend(self.value_sources(index)));
357 if op.is_some() {
358 sources.extend(self.value_sources(lhs));
359 }
360 self.record_writes(lhs, &sources, is_zero_value(rhs));
361 if let Some(local) = lhs_local_var(self.gcx, lhs) {
362 self.set_local(local, sources, rhs);
363 }
364 ControlFlow::Continue(())
365 }
366 ExprKind::Delete(inner) => {
367 let mut sources = Sources::new();
368 for_each_lhs_index(inner, &mut |index| sources.extend(self.value_sources(index)));
369 self.record_writes(inner, &sources, true);
370 self.walk_expr(expr)
371 }
372 ExprKind::Call(callee, ..) => {
373 self.walk_expr(expr)?;
374 if matches!(callee.peel_parens().kind, ExprKind::Ident(_))
375 && let Some(callee_id) = self.gcx.resolved_function(callee)
376 {
377 let gcx = self.gcx;
378 self.analyze_call(callee_id, |index| gcx.call_arg(expr, index));
379 }
380 ControlFlow::Continue(())
381 }
382 _ => self.walk_expr(expr),
383 }
384 }
385}
386
387fn merge_branches(
390 base: State,
391 then_state: State,
392 else_state: State,
393 then_exits: bool,
394 else_exits: bool,
395) -> State {
396 let mut writes = base.writes;
397 for (i, write) in writes.iter_mut().enumerate() {
398 write.evented = then_state.writes[i].evented && else_state.writes[i].evented;
399 }
400 let n = writes.len();
401 writes.extend_from_slice(&then_state.writes[n..]);
402 writes.extend_from_slice(&else_state.writes[n..]);
403
404 let (taint, storage_aliases) = match (then_exits, else_exits) {
405 (true, true) => (base.taint, base.storage_aliases),
406 (true, false) => (else_state.taint, else_state.storage_aliases),
407 (false, true) => (then_state.taint, then_state.storage_aliases),
408 (false, false) => {
409 let mut taint = then_state.taint;
410 for (var_id, sources) in else_state.taint {
411 taint.entry(var_id).or_default().extend(sources);
412 }
413 let storage_aliases = then_state
414 .storage_aliases
415 .into_iter()
416 .filter(|(alias, root)| else_state.storage_aliases.get(alias) == Some(root))
417 .collect();
418 (taint, storage_aliases)
419 }
420 };
421 State { taint, storage_aliases, writes }
422}
423
424fn emitted_event_id(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<EventId> {
425 let ExprKind::Call(callee, ..) = &expr.peel_parens().kind else { return None };
426 match referenced_item(gcx, callee)? {
427 ItemId::Event(event_id) => Some(event_id),
428 _ => None,
429 }
430}
431
432fn event_mentions_state_var(gcx: Gcx<'_>, event_id: EventId, var_id: VariableId) -> bool {
435 let Some(var_name) = gcx.hir.variable(var_id).name else { return false };
436 let var_name = normalize(var_name.as_str());
437 let mut keywords = vec![var_name.as_str()];
438 keywords.extend(var_name.strip_suffix('s').filter(|singular| !singular.is_empty()));
439 let roles = ["owner", "admin", "guardian", "manager", "role"];
440 keywords.extend(roles.into_iter().filter(|role| var_name.contains(role)));
441
442 let event = gcx.hir.event(event_id);
443 let param_names = event.parameters.iter().filter_map(|&p| gcx.hir.variable(p).name);
444 iter::once(event.name).chain(param_names).any(|name| {
445 let name = normalize(name.as_str());
446 keywords.iter().any(|keyword| !keyword.is_empty() && name.contains(keyword))
447 })
448}
449
450fn normalize(name: &str) -> String {
451 name.chars().filter(char::is_ascii_alphanumeric).map(|c| c.to_ascii_lowercase()).collect()
452}