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