1use super::ProtectedVars;
8use crate::{
9 linter::{LateLintPass, LintContext},
10 sol::{Severity, SolLint, analysis::primitives::branch_always_exits},
11};
12use solar::{
13 ast::{BinOpKind, ContractKind, DataLocation, ElementaryType, FunctionKind, Visibility},
14 interface::sym,
15 sema::{
16 Gcx,
17 hir::{
18 self, ContractId, ExprId, ExprKind, FunctionId, ItemId, NatSpecKind, Res, StmtKind,
19 VariableId,
20 },
21 ty::{Ty, TyAbiPrinter, TyAbiPrinterMode, TyKind},
22 },
23};
24use std::collections::{HashMap, HashSet};
25
26type StorageRoots = HashSet<VariableId>;
27type RootMap = HashMap<VariableId, StorageRoots>;
28
29declare_forge_lint!(
30 PROTECTED_VARS,
31 Severity::High,
32 "protected-vars",
33 "protected variable is written without its required protection"
34);
35
36impl<'hir> LateLintPass<'hir> for ProtectedVars {
37 fn check_nested_contract(
38 &mut self,
39 ctx: &LintContext,
40 gcx: Gcx<'hir>,
41 hir: &'hir hir::Hir<'hir>,
42 contract_id: ContractId,
43 ) {
44 let contract = hir.contract(contract_id);
45 if !matches!(contract.kind, ContractKind::Contract | ContractKind::AbstractContract)
46 || contract.linearization_failed()
47 || !is_most_derived_contract(hir, contract_id)
48 {
49 return;
50 }
51
52 let protected = protected_variables(gcx, hir, contract.linearized_bases);
53 if protected.is_empty() {
54 return;
55 }
56
57 let targets = ProtectionTargets::new(gcx, hir, contract.linearized_bases);
58 for entry_id in effective_entry_points(gcx, hir, contract.linearized_bases) {
59 let mut analyzer = EntryAnalyzer::new(gcx, hir, contract.linearized_bases);
60 let writes = analyzer.analyze(entry_id);
61
62 let mut writes: Vec<_> = writes.into_iter().collect();
63 writes.sort_unstable_by_key(|(variable_id, _)| *variable_id);
64 for (var_id, guards) in writes {
65 let Some(requirements) = protected.get(&var_id) else { continue };
66 for requirement in requirements {
67 let entry = hir.function(entry_id);
68 let span = entry.name.map_or(entry.keyword_span(), |name| name.span);
69 let contract_context = if entry.contract == Some(contract_id) {
70 String::new()
71 } else {
72 format!(" in most-derived contract `{}`", contract.name)
73 };
74 let variable = hir
75 .variable(var_id)
76 .name
77 .map_or_else(|| "<unnamed>".to_string(), |name| name.as_str().to_string());
78 match requirement {
79 ProtectionRequirement::Signature(signature) => {
80 if targets.resolve(signature).is_some_and(|target| guards.contains(&target)) {
81 continue;
82 }
83 ctx.emit_with_msg(
84 &PROTECTED_VARS,
85 span,
86 format!(
87 "protected variable `{variable}` is written without `{signature}`{contract_context}"
88 ),
89 );
90 }
91 ProtectionRequirement::Malformed => ctx.emit_with_msg(
92 &PROTECTED_VARS,
93 span,
94 format!(
95 "protected variable `{variable}` has a malformed write-protection annotation{contract_context}"
96 ),
97 ),
98 }
99 }
100 }
101 }
102 }
103}
104
105fn is_most_derived_contract(hir: &hir::Hir<'_>, contract_id: ContractId) -> bool {
108 !hir.contract_ids().any(|candidate_id| {
109 candidate_id != contract_id
110 && hir
111 .contract(candidate_id)
112 .linearized_bases
113 .get(1..)
114 .is_some_and(|bases| bases.contains(&contract_id))
115 })
116}
117
118fn protected_variables(
119 gcx: Gcx<'_>,
120 hir: &hir::Hir<'_>,
121 bases: &[ContractId],
122) -> HashMap<VariableId, Vec<ProtectionRequirement>> {
123 let mut protected = HashMap::new();
124
125 for &contract_id in bases {
126 for var_id in hir.contract(contract_id).variables() {
127 let var = hir.variable(var_id);
128 if !var.kind.is_state() {
129 continue;
130 }
131
132 let mut requirements = Vec::new();
133 for item in gcx.natspec_doc_comments(var.doc) {
134 let NatSpecKind::Custom { name } = item.kind else { continue };
135 if name.as_str() != "security" {
136 continue;
137 }
138 let content = item.content();
139 let requirement = if let Some(signature) = parse_write_protection(content) {
140 ProtectionRequirement::Signature(signature.to_owned())
141 } else if has_write_protection_token(content) {
142 ProtectionRequirement::Malformed
143 } else {
144 continue;
145 };
146 if !requirements.contains(&requirement) {
147 requirements.push(requirement);
148 }
149 }
150 if !requirements.is_empty() {
151 protected.insert(var_id, requirements);
152 }
153 }
154 }
155
156 protected
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
160enum ProtectionRequirement {
161 Signature(String),
162 Malformed,
163}
164
165fn parse_write_protection(content: &str) -> Option<&str> {
166 let index = write_protection_token(content)?;
167 let value = content[index + "write-protection".len()..].strip_prefix("=\"")?;
168 let (signature, _) = value.split_once('"')?;
169 (!signature.is_empty()).then_some(signature)
170}
171
172fn has_write_protection_token(content: &str) -> bool {
173 write_protection_token(content).is_some()
174}
175
176fn write_protection_token(content: &str) -> Option<usize> {
177 content.match_indices("write-protection").find_map(|(index, token)| {
178 let before = content[..index].chars().next_back();
179 let after = content[index + token.len()..].chars().next();
180 let is_token_character =
181 |character: char| character.is_alphanumeric() || matches!(character, '_' | '-');
182 (before.is_none_or(|character| !is_token_character(character))
183 && after.is_none_or(|character| !is_token_character(character)))
184 .then_some(index)
185 })
186}
187
188struct ProtectionTargets {
189 functions: HashMap<String, FunctionId>,
190 modifiers: HashMap<String, FunctionId>,
191}
192
193impl ProtectionTargets {
194 fn new(gcx: Gcx<'_>, hir: &hir::Hir<'_>, bases: &[ContractId]) -> Self {
195 let mut this = Self { functions: HashMap::new(), modifiers: HashMap::new() };
196
197 for &contract_id in bases {
200 for function_id in hir.contract(contract_id).functions() {
201 let function = hir.function(function_id);
202 if function.name.is_none() {
203 continue;
204 }
205 match function.kind {
206 FunctionKind::Function => {
207 let signature = callable_signature(gcx, hir, function_id);
208 this.functions.entry(signature).or_insert(function_id);
209 }
210 FunctionKind::Modifier => {
211 let signature = callable_signature(gcx, hir, function_id);
212 this.modifiers.entry(signature).or_insert(function_id);
213 }
214 FunctionKind::Constructor | FunctionKind::Fallback | FunctionKind::Receive => {}
215 }
216 }
217 }
218
219 this
220 }
221
222 fn resolve(&self, signature: &str) -> Option<FunctionId> {
223 self.functions.get(signature).or_else(|| self.modifiers.get(signature)).copied()
224 }
225}
226
227fn callable_signature(gcx: Gcx<'_>, hir: &hir::Hir<'_>, function_id: FunctionId) -> String {
228 let function = hir.function(function_id);
229 let mut signature = function.name.unwrap().as_str().to_owned();
230 signature.push('(');
231 for (index, ¶meter) in function.parameters.iter().enumerate() {
232 if index > 0 {
233 signature.push(',');
234 }
235 let ty = gcx.type_of_item(parameter.into());
236 if function.kind == FunctionKind::Modifier {
237 signature.push_str(&source_type_signature(gcx, ty));
238 } else {
239 signature.push_str(&slither_function_parameter(gcx, ty, &mut HashSet::new()));
240 }
241 }
242 signature.push(')');
243 signature
244}
245
246fn source_type_signature<'gcx>(gcx: Gcx<'gcx>, ty: Ty<'gcx>) -> String {
248 ty.display(gcx)
249 .to_string()
250 .replace("contract ", "")
251 .replace("struct ", "")
252 .replace("enum ", "")
253 .replace(" storage", "")
254 .replace(" memory", "")
255 .replace(" calldata", "")
256 .replace(" external", "")
257 .replace(" internal", "")
258 .replace(" pure", "")
259 .replace(" view", "")
260 .replace(" payable", "")
261 .replace("function ", "function")
262 .replace("returns ", "returns")
263}
264
265fn slither_function_parameter<'gcx>(
267 gcx: Gcx<'gcx>,
268 ty: Ty<'gcx>,
269 seen_structs: &mut HashSet<hir::StructId>,
270) -> String {
271 match ty.kind {
272 TyKind::Fn(_) | TyKind::Mapping(..) => source_type_signature(gcx, ty),
273 TyKind::Ref(inner, _) => slither_function_parameter(gcx, inner, seen_structs),
274 TyKind::DynArray(inner) => {
275 format!("{}[]", slither_function_parameter(gcx, inner, seen_structs))
276 }
277 TyKind::Array(inner, length) => {
278 format!("{}[{length}]", slither_function_parameter(gcx, inner, seen_structs))
279 }
280 TyKind::Struct(struct_id) => {
281 if !seen_structs.insert(struct_id) {
282 return source_type_signature(gcx, ty);
283 }
284 let fields = gcx
285 .struct_field_types(struct_id)
286 .iter()
287 .map(|&field| slither_function_parameter(gcx, field, seen_structs))
288 .collect::<Vec<_>>()
289 .join(",");
290 format!("({fields})")
291 }
292 _ => {
293 let mut signature = String::new();
294 TyAbiPrinter::new(gcx, &mut signature, TyAbiPrinterMode::Signature)
295 .print(ty)
296 .expect("writing to a String cannot fail");
297 signature
298 }
299 }
300}
301
302fn effective_entry_points(
303 gcx: Gcx<'_>,
304 hir: &hir::Hir<'_>,
305 bases: &[ContractId],
306) -> Vec<FunctionId> {
307 let mut seen_functions = HashSet::new();
308 let mut seen_fallback = false;
309 let mut seen_receive = false;
310 let mut entries = Vec::new();
311
312 for &contract_id in bases {
313 for function_id in hir.contract(contract_id).all_functions() {
314 let function = hir.function(function_id);
315 match function.kind {
316 FunctionKind::Function => {
317 if !matches!(function.visibility, Visibility::Public | Visibility::External) {
318 continue;
319 }
320 let signature = gcx.item_signature(ItemId::Function(function_id));
321 if seen_functions.insert(signature) {
322 entries.push(function_id);
323 }
324 }
325 FunctionKind::Fallback if !seen_fallback => {
326 seen_fallback = true;
327 entries.push(function_id);
328 }
329 FunctionKind::Receive if !seen_receive => {
330 seen_receive = true;
331 entries.push(function_id);
332 }
333 FunctionKind::Constructor
334 | FunctionKind::Modifier
335 | FunctionKind::Fallback
336 | FunctionKind::Receive => {}
337 }
338 }
339 }
340
341 entries
342}
343
344#[derive(Clone, Default, PartialEq, Eq)]
345struct AliasState {
346 storage: RootMap,
347 slots: RootMap,
348}
349
350#[derive(Clone, Default, PartialEq, Eq)]
351struct FlowState {
352 aliases: AliasState,
353 guards: HashSet<FunctionId>,
354}
355
356#[derive(Clone, Debug, PartialEq, Eq, Hash)]
358struct CallContext {
359 function_id: FunctionId,
360 storage: Vec<(VariableId, Vec<VariableId>)>,
361 slots: Vec<(VariableId, Vec<VariableId>)>,
362 guards: Vec<FunctionId>,
363}
364
365#[derive(Clone, Default)]
366struct LoopFlow {
367 breaks: Option<FlowState>,
368 continues: Option<FlowState>,
369 completes: bool,
370}
371
372#[derive(Clone, PartialEq, Eq)]
373struct CallSummary {
374 returns: Vec<StorageRoots>,
375 guards: HashSet<FunctionId>,
376 completes: bool,
377}
378
379struct FunctionSummary {
380 returns: Vec<StorageRoots>,
381 completes: bool,
382}
383
384#[derive(Clone, Copy)]
385struct ModifierContinuation<'hir> {
386 modifiers: &'hir [hir::Modifier<'hir>],
387 next: usize,
388 body: hir::Block<'hir>,
389}
390
391impl CallContext {
392 fn new(
393 function_id: FunctionId,
394 function: &hir::Function<'_>,
395 aliases: &AliasState,
396 guards: &HashSet<FunctionId>,
397 ) -> Self {
398 let roots = |aliases: &RootMap| {
399 function
400 .parameters
401 .iter()
402 .filter_map(|¶meter| {
403 let mut roots: Vec<_> = aliases.get(¶meter)?.iter().copied().collect();
404 roots.sort_unstable();
405 Some((parameter, roots))
406 })
407 .collect()
408 };
409 let mut guards: Vec<_> = guards.iter().copied().collect();
410 guards.sort_unstable();
411 Self { function_id, storage: roots(&aliases.storage), slots: roots(&aliases.slots), guards }
412 }
413}
414
415struct EntryAnalyzer<'hir> {
416 gcx: Gcx<'hir>,
417 hir: &'hir hir::Hir<'hir>,
418 bases: &'hir [ContractId],
419 writes: HashMap<VariableId, HashSet<FunctionId>>,
420 aliases: AliasState,
421 guards: HashSet<FunctionId>,
422 call_returns: HashMap<ExprId, Vec<StorageRoots>>,
423 call_summaries: HashMap<CallContext, CallSummary>,
424 seen_calls: HashSet<CallContext>,
425 evaluated_calls: HashSet<CallContext>,
426 stack: Vec<FunctionId>,
427 return_stack: Vec<Vec<StorageRoots>>,
428 return_flow: Vec<Option<FlowState>>,
429 loop_flow: Vec<LoopFlow>,
430 modifier_continuations: Vec<ModifierContinuation<'hir>>,
431 assembly_depth: usize,
432}
433
434impl<'hir> EntryAnalyzer<'hir> {
435 fn new(gcx: Gcx<'hir>, hir: &'hir hir::Hir<'hir>, bases: &'hir [ContractId]) -> Self {
436 Self {
437 gcx,
438 hir,
439 bases,
440 writes: HashMap::new(),
441 aliases: AliasState::default(),
442 guards: HashSet::new(),
443 call_returns: HashMap::new(),
444 call_summaries: HashMap::new(),
445 seen_calls: HashSet::new(),
446 evaluated_calls: HashSet::new(),
447 stack: Vec::new(),
448 return_stack: Vec::new(),
449 return_flow: Vec::new(),
450 loop_flow: Vec::new(),
451 modifier_continuations: Vec::new(),
452 assembly_depth: 0,
453 }
454 }
455
456 fn analyze(&mut self, entry_id: FunctionId) -> HashMap<VariableId, HashSet<FunctionId>> {
457 let mut previous_writes = HashMap::new();
458 loop {
459 self.reset_analysis_pass();
460 let previous_summaries = self.call_summaries.clone();
461 let _ = self.analyze_function(entry_id);
462 if self.writes == previous_writes && self.call_summaries == previous_summaries {
463 return std::mem::take(&mut self.writes);
464 }
465 previous_writes = self.writes.clone();
466 }
467 }
468
469 fn reset_analysis_pass(&mut self) {
470 self.writes.clear();
471 self.aliases = AliasState::default();
472 self.guards.clear();
473 self.call_returns.clear();
474 self.seen_calls.clear();
475 self.evaluated_calls.clear();
476 self.stack.clear();
477 self.return_stack.clear();
478 self.return_flow.clear();
479 self.loop_flow.clear();
480 self.modifier_continuations.clear();
481 self.assembly_depth = 0;
482 }
483
484 fn analyze_function(&mut self, function_id: FunctionId) -> FunctionSummary {
485 let function = self.hir.function(function_id);
486 let Some(body) = function.body else {
487 return FunctionSummary {
488 returns: function.returns.iter().map(|_| StorageRoots::new()).collect(),
489 completes: true,
490 };
491 };
492 self.stack.push(function_id);
493 self.return_stack.push(function.returns.iter().map(|_| StorageRoots::new()).collect());
494 self.return_flow.push(None);
495 let completes = self.analyze_modifier_chain(function.modifiers, 0, body);
496 let falls_through = completes && !body.stmts.iter().any(branch_always_exits);
497 if falls_through {
498 self.capture_named_returns();
499 }
500 let completes = completes || self.return_flow.last().is_some_and(Option::is_some);
501 let returns = self.return_stack.pop().expect("return frame must exist");
502 self.return_flow.pop().expect("return flow frame must exist");
503 self.stack.pop();
504 FunctionSummary { returns, completes }
505 }
506
507 fn analyze_modifier_chain(
508 &mut self,
509 modifiers: &'hir [hir::Modifier<'hir>],
510 index: usize,
511 body: hir::Block<'hir>,
512 ) -> bool {
513 let Some(modifier) = modifiers.get(index) else {
514 let previous_returns = self.return_flow.last_mut().and_then(Option::take);
515 let falls_through = self.analyze_block(body);
516 let body_returns = self.return_flow.last_mut().and_then(Option::take);
517
518 *self.return_flow.last_mut().expect("return flow frame must exist") = previous_returns;
524
525 let mut completions = body_returns;
526 if falls_through {
527 merge_flow_state_into(&mut completions, &self.flow_state());
528 }
529 if let Some(state) = completions {
530 self.set_flow_state(state);
531 return true;
532 }
533 return false;
534 };
535 for argument in modifier.args.exprs() {
536 if !self.analyze_expr(argument) {
537 return false;
538 }
539 }
540
541 let Some(declared_id) = modifier.id.as_function() else { return false };
542 let modifier_id = self.dispatch_function(declared_id);
543 self.guards.insert(modifier_id);
544 let arguments = self.ordered_call_arguments(declared_id, modifier.args, None);
545 let source_aliases = self.aliases.clone();
546 self.bind_call_arguments(modifier_id, &arguments, &source_aliases);
547
548 let Some(modifier_body) = self.hir.function(modifier_id).body else { return false };
549 self.modifier_continuations.push(ModifierContinuation { modifiers, next: index + 1, body });
550 let completes = self.analyze_block(modifier_body);
551 self.modifier_continuations.pop();
552 completes
553 }
554
555 fn analyze_call(
556 &mut self,
557 function_id: FunctionId,
558 arguments: &[&'hir hir::Expr<'hir>],
559 ) -> CallSummary {
560 let function = self.hir.function(function_id);
561 let saved_aliases = std::mem::take(&mut self.aliases);
562 self.bind_call_arguments(function_id, arguments, &saved_aliases);
563
564 let context = CallContext::new(function_id, function, &self.aliases, &self.guards);
565 if self.seen_calls.contains(&context) {
566 let summary = self.call_summaries.get(&context).cloned().unwrap_or_else(|| {
567 CallSummary { returns: Vec::new(), guards: self.guards.clone(), completes: false }
568 });
569 self.aliases = saved_aliases;
570 self.guards = summary.guards.clone();
571 return summary;
572 }
573 if self.evaluated_calls.contains(&context)
574 && let Some(summary) = self.call_summaries.get(&context).cloned()
575 {
576 self.aliases = saved_aliases;
577 self.guards = summary.guards.clone();
578 return summary;
579 }
580
581 self.seen_calls.insert(context.clone());
582 self.evaluated_calls.insert(context.clone());
583 let function_summary = self.analyze_function(function_id);
584 let summary = CallSummary {
585 returns: function_summary.returns,
586 guards: self.guards.clone(),
587 completes: function_summary.completes,
588 };
589 self.seen_calls.remove(&context);
590 self.call_summaries.insert(context, summary.clone());
591 self.aliases = saved_aliases;
592 summary
593 }
594
595 fn bind_call_arguments(
596 &mut self,
597 function_id: FunctionId,
598 arguments: &[&'hir hir::Expr<'hir>],
599 source_aliases: &AliasState,
600 ) {
601 let function = self.hir.function(function_id);
602 for (parameter, &argument) in function.parameters.iter().copied().zip(arguments) {
603 if self.hir.variable(parameter).data_location == Some(DataLocation::Storage) {
604 let roots =
605 state_lhs_vars(self.hir, argument, &source_aliases.storage, &self.call_returns);
606 if roots.is_empty() {
607 self.aliases.storage.remove(¶meter);
608 } else {
609 self.aliases.storage.insert(parameter, roots);
610 }
611 }
612 if function.is_yul {
613 let roots = slot_roots(
614 self.hir,
615 argument,
616 &source_aliases.storage,
617 &source_aliases.slots,
618 &self.call_returns,
619 );
620 if roots.is_empty() {
621 self.aliases.slots.remove(¶meter);
622 } else {
623 self.aliases.slots.insert(parameter, roots);
624 }
625 }
626 }
627 }
628
629 fn analyze_block(&mut self, block: hir::Block<'hir>) -> bool {
630 for statement in block.stmts {
631 if !self.analyze_stmt(statement) {
632 return false;
633 }
634 }
635 true
636 }
637
638 fn analyze_stmt(&mut self, statement: &'hir hir::Stmt<'hir>) -> bool {
639 match statement.kind {
640 StmtKind::DeclSingle(variable_id) => {
641 let variable = self.hir.variable(variable_id);
642 if let Some(initializer) = variable.initializer {
643 if !self.analyze_expr(initializer) {
644 return false;
645 }
646 self.set_storage_alias(variable_id, initializer);
647 if self.assembly_depth > 0 {
648 self.set_slot_alias(variable_id, initializer);
649 }
650 }
651 true
652 }
653 StmtKind::DeclMulti(variables, expression) => {
654 if !self.analyze_expr(expression) {
655 return false;
656 }
657 self.set_decl_aliases(variables, expression);
658 true
659 }
660 StmtKind::Emit(expression) | StmtKind::Expr(expression) => {
661 self.analyze_expr(expression) && !branch_always_exits(statement)
662 }
663 StmtKind::Revert(expression) => {
664 let _ = self.analyze_expr(expression);
665 false
666 }
667 StmtKind::Return(Some(expression)) => {
668 if self.analyze_expr(expression) {
669 self.set_return_aliases(expression);
670 self.capture_return_flow();
671 }
672 false
673 }
674 StmtKind::Return(None) => {
675 self.capture_named_returns();
676 self.capture_return_flow();
677 false
678 }
679 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => self.analyze_block(block),
680 StmtKind::AssemblyBlock(block) => {
681 self.assembly_depth += 1;
682 let continues = self.analyze_block(block);
683 self.assembly_depth -= 1;
684 continues
685 }
686 StmtKind::Loop(block, source) => {
687 let mut head = self.flow_state();
688 let mut exits = None;
689 loop {
690 self.set_flow_state(head.clone());
691 let flow = self.analyze_loop_iteration(block);
692 let normal = flow.completes.then(|| self.flow_state());
693 let continue_flow = if source == hir::LoopSource::DoWhile {
694 flow.continues.as_ref().and_then(|continues| {
695 self.analyze_do_while_continue(block, continues.clone())
696 })
697 } else {
698 None
699 };
700 if let Some(breaks) = &flow.breaks {
701 merge_flow_state_into(&mut exits, breaks);
702 }
703 if let Some((continue_flow, _)) = &continue_flow
704 && let Some(breaks) = &continue_flow.breaks
705 {
706 merge_flow_state_into(&mut exits, breaks);
707 }
708
709 let mut backedges = None;
710 if let Some(normal) = &normal {
711 merge_flow_state_into(&mut backedges, normal);
712 }
713 if let Some((continue_flow, completion)) = &continue_flow {
714 if let Some(completion) = completion {
715 merge_flow_state_into(&mut backedges, completion);
716 }
717 if let Some(continues) = &continue_flow.continues {
718 merge_flow_state_into(&mut backedges, continues);
719 }
720 } else if let Some(continues) = &flow.continues {
721 merge_flow_state_into(&mut backedges, continues);
722 }
723 let Some(backedges) = backedges else { break };
724 let next = merge_flow_states(&head, &backedges);
725 if next == head {
726 break;
727 }
728 head = next;
729 }
730 if let Some(exits) = exits {
731 self.set_flow_state(exits);
732 true
733 } else {
734 false
735 }
736 }
737 StmtKind::If(condition, then_statement, else_statement) => {
738 if !self.analyze_expr(condition) {
739 return false;
740 }
741 let before = self.flow_state();
742 let then_continues = self.analyze_stmt(then_statement);
743 let then_state = self.flow_state();
744 self.set_flow_state(before);
745 let else_continues = if let Some(else_statement) = else_statement {
746 self.analyze_stmt(else_statement)
747 } else {
748 true
749 };
750 let else_state = self.flow_state();
751 let merged = match (then_continues, else_continues) {
752 (false, true) => else_state,
753 (true, false) => then_state,
754 _ => merge_flow_states(&then_state, &else_state),
755 };
756 self.set_flow_state(merged);
757 then_continues || else_continues
758 }
759 StmtKind::Try(try_statement) => {
760 if !self.analyze_expr(&try_statement.expr) {
761 return false;
762 }
763 let before = self.flow_state();
764 let mut merged = None;
765 for clause in try_statement.clauses {
766 self.set_flow_state(before.clone());
767 if self.analyze_block(clause.block) {
768 merge_flow_state_into(&mut merged, &self.flow_state());
769 }
770 }
771 if let Some(merged) = merged {
772 self.set_flow_state(merged);
773 true
774 } else {
775 false
776 }
777 }
778 StmtKind::Switch(switch) => {
779 if !self.analyze_expr(switch.selector) {
780 return false;
781 }
782 let before = self.flow_state();
783 let has_default = switch.cases.last().is_some_and(|case| case.constant.is_none());
784 let mut merged = (!has_default).then_some(before.clone());
785 for case in switch.cases {
786 self.set_flow_state(before.clone());
787 if self.analyze_block(case.body) {
788 merge_flow_state_into(&mut merged, &self.flow_state());
789 }
790 }
791 if let Some(merged) = merged {
792 self.set_flow_state(merged);
793 true
794 } else {
795 false
796 }
797 }
798 StmtKind::Break => {
799 let state = self.flow_state();
800 if let Some(flow) = self.loop_flow.last_mut() {
801 merge_flow_state_into(&mut flow.breaks, &state);
802 }
803 false
804 }
805 StmtKind::Continue => {
806 let state = self.flow_state();
807 if let Some(flow) = self.loop_flow.last_mut() {
808 merge_flow_state_into(&mut flow.continues, &state);
809 }
810 false
811 }
812 StmtKind::Placeholder => {
813 if let Some(continuation) = self.modifier_continuations.last().copied() {
814 self.analyze_modifier_chain(
815 continuation.modifiers,
816 continuation.next,
817 continuation.body,
818 )
819 } else {
820 true
821 }
822 }
823 StmtKind::Err(_) => true,
824 }
825 }
826
827 fn analyze_loop_iteration(&mut self, block: hir::Block<'hir>) -> LoopFlow {
828 self.loop_flow.push(LoopFlow::default());
829 let completes = self.analyze_block(block);
830 let mut flow = self.loop_flow.pop().expect("loop flow frame must exist");
831 flow.completes = completes;
832 flow
833 }
834
835 fn analyze_do_while_continue(
836 &mut self,
837 block: hir::Block<'hir>,
838 state: FlowState,
839 ) -> Option<(LoopFlow, Option<FlowState>)> {
840 let epilogue = block.stmts.last().filter(|stmt| is_loop_termination_if(stmt))?;
841 self.set_flow_state(state);
842 self.loop_flow.push(LoopFlow::default());
843 let completes = self.analyze_stmt(epilogue);
844 let completion = completes.then(|| self.flow_state());
845 let mut flow = self.loop_flow.pop().expect("loop flow frame must exist");
846 flow.completes = completes;
847 Some((flow, completion))
848 }
849
850 fn flow_state(&self) -> FlowState {
851 FlowState { aliases: self.aliases.clone(), guards: self.guards.clone() }
852 }
853
854 fn set_flow_state(&mut self, state: FlowState) {
855 self.aliases = state.aliases;
856 self.guards = state.guards;
857 }
858
859 fn capture_return_flow(&mut self) {
860 let state = self.flow_state();
861 if let Some(exits) = self.return_flow.last_mut() {
862 merge_flow_state_into(exits, &state);
863 }
864 }
865
866 fn analyze_expr(&mut self, expression: &'hir hir::Expr<'hir>) -> bool {
867 match &expression.peel_parens().kind {
868 ExprKind::Assign(lhs, operator, rhs) => {
869 if !self.analyze_expr(rhs) {
870 return false;
871 }
872 if !self.analyze_lhs(lhs) {
873 return false;
874 }
875 self.apply_assignment(lhs, rhs, operator.is_some());
876 true
877 }
878 ExprKind::Delete(inner) => {
879 if !self.analyze_lhs(inner) {
880 return false;
881 }
882 self.record_write(inner);
883 true
884 }
885 ExprKind::Unary(operator, inner) => {
886 if !self.analyze_expr(inner) {
887 return false;
888 }
889 if operator.kind.has_side_effects() {
890 self.record_write(inner);
891 }
892 true
893 }
894 ExprKind::Call(callee, args, options) => {
895 if !self.analyze_expr(callee) {
896 return false;
897 }
898 if let Some(options) = options {
899 for option in options.args {
900 if !self.analyze_expr(&option.value) {
901 return false;
902 }
903 }
904 }
905 for argument in args.exprs() {
906 if !self.analyze_expr(argument) {
907 return false;
908 }
909 }
910
911 if let ExprKind::Member(base, member) = &callee.peel_parens().kind
912 && matches!(member.as_str(), "push" | "pop")
913 && is_dynamic_array_or_bytes(self.gcx, base)
914 {
915 self.record_write(base);
916 if member.as_str() == "push" && args.is_empty() {
917 let roots = self.storage_roots(base);
918 self.store_call_returns(expression.id, vec![roots]);
919 }
920 }
921
922 if is_persistent_storage_write_builtin(callee)
923 && let Some(slot) = args.exprs().next()
924 {
925 let roots = slot_roots(
926 self.hir,
927 slot,
928 &self.aliases.storage,
929 &self.aliases.slots,
930 &self.call_returns,
931 );
932 self.record_roots(roots);
933 }
934
935 if let Some((declared_id, function_id, receiver)) =
936 self.resolved_internal_call(callee)
937 {
938 self.guards.insert(function_id);
939 let arguments = self.ordered_call_arguments(declared_id, *args, receiver);
940 let summary = self.analyze_call(function_id, &arguments);
941 self.store_call_returns(expression.id, summary.returns);
942 return summary.completes;
943 }
944 true
945 }
946 ExprKind::Binary(lhs, operator, rhs) => {
947 if !self.analyze_expr(lhs) {
948 return false;
949 }
950 if matches!(operator.kind, BinOpKind::And | BinOpKind::Or) {
951 let short_circuit = self.flow_state();
952 if self.analyze_expr(rhs) {
953 let evaluated = self.flow_state();
954 self.set_flow_state(merge_flow_states(&short_circuit, &evaluated));
955 } else {
956 self.set_flow_state(short_circuit);
957 }
958 true
959 } else {
960 self.analyze_expr(rhs)
961 }
962 }
963 ExprKind::Index(base, index) => {
964 if !self.analyze_expr(base) {
965 return false;
966 }
967 if let Some(index) = index { self.analyze_expr(index) } else { true }
968 }
969 ExprKind::Slice(base, start, end) => {
970 if !self.analyze_expr(base) {
971 return false;
972 }
973 if let Some(start) = start
974 && !self.analyze_expr(start)
975 {
976 return false;
977 }
978 if let Some(end) = end { self.analyze_expr(end) } else { true }
979 }
980 ExprKind::Member(base, _) | ExprKind::YulMember(base, _) | ExprKind::Payable(base) => {
981 self.analyze_expr(base)
982 }
983 ExprKind::Ternary(condition, if_true, if_false) => {
984 if !self.analyze_expr(condition) {
985 return false;
986 }
987 let before = self.flow_state();
988 let true_completes = self.analyze_expr(if_true);
989 let true_state = self.flow_state();
990 self.set_flow_state(before);
991 let false_completes = self.analyze_expr(if_false);
992 let false_state = self.flow_state();
993 match (true_completes, false_completes) {
994 (true, true) => {
995 self.set_flow_state(merge_flow_states(&true_state, &false_state))
996 }
997 (true, false) => self.set_flow_state(true_state),
998 (false, true) => self.set_flow_state(false_state),
999 (false, false) => {}
1000 }
1001 true_completes || false_completes
1002 }
1003 ExprKind::Array(expressions) => {
1004 for expression in *expressions {
1005 if !self.analyze_expr(expression) {
1006 return false;
1007 }
1008 }
1009 true
1010 }
1011 ExprKind::Tuple(expressions) => {
1012 for expression in expressions.iter().copied().flatten() {
1013 if !self.analyze_expr(expression) {
1014 return false;
1015 }
1016 }
1017 true
1018 }
1019 ExprKind::New(_) | ExprKind::TypeCall(_) | ExprKind::Type(_) => true,
1020 ExprKind::Ident(_) | ExprKind::Lit(_) | ExprKind::Err(_) => true,
1021 }
1022 }
1023
1024 fn analyze_lhs(&mut self, expression: &'hir hir::Expr<'hir>) -> bool {
1025 match &expression.peel_parens().kind {
1026 ExprKind::Index(base, index) => {
1027 if !self.analyze_lhs(base) {
1028 return false;
1029 }
1030 if let Some(index) = index { self.analyze_expr(index) } else { true }
1031 }
1032 ExprKind::Slice(base, start, end) => {
1033 if !self.analyze_lhs(base) {
1034 return false;
1035 }
1036 if let Some(start) = start
1037 && !self.analyze_expr(start)
1038 {
1039 return false;
1040 }
1041 if let Some(end) = end { self.analyze_expr(end) } else { true }
1042 }
1043 ExprKind::Member(base, _) | ExprKind::YulMember(base, _) | ExprKind::Payable(base) => {
1044 self.analyze_lhs(base)
1045 }
1046 ExprKind::Tuple(expressions) => {
1047 for expression in expressions.iter().copied().flatten() {
1048 if !self.analyze_lhs(expression) {
1049 return false;
1050 }
1051 }
1052 true
1053 }
1054 ExprKind::Call(..) => self.analyze_expr(expression),
1055 _ => true,
1056 }
1057 }
1058
1059 fn record_write(&mut self, expression: &hir::Expr<'_>) {
1060 self.record_roots(self.storage_roots(expression));
1061 }
1062
1063 fn record_roots(&mut self, roots: StorageRoots) {
1064 for variable_id in roots {
1065 self.writes
1066 .entry(variable_id)
1067 .and_modify(|guards| guards.retain(|guard| self.guards.contains(guard)))
1068 .or_insert_with(|| self.guards.clone());
1069 }
1070 }
1071
1072 fn storage_roots(&self, expression: &hir::Expr<'_>) -> StorageRoots {
1073 state_lhs_vars(self.hir, expression, &self.aliases.storage, &self.call_returns)
1074 }
1075
1076 fn set_storage_alias(&mut self, variable_id: VariableId, initializer: &'hir hir::Expr<'hir>) {
1077 let variable = self.hir.variable(variable_id);
1078 if variable.kind.is_state() || variable.data_location != Some(DataLocation::Storage) {
1079 self.aliases.storage.remove(&variable_id);
1080 return;
1081 }
1082
1083 let roots = self.storage_roots(initializer);
1084 self.set_storage_alias_roots(variable_id, roots);
1085 }
1086
1087 fn set_storage_alias_roots(&mut self, variable_id: VariableId, roots: StorageRoots) {
1088 let variable = self.hir.variable(variable_id);
1089 if !variable.kind.is_state()
1090 && variable.data_location == Some(DataLocation::Storage)
1091 && !roots.is_empty()
1092 {
1093 self.aliases.storage.insert(variable_id, roots);
1094 } else {
1095 self.aliases.storage.remove(&variable_id);
1096 }
1097 }
1098
1099 fn set_slot_alias(&mut self, variable_id: VariableId, initializer: &'hir hir::Expr<'hir>) {
1100 let roots = slot_roots(
1101 self.hir,
1102 initializer,
1103 &self.aliases.storage,
1104 &self.aliases.slots,
1105 &self.call_returns,
1106 );
1107 self.set_slot_alias_roots(variable_id, roots);
1108 }
1109
1110 fn set_slot_alias_roots(&mut self, variable_id: VariableId, roots: StorageRoots) {
1111 if roots.is_empty() {
1112 self.aliases.slots.remove(&variable_id);
1113 } else {
1114 self.aliases.slots.insert(variable_id, roots);
1115 }
1116 }
1117
1118 fn apply_assignment(
1119 &mut self,
1120 lhs: &'hir hir::Expr<'hir>,
1121 rhs: &'hir hir::Expr<'hir>,
1122 compound: bool,
1123 ) {
1124 if !compound
1125 && let ExprKind::YulMember(base, member) = &lhs.peel_parens().kind
1126 && member.as_str() == "slot"
1127 && let Some(local) = lhs_local_var(self.hir, base)
1128 {
1129 let roots = slot_roots(
1130 self.hir,
1131 rhs,
1132 &self.aliases.storage,
1133 &self.aliases.slots,
1134 &self.call_returns,
1135 );
1136 self.set_storage_alias_roots(local, roots);
1137 return;
1138 }
1139
1140 if !compound && let ExprKind::Tuple(expressions) = &lhs.peel_parens().kind {
1141 let outputs = expressions.len();
1142 for (index, expression) in expressions.iter().copied().enumerate() {
1143 let Some(expression) = expression else { continue };
1144 if let Some(local) = lhs_local_var(self.hir, expression) {
1145 let roots = self.storage_roots_for_output(rhs, index, outputs);
1146 if self.assembly_depth > 0 {
1147 self.set_slot_alias_roots(local, roots.clone());
1148 }
1149 self.set_storage_alias_roots(local, roots);
1150 } else {
1151 self.record_write(expression);
1152 }
1153 }
1154 return;
1155 }
1156
1157 if !compound && let Some(local) = lhs_local_var(self.hir, lhs) {
1158 self.set_storage_alias(local, rhs);
1159 if self.assembly_depth > 0 {
1160 self.set_slot_alias(local, rhs);
1161 }
1162 return;
1163 }
1164
1165 self.record_write(lhs);
1166 }
1167
1168 fn set_decl_aliases(
1169 &mut self,
1170 variables: &[Option<VariableId>],
1171 expression: &'hir hir::Expr<'hir>,
1172 ) {
1173 for (index, variable_id) in variables.iter().copied().enumerate() {
1174 let Some(variable_id) = variable_id else { continue };
1175 let roots = self.storage_roots_for_output(expression, index, variables.len());
1176 if self.assembly_depth > 0 {
1177 self.set_slot_alias_roots(variable_id, roots.clone());
1178 }
1179 self.set_storage_alias_roots(variable_id, roots);
1180 }
1181 }
1182
1183 fn set_return_aliases(&mut self, expression: &'hir hir::Expr<'hir>) {
1184 let Some(&function_id) = self.stack.last() else { return };
1185 let returns = self.hir.function(function_id).returns;
1186 let roots: Vec<_> = returns
1187 .iter()
1188 .enumerate()
1189 .map(|(index, _)| self.storage_roots_for_output(expression, index, returns.len()))
1190 .collect();
1191 let Some(frame) = self.return_stack.last_mut() else { return };
1192 for (returned, roots) in frame.iter_mut().zip(roots) {
1193 returned.extend(roots);
1194 }
1195 }
1196
1197 fn capture_named_returns(&mut self) {
1198 let Some(&function_id) = self.stack.last() else { return };
1199 let function = self.hir.function(function_id);
1200 let aliases = if function.is_yul { &self.aliases.slots } else { &self.aliases.storage };
1201 let roots: Vec<_> = function
1202 .returns
1203 .iter()
1204 .map(|return_id| aliases.get(return_id).cloned().unwrap_or_default())
1205 .collect();
1206 let Some(frame) = self.return_stack.last_mut() else { return };
1207 for (returned, roots) in frame.iter_mut().zip(roots) {
1208 returned.extend(roots);
1209 }
1210 }
1211
1212 fn storage_roots_for_output(
1213 &self,
1214 expression: &hir::Expr<'_>,
1215 index: usize,
1216 outputs: usize,
1217 ) -> StorageRoots {
1218 if let ExprKind::Tuple(expressions) = &expression.peel_parens().kind
1219 && outputs > 1
1220 {
1221 return expressions
1222 .get(index)
1223 .and_then(|expression| *expression)
1224 .map_or_else(StorageRoots::new, |expression| self.storage_roots(expression));
1225 }
1226 if let ExprKind::Call(..) = expression.peel_parens().kind {
1227 return self
1228 .call_returns
1229 .get(&expression.id)
1230 .and_then(|returns| returns.get(index))
1231 .cloned()
1232 .unwrap_or_default();
1233 }
1234 if outputs == 1 && index == 0 {
1235 self.storage_roots(expression)
1236 } else {
1237 StorageRoots::new()
1238 }
1239 }
1240
1241 fn store_call_returns(&mut self, expression_id: ExprId, returns: Vec<StorageRoots>) {
1242 if returns.is_empty() {
1243 return;
1244 }
1245 let stored = self.call_returns.entry(expression_id).or_default();
1246 if stored.len() < returns.len() {
1247 stored.resize_with(returns.len(), StorageRoots::new);
1248 }
1249 for (stored, returned) in stored.iter_mut().zip(returns) {
1250 stored.extend(returned);
1251 }
1252 }
1253
1254 fn ordered_call_arguments(
1255 &self,
1256 declared_id: FunctionId,
1257 arguments: hir::CallArgs<'hir>,
1258 receiver: Option<&'hir hir::Expr<'hir>>,
1259 ) -> Vec<&'hir hir::Expr<'hir>> {
1260 let function = self.hir.function(declared_id);
1261 let parameters = &function.parameters[usize::from(receiver.is_some())..];
1262 let mut ordered = Vec::with_capacity(arguments.len() + usize::from(receiver.is_some()));
1263 ordered.extend(receiver);
1264 match arguments.kind {
1265 hir::CallArgsKind::Unnamed(expressions) => ordered.extend(expressions),
1266 hir::CallArgsKind::Named(named) => {
1267 for ¶meter in parameters {
1268 let Some(parameter_name) = self.hir.variable(parameter).name else { continue };
1269 if let Some(argument) = named.iter().find(|arg| arg.name == parameter_name) {
1270 ordered.push(&argument.value);
1271 }
1272 }
1273 }
1274 }
1275 ordered
1276 }
1277
1278 fn resolved_internal_call(
1279 &self,
1280 callee: &'hir hir::Expr<'hir>,
1281 ) -> Option<(FunctionId, FunctionId, Option<&'hir hir::Expr<'hir>>)> {
1282 let resolved = self.gcx.resolved_callee(callee.id);
1283 let (function_id, attached) = if let Some(resolved) = resolved {
1284 let Res::Item(ItemId::Function(function_id)) = resolved.res else { return None };
1285 (function_id, resolved.attached)
1286 } else {
1287 let ExprKind::Ident(resolutions) = &callee.peel_parens().kind else { return None };
1288 let mut functions = resolutions.iter().filter_map(|resolution| match resolution {
1289 Res::Item(ItemId::Function(function_id)) => Some(*function_id),
1290 _ => None,
1291 });
1292 let function_id = functions.next()?;
1293 if functions.next().is_some() {
1294 return None;
1295 }
1296 (function_id, false)
1297 };
1298
1299 match &callee.peel_parens().kind {
1300 ExprKind::Ident(_) => Some((function_id, self.dispatch_function(function_id), None)),
1301 ExprKind::Member(base, _) if attached => Some((function_id, function_id, Some(base))),
1302 ExprKind::Member(base, _)
1303 if self.is_library_function(function_id) || is_static_internal_base(base) =>
1304 {
1305 Some((function_id, function_id, None))
1306 }
1307 _ => None,
1308 }
1309 }
1310
1311 fn is_library_function(&self, function_id: FunctionId) -> bool {
1312 self.hir
1313 .function(function_id)
1314 .contract
1315 .is_some_and(|contract_id| self.hir.contract(contract_id).kind.is_library())
1316 }
1317
1318 fn dispatch_function(&self, function_id: FunctionId) -> FunctionId {
1319 let function = self.hir.function(function_id);
1320 if !function.virtual_ {
1321 return function_id;
1322 }
1323
1324 let signature = callable_signature(self.gcx, self.hir, function_id);
1325 for &contract_id in self.bases {
1326 for candidate_id in self.hir.contract(contract_id).functions() {
1327 let candidate = self.hir.function(candidate_id);
1328 if candidate.kind == function.kind
1329 && callable_signature(self.gcx, self.hir, candidate_id) == signature
1330 {
1331 return candidate_id;
1332 }
1333 }
1334 }
1335 function_id
1336 }
1337}
1338
1339fn lhs_local_var(hir: &hir::Hir<'_>, expression: &hir::Expr<'_>) -> Option<VariableId> {
1340 let ExprKind::Ident(resolutions) = &expression.peel_parens().kind else { return None };
1341 resolutions.iter().find_map(|resolution| match resolution {
1342 Res::Item(ItemId::Variable(variable_id)) if !hir.variable(*variable_id).kind.is_state() => {
1343 Some(*variable_id)
1344 }
1345 _ => None,
1346 })
1347}
1348
1349fn state_lhs_vars(
1350 hir: &hir::Hir<'_>,
1351 expression: &hir::Expr<'_>,
1352 storage_aliases: &RootMap,
1353 call_returns: &HashMap<ExprId, Vec<StorageRoots>>,
1354) -> StorageRoots {
1355 let mut variables = StorageRoots::new();
1356 collect_state_lhs_vars(hir, expression, storage_aliases, call_returns, &mut variables);
1357 variables
1358}
1359
1360fn collect_state_lhs_vars(
1361 hir: &hir::Hir<'_>,
1362 expression: &hir::Expr<'_>,
1363 storage_aliases: &RootMap,
1364 call_returns: &HashMap<ExprId, Vec<StorageRoots>>,
1365 variables: &mut StorageRoots,
1366) {
1367 match &expression.peel_parens().kind {
1368 ExprKind::Ident(resolutions) => {
1369 for resolution in *resolutions {
1370 let Res::Item(ItemId::Variable(variable_id)) = resolution else { continue };
1371 if hir.variable(*variable_id).kind.is_state() {
1372 variables.insert(*variable_id);
1373 } else if let Some(roots) = storage_aliases.get(variable_id) {
1374 variables.extend(roots);
1375 }
1376 }
1377 }
1378 ExprKind::Index(base, _)
1379 | ExprKind::Slice(base, ..)
1380 | ExprKind::Member(base, _)
1381 | ExprKind::YulMember(base, _) => {
1382 collect_state_lhs_vars(hir, base, storage_aliases, call_returns, variables);
1383 }
1384 ExprKind::Payable(base) | ExprKind::Unary(_, base) | ExprKind::Delete(base) => {
1385 collect_state_lhs_vars(hir, base, storage_aliases, call_returns, variables);
1386 }
1387 ExprKind::Tuple(expressions) => {
1388 for expression in expressions.iter().copied().flatten() {
1389 collect_state_lhs_vars(hir, expression, storage_aliases, call_returns, variables);
1390 }
1391 }
1392 ExprKind::Ternary(_, if_true, if_false) => {
1393 collect_state_lhs_vars(hir, if_true, storage_aliases, call_returns, variables);
1394 collect_state_lhs_vars(hir, if_false, storage_aliases, call_returns, variables);
1395 }
1396 ExprKind::Call(..) => {
1397 if let Some(returns) = call_returns.get(&expression.id) {
1398 for roots in returns {
1399 variables.extend(roots);
1400 }
1401 }
1402 }
1403 _ => {}
1404 }
1405}
1406
1407fn slot_roots(
1408 hir: &hir::Hir<'_>,
1409 expression: &hir::Expr<'_>,
1410 storage_aliases: &RootMap,
1411 slot_aliases: &RootMap,
1412 call_returns: &HashMap<ExprId, Vec<StorageRoots>>,
1413) -> StorageRoots {
1414 let mut variables = StorageRoots::new();
1415 collect_slot_roots(
1416 hir,
1417 expression,
1418 storage_aliases,
1419 slot_aliases,
1420 call_returns,
1421 &mut variables,
1422 );
1423 variables
1424}
1425
1426fn collect_slot_roots(
1427 hir: &hir::Hir<'_>,
1428 expression: &hir::Expr<'_>,
1429 storage_aliases: &RootMap,
1430 slot_aliases: &RootMap,
1431 call_returns: &HashMap<ExprId, Vec<StorageRoots>>,
1432 variables: &mut StorageRoots,
1433) {
1434 if let ExprKind::Call(..) = &expression.peel_parens().kind
1435 && let Some(returns) = call_returns.get(&expression.id)
1436 {
1437 for roots in returns {
1438 variables.extend(roots);
1439 }
1440 return;
1441 }
1442 let mut recurse = |expression| {
1443 collect_slot_roots(hir, expression, storage_aliases, slot_aliases, call_returns, variables)
1444 };
1445 match &expression.peel_parens().kind {
1446 ExprKind::Ident(resolutions) => {
1447 for resolution in *resolutions {
1448 let Res::Item(ItemId::Variable(variable_id)) = resolution else { continue };
1449 if let Some(roots) = slot_aliases.get(variable_id) {
1450 variables.extend(roots);
1451 }
1452 }
1453 }
1454 ExprKind::YulMember(base, member) if member.as_str() == "slot" => {
1455 variables.extend(state_lhs_vars(hir, base, storage_aliases, call_returns));
1456 }
1457 ExprKind::Array(expressions) => {
1458 for expression in *expressions {
1459 recurse(expression);
1460 }
1461 }
1462 ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
1463 recurse(lhs);
1464 recurse(rhs);
1465 }
1466 ExprKind::Call(callee, args, options) => {
1467 recurse(callee);
1468 if let Some(options) = options {
1469 for option in options.args {
1470 recurse(&option.value);
1471 }
1472 }
1473 for argument in args.exprs() {
1474 recurse(argument);
1475 }
1476 }
1477 ExprKind::Index(base, index) => {
1478 recurse(base);
1479 if let Some(index) = index {
1480 recurse(index);
1481 }
1482 }
1483 ExprKind::Slice(base, start, end) => {
1484 recurse(base);
1485 if let Some(start) = start {
1486 recurse(start);
1487 }
1488 if let Some(end) = end {
1489 recurse(end);
1490 }
1491 }
1492 ExprKind::Member(base, _)
1493 | ExprKind::YulMember(base, _)
1494 | ExprKind::Payable(base)
1495 | ExprKind::Unary(_, base)
1496 | ExprKind::Delete(base) => recurse(base),
1497 ExprKind::Ternary(condition, if_true, if_false) => {
1498 recurse(condition);
1499 recurse(if_true);
1500 recurse(if_false);
1501 }
1502 ExprKind::Tuple(expressions) => {
1503 for expression in expressions.iter().copied().flatten() {
1504 recurse(expression);
1505 }
1506 }
1507 ExprKind::New(_)
1508 | ExprKind::TypeCall(_)
1509 | ExprKind::Type(_)
1510 | ExprKind::Lit(_)
1511 | ExprKind::Err(_) => {}
1512 }
1513}
1514
1515fn merge_alias_states(lhs: &AliasState, rhs: &AliasState) -> AliasState {
1516 AliasState {
1517 storage: merge_root_maps(&lhs.storage, &rhs.storage),
1518 slots: merge_root_maps(&lhs.slots, &rhs.slots),
1519 }
1520}
1521
1522fn merge_flow_states(lhs: &FlowState, rhs: &FlowState) -> FlowState {
1523 FlowState {
1524 aliases: merge_alias_states(&lhs.aliases, &rhs.aliases),
1525 guards: lhs.guards.intersection(&rhs.guards).copied().collect(),
1526 }
1527}
1528
1529fn merge_flow_state_into(destination: &mut Option<FlowState>, state: &FlowState) {
1530 *destination = Some(
1531 destination
1532 .as_ref()
1533 .map_or_else(|| state.clone(), |current| merge_flow_states(current, state)),
1534 );
1535}
1536
1537fn is_loop_termination_if(statement: &hir::Stmt<'_>) -> bool {
1538 let StmtKind::If(_, then_statement, else_statement) = &statement.kind else { return false };
1539 is_break_stmt(then_statement)
1540 || else_statement.as_ref().is_some_and(|statement| is_break_stmt(statement))
1541}
1542
1543fn is_break_stmt(statement: &hir::Stmt<'_>) -> bool {
1544 match &statement.kind {
1545 StmtKind::Break => true,
1546 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
1547 block.stmts.len() == 1 && is_break_stmt(&block.stmts[0])
1548 }
1549 _ => false,
1550 }
1551}
1552
1553fn merge_root_maps(lhs: &RootMap, rhs: &RootMap) -> RootMap {
1554 let mut merged = lhs.clone();
1555 for (&variable_id, roots) in rhs {
1556 merged.entry(variable_id).or_default().extend(roots);
1557 }
1558 merged
1559}
1560
1561fn is_persistent_storage_write_builtin(callee: &hir::Expr<'_>) -> bool {
1562 let ExprKind::Ident(resolutions) = &callee.peel_parens().kind else { return false };
1563 resolutions.iter().any(|resolution| {
1564 matches!(resolution, Res::Builtin(builtin) if builtin.name().as_str() == "sstore")
1565 })
1566}
1567
1568fn is_static_internal_base(base: &hir::Expr<'_>) -> bool {
1569 let ExprKind::Ident(resolutions) = &base.peel_parens().kind else { return false };
1570 resolutions.iter().any(|resolution| {
1571 matches!(resolution, Res::Item(ItemId::Contract(_)) | Res::Namespace(_))
1572 || matches!(
1573 resolution,
1574 Res::Builtin(builtin) if builtin.name() == sym::super_
1575 )
1576 })
1577}
1578
1579fn is_dynamic_array_or_bytes(gcx: Gcx<'_>, expression: &hir::Expr<'_>) -> bool {
1580 gcx.type_of_expr(expression.peel_parens().id).is_some_and(|ty| {
1581 matches!(
1582 ty.peel_refs().kind,
1583 TyKind::DynArray(_) | TyKind::Elementary(ElementaryType::Bytes)
1584 )
1585 })
1586}