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) => {
767                let (callee, options) = callee.split_call_options();
768                if !(self.analyze_expr(callee)
769                    && options
770                        .iter()
771                        .flat_map(|options| options.args)
772                        .all(|option| self.analyze_expr(&option.value))
773                    && args.exprs().all(|argument| self.analyze_expr(argument)))
774                {
775                    return false;
776                }
777
778                if let ExprKind::Member(base, _) = &callee.peel_parens().kind
779                    && let Some(
780                        builtin @ (Builtin::ArrayPush0 | Builtin::ArrayPush | Builtin::ArrayPop),
781                    ) = self.gcx.resolved_builtin(callee)
782                {
783                    self.record_write(base);
784                    if builtin == Builtin::ArrayPush0 {
785                        let roots = self.storage_roots(base);
786                        self.store_call_returns(expression.id, vec![roots]);
787                    }
788                }
789
790                if self.gcx.resolved_builtin(callee) == Some(Builtin::YulSstore)
791                    && let Some(slot) = args.exprs().next()
792                {
793                    let roots = self.slot_roots(slot);
794                    self.record_roots(roots);
795                }
796
797                if let Some((_, function_id, receiver)) = self.resolved_internal_call(callee) {
798                    self.guards.insert(function_id);
799                    let arguments = receiver
800                        .into_iter()
801                        .chain(
802                            (0..args.len())
803                                .filter_map(|index| self.gcx.call_arg(expression, index)),
804                        )
805                        .collect::<Vec<_>>();
806                    let summary = self.analyze_call(function_id, &arguments);
807                    self.store_call_returns(expression.id, summary.returns);
808                    return summary.completes;
809                }
810                true
811            }
812            ExprKind::CallOptions(callee, options) => {
813                self.analyze_expr(callee)
814                    && options.args.iter().all(|option| self.analyze_expr(&option.value))
815            }
816            ExprKind::Binary(lhs, operator, rhs) => {
817                self.analyze_expr(lhs)
818                    && if matches!(operator.kind, BinOpKind::And | BinOpKind::Or) {
819                        // The right operand may be skipped.
820                        self.analyze_alternatives([Some(*rhs), None], None, |this, rhs| {
821                            rhs.is_none_or(|rhs| this.analyze_expr(rhs))
822                        })
823                    } else {
824                        self.analyze_expr(rhs)
825                    }
826            }
827            ExprKind::Index(base, index) => {
828                self.analyze_expr(base) && index.is_none_or(|index| self.analyze_expr(index))
829            }
830            ExprKind::Slice(base, start, end) => {
831                self.analyze_expr(base)
832                    && start.is_none_or(|start| self.analyze_expr(start))
833                    && end.is_none_or(|end| self.analyze_expr(end))
834            }
835            ExprKind::Member(base, _) | ExprKind::YulMember(base, _) | ExprKind::Payable(base) => {
836                self.analyze_expr(base)
837            }
838            ExprKind::Ternary(condition, if_true, if_false) => {
839                self.analyze_expr(condition)
840                    && self.analyze_alternatives([*if_true, *if_false], None, |this, arm| {
841                        this.analyze_expr(arm)
842                    })
843            }
844            ExprKind::Array(expressions) => {
845                expressions.iter().all(|expression| self.analyze_expr(expression))
846            }
847            ExprKind::Tuple(expressions) => {
848                expressions.iter().flatten().all(|expression| self.analyze_expr(expression))
849            }
850            ExprKind::New(_)
851            | ExprKind::TypeCall(_)
852            | ExprKind::Type(_)
853            | ExprKind::Ident(_)
854            | ExprKind::Lit(_)
855            | ExprKind::Err(_) => true,
856        }
857    }
858
859    fn record_write(&mut self, expression: &hir::Expr<'_>) {
860        self.record_roots(self.storage_roots(expression));
861    }
862
863    fn record_roots(&mut self, roots: StorageRoots) {
864        for variable_id in roots {
865            self.writes
866                .entry(variable_id)
867                .and_modify(|guards| guards.retain(|guard| self.guards.contains(guard)))
868                .or_insert_with(|| self.guards.clone());
869        }
870    }
871
872    fn set_storage_alias(&mut self, variable_id: VariableId, roots: StorageRoots) {
873        let variable = self.gcx.hir.variable(variable_id);
874        if !variable.kind.is_state()
875            && variable.data_location == Some(DataLocation::Storage)
876            && !roots.is_empty()
877        {
878            self.aliases.storage.insert(variable_id, roots);
879        } else {
880            self.aliases.storage.remove(&variable_id);
881        }
882    }
883
884    fn set_slot_alias(&mut self, variable_id: VariableId, roots: StorageRoots) {
885        if roots.is_empty() {
886            self.aliases.slots.remove(&variable_id);
887        } else {
888            self.aliases.slots.insert(variable_id, roots);
889        }
890    }
891
892    /// Storage pointer locals alias the roots they are bound to; inside assembly, Yul locals also
893    /// track the slots they hold.
894    fn alias_local(&mut self, variable_id: VariableId, roots: StorageRoots) {
895        if self.assembly_depth > 0 {
896            self.set_slot_alias(variable_id, roots.clone());
897        }
898        self.set_storage_alias(variable_id, roots);
899    }
900
901    fn alias_local_from_expr(&mut self, variable_id: VariableId, expression: &hir::Expr<'_>) {
902        if self.assembly_depth > 0 {
903            let roots = self.slot_roots(expression);
904            self.set_slot_alias(variable_id, roots);
905        }
906        let roots = self.storage_roots(expression);
907        self.set_storage_alias(variable_id, roots);
908    }
909
910    fn apply_assignment(
911        &mut self,
912        lhs: &'gcx hir::Expr<'gcx>,
913        rhs: &'gcx hir::Expr<'gcx>,
914        compound: bool,
915    ) {
916        let lhs = lhs.peel_parens();
917        if compound {
918            return self.record_write(lhs);
919        }
920        match &lhs.kind {
921            // `pointer.slot := x` retargets a storage pointer.
922            ExprKind::YulMember(base, member)
923                if member.as_str() == "slot"
924                    && let Some(local) = lhs_local_var(self.gcx, base) =>
925            {
926                let roots = self.slot_roots(rhs);
927                self.set_storage_alias(local, roots);
928            }
929            ExprKind::Tuple(expressions) => {
930                for (index, expression) in expressions.iter().enumerate() {
931                    let Some(expression) = expression else { continue };
932                    if let Some(local) = lhs_local_var(self.gcx, expression) {
933                        let roots = self.storage_roots_for_output(rhs, index, expressions.len());
934                        self.alias_local(local, roots);
935                    } else {
936                        self.record_write(expression);
937                    }
938                }
939            }
940            _ => match lhs_local_var(self.gcx, lhs) {
941                Some(local) => self.alias_local_from_expr(local, rhs),
942                None => self.record_write(lhs),
943            },
944        }
945    }
946
947    fn set_return_aliases(&mut self, expression: &'gcx hir::Expr<'gcx>) {
948        let Some(&function_id) = self.stack.last() else { return };
949        let outputs = self.gcx.hir.function(function_id).returns.len();
950        let roots: Vec<_> = (0..outputs)
951            .map(|index| self.storage_roots_for_output(expression, index, outputs))
952            .collect();
953        self.extend_returns(roots);
954    }
955
956    fn capture_named_returns(&mut self) {
957        let Some(&function_id) = self.stack.last() else { return };
958        let function = self.gcx.hir.function(function_id);
959        let aliases = if function.is_yul { &self.aliases.slots } else { &self.aliases.storage };
960        let roots: Vec<_> = function
961            .returns
962            .iter()
963            .map(|return_id| aliases.get(return_id).cloned().unwrap_or_default())
964            .collect();
965        self.extend_returns(roots);
966    }
967
968    fn extend_returns(&mut self, roots: Vec<StorageRoots>) {
969        if let Some(frame) = self.return_stack.last_mut() {
970            for (returned, roots) in frame.iter_mut().zip(roots) {
971                returned.extend(roots);
972            }
973        }
974    }
975
976    /// Storage roots flowing into output `index` of `outputs` from `expression`.
977    fn storage_roots_for_output(
978        &self,
979        expression: &hir::Expr<'_>,
980        index: usize,
981        outputs: usize,
982    ) -> StorageRoots {
983        match &expression.peel_parens().kind {
984            ExprKind::Tuple(expressions) if outputs > 1 => expressions
985                .get(index)
986                .copied()
987                .flatten()
988                .map_or_else(StorageRoots::new, |expression| self.storage_roots(expression)),
989            ExprKind::Call(..) => self
990                .call_returns
991                .get(&expression.id)
992                .and_then(|returns| returns.get(index))
993                .cloned()
994                .unwrap_or_default(),
995            _ if outputs == 1 && index == 0 => self.storage_roots(expression),
996            _ => StorageRoots::new(),
997        }
998    }
999
1000    fn store_call_returns(&mut self, expression_id: ExprId, returns: Vec<StorageRoots>) {
1001        if returns.is_empty() {
1002            return;
1003        }
1004        let stored = self.call_returns.entry(expression_id).or_default();
1005        if stored.len() < returns.len() {
1006            stored.resize_with(returns.len(), StorageRoots::new);
1007        }
1008        for (stored, returned) in stored.iter_mut().zip(returns) {
1009            stored.extend(returned);
1010        }
1011    }
1012
1013    /// Call arguments in declaration order of `declared_id`'s parameters, the attached receiver
1014    /// first.
1015    fn ordered_call_arguments(
1016        &self,
1017        declared_id: FunctionId,
1018        arguments: CallArgs<'gcx>,
1019        receiver: Option<&'gcx hir::Expr<'gcx>>,
1020    ) -> Vec<&'gcx hir::Expr<'gcx>> {
1021        let names = self.gcx.callable_param_names(CallableParamSource::Function {
1022            id: declared_id,
1023            skips_receiver: receiver.is_some(),
1024        });
1025        let arguments = (0..names.len())
1026            .filter_map(|index| arguments.argument_for_parameter(index, Some(&names)));
1027        receiver.into_iter().chain(arguments).collect()
1028    }
1029
1030    /// `(declared, dispatched, attached receiver)` for a call that executes contract code in this
1031    /// storage context: bare identifiers, `using for` attached calls, library calls and
1032    /// `super.`/`Base.`/`Lib.` qualified calls.
1033    fn resolved_internal_call(
1034        &self,
1035        callee: &'gcx hir::Expr<'gcx>,
1036    ) -> Option<(FunctionId, FunctionId, Option<&'gcx hir::Expr<'gcx>>)> {
1037        let resolved = self.gcx.resolved_callee(callee.peel_parens().id)?;
1038        let function_id = resolved.res.as_function()?;
1039        let attached = resolved.attached;
1040        match &callee.peel_parens().kind {
1041            ExprKind::Ident(_) => Some((function_id, self.dispatch_function(function_id), None)),
1042            ExprKind::Member(base, _) if attached => Some((function_id, function_id, Some(base))),
1043            ExprKind::Member(base, _)
1044                if self.is_library_function(function_id)
1045                    || is_static_internal_base(self.gcx, base) =>
1046            {
1047                Some((function_id, function_id, None))
1048            }
1049            _ => None,
1050        }
1051    }
1052
1053    fn is_library_function(&self, function_id: FunctionId) -> bool {
1054        self.gcx
1055            .hir
1056            .function(function_id)
1057            .contract
1058            .is_some_and(|contract_id| self.gcx.hir.contract(contract_id).kind.is_library())
1059    }
1060
1061    /// The most-derived override of a virtual function or modifier in the analyzed hierarchy.
1062    fn dispatch_function(&self, function_id: FunctionId) -> FunctionId {
1063        self.gcx.resolve_virtual_function(self.bases[0], function_id)
1064    }
1065
1066    /// State variables an lvalue may write: state roots, aliased storage pointers and
1067    /// storage-returning calls.
1068    fn storage_roots(&self, expression: &hir::Expr<'_>) -> StorageRoots {
1069        let mut roots = StorageRoots::new();
1070        self.collect_storage_roots(expression, &mut roots);
1071        roots
1072    }
1073
1074    fn collect_storage_roots(&self, expression: &hir::Expr<'_>, roots: &mut StorageRoots) {
1075        let expression = expression.peel_parens();
1076        match &expression.kind {
1077            ExprKind::Ident(_) => {
1078                if let Some(variable_id) = self.gcx.resolved_variable(expression) {
1079                    if self.gcx.hir.variable(variable_id).kind.is_state() {
1080                        roots.insert(variable_id);
1081                    } else if let Some(aliases) = self.aliases.storage.get(&variable_id) {
1082                        roots.extend(aliases);
1083                    }
1084                }
1085            }
1086            ExprKind::Index(base, _)
1087            | ExprKind::Slice(base, ..)
1088            | ExprKind::Member(base, _)
1089            | ExprKind::YulMember(base, _)
1090            | ExprKind::Payable(base)
1091            | ExprKind::Unary(_, base)
1092            | ExprKind::Delete(base) => self.collect_storage_roots(base, roots),
1093            ExprKind::Tuple(expressions) => {
1094                for expression in expressions.iter().flatten() {
1095                    self.collect_storage_roots(expression, roots);
1096                }
1097            }
1098            ExprKind::Ternary(_, if_true, if_false) => {
1099                self.collect_storage_roots(if_true, roots);
1100                self.collect_storage_roots(if_false, roots);
1101            }
1102            ExprKind::Call(..) => {
1103                roots.extend(self.call_returns.get(&expression.id).into_iter().flatten().flatten());
1104            }
1105            _ => {}
1106        }
1107    }
1108
1109    /// State variables whose slot a Yul expression may evaluate to.
1110    fn slot_roots(&self, expression: &hir::Expr<'_>) -> StorageRoots {
1111        let mut roots = StorageRoots::new();
1112        self.collect_slot_roots(expression, &mut roots);
1113        roots
1114    }
1115
1116    fn collect_slot_roots(&self, expression: &hir::Expr<'_>, roots: &mut StorageRoots) {
1117        let expression = expression.peel_parens();
1118        match &expression.kind {
1119            ExprKind::Ident(_) => {
1120                if let Some(variable_id) = self.gcx.resolved_variable(expression)
1121                    && let Some(aliases) = self.aliases.slots.get(&variable_id)
1122                {
1123                    roots.extend(aliases);
1124                }
1125            }
1126            ExprKind::YulMember(base, member) if member.as_str() == "slot" => {
1127                self.collect_storage_roots(base, roots);
1128            }
1129            ExprKind::Call(..) if self.call_returns.contains_key(&expression.id) => {
1130                roots.extend(self.call_returns[&expression.id].iter().flatten());
1131            }
1132            // Any other expression may propagate a slot computed from its operands.
1133            ExprKind::Call(callee, args) => {
1134                let (callee, options) = callee.split_call_options();
1135                self.collect_slot_roots(callee, roots);
1136                for option in options.iter().flat_map(|options| options.args) {
1137                    self.collect_slot_roots(&option.value, roots);
1138                }
1139                for argument in args.exprs() {
1140                    self.collect_slot_roots(argument, roots);
1141                }
1142            }
1143            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
1144                self.collect_slot_roots(lhs, roots);
1145                self.collect_slot_roots(rhs, roots);
1146            }
1147            ExprKind::Index(base, index) => {
1148                for expression in [Some(*base), *index].into_iter().flatten() {
1149                    self.collect_slot_roots(expression, roots);
1150                }
1151            }
1152            ExprKind::Slice(base, start, end) => {
1153                for expression in [Some(*base), *start, *end].into_iter().flatten() {
1154                    self.collect_slot_roots(expression, roots);
1155                }
1156            }
1157            ExprKind::Ternary(condition, if_true, if_false) => {
1158                for expression in [condition, if_true, if_false] {
1159                    self.collect_slot_roots(expression, roots);
1160                }
1161            }
1162            ExprKind::Member(base, _)
1163            | ExprKind::YulMember(base, _)
1164            | ExprKind::Payable(base)
1165            | ExprKind::Unary(_, base)
1166            | ExprKind::Delete(base) => self.collect_slot_roots(base, roots),
1167            ExprKind::Array(expressions) => {
1168                for expression in *expressions {
1169                    self.collect_slot_roots(expression, roots);
1170                }
1171            }
1172            ExprKind::Tuple(expressions) => {
1173                for expression in expressions.iter().flatten() {
1174                    self.collect_slot_roots(expression, roots);
1175                }
1176            }
1177            _ => {}
1178        }
1179    }
1180}
1181
1182/// `super.f`, `Base.f` or `Lib.f`: a statically dispatched internal call.
1183fn is_static_internal_base(gcx: Gcx<'_>, base: &hir::Expr<'_>) -> bool {
1184    is_builtin(gcx, base, sym::super_)
1185        || matches!(
1186            gcx.resolved_expr(base),
1187            Some(Res::Item(ItemId::Contract(_)) | Res::Namespace(_))
1188        )
1189}