Skip to main content

forge_lint/sol/med/
ecrecover.rs

1use super::Ecrecover;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{
5        Severity, SolLint,
6        analysis::{is_exit_call, is_require_or_assert, loop_update, tuple_elems},
7    },
8};
9use alloy_primitives::{U256, uint};
10use solar::{
11    ast::{BinOpKind, ElementaryType, UnOpKind},
12    interface::{Span, Symbol, data_structures::Never},
13    sema::{
14        Gcx,
15        builtins::Builtin,
16        eval::ConstValue,
17        hir::{
18            self, Expr, ExprId, ExprKind, LoopSource, StateMutability, Stmt, StmtKind, TypeKind,
19            VariableId, Visit,
20        },
21        ty::TyKind,
22    },
23};
24use std::{
25    collections::{HashMap, HashSet},
26    mem,
27    ops::ControlFlow,
28};
29
30declare_forge_lint!(
31    ECRECOVER,
32    Severity::Med,
33    "ecrecover",
34    "`ecrecover` call does not reject malleable signatures"
35);
36
37/// Largest canonical secp256k1 `s` value, `n / 2`.
38const SECP256K1_HALF_ORDER: U256 =
39    uint!(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0_U256);
40
41impl<'gcx> LateLintPass<'gcx> for Ecrecover {
42    fn check_function(
43        &mut self,
44        ctx: &LintContext,
45        gcx: Gcx<'gcx>,
46        func: &'gcx hir::Function<'gcx>,
47    ) {
48        let Some(body) = func.body else { return };
49        let mut analyzer = Analyzer {
50            gcx,
51            returns: func.returns,
52            state: FlowState::default(),
53            next_value: 0,
54            hits: Vec::new(),
55            deferred: HashMap::new(),
56            loop_exits: Vec::new(),
57            loop_next: None,
58        };
59        if analyzer.run_block(body.stmts) {
60            analyzer.use_return_values();
61        }
62        for span in analyzer.hits {
63            ctx.emit(&ECRECOVER, span);
64        }
65    }
66}
67
68/// A tracked place: a whole variable, or a struct field reached from a variable (`sig.s`).
69///
70/// A field that is only ever read has no `values` entry and resolves to `ValueId::Initial(key)`,
71/// so the key carries an epoch that [`FlowState::set`] bumps whenever the whole base variable is
72/// reassigned; otherwise a guard on `sig.s` would survive `sig = other;`. Writes through memory
73/// or storage references also reset the fields of other variables in the same location.
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
75enum ValueKey {
76    Var(VariableId),
77    Field(VariableId, Symbol, u32),
78}
79
80impl ValueKey {
81    const fn var(self) -> VariableId {
82        match self {
83            Self::Var(var) | Self::Field(var, ..) => var,
84        }
85    }
86}
87
88/// Symbolic identity of a value: a place's incoming value or the result of an assignment.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90enum ValueId {
91    Initial(ValueKey),
92    Assigned(u32),
93}
94
95/// An `ecrecover` result held in a local that has been neither observed nor validated yet.
96#[derive(Clone, Copy, PartialEq, Eq)]
97struct PendingRecovery {
98    signature: Option<ValueId>,
99    span: Span,
100}
101
102#[derive(Clone, Default)]
103struct FlowState {
104    values: HashMap<ValueKey, ValueId>,
105    /// Values proven to be a canonical (low) `s`.
106    low_s: HashSet<ValueId>,
107    pending: HashMap<ValueId, Vec<PendingRecovery>>,
108    /// Current field epoch per variable, see [`ValueKey`].
109    field_epoch: HashMap<VariableId, u32>,
110}
111
112impl FlowState {
113    fn value(&self, key: ValueKey) -> ValueId {
114        self.values.get(&key).copied().unwrap_or(ValueId::Initial(key))
115    }
116
117    fn field_key(&self, var: VariableId, field: Symbol) -> ValueKey {
118        ValueKey::Field(var, field, self.field_epoch.get(&var).copied().unwrap_or(0))
119    }
120
121    /// Sets the value of `key`. Setting a whole variable drops its tracked fields and starts a
122    /// new field epoch.
123    fn set(&mut self, key: ValueKey, value: ValueId) {
124        if let ValueKey::Var(var) = key {
125            self.reset_fields(var);
126        }
127        self.values.insert(key, value);
128    }
129
130    fn reset_fields(&mut self, var: VariableId) {
131        self.values.retain(|key, _| !matches!(key, ValueKey::Field(base, ..) if *base == var));
132        *self.field_epoch.entry(var).or_insert(0) += 1;
133    }
134
135    /// Variables with a tracked place or a proven initial value.
136    fn tracked_vars(&self) -> HashSet<VariableId> {
137        let initial = self.low_s.iter().filter_map(|value| match value {
138            ValueId::Initial(key) => Some(key.var()),
139            ValueId::Assigned(_) => None,
140        });
141        self.values.keys().map(|key| key.var()).chain(initial).collect()
142    }
143
144    fn add_pending(&mut self, value: ValueId, recovery: PendingRecovery) {
145        let recoveries = self.pending.entry(value).or_default();
146        if !recoveries.contains(&recovery) {
147            recoveries.push(recovery);
148        }
149    }
150}
151
152/// A place written by an assignment, paired with the expression it receives.
153type Pair<'gcx> = (Option<ValueKey>, Option<&'gcx Expr<'gcx>>);
154
155/// The `s` argument of an `ecrecover` call.
156type Signature<'gcx> = &'gcx Expr<'gcx>;
157
158struct Analyzer<'gcx> {
159    gcx: Gcx<'gcx>,
160    returns: &'gcx [VariableId],
161    state: FlowState,
162    next_value: u32,
163    hits: Vec<Span>,
164    /// `ecrecover` calls whose result is being stored into a local. Their recovery is captured
165    /// here instead of being reported at the call site.
166    deferred: HashMap<ExprId, Option<PendingRecovery>>,
167    /// States reaching `break`/`continue` or the end of the innermost loop body.
168    loop_exits: Vec<FlowState>,
169    /// Update statement of the innermost `for` loop, which `continue` still executes.
170    loop_next: Option<&'gcx Stmt<'gcx>>,
171}
172
173impl<'gcx> Analyzer<'gcx> {
174    const fn fresh_value(&mut self) -> ValueId {
175        self.next_value += 1;
176        ValueId::Assigned(self.next_value)
177    }
178
179    fn join(&mut self, left: FlowState, right: FlowState) -> FlowState {
180        // Keep the highest epoch so a field read after the join never reuses an identity that
181        // was reset in either branch.
182        let mut field_epoch = left.field_epoch.clone();
183        for (var, epoch) in &right.field_epoch {
184            let entry = field_epoch.entry(*var).or_insert(0);
185            *entry = (*entry).max(*epoch);
186        }
187        let mut joined = FlowState {
188            low_s: left.low_s.intersection(&right.low_s).copied().collect(),
189            field_epoch,
190            ..FlowState::default()
191        };
192        let mut merged = HashMap::new();
193        let keys: HashSet<_> = left.values.keys().chain(right.values.keys()).copied().collect();
194        for key in keys {
195            let (l, r) = (left.value(key), right.value(key));
196            let value = if l == r {
197                l
198            } else {
199                let value = *merged.entry((l, r)).or_insert_with(|| self.fresh_value());
200                if left.low_s.contains(&l) && right.low_s.contains(&r) {
201                    joined.low_s.insert(value);
202                }
203                value
204            };
205            joined.values.insert(key, value);
206            for recovery in left.pending.get(&l).into_iter().chain(right.pending.get(&r)).flatten()
207            {
208                joined.add_pending(value, *recovery);
209            }
210        }
211        joined
212    }
213
214    /// Continues from the join of `states`; returns `false` when no path continues.
215    fn join_all(&mut self, states: Vec<FlowState>) -> bool {
216        let Some(joined) = states.into_iter().reduce(|l, r| self.join(l, r)) else { return false };
217        self.state = joined;
218        true
219    }
220
221    fn emit_hit(&mut self, span: Span) {
222        if !self.hits.contains(&span) {
223            self.hits.push(span);
224        }
225    }
226
227    fn use_value(&mut self, value: ValueId) {
228        for recovery in self.state.pending.remove(&value).unwrap_or_default() {
229            self.emit_hit(recovery.span);
230        }
231    }
232
233    fn use_return_values(&mut self) {
234        for &var in self.returns {
235            self.use_value(self.state.value(ValueKey::Var(var)));
236        }
237    }
238
239    fn use_all_pending(&mut self) {
240        for recoveries in mem::take(&mut self.state.pending).into_values() {
241            for recovery in recoveries {
242                self.emit_hit(recovery.span);
243            }
244        }
245    }
246
247    /// Drops pending recoveries whose signature has since been proven canonical.
248    fn validate_pending(&mut self) {
249        let low_s = &self.state.low_s;
250        self.state.pending.retain(|_, recoveries| {
251            recoveries.retain(|r| !r.signature.is_some_and(|s| low_s.contains(&s)));
252            !recoveries.is_empty()
253        });
254    }
255
256    fn current_value(&self, expr: &Expr<'_>) -> Option<ValueId> {
257        match &expr.peel_parens().kind {
258            ExprKind::Assign(lhs, None, _) => self.current_value(lhs),
259            _ => self.place_key(expr).map(|key| self.state.value(key)),
260        }
261    }
262
263    /// The tracked place an expression denotes: a variable or a `var.field` member, through
264    /// parens and `uint256(..)`/`bytes32(..)` casts.
265    fn place_key(&self, expr: &Expr<'_>) -> Option<ValueKey> {
266        match &expr.peel_parens().kind {
267            ExprKind::Call(callee, args, _) if is_transparent_cast(callee) && args.len() == 1 => {
268                self.place_key(args.exprs().next()?)
269            }
270            ExprKind::Member(base, field) => {
271                var_of(self.gcx, base).map(|var| self.state.field_key(var, field.name))
272            }
273            _ => var_of(self.gcx, expr).map(ValueKey::Var),
274        }
275    }
276
277    /// Memory and storage variables passed by reference to an internal function (including the
278    /// receiver of a `using for` call), which the callee may write through.
279    fn reference_args(
280        &self,
281        callee: &'gcx Expr<'gcx>,
282        args: &'gcx hir::CallArgs<'gcx>,
283    ) -> Vec<VariableId> {
284        let callee = callee.peel_parens();
285        let Some(TyKind::Fn(function)) = self.gcx.type_of_expr(callee.id).map(|ty| ty.kind) else {
286            return Vec::new();
287        };
288        if !function.is_internal() {
289            return Vec::new();
290        }
291        let receiver = match &callee.kind {
292            ExprKind::Member(base, _)
293                if self.gcx.resolved_callee(callee.id).is_some_and(|c| c.attached) =>
294            {
295                Some(*base)
296            }
297            _ => None,
298        };
299        receiver
300            .into_iter()
301            .chain(args.exprs())
302            .filter_map(|expr| var_of(self.gcx, expr))
303            .filter(|&var| {
304                matches!(
305                    self.gcx.hir.variable(var).data_location,
306                    Some(hir::DataLocation::Memory | hir::DataLocation::Storage)
307                )
308            })
309            .collect()
310    }
311
312    fn is_local(&self, var: VariableId) -> bool {
313        self.gcx.hir.variable(var).is_local_variable()
314    }
315
316    /// The call expression and signature argument of a builtin `ecrecover` call.
317    fn ecrecover_call(
318        &self,
319        expr: &'gcx Expr<'gcx>,
320    ) -> Option<(&'gcx Expr<'gcx>, Signature<'gcx>)> {
321        let expr = expr.peel_parens();
322        let ExprKind::Call(callee, args, _) = &expr.kind else { return None };
323        let callee = self.gcx.resolved_builtin(callee.peel_parens());
324        (callee == Some(Builtin::EcRecover) && args.len() == 4)
325            .then_some(expr)
326            .zip(args.exprs().nth(3))
327    }
328
329    fn pending_recovery(&self, expr: &'gcx Expr<'gcx>) -> Option<PendingRecovery> {
330        let (call, signature) = self.ecrecover_call(expr)?;
331        (!self.is_proven_low_s(signature))
332            .then(|| PendingRecovery { signature: self.current_value(signature), span: call.span })
333    }
334
335    /// The arms of `cond ? then : otherwise` that may execute.
336    fn live_arms<T>(&self, cond: &Expr<'_>, then: T, otherwise: T) -> impl Iterator<Item = T> {
337        match self.const_bool(cond) {
338            Some(true) => [Some(then), None],
339            Some(false) => [None, Some(otherwise)],
340            None => [Some(then), Some(otherwise)],
341        }
342        .into_iter()
343        .flatten()
344    }
345
346    /// `ecrecover` calls whose result becomes the value of `expr`.
347    fn result_calls(&self, expr: &'gcx Expr<'gcx>, out: &mut Vec<ExprId>) {
348        let expr = expr.peel_parens();
349        if self.ecrecover_call(expr).is_some() {
350            out.push(expr.id);
351        }
352        match &expr.kind {
353            ExprKind::Ternary(cond, then, otherwise) => {
354                for arm in self.live_arms(cond, *then, *otherwise) {
355                    self.result_calls(arm, out);
356                }
357            }
358            ExprKind::Assign(_, None, rhs) => self.result_calls(rhs, out),
359            _ => {}
360        }
361    }
362
363    fn const_value(&self, expr: &Expr<'_>) -> Option<U256> {
364        let expr = expr.peel_parens();
365        match &expr.kind {
366            ExprKind::Call(callee, args, _) if is_transparent_cast(callee) && args.len() == 1 => {
367                self.const_value(args.exprs().next()?)
368            }
369            // Fold arithmetic with wrapping semantics so `unchecked` bounds evaluate.
370            ExprKind::Binary(lhs, op, rhs)
371                if matches!(op.kind, BinOpKind::Add | BinOpKind::Sub | BinOpKind::Mul) =>
372            {
373                let (lhs, rhs) = (self.const_value(lhs)?, self.const_value(rhs)?);
374                Some(match op.kind {
375                    BinOpKind::Add => lhs.wrapping_add(rhs),
376                    BinOpKind::Sub => lhs.wrapping_sub(rhs),
377                    _ => lhs.wrapping_mul(rhs),
378                })
379            }
380            _ if self.gcx.resolved_builtin(expr) == Some(Builtin::TypeMax) => {
381                let TyKind::Elementary(ElementaryType::UInt(size)) =
382                    self.gcx.type_of_expr(expr.id)?.kind
383                else {
384                    return None;
385                };
386                Some(U256::MAX >> (256 - size.bits()))
387            }
388            _ => self.gcx.try_eval_const(expr).ok()?.as_u256(),
389        }
390    }
391
392    fn const_bool(&self, expr: &Expr<'_>) -> Option<bool> {
393        match self.gcx.try_eval_const_value(expr).ok()? {
394            ConstValue::Bool(value) => Some(*value),
395            _ => None,
396        }
397    }
398
399    fn is_proven_low_s(&self, expr: &'gcx Expr<'gcx>) -> bool {
400        self.const_value(expr).is_some_and(|value| value <= SECP256K1_HALF_ORDER)
401            || self.current_value(expr).is_some_and(|value| self.state.low_s.contains(&value))
402            || matches!(&expr.peel_parens().kind, ExprKind::Ternary(cond, then, otherwise)
403                if self.live_arms(cond, *then, *otherwise).all(|arm| self.is_proven_low_s(arm)))
404    }
405
406    /// The `(value, proven low)` a variable takes when assigned `rhs`.
407    fn assigned(&self, rhs: Option<&'gcx Expr<'gcx>>) -> (Option<ValueId>, bool) {
408        (
409            rhs.and_then(|rhs| self.current_value(rhs)),
410            rhs.is_some_and(|rhs| self.is_proven_low_s(rhs)),
411        )
412    }
413
414    fn assign(&mut self, key: ValueKey, (value, low_s): (Option<ValueId>, bool)) {
415        let value = value.unwrap_or_else(|| self.fresh_value());
416        if low_s {
417            self.state.low_s.insert(value);
418        }
419        if let ValueKey::Field(var, field, _) = key {
420            // Writing through a memory or storage reference may write any other variable of
421            // that location too.
422            if let Some(location) = self.aliasable_location(var) {
423                for other in self.state.tracked_vars() {
424                    if other != var && self.aliasable_location(other) == Some(location) {
425                        self.state.reset_fields(other);
426                    }
427                }
428            }
429            let key = self.state.field_key(var, field);
430            self.state.set(key, value);
431        } else {
432            self.state.set(key, value);
433        }
434    }
435
436    /// The data location through which `var` may alias other variables.
437    fn aliasable_location(&self, var: VariableId) -> Option<hir::DataLocation> {
438        let var = self.gcx.hir.variable(var);
439        if var.kind.is_state() && var.mutability.is_none() {
440            return Some(hir::DataLocation::Storage);
441        }
442        var.data_location
443            .filter(|loc| matches!(loc, hir::DataLocation::Memory | hir::DataLocation::Storage))
444    }
445
446    /// Pairs every place written by `lhs` with the expression it receives.
447    fn pairs(
448        &self,
449        lhs: &'gcx Expr<'gcx>,
450        rhs: Option<&'gcx Expr<'gcx>>,
451        out: &mut Vec<Pair<'gcx>>,
452    ) {
453        let Some(elems) = tuple_elems(lhs) else { return out.push((self.place_key(lhs), rhs)) };
454        let rhs_elems = rhs.and_then(tuple_elems);
455        for (i, lhs) in elems.iter().enumerate() {
456            let rhs = rhs_elems.and_then(|elems| elems.get(i).copied().flatten());
457            match lhs {
458                Some(lhs) => self.pairs(lhs, rhs, out),
459                None => out.push((None, rhs)),
460            }
461        }
462    }
463
464    /// Assigns every pair, reading all right-hand sides first so tuple swaps are exact.
465    fn assign_pairs(&mut self, pairs: &[Pair<'gcx>]) {
466        let assigned: Vec<_> =
467            pairs.iter().filter_map(|(key, rhs)| Some(((*key)?, self.assigned(*rhs)))).collect();
468        for (key, assigned) in assigned {
469            self.assign(key, assigned);
470        }
471    }
472
473    fn assign_lhs(&mut self, lhs: &'gcx Expr<'gcx>, rhs: Option<&'gcx Expr<'gcx>>) {
474        let mut pairs = Vec::new();
475        self.pairs(lhs, rhs, &mut pairs);
476        self.assign_pairs(&pairs);
477    }
478
479    /// Models a statement-level store of `rhs` into `pairs`. Recoveries stored into locals stay
480    /// pending until the local is read or the signature validated; any other destination
481    /// observes the result immediately.
482    fn store(&mut self, pairs: &[Pair<'gcx>], rhs: Option<&'gcx Expr<'gcx>>) {
483        let mut calls = Vec::new();
484        for &(key, rhs) in pairs {
485            let local = key.filter(|key| self.is_local(key.var()));
486            let mut result_calls = Vec::new();
487            if let Some(rhs) = rhs {
488                self.result_calls(rhs, &mut result_calls);
489            }
490            for call in result_calls {
491                match local {
492                    Some(_) => self.deferred.insert(call, None),
493                    None => self.deferred.remove(&call),
494                };
495                calls.push((call, local));
496            }
497        }
498        let local_target = matches!(pairs, [(Some(key), _)] if self.is_local(key.var()));
499        if let Some(rhs) = rhs {
500            match &rhs.peel_parens().kind {
501                ExprKind::Assign(lhs, None, inner) if local_target => self.store_expr(lhs, inner),
502                // Copying a variable into a local does not observe its value.
503                _ if local_target && self.current_value(rhs).is_some() => {}
504                _ => {
505                    let _ = self.visit_expr(rhs);
506                }
507            }
508        }
509        self.assign_pairs(pairs);
510        for (call, key) in calls {
511            if let (Some(key), Some(Some(recovery))) = (key, self.deferred.remove(&call)) {
512                let value = self.state.value(key);
513                self.state.add_pending(value, recovery);
514            }
515        }
516    }
517
518    fn store_expr(&mut self, lhs: &'gcx Expr<'gcx>, rhs: &'gcx Expr<'gcx>) {
519        let mut pairs = Vec::new();
520        self.pairs(lhs, Some(rhs), &mut pairs);
521        self.store(&pairs, Some(rhs));
522    }
523
524    /// Visits an lvalue without treating the written variables as reads.
525    fn visit_lhs(&mut self, lhs: &'gcx Expr<'gcx>) {
526        if let Some(elems) = tuple_elems(lhs) {
527            for elem in elems.iter().flatten() {
528                self.visit_lhs(elem);
529            }
530        } else if self.place_key(lhs).is_none() {
531            let _ = self.visit_expr(lhs);
532        }
533    }
534
535    /// Forgets mutable state variables and storage pointers, which any state-mutating call may
536    /// have written.
537    fn invalidate_mutable_state(&mut self) {
538        for var in self.state.tracked_vars() {
539            let variable = self.gcx.hir.variable(var);
540            if (variable.kind.is_state() && variable.mutability.is_none())
541                || variable.data_location == Some(hir::DataLocation::Storage)
542            {
543                self.assign(ValueKey::Var(var), (None, false));
544            }
545        }
546    }
547
548    fn has_side_effect(&self, expr: &'gcx Expr<'gcx>) -> bool {
549        SideEffects(self).visit_expr(expr).is_break()
550    }
551
552    fn assume(&mut self, predicate: &'gcx Expr<'gcx>, negate: bool) {
553        self.add_facts(predicate, negate);
554        self.validate_pending();
555    }
556
557    fn add_facts(&mut self, predicate: &'gcx Expr<'gcx>, negate: bool) {
558        // Facts are derived from the current values, which a side effect may have replaced.
559        if self.has_side_effect(predicate) {
560            return;
561        }
562        match &predicate.peel_parens().kind {
563            ExprKind::Ternary(cond, then, otherwise) => {
564                if let Some(value) = self.const_bool(cond) {
565                    self.add_facts(if value { then } else { otherwise }, negate);
566                }
567            }
568            ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
569                self.add_facts(inner, !negate);
570            }
571            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
572                let is_and = op.kind == BinOpKind::And;
573                // A constant operand either decides the result or defers to the other operand.
574                for (side, other) in [(lhs, rhs), (rhs, lhs)] {
575                    if let Some(value) = self.const_bool(side) {
576                        if is_and == value {
577                            self.add_facts(other, negate);
578                        }
579                        return;
580                    }
581                }
582                if is_and == negate {
583                    // Only facts established by both operands hold.
584                    let baseline = self.state.low_s.clone();
585                    self.add_facts(lhs, negate);
586                    let from_lhs = mem::replace(&mut self.state.low_s, baseline.clone());
587                    self.add_facts(rhs, negate);
588                    let from_rhs = mem::replace(&mut self.state.low_s, baseline);
589                    self.state.low_s.extend(from_lhs.intersection(&from_rhs));
590                } else {
591                    self.add_facts(lhs, negate);
592                    self.add_facts(rhs, negate);
593                }
594            }
595            ExprKind::Binary(lhs, op, rhs) => {
596                let op = if negate { negate_comparison(op.kind) } else { op.kind };
597                for (candidate, bound, op) in [(lhs, rhs, op), (rhs, lhs, reverse_comparison(op))] {
598                    let (Some(value), Some(bound)) =
599                        (self.current_value(candidate), self.const_value(bound))
600                    else {
601                        continue;
602                    };
603                    let proves_low = match op {
604                        BinOpKind::Lt => bound <= SECP256K1_HALF_ORDER + U256::from(1),
605                        BinOpKind::Le | BinOpKind::Eq => bound <= SECP256K1_HALF_ORDER,
606                        _ => false,
607                    };
608                    if proves_low {
609                        self.state.low_s.insert(value);
610                    }
611                }
612            }
613            _ => {}
614        }
615    }
616
617    /// Runs `then` and `otherwise` under the respective assumptions on the already visited
618    /// `cond`, then continues from the join of the arms that did not exit.
619    fn branch(
620        &mut self,
621        cond: &'gcx Expr<'gcx>,
622        then: impl FnOnce(&mut Self) -> bool,
623        otherwise: impl FnOnce(&mut Self) -> bool,
624    ) -> bool {
625        if let Some(value) = self.const_bool(cond) {
626            self.assume(cond, !value);
627            return if value { then(self) } else { otherwise(self) };
628        }
629        let baseline = self.state.clone();
630        self.assume(cond, false);
631        let then_live = then(self);
632        let after_then = mem::replace(&mut self.state, baseline);
633        self.assume(cond, true);
634        let else_live = otherwise(self);
635        if then_live && else_live {
636            let after_else = mem::take(&mut self.state);
637            self.state = self.join(after_then, after_else);
638        } else if then_live {
639            self.state = after_then;
640        }
641        then_live || else_live
642    }
643
644    fn run_block(&mut self, stmts: &'gcx [Stmt<'gcx>]) -> bool {
645        stmts.iter().all(|stmt| self.run_stmt(stmt))
646    }
647
648    /// Runs `stmt`; returns `false` when control cannot continue past it.
649    fn run_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> bool {
650        match &stmt.kind {
651            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => self.run_block(block.stmts),
652            StmtKind::If(cond, then, otherwise) => {
653                let _ = self.visit_expr(cond);
654                self.branch(
655                    cond,
656                    |this| this.run_stmt(then),
657                    |this| otherwise.is_none_or(|otherwise| this.run_stmt(otherwise)),
658                )
659            }
660            StmtKind::Loop(block, source) => self.run_loop(block, *source),
661            StmtKind::Try(stmt_try) => {
662                let _ = self.visit_expr(&stmt_try.expr);
663                let after_call = self.state.clone();
664                let mut live = Vec::new();
665                for clause in stmt_try.clauses {
666                    self.state = after_call.clone();
667                    if self.run_block(clause.block.stmts) {
668                        live.push(self.state.clone());
669                    }
670                }
671                self.join_all(live)
672            }
673            StmtKind::Break | StmtKind::Continue => {
674                if matches!(stmt.kind, StmtKind::Continue)
675                    && let Some(next) = self.loop_next
676                {
677                    self.run_stmt(next);
678                }
679                self.loop_exits.push(self.state.clone());
680                false
681            }
682            StmtKind::DeclSingle(var) => {
683                let init = self.gcx.hir.variable(*var).initializer;
684                self.store(&[(Some(ValueKey::Var(*var)), init)], init);
685                true
686            }
687            StmtKind::DeclMulti(vars, init) => {
688                let pairs: Vec<_> = match tuple_elems(init) {
689                    Some(elems) => vars
690                        .iter()
691                        .zip(elems)
692                        .map(|(var, rhs)| (var.map(ValueKey::Var), *rhs))
693                        .collect(),
694                    None => vars.iter().map(|var| (var.map(ValueKey::Var), None)).collect(),
695                };
696                self.store(&pairs, Some(init));
697                true
698            }
699            StmtKind::Expr(expr) => {
700                match &expr.peel_parens().kind {
701                    ExprKind::Assign(lhs, None, rhs) => self.store_expr(lhs, rhs),
702                    _ => {
703                        let _ = self.visit_expr(expr);
704                    }
705                }
706                !is_exit_call(self.gcx, expr)
707            }
708            StmtKind::AssemblyBlock(_) | StmtKind::Err(_) => {
709                // Inline assembly is opaque and may observe any local before changing it. Flush
710                // deferred recoveries before discarding facts so assembly cannot hide a warning.
711                self.use_all_pending();
712                self.state = FlowState::default();
713                true
714            }
715            StmtKind::Return(None) => {
716                self.use_return_values();
717                false
718            }
719            StmtKind::Return(Some(expr)) | StmtKind::Revert(expr) => {
720                let _ = self.visit_expr(expr);
721                false
722            }
723            _ => {
724                let _ = self.walk_stmt(stmt);
725                true
726            }
727        }
728    }
729
730    fn run_loop(&mut self, block: &'gcx hir::Block<'gcx>, source: LoopSource<'gcx>) -> bool {
731        let next = loop_update(source);
732        let outer_exits = mem::take(&mut self.loop_exits);
733        let outer_next = mem::replace(&mut self.loop_next, next);
734        // `do { .. } while (false)` runs exactly once. Any other loop may carry the effects of
735        // one iteration into the next, so run the body once silently and start over from the
736        // join of the entry state with every state reaching the back edge.
737        let single_iteration = matches!(source, LoopSource::DoWhile)
738            && matches!(block.stmts.last().map(|stmt| &stmt.kind),
739                Some(StmtKind::If(cond, ..)) if self.const_bool(cond) == Some(false));
740        if !single_iteration {
741            let entry = self.state.clone();
742            let hits = self.hits.len();
743            self.run_loop_body(block);
744            self.hits.truncate(hits);
745            let mut states = mem::take(&mut self.loop_exits);
746            states.push(entry);
747            self.join_all(states);
748        }
749        self.run_loop_body(block);
750        let exits = mem::replace(&mut self.loop_exits, outer_exits);
751        self.loop_next = outer_next;
752        self.join_all(exits)
753    }
754
755    fn run_loop_body(&mut self, block: &'gcx hir::Block<'gcx>) {
756        if self.run_block(block.stmts) && self.loop_next.is_none_or(|next| self.run_stmt(next)) {
757            self.loop_exits.push(self.state.clone());
758        }
759    }
760}
761
762impl<'gcx> Visit<'gcx> for Analyzer<'gcx> {
763    type BreakValue = Never;
764
765    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
766        &self.gcx.hir
767    }
768
769    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Never> {
770        match &expr.kind {
771            ExprKind::Ident(_) => {
772                if let Some(value) = self.current_value(expr) {
773                    self.use_value(value);
774                }
775            }
776            ExprKind::Binary(lhs, op, rhs) if matches!(op.kind, BinOpKind::And | BinOpKind::Or) => {
777                let _ = self.visit_expr(lhs);
778                let run_rhs = |this: &mut Self| {
779                    let _ = this.visit_expr(rhs);
780                    true
781                };
782                let skip_rhs = |_: &mut Self| true;
783                if op.kind == BinOpKind::And {
784                    self.branch(lhs, run_rhs, skip_rhs);
785                } else {
786                    self.branch(lhs, skip_rhs, run_rhs);
787                }
788            }
789            ExprKind::Ternary(cond, then, otherwise) => {
790                let _ = self.visit_expr(cond);
791                self.branch(
792                    cond,
793                    |this| {
794                        let _ = this.visit_expr(then);
795                        true
796                    },
797                    |this| {
798                        let _ = this.visit_expr(otherwise);
799                        true
800                    },
801                );
802            }
803            ExprKind::Call(callee, args, _) if is_require_or_assert(self.gcx, callee) => {
804                let _ = self.walk_expr(expr);
805                if let Some(cond) = args.exprs().next() {
806                    self.assume(cond, false);
807                }
808            }
809            ExprKind::Assign(lhs, None, rhs) => {
810                self.visit_lhs(lhs);
811                let _ = self.visit_expr(rhs);
812                self.assign_lhs(lhs, Some(rhs));
813            }
814            ExprKind::Assign(lhs, Some(_), _) => {
815                let _ = self.walk_expr(expr);
816                self.assign_lhs(lhs, None);
817            }
818            ExprKind::Delete(target) => {
819                self.visit_lhs(target);
820                if let Some(key) = self.place_key(target) {
821                    self.assign(key, (None, true));
822                }
823            }
824            ExprKind::Unary(op, target) if op.kind.has_side_effects() => {
825                let _ = self.walk_expr(expr);
826                if let Some(key) = self.place_key(target) {
827                    self.assign(key, (None, false));
828                }
829            }
830            ExprKind::Call(callee, args, _) => {
831                let _ = self.walk_expr(expr);
832                if call_may_mutate_state(self.gcx, callee) {
833                    self.invalidate_mutable_state();
834                }
835                for var in self.reference_args(callee, args) {
836                    self.assign(ValueKey::Var(var), (None, false));
837                }
838                if let Some(recovery) = self.pending_recovery(expr) {
839                    match self.deferred.get_mut(&expr.id) {
840                        Some(captured) => *captured = Some(recovery),
841                        None => self.emit_hit(recovery.span),
842                    }
843                }
844            }
845            _ => {
846                let _ = self.walk_expr(expr);
847            }
848        }
849        ControlFlow::Continue(())
850    }
851}
852
853/// Finds writes and state-mutating calls that run when an expression is evaluated.
854struct SideEffects<'a, 'gcx>(&'a Analyzer<'gcx>);
855
856impl<'gcx> Visit<'gcx> for SideEffects<'_, 'gcx> {
857    type BreakValue = ();
858
859    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
860        &self.0.gcx.hir
861    }
862
863    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
864        match &expr.kind {
865            ExprKind::Assign(..) | ExprKind::Delete(_) => ControlFlow::Break(()),
866            ExprKind::Unary(op, _) if op.kind.has_side_effects() => ControlFlow::Break(()),
867            ExprKind::Call(callee, ..) if call_may_mutate_state(self.0.gcx, callee) => {
868                ControlFlow::Break(())
869            }
870            ExprKind::Ternary(cond, then, otherwise) => {
871                self.visit_expr(cond)?;
872                for arm in self.0.live_arms(cond, *then, *otherwise) {
873                    self.visit_expr(arm)?;
874                }
875                ControlFlow::Continue(())
876            }
877            ExprKind::Binary(lhs, op, _)
878                if matches!(op.kind, BinOpKind::And | BinOpKind::Or)
879                    && self
880                        .0
881                        .const_bool(lhs)
882                        .is_some_and(|value| (op.kind == BinOpKind::And) != value) =>
883            {
884                self.visit_expr(lhs)
885            }
886            _ => self.walk_expr(expr),
887        }
888    }
889}
890
891/// The variable an expression denotes, through parens and `uint256(..)`/`bytes32(..)` casts.
892fn var_of(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<VariableId> {
893    match &expr.peel_parens().kind {
894        ExprKind::Ident(_) => gcx.resolved_variable(expr),
895        ExprKind::Call(callee, args, _) if is_transparent_cast(callee) && args.len() == 1 => {
896            args.exprs().next().and_then(|expr| var_of(gcx, expr))
897        }
898        _ => None,
899    }
900}
901
902fn is_transparent_cast(callee: &Expr<'_>) -> bool {
903    matches!(
904        &callee.peel_parens().kind,
905        ExprKind::Type(hir::Type {
906            kind: TypeKind::Elementary(
907                ElementaryType::UInt(size) | ElementaryType::FixedBytes(size)
908            ),
909            ..
910        }) if size.bits() == 256
911    )
912}
913
914const fn negate_comparison(op: BinOpKind) -> BinOpKind {
915    match op {
916        BinOpKind::Lt => BinOpKind::Ge,
917        BinOpKind::Le => BinOpKind::Gt,
918        BinOpKind::Gt => BinOpKind::Le,
919        BinOpKind::Ge => BinOpKind::Lt,
920        BinOpKind::Eq => BinOpKind::Ne,
921        BinOpKind::Ne => BinOpKind::Eq,
922        _ => op,
923    }
924}
925
926const fn reverse_comparison(op: BinOpKind) -> BinOpKind {
927    match op {
928        BinOpKind::Lt => BinOpKind::Gt,
929        BinOpKind::Le => BinOpKind::Ge,
930        BinOpKind::Gt => BinOpKind::Lt,
931        BinOpKind::Ge => BinOpKind::Le,
932        _ => op,
933    }
934}
935
936fn call_may_mutate_state(gcx: Gcx<'_>, callee: &Expr<'_>) -> bool {
937    let callee = callee.peel_parens();
938    if matches!(callee.kind, ExprKind::Type(_)) {
939        return false;
940    }
941    if let Some(ty) = gcx.type_of_expr(callee.id)
942        && let TyKind::Fn(function) = ty.peel_refs().kind
943    {
944        return function.state_mutability > StateMutability::View;
945    }
946    gcx.resolved_builtin(callee).is_none()
947}