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