Skip to main content

forge_lint/sol/high/
protected_vars.rs

1//! Slither-compatible protected-variable control-flow analysis.
2//!
3//! Storage references are tracked as may-alias sets across internal calls and control-flow joins.
4//! Calls are memoized by their storage, slot, and guard context so recursive propagation
5//! terminates.
6
7use 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
105/// Slither analyzes the effective entry points of leaf contracts so inherited declarations are
106/// interpreted in the context in which they are ultimately deployed.
107fn 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        // Linearization starts at the most-derived contract. Keeping the first signature excludes
198        // shadowed base members, matching the effective member set used by Slither.
199        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, &parameter) 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
246/// Formats the source-level types used by Slither modifier signatures.
247fn 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
265/// Formats the Solidity-signature types used by Slither function lookup.
266fn 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/// A finite call-graph key that distinguishes storage aliases without depending on values.
357#[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(|&parameter| {
403                    let mut roots: Vec<_> = aliases.get(&parameter)?.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            // Returns from the function body resume in each enclosing modifier postlude. They
519            // therefore become ordinary placeholder completions here and must not also escape
520            // the whole modifier chain through `return_flow`: a reverting postlude can still
521            // prevent the call from completing. Keep only returns captured in modifier prefixes
522            // outside the body continuation.
523            *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(&parameter);
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(&parameter);
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 &parameter 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}