1use super::ProtectedVars;
8use crate::{
9 linter::{LateLintPass, LintContext},
10 sol::{
11 Severity, SolLint,
12 analysis::{
13 branch_always_exits, is_builtin, is_loop_termination_if, lhs_local_var, loop_update,
14 runtime_entry_points,
15 },
16 },
17};
18use solar::{
19 ast::{BinOpKind, ContractKind, DataLocation, FunctionKind},
20 interface::sym,
21 sema::{
22 Gcx,
23 builtins::Builtin,
24 hir::{
25 self, CallArgs, ContractId, ExprId, ExprKind, FunctionId, ItemId, LoopSource,
26 NatSpecKind, Res, StmtKind, VariableId,
27 },
28 ty::{CallableParamSource, Ty, TyAbiPrinter, TyAbiPrinterMode, TyKind},
29 },
30};
31use std::collections::{HashMap, HashSet};
32
33type StorageRoots = HashSet<VariableId>;
34type RootMap = HashMap<VariableId, StorageRoots>;
35
36declare_forge_lint!(
37 PROTECTED_VARS,
38 Severity::High,
39 "protected-vars",
40 "protected variable is written without its required protection"
41);
42
43impl<'gcx> LateLintPass<'gcx> for ProtectedVars {
44 fn check_nested_contract(
45 &mut self,
46 ctx: &LintContext,
47 gcx: Gcx<'gcx>,
48 contract_id: ContractId,
49 ) {
50 let contract = gcx.hir.contract(contract_id);
51 if !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract)
52 || contract.linearization_failed()
53 || !is_most_derived_contract(&gcx.hir, contract_id)
54 {
55 return;
56 }
57 let bases = contract.linearized_bases;
58
59 let protected = protected_variables(gcx, bases);
60 if protected.is_empty() {
61 return;
62 }
63 let targets = protection_targets(gcx, bases);
64
65 let entries = runtime_entry_points(gcx, contract_id);
68
69 for entry_id in entries {
70 let entry = gcx.hir.function(entry_id);
71 let span = entry.name.map_or(entry.keyword_span(), |name| name.span);
72 let context = if entry.contract == Some(contract_id) {
73 String::new()
74 } else {
75 format!(" in most-derived contract `{}`", contract.name)
76 };
77 let mut writes: Vec<_> = analyze_entry(gcx, bases, entry_id).into_iter().collect();
78 writes.sort_unstable_by_key(|(var_id, _)| *var_id);
79 for (var_id, guards) in writes {
80 let Some(requirements) = protected.get(&var_id) else { continue };
81 let variable = gcx
82 .hir
83 .variable(var_id)
84 .name
85 .map_or("<unnamed>".to_string(), |n| n.to_string());
86 for requirement in requirements {
87 let msg = match requirement {
88 Some(signature) => {
89 if targets.get(signature).is_some_and(|target| guards.contains(target))
90 {
91 continue;
92 }
93 format!(
94 "protected variable `{variable}` is written without `{signature}`{context}"
95 )
96 }
97 None => format!(
98 "protected variable `{variable}` has a malformed write-protection annotation{context}"
99 ),
100 };
101 ctx.emit_with_msg(&PROTECTED_VARS, span, msg);
102 }
103 }
104 }
105 }
106}
107
108fn is_most_derived_contract(hir: &hir::Hir<'_>, contract_id: ContractId) -> bool {
111 !hir.contract_ids().any(|candidate_id| {
112 candidate_id != contract_id
113 && hir.contract(candidate_id).linearized_bases[1..].contains(&contract_id)
114 })
115}
116
117fn protected_variables(
120 gcx: Gcx<'_>,
121 bases: &[ContractId],
122) -> HashMap<VariableId, Vec<Option<String>>> {
123 let mut protected = HashMap::new();
124 for var_id in bases.iter().flat_map(|&cid| gcx.hir.contract(cid).variables()) {
125 let var = gcx.hir.variable(var_id);
126 if !var.kind.is_state() {
127 continue;
128 }
129 let mut requirements = Vec::new();
130 for item in gcx.natspec_doc_comments(var.doc) {
131 let NatSpecKind::Custom { name } = item.kind else { continue };
132 let content = item.content();
133 let Some(index) = write_protection_token(content) else { continue };
134 if name.as_str() != "security" {
135 continue;
136 }
137 let requirement = content[index + "write-protection".len()..]
138 .strip_prefix("=\"")
139 .and_then(|value| value.split_once('"'))
140 .map(|(signature, _)| signature)
141 .filter(|signature| !signature.is_empty())
142 .map(str::to_owned);
143 if !requirements.contains(&requirement) {
144 requirements.push(requirement);
145 }
146 }
147 if !requirements.is_empty() {
148 protected.insert(var_id, requirements);
149 }
150 }
151 protected
152}
153
154fn write_protection_token(content: &str) -> Option<usize> {
156 let is_token_char = |c: char| c.is_alphanumeric() || matches!(c, '_' | '-');
157 content.match_indices("write-protection").find_map(|(index, token)| {
158 let before = content[..index].chars().next_back();
159 let after = content[index + token.len()..].chars().next();
160 (!before.is_some_and(is_token_char) && !after.is_some_and(is_token_char)).then_some(index)
161 })
162}
163
164fn protection_targets(gcx: Gcx<'_>, bases: &[ContractId]) -> HashMap<String, FunctionId> {
167 let mut targets = HashMap::new();
168 for kind in [FunctionKind::Function, FunctionKind::Modifier] {
169 for fid in bases.iter().flat_map(|&cid| gcx.hir.contract(cid).functions()) {
170 let function = gcx.hir.function(fid);
171 if function.kind == kind && function.name.is_some() {
172 targets.entry(callable_signature(gcx, fid)).or_insert(fid);
173 }
174 }
175 }
176 targets
177}
178
179fn callable_signature(gcx: Gcx<'_>, function_id: FunctionId) -> String {
180 let function = gcx.hir.function(function_id);
181 let params = function.parameters.iter().map(|¶meter| {
182 let ty = gcx.type_of_item(parameter.into());
183 if function.kind == FunctionKind::Modifier {
184 source_type_signature(gcx, ty)
185 } else {
186 slither_function_parameter(gcx, ty, &mut HashSet::new())
187 }
188 });
189 format!("{}({})", function.name.unwrap().as_str(), params.collect::<Vec<_>>().join(","))
190}
191
192fn source_type_signature<'gcx>(gcx: Gcx<'gcx>, ty: Ty<'gcx>) -> String {
194 let mut signature = ty.display(gcx).to_string();
195 for prefix in ["contract ", "struct ", "enum "] {
196 signature = signature.replace(prefix, "");
197 }
198 for suffix in
199 [" storage", " memory", " calldata", " external", " internal", " pure", " view", " payable"]
200 {
201 signature = signature.replace(suffix, "");
202 }
203 signature.replace("function ", "function").replace("returns ", "returns")
204}
205
206fn slither_function_parameter<'gcx>(
208 gcx: Gcx<'gcx>,
209 ty: Ty<'gcx>,
210 seen_structs: &mut HashSet<hir::StructId>,
211) -> String {
212 match ty.kind {
213 TyKind::Fn(_) | TyKind::Mapping(..) => source_type_signature(gcx, ty),
214 TyKind::Ref(inner, _) => slither_function_parameter(gcx, inner, seen_structs),
215 TyKind::DynArray(inner) => {
216 format!("{}[]", slither_function_parameter(gcx, inner, seen_structs))
217 }
218 TyKind::Array(inner, length) => {
219 format!("{}[{length}]", slither_function_parameter(gcx, inner, seen_structs))
220 }
221 TyKind::Struct(struct_id) => {
222 if !seen_structs.insert(struct_id) {
223 return source_type_signature(gcx, ty);
224 }
225 let fields = gcx
226 .struct_field_types(struct_id)
227 .iter()
228 .map(|&field| slither_function_parameter(gcx, field, seen_structs))
229 .collect::<Vec<_>>();
230 format!("({})", fields.join(","))
231 }
232 _ => {
233 let mut signature = String::new();
234 TyAbiPrinter::new(gcx, &mut signature, TyAbiPrinterMode::Signature)
235 .print(ty)
236 .expect("writing to a String cannot fail");
237 signature
238 }
239 }
240}
241
242#[derive(Clone, Default, PartialEq, Eq)]
243struct AliasState {
244 storage: RootMap,
246 slots: RootMap,
248}
249
250#[derive(Clone, Default, PartialEq, Eq)]
251struct FlowState {
252 aliases: AliasState,
253 guards: HashSet<FunctionId>,
255}
256
257impl FlowState {
258 fn merge(&self, other: &Self) -> Self {
259 fn merge_roots(lhs: &RootMap, rhs: &RootMap) -> RootMap {
260 let mut merged = lhs.clone();
261 for (&var_id, roots) in rhs {
262 merged.entry(var_id).or_default().extend(roots);
263 }
264 merged
265 }
266 Self {
267 aliases: AliasState {
268 storage: merge_roots(&self.aliases.storage, &other.aliases.storage),
269 slots: merge_roots(&self.aliases.slots, &other.aliases.slots),
270 },
271 guards: self.guards.intersection(&other.guards).copied().collect(),
272 }
273 }
274}
275
276fn join(destination: &mut Option<FlowState>, state: FlowState) {
278 *destination = Some(match destination.take() {
279 Some(current) => current.merge(&state),
280 None => state,
281 });
282}
283
284#[derive(Clone, Debug, PartialEq, Eq, Hash)]
286struct CallContext {
287 function_id: FunctionId,
288 storage: Vec<(VariableId, Vec<VariableId>)>,
289 slots: Vec<(VariableId, Vec<VariableId>)>,
290 guards: Vec<FunctionId>,
291}
292
293impl CallContext {
294 fn new(
295 function_id: FunctionId,
296 function: &hir::Function<'_>,
297 aliases: &AliasState,
298 guards: &HashSet<FunctionId>,
299 ) -> Self {
300 let roots = |aliases: &RootMap| {
301 function
302 .parameters
303 .iter()
304 .filter_map(|¶meter| {
305 let mut roots: Vec<_> = aliases.get(¶meter)?.iter().copied().collect();
306 roots.sort_unstable();
307 Some((parameter, roots))
308 })
309 .collect()
310 };
311 let mut guards: Vec<_> = guards.iter().copied().collect();
312 guards.sort_unstable();
313 Self { function_id, storage: roots(&aliases.storage), slots: roots(&aliases.slots), guards }
314 }
315}
316
317#[derive(Clone, PartialEq, Eq)]
318struct CallSummary {
319 returns: Vec<StorageRoots>,
321 guards: HashSet<FunctionId>,
323 completes: bool,
325}
326
327#[derive(Default)]
329struct LoopFlow {
330 breaks: Option<FlowState>,
331 continues: Option<FlowState>,
332}
333
334#[derive(Clone, Copy)]
336struct ModifierContinuation<'gcx> {
337 modifiers: &'gcx [hir::Modifier<'gcx>],
338 next: usize,
339 body: hir::Block<'gcx>,
340}
341
342fn analyze_entry<'gcx>(
345 gcx: Gcx<'gcx>,
346 bases: &'gcx [ContractId],
347 entry_id: FunctionId,
348) -> HashMap<VariableId, HashSet<FunctionId>> {
349 let mut call_summaries = HashMap::new();
350 let mut previous_writes = HashMap::new();
351 loop {
352 let mut analyzer = EntryAnalyzer {
353 gcx,
354 bases,
355 writes: HashMap::new(),
356 aliases: AliasState::default(),
357 guards: HashSet::new(),
358 call_returns: HashMap::new(),
359 call_summaries: call_summaries.clone(),
360 seen_calls: HashSet::new(),
361 evaluated_calls: HashSet::new(),
362 stack: Vec::new(),
363 return_stack: Vec::new(),
364 return_flow: Vec::new(),
365 loop_flow: Vec::new(),
366 modifier_continuations: Vec::new(),
367 assembly_depth: 0,
368 };
369 analyzer.analyze_function(entry_id);
370 if analyzer.writes == previous_writes && analyzer.call_summaries == call_summaries {
371 return analyzer.writes;
372 }
373 previous_writes = analyzer.writes;
374 call_summaries = analyzer.call_summaries;
375 }
376}
377
378struct EntryAnalyzer<'gcx> {
379 gcx: Gcx<'gcx>,
380 bases: &'gcx [ContractId],
381 writes: HashMap<VariableId, HashSet<FunctionId>>,
383 aliases: AliasState,
384 guards: HashSet<FunctionId>,
385 call_returns: HashMap<ExprId, Vec<StorageRoots>>,
387 call_summaries: HashMap<CallContext, CallSummary>,
388 seen_calls: HashSet<CallContext>,
390 evaluated_calls: HashSet<CallContext>,
392 stack: Vec<FunctionId>,
393 return_stack: Vec<Vec<StorageRoots>>,
395 return_flow: Vec<Option<FlowState>>,
397 loop_flow: Vec<LoopFlow>,
398 modifier_continuations: Vec<ModifierContinuation<'gcx>>,
399 assembly_depth: usize,
400}
401
402impl<'gcx> EntryAnalyzer<'gcx> {
403 fn analyze_function(&mut self, function_id: FunctionId) -> CallSummary {
404 let function = self.gcx.hir.function(function_id);
405 let empty_returns = || function.returns.iter().map(|_| StorageRoots::new()).collect();
406 let Some(body) = function.body else {
407 return CallSummary {
408 returns: empty_returns(),
409 guards: self.guards.clone(),
410 completes: true,
411 };
412 };
413 self.stack.push(function_id);
414 self.return_stack.push(empty_returns());
415 self.return_flow.push(None);
416 let completes = self.analyze_modifier_chain(function.modifiers, 0, body);
417 if completes && !body.stmts.iter().any(|expr| branch_always_exits(self.gcx, expr)) {
418 self.capture_named_returns();
419 }
420 let returned = self.return_flow.pop().expect("return flow frame").is_some();
421 let returns = self.return_stack.pop().expect("return frame");
422 self.stack.pop();
423 CallSummary { returns, guards: self.guards.clone(), completes: completes || returned }
424 }
425
426 fn analyze_modifier_chain(
429 &mut self,
430 modifiers: &'gcx [hir::Modifier<'gcx>],
431 index: usize,
432 body: hir::Block<'gcx>,
433 ) -> bool {
434 let Some(modifier) = modifiers.get(index) else {
435 let previous_returns = self.return_flow.last_mut().and_then(Option::take);
436 let falls_through = self.analyze_block(body);
437 let mut completions = self.return_flow.last_mut().and_then(Option::take);
438
439 *self.return_flow.last_mut().expect("return flow frame") = previous_returns;
445
446 if falls_through {
447 join(&mut completions, self.flow_state());
448 }
449 let Some(state) = completions else { return false };
450 self.set_flow_state(state);
451 return true;
452 };
453 if !modifier.args.exprs().all(|argument| self.analyze_expr(argument)) {
454 return false;
455 }
456
457 let Some(declared_id) = modifier.id.as_function() else { return false };
458 let Some(modifier_id) = self.gcx.resolve_modifier_target(self.bases[0], modifier) else {
459 return false;
460 };
461 self.guards.insert(modifier_id);
462 let arguments = self.ordered_call_arguments(declared_id, modifier.args, None);
463 let bound = self.argument_aliases(modifier_id, &arguments);
464 for parameter in self.gcx.hir.function(modifier_id).parameters {
465 self.aliases.storage.remove(parameter);
466 }
467 self.aliases.storage.extend(bound.storage);
468
469 let Some(modifier_body) = self.gcx.hir.function(modifier_id).body else { return false };
470 self.modifier_continuations.push(ModifierContinuation { modifiers, next: index + 1, body });
471 let completes = self.analyze_block(modifier_body);
472 self.modifier_continuations.pop();
473 completes
474 }
475
476 fn analyze_call(
477 &mut self,
478 function_id: FunctionId,
479 arguments: &[&'gcx hir::Expr<'gcx>],
480 ) -> CallSummary {
481 let bound = self.argument_aliases(function_id, arguments);
482 let saved_aliases = std::mem::replace(&mut self.aliases, bound);
483
484 let function = self.gcx.hir.function(function_id);
485 let context = CallContext::new(function_id, function, &self.aliases, &self.guards);
486 let cached = if self.seen_calls.contains(&context) {
487 Some(self.call_summaries.get(&context).cloned().unwrap_or_else(|| CallSummary {
489 returns: Vec::new(),
490 guards: self.guards.clone(),
491 completes: false,
492 }))
493 } else if self.evaluated_calls.contains(&context) {
494 self.call_summaries.get(&context).cloned()
495 } else {
496 None
497 };
498 let summary = match cached {
499 Some(summary) => {
500 self.guards = summary.guards.clone();
501 summary
502 }
503 None => {
504 self.seen_calls.insert(context.clone());
505 self.evaluated_calls.insert(context.clone());
506 let summary = self.analyze_function(function_id);
507 self.seen_calls.remove(&context);
508 self.call_summaries.insert(context, summary.clone());
509 summary
510 }
511 };
512 self.aliases = saved_aliases;
513 summary
514 }
515
516 fn argument_aliases(
518 &self,
519 function_id: FunctionId,
520 arguments: &[&'gcx hir::Expr<'gcx>],
521 ) -> AliasState {
522 let function = self.gcx.hir.function(function_id);
523 let mut bound = AliasState::default();
524 for (¶meter, &argument) in function.parameters.iter().zip(arguments) {
525 if self.gcx.hir.variable(parameter).data_location == Some(DataLocation::Storage) {
526 let roots = self.storage_roots(argument);
527 if !roots.is_empty() {
528 bound.storage.insert(parameter, roots);
529 }
530 }
531 if function.is_yul {
532 let roots = self.slot_roots(argument);
533 if !roots.is_empty() {
534 bound.slots.insert(parameter, roots);
535 }
536 }
537 }
538 bound
539 }
540
541 fn analyze_block(&mut self, block: hir::Block<'gcx>) -> bool {
542 block.stmts.iter().all(|statement| self.analyze_stmt(statement))
543 }
544
545 fn analyze_alternatives<T: Copy>(
549 &mut self,
550 alternatives: impl IntoIterator<Item = T>,
551 mut merged: Option<FlowState>,
552 analyze: impl Fn(&mut Self, T) -> bool,
553 ) -> bool {
554 let before = self.flow_state();
555 for alternative in alternatives {
556 self.set_flow_state(before.clone());
557 if analyze(self, alternative) {
558 join(&mut merged, self.flow_state());
559 }
560 }
561 let Some(merged) = merged else { return false };
562 self.set_flow_state(merged);
563 true
564 }
565
566 fn analyze_stmt(&mut self, statement: &'gcx hir::Stmt<'gcx>) -> bool {
568 match statement.kind {
569 StmtKind::DeclSingle(variable_id) => {
570 let Some(initializer) = self.gcx.hir.variable(variable_id).initializer else {
571 return true;
572 };
573 if !self.analyze_expr(initializer) {
574 return false;
575 }
576 self.alias_local_from_expr(variable_id, initializer);
577 true
578 }
579 StmtKind::DeclMulti(variables, expression) => {
580 if !self.analyze_expr(expression) {
581 return false;
582 }
583 for (index, variable_id) in variables.iter().enumerate() {
584 if let Some(variable_id) = variable_id {
585 let roots =
586 self.storage_roots_for_output(expression, index, variables.len());
587 self.alias_local(*variable_id, roots);
588 }
589 }
590 true
591 }
592 StmtKind::Emit(expression) | StmtKind::Expr(expression) => {
593 self.analyze_expr(expression) && !branch_always_exits(self.gcx, statement)
594 }
595 StmtKind::Revert(expression) => {
596 self.analyze_expr(expression);
597 false
598 }
599 StmtKind::Return(Some(expression)) => {
600 if self.analyze_expr(expression) {
601 self.set_return_aliases(expression);
602 self.capture_return_flow();
603 }
604 false
605 }
606 StmtKind::Return(None) => {
607 self.capture_named_returns();
608 self.capture_return_flow();
609 false
610 }
611 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => self.analyze_block(block),
612 StmtKind::AssemblyBlock(block) => {
613 self.assembly_depth += 1;
614 let continues = self.analyze_block(block);
615 self.assembly_depth -= 1;
616 continues
617 }
618 StmtKind::Loop(block, source) => self.analyze_loop(block, source),
619 StmtKind::If(condition, then_statement, else_statement) => {
620 self.analyze_expr(condition)
621 && self.analyze_alternatives(
622 [Some(then_statement), else_statement],
623 None,
624 |this, arm| arm.is_none_or(|arm| this.analyze_stmt(arm)),
625 )
626 }
627 StmtKind::Try(try_statement) => {
628 self.analyze_expr(&try_statement.expr)
629 && self.analyze_alternatives(try_statement.clauses, None, |this, clause| {
630 this.analyze_block(clause.block)
631 })
632 }
633 StmtKind::Switch(switch) => {
634 if !self.analyze_expr(switch.selector) {
635 return false;
636 }
637 let has_default = switch.cases.last().is_some_and(|case| case.constant.is_none());
640 let fallthrough = (!has_default).then(|| self.flow_state());
641 self.analyze_alternatives(switch.cases, fallthrough, |this, case| {
642 this.analyze_block(case.body)
643 })
644 }
645 StmtKind::Break | StmtKind::Continue => {
646 let state = self.flow_state();
647 if let Some(flow) = self.loop_flow.last_mut() {
648 let destination = if matches!(statement.kind, StmtKind::Break) {
649 &mut flow.breaks
650 } else {
651 &mut flow.continues
652 };
653 join(destination, state);
654 }
655 false
656 }
657 StmtKind::Placeholder => match self.modifier_continuations.last().copied() {
658 Some(cont) => self.analyze_modifier_chain(cont.modifiers, cont.next, cont.body),
659 None => true,
660 },
661 StmtKind::Err(_) => true,
662 }
663 }
664
665 fn analyze_loop(&mut self, block: hir::Block<'gcx>, source: LoopSource<'gcx>) -> bool {
668 let mut head = self.flow_state();
669 let mut exits = None;
670 loop {
671 self.set_flow_state(head.clone());
672 let (mut breaks, continues, completes) = self.analyze_loop_stmts(|this| {
673 this.analyze_block(block)
674 && loop_update(source).is_none_or(|update| this.analyze_stmt(update))
675 });
676 let mut backedges = completes.then(|| self.flow_state());
677 let epilogue = matches!(source, LoopSource::DoWhile)
679 .then(|| block.stmts.last())
680 .flatten()
681 .filter(|epilogue| is_loop_termination_if(epilogue));
682 match (continues, epilogue) {
683 (Some(state), Some(epilogue)) => {
684 self.set_flow_state(state);
685 let (epilogue_breaks, epilogue_continues, completes) =
686 self.analyze_loop_stmts(|this| this.analyze_stmt(epilogue));
687 if completes {
688 join(&mut backedges, self.flow_state());
689 }
690 breaks = breaks.into_iter().chain(epilogue_breaks).reduce(|a, b| a.merge(&b));
691 backedges =
692 backedges.into_iter().chain(epilogue_continues).reduce(|a, b| a.merge(&b));
693 }
694 (Some(state), None) => join(&mut backedges, state),
695 (None, _) => {}
696 }
697 if let Some(breaks) = breaks {
698 join(&mut exits, breaks);
699 }
700 let Some(backedges) = backedges else { break };
701 let next = head.merge(&backedges);
702 if next == head {
703 break;
704 }
705 head = next;
706 }
707 let Some(exits) = exits else { return false };
708 self.set_flow_state(exits);
709 true
710 }
711
712 fn analyze_loop_stmts(
715 &mut self,
716 analyze: impl FnOnce(&mut Self) -> bool,
717 ) -> (Option<FlowState>, Option<FlowState>, bool) {
718 self.loop_flow.push(LoopFlow::default());
719 let completes = analyze(self);
720 let flow = self.loop_flow.pop().expect("loop flow frame");
721 (flow.breaks, flow.continues, completes)
722 }
723
724 fn flow_state(&self) -> FlowState {
725 FlowState { aliases: self.aliases.clone(), guards: self.guards.clone() }
726 }
727
728 fn set_flow_state(&mut self, state: FlowState) {
729 self.aliases = state.aliases;
730 self.guards = state.guards;
731 }
732
733 fn capture_return_flow(&mut self) {
734 let state = self.flow_state();
735 if let Some(exits) = self.return_flow.last_mut() {
736 join(exits, state);
737 }
738 }
739
740 fn analyze_expr(&mut self, expression: &'gcx hir::Expr<'gcx>) -> bool {
742 match &expression.peel_parens().kind {
743 ExprKind::Assign(lhs, operator, rhs) => {
744 if !(self.analyze_expr(rhs) && self.analyze_expr(lhs)) {
745 return false;
746 }
747 self.apply_assignment(lhs, rhs, operator.is_some());
748 true
749 }
750 ExprKind::Delete(inner) => {
751 if !self.analyze_expr(inner) {
752 return false;
753 }
754 self.record_write(inner);
755 true
756 }
757 ExprKind::Unary(operator, inner) => {
758 if !self.analyze_expr(inner) {
759 return false;
760 }
761 if operator.kind.has_side_effects() {
762 self.record_write(inner);
763 }
764 true
765 }
766 ExprKind::Call(callee, args, options) => {
767 if !(self.analyze_expr(callee)
768 && options
769 .iter()
770 .flat_map(|options| options.args)
771 .all(|option| self.analyze_expr(&option.value))
772 && args.exprs().all(|argument| self.analyze_expr(argument)))
773 {
774 return false;
775 }
776
777 if let ExprKind::Member(base, _) = &callee.peel_parens().kind
778 && let Some(
779 builtin @ (Builtin::ArrayPush0 | Builtin::ArrayPush | Builtin::ArrayPop),
780 ) = self.gcx.resolved_builtin(callee)
781 {
782 self.record_write(base);
783 if builtin == Builtin::ArrayPush0 {
784 let roots = self.storage_roots(base);
785 self.store_call_returns(expression.id, vec![roots]);
786 }
787 }
788
789 if self.gcx.resolved_builtin(callee) == Some(Builtin::YulSstore)
790 && let Some(slot) = args.exprs().next()
791 {
792 let roots = self.slot_roots(slot);
793 self.record_roots(roots);
794 }
795
796 if let Some((_, function_id, receiver)) = self.resolved_internal_call(callee) {
797 self.guards.insert(function_id);
798 let arguments = receiver
799 .into_iter()
800 .chain(
801 (0..args.len())
802 .filter_map(|index| self.gcx.call_arg(expression, index)),
803 )
804 .collect::<Vec<_>>();
805 let summary = self.analyze_call(function_id, &arguments);
806 self.store_call_returns(expression.id, summary.returns);
807 return summary.completes;
808 }
809 true
810 }
811 ExprKind::Binary(lhs, operator, rhs) => {
812 self.analyze_expr(lhs)
813 && if matches!(operator.kind, BinOpKind::And | BinOpKind::Or) {
814 self.analyze_alternatives([Some(*rhs), None], None, |this, rhs| {
816 rhs.is_none_or(|rhs| this.analyze_expr(rhs))
817 })
818 } else {
819 self.analyze_expr(rhs)
820 }
821 }
822 ExprKind::Index(base, index) => {
823 self.analyze_expr(base) && index.is_none_or(|index| self.analyze_expr(index))
824 }
825 ExprKind::Slice(base, start, end) => {
826 self.analyze_expr(base)
827 && start.is_none_or(|start| self.analyze_expr(start))
828 && end.is_none_or(|end| self.analyze_expr(end))
829 }
830 ExprKind::Member(base, _) | ExprKind::YulMember(base, _) | ExprKind::Payable(base) => {
831 self.analyze_expr(base)
832 }
833 ExprKind::Ternary(condition, if_true, if_false) => {
834 self.analyze_expr(condition)
835 && self.analyze_alternatives([*if_true, *if_false], None, |this, arm| {
836 this.analyze_expr(arm)
837 })
838 }
839 ExprKind::Array(expressions) => {
840 expressions.iter().all(|expression| self.analyze_expr(expression))
841 }
842 ExprKind::Tuple(expressions) => {
843 expressions.iter().flatten().all(|expression| self.analyze_expr(expression))
844 }
845 ExprKind::New(_)
846 | ExprKind::TypeCall(_)
847 | ExprKind::Type(_)
848 | ExprKind::Ident(_)
849 | ExprKind::Lit(_)
850 | ExprKind::Err(_) => true,
851 }
852 }
853
854 fn record_write(&mut self, expression: &hir::Expr<'_>) {
855 self.record_roots(self.storage_roots(expression));
856 }
857
858 fn record_roots(&mut self, roots: StorageRoots) {
859 for variable_id in roots {
860 self.writes
861 .entry(variable_id)
862 .and_modify(|guards| guards.retain(|guard| self.guards.contains(guard)))
863 .or_insert_with(|| self.guards.clone());
864 }
865 }
866
867 fn set_storage_alias(&mut self, variable_id: VariableId, roots: StorageRoots) {
868 let variable = self.gcx.hir.variable(variable_id);
869 if !variable.kind.is_state()
870 && variable.data_location == Some(DataLocation::Storage)
871 && !roots.is_empty()
872 {
873 self.aliases.storage.insert(variable_id, roots);
874 } else {
875 self.aliases.storage.remove(&variable_id);
876 }
877 }
878
879 fn set_slot_alias(&mut self, variable_id: VariableId, roots: StorageRoots) {
880 if roots.is_empty() {
881 self.aliases.slots.remove(&variable_id);
882 } else {
883 self.aliases.slots.insert(variable_id, roots);
884 }
885 }
886
887 fn alias_local(&mut self, variable_id: VariableId, roots: StorageRoots) {
890 if self.assembly_depth > 0 {
891 self.set_slot_alias(variable_id, roots.clone());
892 }
893 self.set_storage_alias(variable_id, roots);
894 }
895
896 fn alias_local_from_expr(&mut self, variable_id: VariableId, expression: &hir::Expr<'_>) {
897 if self.assembly_depth > 0 {
898 let roots = self.slot_roots(expression);
899 self.set_slot_alias(variable_id, roots);
900 }
901 let roots = self.storage_roots(expression);
902 self.set_storage_alias(variable_id, roots);
903 }
904
905 fn apply_assignment(
906 &mut self,
907 lhs: &'gcx hir::Expr<'gcx>,
908 rhs: &'gcx hir::Expr<'gcx>,
909 compound: bool,
910 ) {
911 let lhs = lhs.peel_parens();
912 if compound {
913 return self.record_write(lhs);
914 }
915 match &lhs.kind {
916 ExprKind::YulMember(base, member)
918 if member.as_str() == "slot"
919 && let Some(local) = lhs_local_var(self.gcx, base) =>
920 {
921 let roots = self.slot_roots(rhs);
922 self.set_storage_alias(local, roots);
923 }
924 ExprKind::Tuple(expressions) => {
925 for (index, expression) in expressions.iter().enumerate() {
926 let Some(expression) = expression else { continue };
927 if let Some(local) = lhs_local_var(self.gcx, expression) {
928 let roots = self.storage_roots_for_output(rhs, index, expressions.len());
929 self.alias_local(local, roots);
930 } else {
931 self.record_write(expression);
932 }
933 }
934 }
935 _ => match lhs_local_var(self.gcx, lhs) {
936 Some(local) => self.alias_local_from_expr(local, rhs),
937 None => self.record_write(lhs),
938 },
939 }
940 }
941
942 fn set_return_aliases(&mut self, expression: &'gcx hir::Expr<'gcx>) {
943 let Some(&function_id) = self.stack.last() else { return };
944 let outputs = self.gcx.hir.function(function_id).returns.len();
945 let roots: Vec<_> = (0..outputs)
946 .map(|index| self.storage_roots_for_output(expression, index, outputs))
947 .collect();
948 self.extend_returns(roots);
949 }
950
951 fn capture_named_returns(&mut self) {
952 let Some(&function_id) = self.stack.last() else { return };
953 let function = self.gcx.hir.function(function_id);
954 let aliases = if function.is_yul { &self.aliases.slots } else { &self.aliases.storage };
955 let roots: Vec<_> = function
956 .returns
957 .iter()
958 .map(|return_id| aliases.get(return_id).cloned().unwrap_or_default())
959 .collect();
960 self.extend_returns(roots);
961 }
962
963 fn extend_returns(&mut self, roots: Vec<StorageRoots>) {
964 if let Some(frame) = self.return_stack.last_mut() {
965 for (returned, roots) in frame.iter_mut().zip(roots) {
966 returned.extend(roots);
967 }
968 }
969 }
970
971 fn storage_roots_for_output(
973 &self,
974 expression: &hir::Expr<'_>,
975 index: usize,
976 outputs: usize,
977 ) -> StorageRoots {
978 match &expression.peel_parens().kind {
979 ExprKind::Tuple(expressions) if outputs > 1 => expressions
980 .get(index)
981 .copied()
982 .flatten()
983 .map_or_else(StorageRoots::new, |expression| self.storage_roots(expression)),
984 ExprKind::Call(..) => self
985 .call_returns
986 .get(&expression.id)
987 .and_then(|returns| returns.get(index))
988 .cloned()
989 .unwrap_or_default(),
990 _ if outputs == 1 && index == 0 => self.storage_roots(expression),
991 _ => StorageRoots::new(),
992 }
993 }
994
995 fn store_call_returns(&mut self, expression_id: ExprId, returns: Vec<StorageRoots>) {
996 if returns.is_empty() {
997 return;
998 }
999 let stored = self.call_returns.entry(expression_id).or_default();
1000 if stored.len() < returns.len() {
1001 stored.resize_with(returns.len(), StorageRoots::new);
1002 }
1003 for (stored, returned) in stored.iter_mut().zip(returns) {
1004 stored.extend(returned);
1005 }
1006 }
1007
1008 fn ordered_call_arguments(
1011 &self,
1012 declared_id: FunctionId,
1013 arguments: CallArgs<'gcx>,
1014 receiver: Option<&'gcx hir::Expr<'gcx>>,
1015 ) -> Vec<&'gcx hir::Expr<'gcx>> {
1016 let names = self.gcx.callable_param_names(CallableParamSource::Function {
1017 id: declared_id,
1018 skips_receiver: receiver.is_some(),
1019 });
1020 let arguments = (0..names.len())
1021 .filter_map(|index| arguments.argument_for_parameter(index, Some(&names)));
1022 receiver.into_iter().chain(arguments).collect()
1023 }
1024
1025 fn resolved_internal_call(
1029 &self,
1030 callee: &'gcx hir::Expr<'gcx>,
1031 ) -> Option<(FunctionId, FunctionId, Option<&'gcx hir::Expr<'gcx>>)> {
1032 let resolved = self.gcx.resolved_callee(callee.peel_parens().id)?;
1033 let function_id = resolved.res.as_function()?;
1034 let attached = resolved.attached;
1035 match &callee.peel_parens().kind {
1036 ExprKind::Ident(_) => Some((function_id, self.dispatch_function(function_id), None)),
1037 ExprKind::Member(base, _) if attached => Some((function_id, function_id, Some(base))),
1038 ExprKind::Member(base, _)
1039 if self.is_library_function(function_id)
1040 || is_static_internal_base(self.gcx, base) =>
1041 {
1042 Some((function_id, function_id, None))
1043 }
1044 _ => None,
1045 }
1046 }
1047
1048 fn is_library_function(&self, function_id: FunctionId) -> bool {
1049 self.gcx
1050 .hir
1051 .function(function_id)
1052 .contract
1053 .is_some_and(|contract_id| self.gcx.hir.contract(contract_id).kind.is_library())
1054 }
1055
1056 fn dispatch_function(&self, function_id: FunctionId) -> FunctionId {
1058 self.gcx.resolve_virtual_function(self.bases[0], function_id)
1059 }
1060
1061 fn storage_roots(&self, expression: &hir::Expr<'_>) -> StorageRoots {
1064 let mut roots = StorageRoots::new();
1065 self.collect_storage_roots(expression, &mut roots);
1066 roots
1067 }
1068
1069 fn collect_storage_roots(&self, expression: &hir::Expr<'_>, roots: &mut StorageRoots) {
1070 let expression = expression.peel_parens();
1071 match &expression.kind {
1072 ExprKind::Ident(_) => {
1073 if let Some(variable_id) = self.gcx.resolved_variable(expression) {
1074 if self.gcx.hir.variable(variable_id).kind.is_state() {
1075 roots.insert(variable_id);
1076 } else if let Some(aliases) = self.aliases.storage.get(&variable_id) {
1077 roots.extend(aliases);
1078 }
1079 }
1080 }
1081 ExprKind::Index(base, _)
1082 | ExprKind::Slice(base, ..)
1083 | ExprKind::Member(base, _)
1084 | ExprKind::YulMember(base, _)
1085 | ExprKind::Payable(base)
1086 | ExprKind::Unary(_, base)
1087 | ExprKind::Delete(base) => self.collect_storage_roots(base, roots),
1088 ExprKind::Tuple(expressions) => {
1089 for expression in expressions.iter().flatten() {
1090 self.collect_storage_roots(expression, roots);
1091 }
1092 }
1093 ExprKind::Ternary(_, if_true, if_false) => {
1094 self.collect_storage_roots(if_true, roots);
1095 self.collect_storage_roots(if_false, roots);
1096 }
1097 ExprKind::Call(..) => {
1098 roots.extend(self.call_returns.get(&expression.id).into_iter().flatten().flatten());
1099 }
1100 _ => {}
1101 }
1102 }
1103
1104 fn slot_roots(&self, expression: &hir::Expr<'_>) -> StorageRoots {
1106 let mut roots = StorageRoots::new();
1107 self.collect_slot_roots(expression, &mut roots);
1108 roots
1109 }
1110
1111 fn collect_slot_roots(&self, expression: &hir::Expr<'_>, roots: &mut StorageRoots) {
1112 let expression = expression.peel_parens();
1113 match &expression.kind {
1114 ExprKind::Ident(_) => {
1115 if let Some(variable_id) = self.gcx.resolved_variable(expression)
1116 && let Some(aliases) = self.aliases.slots.get(&variable_id)
1117 {
1118 roots.extend(aliases);
1119 }
1120 }
1121 ExprKind::YulMember(base, member) if member.as_str() == "slot" => {
1122 self.collect_storage_roots(base, roots);
1123 }
1124 ExprKind::Call(..) if self.call_returns.contains_key(&expression.id) => {
1125 roots.extend(self.call_returns[&expression.id].iter().flatten());
1126 }
1127 ExprKind::Call(callee, args, options) => {
1129 self.collect_slot_roots(callee, roots);
1130 for option in options.iter().flat_map(|options| options.args) {
1131 self.collect_slot_roots(&option.value, roots);
1132 }
1133 for argument in args.exprs() {
1134 self.collect_slot_roots(argument, roots);
1135 }
1136 }
1137 ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
1138 self.collect_slot_roots(lhs, roots);
1139 self.collect_slot_roots(rhs, roots);
1140 }
1141 ExprKind::Index(base, index) => {
1142 for expression in [Some(*base), *index].into_iter().flatten() {
1143 self.collect_slot_roots(expression, roots);
1144 }
1145 }
1146 ExprKind::Slice(base, start, end) => {
1147 for expression in [Some(*base), *start, *end].into_iter().flatten() {
1148 self.collect_slot_roots(expression, roots);
1149 }
1150 }
1151 ExprKind::Ternary(condition, if_true, if_false) => {
1152 for expression in [condition, if_true, if_false] {
1153 self.collect_slot_roots(expression, roots);
1154 }
1155 }
1156 ExprKind::Member(base, _)
1157 | ExprKind::YulMember(base, _)
1158 | ExprKind::Payable(base)
1159 | ExprKind::Unary(_, base)
1160 | ExprKind::Delete(base) => self.collect_slot_roots(base, roots),
1161 ExprKind::Array(expressions) => {
1162 for expression in *expressions {
1163 self.collect_slot_roots(expression, roots);
1164 }
1165 }
1166 ExprKind::Tuple(expressions) => {
1167 for expression in expressions.iter().flatten() {
1168 self.collect_slot_roots(expression, roots);
1169 }
1170 }
1171 _ => {}
1172 }
1173 }
1174}
1175
1176fn is_static_internal_base(gcx: Gcx<'_>, base: &hir::Expr<'_>) -> bool {
1178 is_builtin(gcx, base, sym::super_)
1179 || matches!(
1180 gcx.resolved_expr(base),
1181 Some(Res::Item(ItemId::Contract(_)) | Res::Namespace(_))
1182 )
1183}