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::primitives::{branch_always_exits, is_require_or_assert},
7    },
8};
9use alloy_primitives::{U256, uint};
10use solar::{
11    ast::{BinOpKind, ElementaryType, UnOpKind},
12    interface::{Span, data_structures::Never},
13    sema::{
14        Gcx,
15        builtins::Builtin,
16        eval::ConstValue,
17        hir::{
18            self, ExprKind, ItemId, LoopSource, Res, StateMutability, StmtKind, TypeKind, Visit,
19        },
20        ty::TyKind,
21    },
22};
23use std::{
24    collections::{HashMap, HashSet},
25    ops::ControlFlow,
26};
27
28declare_forge_lint!(
29    ECRECOVER,
30    Severity::Med,
31    "ecrecover",
32    "ecrecover should reject malleable signatures"
33);
34
35/// Largest canonical secp256k1 `s` value, `n / 2`.
36const SECP256K1_HALF_ORDER: U256 =
37    uint!(0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0_U256);
38
39impl<'hir> LateLintPass<'hir> for Ecrecover {
40    fn check_function(
41        &mut self,
42        ctx: &LintContext,
43        gcx: Gcx<'hir>,
44        hir: &'hir hir::Hir<'hir>,
45        func: &'hir hir::Function<'hir>,
46    ) {
47        let Some(body) = func.body else { return };
48        let mut analyzer = Analyzer::new(gcx, hir, func.returns);
49        let mut falls_through = true;
50        for stmt in body.stmts {
51            let _ = analyzer.visit_stmt(stmt);
52            if analyzer.stmt_always_exits(stmt) {
53                falls_through = false;
54                break;
55            }
56        }
57        if falls_through {
58            analyzer.use_return_values();
59        }
60        for span in analyzer.hits {
61            ctx.emit(&ECRECOVER, span);
62        }
63    }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
67enum ValueId {
68    Initial(hir::VariableId),
69    Assigned(u32),
70}
71
72#[derive(Clone, Copy, Default)]
73struct AssignedValue {
74    value: Option<ValueId>,
75    low_s: bool,
76}
77
78#[derive(Clone, Copy)]
79struct PendingRecovery {
80    signature: Option<ValueId>,
81    span: Span,
82}
83
84#[derive(Clone, Default)]
85struct FlowState {
86    values: HashMap<hir::VariableId, ValueId>,
87    low_s: HashSet<ValueId>,
88    pending: HashMap<ValueId, Vec<PendingRecovery>>,
89}
90
91#[derive(Clone, Default)]
92struct LoopEffects {
93    values: HashSet<hir::VariableId>,
94    mutable_state: bool,
95    all_values: bool,
96}
97
98impl LoopEffects {
99    fn merge(&mut self, other: Self) {
100        self.values.extend(other.values);
101        self.mutable_state |= other.mutable_state;
102        self.all_values |= other.all_values;
103    }
104}
105
106impl FlowState {
107    fn value(&self, var: hir::VariableId) -> ValueId {
108        self.values.get(&var).copied().unwrap_or(ValueId::Initial(var))
109    }
110}
111
112struct Analyzer<'hir> {
113    gcx: Gcx<'hir>,
114    hir: &'hir hir::Hir<'hir>,
115    returns: &'hir [hir::VariableId],
116    state: FlowState,
117    next_value: u32,
118    hits: Vec<Span>,
119    ignored_reads: HashSet<ValueId>,
120    deferred_calls: HashSet<hir::ExprId>,
121    captured_recoveries: HashMap<hir::ExprId, PendingRecovery>,
122    stored_results: HashSet<hir::ExprId>,
123}
124
125impl<'hir> Analyzer<'hir> {
126    fn new(gcx: Gcx<'hir>, hir: &'hir hir::Hir<'hir>, returns: &'hir [hir::VariableId]) -> Self {
127        Self {
128            gcx,
129            hir,
130            returns,
131            state: FlowState::default(),
132            next_value: 0,
133            hits: Vec::new(),
134            ignored_reads: HashSet::new(),
135            deferred_calls: HashSet::new(),
136            captured_recoveries: HashMap::new(),
137            stored_results: HashSet::new(),
138        }
139    }
140
141    const fn fresh_value(&mut self) -> ValueId {
142        let value = ValueId::Assigned(self.next_value);
143        self.next_value += 1;
144        value
145    }
146
147    fn snapshot(&self) -> FlowState {
148        self.state.clone()
149    }
150
151    fn restore(&mut self, state: FlowState) {
152        self.state = state;
153    }
154
155    fn join(&mut self, left: FlowState, right: FlowState) -> FlowState {
156        let mut joined = FlowState {
157            low_s: left.low_s.intersection(&right.low_s).copied().collect(),
158            ..FlowState::default()
159        };
160        let vars: HashSet<_> = left.values.keys().chain(right.values.keys()).copied().collect();
161        let mut joined_values = HashMap::new();
162
163        for var in &vars {
164            let var = *var;
165            let left_value = left.value(var);
166            let right_value = right.value(var);
167            if left_value == right_value {
168                joined.values.insert(var, left_value);
169                continue;
170            }
171
172            let value = *joined_values
173                .entry((left_value, right_value))
174                .or_insert_with(|| self.fresh_value());
175            if left.low_s.contains(&left_value) && right.low_s.contains(&right_value) {
176                joined.low_s.insert(value);
177            }
178            joined.values.insert(var, value);
179        }
180        for var in vars {
181            let value = joined.value(var);
182            for recovery in left
183                .pending
184                .get(&left.value(var))
185                .into_iter()
186                .flatten()
187                .chain(right.pending.get(&right.value(var)).into_iter().flatten())
188                .copied()
189            {
190                let recoveries = joined.pending.entry(value).or_default();
191                if !recoveries.iter().any(|existing| {
192                    existing.span == recovery.span && existing.signature == recovery.signature
193                }) {
194                    recoveries.push(recovery);
195                }
196            }
197        }
198        joined
199    }
200
201    fn emit_hit(&mut self, span: Span) {
202        if !self.hits.contains(&span) {
203            self.hits.push(span);
204        }
205    }
206
207    fn record_pending(&mut self, var: hir::VariableId, recovery: PendingRecovery) {
208        let value = self.state.value(var);
209        let recoveries = self.state.pending.entry(value).or_default();
210        if !recoveries.iter().any(|existing| {
211            existing.span == recovery.span && existing.signature == recovery.signature
212        }) {
213            recoveries.push(recovery);
214        }
215    }
216
217    fn use_value(&mut self, value: ValueId) {
218        if self.ignored_reads.contains(&value) {
219            return;
220        }
221        if let Some(recoveries) = self.state.pending.remove(&value) {
222            for recovery in recoveries {
223                self.emit_hit(recovery.span);
224            }
225        }
226    }
227
228    fn use_return_values(&mut self) {
229        let values: Vec<_> = self.returns.iter().map(|var| self.state.value(*var)).collect();
230        for value in values {
231            self.use_value(value);
232        }
233    }
234
235    fn use_all_pending(&mut self) {
236        for recoveries in std::mem::take(&mut self.state.pending).into_values() {
237            for recovery in recoveries {
238                self.emit_hit(recovery.span);
239            }
240        }
241    }
242
243    fn validate_pending(&mut self) {
244        let low_s = &self.state.low_s;
245        self.state.pending.retain(|_, recoveries| {
246            recoveries.retain(|recovery| {
247                !recovery.signature.is_some_and(|signature| low_s.contains(&signature))
248            });
249            !recoveries.is_empty()
250        });
251    }
252
253    fn current_value(&self, expr: &'hir hir::Expr<'hir>) -> Option<ValueId> {
254        match &expr.peel_parens().kind {
255            ExprKind::Ident(reses) => reses.iter().find_map(|res| match res {
256                Res::Item(ItemId::Variable(var)) => Some(self.state.value(*var)),
257                _ => None,
258            }),
259            ExprKind::Call(callee, args, _)
260                if is_transparent_signature_cast(callee) && args.len() == 1 =>
261            {
262                args.exprs().next().and_then(|arg| self.current_value(arg))
263            }
264            ExprKind::Assign(lhs, None, _) => self.current_value(lhs),
265            _ => None,
266        }
267    }
268
269    fn deferable_target(&self, expr: &'hir hir::Expr<'hir>) -> Option<hir::VariableId> {
270        underlying_var(expr).filter(|var| self.hir.variable(*var).is_local_variable())
271    }
272
273    fn ecrecover_call_id(&self, expr: &'hir hir::Expr<'hir>) -> Option<hir::ExprId> {
274        let expr = expr.peel_parens();
275        let ExprKind::Call(callee, args, _) = &expr.kind else { return None };
276        (is_ecrecover_builtin(self.gcx, callee) && args.len() == 4).then_some(expr.id)
277    }
278
279    fn collect_result_calls(&self, expr: &'hir hir::Expr<'hir>, calls: &mut HashSet<hir::ExprId>) {
280        let expr = expr.peel_parens();
281        if let Some(call) = self.ecrecover_call_id(expr) {
282            calls.insert(call);
283            return;
284        }
285        match &expr.kind {
286            ExprKind::Ternary(condition, then_expr, else_expr) => {
287                match self.const_bool(condition) {
288                    Some(true) => self.collect_result_calls(then_expr, calls),
289                    Some(false) => self.collect_result_calls(else_expr, calls),
290                    None => {
291                        self.collect_result_calls(then_expr, calls);
292                        self.collect_result_calls(else_expr, calls);
293                    }
294                }
295            }
296            ExprKind::Assign(_, None, rhs) => self.collect_result_calls(rhs, calls),
297            _ => {}
298        }
299    }
300
301    fn collect_recovery_targets(
302        &self,
303        lhs: &'hir hir::Expr<'hir>,
304        rhs: &'hir hir::Expr<'hir>,
305        targets: &mut HashMap<hir::ExprId, hir::VariableId>,
306        observable: &mut HashSet<hir::ExprId>,
307    ) {
308        if let ExprKind::Tuple(lhs_elems) = &lhs.peel_parens().kind
309            && let ExprKind::Tuple(rhs_elems) = &rhs.peel_parens().kind
310        {
311            for (lhs, rhs) in lhs_elems.iter().zip(rhs_elems.iter()) {
312                if let (Some(lhs), Some(rhs)) = (lhs, rhs) {
313                    self.collect_recovery_targets(lhs, rhs, targets, observable);
314                }
315            }
316        } else {
317            let mut calls = HashSet::new();
318            self.collect_result_calls(rhs, &mut calls);
319            if let Some(var) = self.deferable_target(lhs) {
320                targets.extend(calls.into_iter().map(|call| (call, var)));
321            } else {
322                observable.extend(calls);
323            }
324        }
325    }
326
327    fn visit_stored_expr(
328        &mut self,
329        expr: &'hir hir::Expr<'hir>,
330        store_locally: bool,
331        targets: &HashMap<hir::ExprId, hir::VariableId>,
332    ) -> Vec<(hir::VariableId, PendingRecovery)> {
333        let ignored_reads = self.ignored_reads.clone();
334        let deferred_calls = self.deferred_calls.clone();
335        let stored_results = self.stored_results.clone();
336        if store_locally {
337            // Local copies are not observable, and `ecrecover` itself is pure. Keep the
338            // recovered value pending until it is validated or actually read.
339            if let Some(value) = self.current_value(expr) {
340                self.ignored_reads.insert(value);
341            }
342            self.deferred_calls.extend(targets.keys().copied());
343            if matches!(expr.peel_parens().kind, ExprKind::Assign(..)) {
344                self.stored_results.insert(expr.peel_parens().id);
345            }
346        }
347        let _ = self.visit_expr(expr);
348        let recoveries = targets
349            .iter()
350            .filter_map(|(call, var)| {
351                self.captured_recoveries.remove(call).map(|recovery| (*var, recovery))
352            })
353            .collect();
354        self.ignored_reads = ignored_reads;
355        self.deferred_calls = deferred_calls;
356        self.stored_results = stored_results;
357        recoveries
358    }
359
360    fn visit_discarded_expr(&mut self, expr: &'hir hir::Expr<'hir>) {
361        let stored_results = self.stored_results.clone();
362        if matches!(expr.peel_parens().kind, ExprKind::Assign(..)) {
363            self.stored_results.insert(expr.peel_parens().id);
364        }
365        let _ = self.visit_expr(expr);
366        self.stored_results = stored_results;
367    }
368
369    fn visit_assignment_lhs(&mut self, lhs: &'hir hir::Expr<'hir>) {
370        let ignored_reads = self.ignored_reads.clone();
371        let mut vars = HashSet::new();
372        collect_lhs_vars(lhs, &mut vars);
373        for var in vars {
374            self.ignored_reads.insert(self.state.value(var));
375        }
376        let _ = self.visit_expr(lhs);
377        self.ignored_reads = ignored_reads;
378    }
379
380    fn const_value(&self, expr: &'hir hir::Expr<'hir>) -> Option<U256> {
381        if let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind
382            && is_transparent_signature_cast(callee)
383            && args.len() == 1
384        {
385            return args.exprs().next().and_then(|arg| self.const_value(arg));
386        }
387        if self.gcx.resolved_builtin(expr) == Some(Builtin::TypeMax)
388            && let Some(ty) = self.gcx.type_of_expr(expr.peel_parens().id)
389            && let TyKind::Elementary(ElementaryType::UInt(size)) = ty.kind
390        {
391            return Some(U256::MAX >> (256 - size.bits()));
392        }
393        if let ExprKind::Binary(lhs, op, rhs) = &expr.peel_parens().kind
394            && matches!(op.kind, BinOpKind::Add | BinOpKind::Sub | BinOpKind::Mul)
395        {
396            let lhs = self.const_value(lhs)?;
397            let rhs = self.const_value(rhs)?;
398            return match op.kind {
399                BinOpKind::Add => Some(lhs.wrapping_add(rhs)),
400                BinOpKind::Sub => Some(lhs.wrapping_sub(rhs)),
401                BinOpKind::Mul => Some(lhs.wrapping_mul(rhs)),
402                _ => unreachable!(),
403            };
404        }
405        if let Some(value) = self.gcx.try_eval_const(expr).ok().and_then(|value| value.as_u256()) {
406            return Some(value);
407        }
408        None
409    }
410
411    fn const_bool(&self, expr: &'hir hir::Expr<'hir>) -> Option<bool> {
412        constant_bool(self.gcx, expr)
413    }
414
415    fn stmt_always_exits(&self, stmt: &'hir hir::Stmt<'hir>) -> bool {
416        match &stmt.kind {
417            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
418                block.stmts.iter().any(|stmt| self.stmt_always_exits(stmt))
419            }
420            StmtKind::If(condition, then_stmt, else_stmt) => match self.const_bool(condition) {
421                Some(true) => self.stmt_always_exits(then_stmt),
422                Some(false) => else_stmt.is_some_and(|else_stmt| self.stmt_always_exits(else_stmt)),
423                None => {
424                    self.stmt_always_exits(then_stmt)
425                        && else_stmt.is_some_and(|else_stmt| self.stmt_always_exits(else_stmt))
426                }
427            },
428            _ => branch_always_exits(stmt),
429        }
430    }
431
432    fn is_proven_low_s(&self, expr: &'hir hir::Expr<'hir>) -> bool {
433        self.const_value(expr).is_some_and(|value| value <= SECP256K1_HALF_ORDER)
434            || self.current_value(expr).is_some_and(|value| self.state.low_s.contains(&value))
435            || match &expr.peel_parens().kind {
436                ExprKind::Ternary(condition, then_expr, else_expr) => {
437                    match self.const_bool(condition) {
438                        Some(true) => self.is_proven_low_s(then_expr),
439                        Some(false) => self.is_proven_low_s(else_expr),
440                        None => self.is_proven_low_s(then_expr) && self.is_proven_low_s(else_expr),
441                    }
442                }
443                _ => false,
444            }
445    }
446
447    fn assigned_value(&self, rhs: Option<&'hir hir::Expr<'hir>>) -> AssignedValue {
448        AssignedValue {
449            value: rhs.and_then(|rhs| self.current_value(rhs)),
450            low_s: rhs.is_some_and(|rhs| self.is_proven_low_s(rhs)),
451        }
452    }
453
454    fn assign_var_value(&mut self, var: hir::VariableId, assigned: AssignedValue) {
455        let value = assigned.value.unwrap_or_else(|| self.fresh_value());
456        if assigned.low_s {
457            self.state.low_s.insert(value);
458        }
459        self.state.values.insert(var, value);
460    }
461
462    fn assign_var(&mut self, var: hir::VariableId, rhs: Option<&'hir hir::Expr<'hir>>) {
463        self.assign_var_value(var, self.assigned_value(rhs));
464    }
465
466    fn assign_lhs(&mut self, lhs: &'hir hir::Expr<'hir>, rhs: Option<&'hir hir::Expr<'hir>>) {
467        let mut assignments = Vec::new();
468        self.collect_assignments(lhs, rhs, &mut assignments);
469        for (var, assigned) in assignments {
470            self.assign_var_value(var, assigned);
471        }
472    }
473
474    fn collect_assignments(
475        &self,
476        lhs: &'hir hir::Expr<'hir>,
477        rhs: Option<&'hir hir::Expr<'hir>>,
478        assignments: &mut Vec<(hir::VariableId, AssignedValue)>,
479    ) {
480        if let ExprKind::Tuple(lhs_elems) = &lhs.peel_parens().kind {
481            let rhs_elems = match rhs.map(|rhs| &rhs.peel_parens().kind) {
482                Some(ExprKind::Tuple(elems)) => Some(*elems),
483                _ => None,
484            };
485            for (index, lhs) in lhs_elems.iter().enumerate() {
486                let Some(lhs) = lhs else { continue };
487                let rhs = rhs_elems.and_then(|elems| elems.get(index)).copied().flatten();
488                self.collect_assignments(lhs, rhs, assignments);
489            }
490        } else if let Some(var) = underlying_var(lhs) {
491            assignments.push((var, self.assigned_value(rhs)));
492        }
493    }
494
495    fn mark_deleted(&mut self, target: &'hir hir::Expr<'hir>) {
496        let Some(var) = underlying_var(target) else { return };
497        let value = self.fresh_value();
498        self.state.values.insert(var, value);
499        self.state.low_s.insert(value);
500    }
501
502    fn invalidate(&mut self, target: &'hir hir::Expr<'hir>) {
503        let Some(var) = underlying_var(target) else { return };
504        self.invalidate_var(var);
505    }
506
507    fn invalidate_var(&mut self, var: hir::VariableId) {
508        let value = self.fresh_value();
509        self.state.values.insert(var, value);
510    }
511
512    fn tracked_vars(&self) -> HashSet<hir::VariableId> {
513        self.state
514            .values
515            .keys()
516            .copied()
517            .chain(self.state.low_s.iter().filter_map(|value| match value {
518                ValueId::Initial(var) => Some(*var),
519                ValueId::Assigned(_) => None,
520            }))
521            .collect()
522    }
523
524    fn invalidate_mutable_state(&mut self) {
525        let vars: Vec<_> = self
526            .tracked_vars()
527            .into_iter()
528            .filter(|var| {
529                let var = self.hir.variable(*var);
530                var.kind.is_state() && !var.is_constant() && !var.is_immutable()
531            })
532            .collect();
533        for var in vars {
534            self.invalidate_var(var);
535        }
536    }
537
538    fn invalidate_loop_carried(&mut self, block: &'hir hir::Block<'hir>, source: LoopSource) {
539        if matches!(source, LoopSource::DoWhile)
540            && block.stmts.last().is_some_and(|stmt| {
541                matches!(
542                    &stmt.kind,
543                    StmtKind::If(condition, _, _) if self.const_bool(condition) == Some(false)
544                )
545            })
546        {
547            return;
548        }
549
550        let mut backedges = Vec::new();
551        if let Some(fallthrough) =
552            self.collect_loop_effects_stmts(block.stmts, LoopEffects::default(), &mut backedges)
553        {
554            backedges.push(fallthrough);
555        }
556        let Some(mut effects) = backedges.into_iter().reduce(|mut left, right| {
557            left.merge(right);
558            left
559        }) else {
560            return;
561        };
562        if matches!(source, LoopSource::ForWithUpdate)
563            && let Some(next) = for_loop_next_expr(block)
564        {
565            self.add_expr_effects(next, &mut effects);
566        }
567
568        if effects.all_values {
569            effects.values.extend(self.tracked_vars());
570        }
571        if effects.mutable_state {
572            self.invalidate_mutable_state();
573        }
574        for var in effects.values {
575            self.invalidate_var(var);
576        }
577    }
578
579    fn collect_loop_effects_stmts(
580        &self,
581        stmts: &'hir [hir::Stmt<'hir>],
582        mut effects: LoopEffects,
583        backedges: &mut Vec<LoopEffects>,
584    ) -> Option<LoopEffects> {
585        for stmt in stmts {
586            effects = self.collect_loop_effects_stmt(stmt, effects, backedges)?;
587        }
588        Some(effects)
589    }
590
591    fn collect_loop_effects_stmt(
592        &self,
593        stmt: &'hir hir::Stmt<'hir>,
594        mut effects: LoopEffects,
595        backedges: &mut Vec<LoopEffects>,
596    ) -> Option<LoopEffects> {
597        match &stmt.kind {
598            StmtKind::Break => None,
599            StmtKind::Continue => {
600                backedges.push(effects);
601                None
602            }
603            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
604                self.collect_loop_effects_stmts(block.stmts, effects, backedges)
605            }
606            StmtKind::If(condition, then_stmt, else_stmt) => {
607                self.add_expr_effects(condition, &mut effects);
608                match self.const_bool(condition) {
609                    Some(true) => self.collect_loop_effects_stmt(then_stmt, effects, backedges),
610                    Some(false) => {
611                        if let Some(else_stmt) = else_stmt {
612                            self.collect_loop_effects_stmt(else_stmt, effects, backedges)
613                        } else {
614                            Some(effects)
615                        }
616                    }
617                    None => {
618                        let after_then =
619                            self.collect_loop_effects_stmt(then_stmt, effects.clone(), backedges);
620                        let after_else = if let Some(else_stmt) = else_stmt {
621                            self.collect_loop_effects_stmt(else_stmt, effects, backedges)
622                        } else {
623                            Some(effects)
624                        };
625                        merge_loop_effect_paths(after_then, after_else)
626                    }
627                }
628            }
629            StmtKind::Try(stmt_try) => {
630                self.add_expr_effects(&stmt_try.expr, &mut effects);
631                let mut fallthrough = None;
632                for clause in stmt_try.clauses {
633                    let clause_effects = self.collect_loop_effects_stmts(
634                        clause.block.stmts,
635                        effects.clone(),
636                        backedges,
637                    );
638                    fallthrough = merge_loop_effect_paths(fallthrough, clause_effects);
639                }
640                fallthrough
641            }
642            StmtKind::Loop(..) => {
643                self.add_stmt_effects(stmt, &mut effects);
644                Some(effects)
645            }
646            StmtKind::Return(expr) => {
647                if let Some(expr) = expr {
648                    self.add_expr_effects(expr, &mut effects);
649                }
650                None
651            }
652            StmtKind::Revert(expr) => {
653                self.add_expr_effects(expr, &mut effects);
654                None
655            }
656            StmtKind::AssemblyBlock(_) | StmtKind::Err(_) => {
657                effects.all_values = true;
658                Some(effects)
659            }
660            _ => {
661                self.add_stmt_effects(stmt, &mut effects);
662                (!self.stmt_always_exits(stmt)).then_some(effects)
663            }
664        }
665    }
666
667    fn add_stmt_effects(&self, stmt: &'hir hir::Stmt<'hir>, effects: &mut LoopEffects) {
668        match &stmt.kind {
669            StmtKind::DeclSingle(var) => {
670                if let Some(initializer) = self.hir.variable(*var).initializer {
671                    self.add_expr_effects(initializer, effects);
672                }
673            }
674            StmtKind::DeclMulti(_, initializer)
675            | StmtKind::Emit(initializer)
676            | StmtKind::Revert(initializer)
677            | StmtKind::Expr(initializer) => self.add_expr_effects(initializer, effects),
678            StmtKind::Return(Some(expr)) => self.add_expr_effects(expr, effects),
679            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
680                for stmt in block.stmts {
681                    self.add_stmt_effects(stmt, effects);
682                }
683            }
684            StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => {
685                effects.all_values = true;
686            }
687            StmtKind::If(condition, then_stmt, else_stmt) => {
688                self.add_expr_effects(condition, effects);
689                self.add_stmt_effects(then_stmt, effects);
690                if let Some(else_stmt) = else_stmt {
691                    self.add_stmt_effects(else_stmt, effects);
692                }
693            }
694            StmtKind::Try(stmt_try) => {
695                self.add_expr_effects(&stmt_try.expr, effects);
696                for clause in stmt_try.clauses {
697                    for stmt in clause.block.stmts {
698                        self.add_stmt_effects(stmt, effects);
699                    }
700                }
701            }
702            StmtKind::Return(None)
703            | StmtKind::Break
704            | StmtKind::Continue
705            | StmtKind::Placeholder => {}
706        }
707    }
708
709    fn add_expr_effects(&self, expr: &'hir hir::Expr<'hir>, effects: &mut LoopEffects) {
710        match &expr.peel_parens().kind {
711            ExprKind::Assign(lhs, _, rhs) => {
712                self.add_expr_effects(lhs, effects);
713                self.add_expr_effects(rhs, effects);
714                collect_lhs_vars(lhs, &mut effects.values);
715            }
716            ExprKind::Delete(target) => {
717                self.add_expr_effects(target, effects);
718                collect_lhs_vars(target, &mut effects.values);
719            }
720            ExprKind::Unary(op, target) => {
721                self.add_expr_effects(target, effects);
722                if matches!(
723                    op.kind,
724                    UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec
725                ) {
726                    collect_lhs_vars(target, &mut effects.values);
727                }
728            }
729            ExprKind::Array(exprs) => {
730                for expr in *exprs {
731                    self.add_expr_effects(expr, effects);
732                }
733            }
734            ExprKind::Binary(lhs, op, rhs) => {
735                self.add_expr_effects(lhs, effects);
736                let short_circuits = matches!(op.kind, BinOpKind::And | BinOpKind::Or)
737                    && self
738                        .const_bool(lhs)
739                        .is_some_and(|value| matches!(op.kind, BinOpKind::And) != value);
740                if !short_circuits {
741                    self.add_expr_effects(rhs, effects);
742                }
743            }
744            ExprKind::Call(callee, args, options) => {
745                self.add_expr_effects(callee, effects);
746                for arg in args.exprs() {
747                    self.add_expr_effects(arg, effects);
748                }
749                if let Some(options) = options {
750                    for arg in options.args {
751                        self.add_expr_effects(&arg.value, effects);
752                    }
753                }
754                effects.mutable_state |= call_may_mutate_state(self.gcx, self.hir, callee);
755            }
756            ExprKind::Index(base, index) => {
757                self.add_expr_effects(base, effects);
758                if let Some(index) = index {
759                    self.add_expr_effects(index, effects);
760                }
761            }
762            ExprKind::Slice(base, start, end) => {
763                self.add_expr_effects(base, effects);
764                if let Some(start) = start {
765                    self.add_expr_effects(start, effects);
766                }
767                if let Some(end) = end {
768                    self.add_expr_effects(end, effects);
769                }
770            }
771            ExprKind::Member(base, _) | ExprKind::Payable(base) | ExprKind::YulMember(base, _) => {
772                self.add_expr_effects(base, effects);
773            }
774            ExprKind::Ternary(condition, then_expr, else_expr) => {
775                self.add_expr_effects(condition, effects);
776                match self.const_bool(condition) {
777                    Some(true) => self.add_expr_effects(then_expr, effects),
778                    Some(false) => self.add_expr_effects(else_expr, effects),
779                    None => {
780                        self.add_expr_effects(then_expr, effects);
781                        self.add_expr_effects(else_expr, effects);
782                    }
783                }
784            }
785            ExprKind::Tuple(exprs) => {
786                for expr in exprs.iter().flatten() {
787                    self.add_expr_effects(expr, effects);
788                }
789            }
790            ExprKind::Lit(_)
791            | ExprKind::Ident(_)
792            | ExprKind::New(_)
793            | ExprKind::TypeCall(_)
794            | ExprKind::Type(_)
795            | ExprKind::Err(_) => {}
796        }
797    }
798
799    fn add_facts(&mut self, predicate: &'hir hir::Expr<'hir>, negate: bool) {
800        if expr_has_fact_side_effect(self.gcx, self.hir, predicate) {
801            return;
802        }
803        match &predicate.peel_parens().kind {
804            ExprKind::Ternary(condition, then_expr, else_expr) => {
805                if let Some(value) = self.const_bool(condition) {
806                    self.add_facts(if value { then_expr } else { else_expr }, negate);
807                }
808            }
809            ExprKind::Binary(lhs, op, rhs) => {
810                if matches!(op.kind, BinOpKind::And | BinOpKind::Or) {
811                    if let Some(value) = self.const_bool(lhs) {
812                        let determines_result = matches!(op.kind, BinOpKind::And) != value;
813                        if !determines_result {
814                            self.add_facts(rhs, negate);
815                        }
816                        return;
817                    }
818                    if let Some(value) = self.const_bool(rhs) {
819                        let determines_result = matches!(op.kind, BinOpKind::And) != value;
820                        if !determines_result {
821                            self.add_facts(lhs, negate);
822                        }
823                        return;
824                    }
825                }
826                let conjunctive =
827                    matches!((op.kind, negate), (BinOpKind::And, false) | (BinOpKind::Or, true));
828                let disjunctive =
829                    matches!((op.kind, negate), (BinOpKind::Or, false) | (BinOpKind::And, true));
830                if conjunctive {
831                    self.add_facts(lhs, negate);
832                    self.add_facts(rhs, negate);
833                } else if disjunctive {
834                    self.add_disjunctive_facts(lhs, rhs, negate);
835                } else {
836                    self.add_comparison_fact(lhs, op.kind, rhs, negate);
837                }
838            }
839            ExprKind::Unary(op, inner) if op.kind == UnOpKind::Not => {
840                self.add_facts(inner, !negate);
841            }
842            _ => {}
843        }
844    }
845
846    fn assume(&mut self, predicate: &'hir hir::Expr<'hir>, negate: bool) {
847        self.add_facts(predicate, negate);
848        self.validate_pending();
849    }
850
851    fn add_disjunctive_facts(
852        &mut self,
853        lhs: &'hir hir::Expr<'hir>,
854        rhs: &'hir hir::Expr<'hir>,
855        negate: bool,
856    ) {
857        let baseline = self.state.low_s.clone();
858        self.add_facts(lhs, negate);
859        let lhs_added: HashSet<_> = self.state.low_s.difference(&baseline).copied().collect();
860        self.state.low_s.clone_from(&baseline);
861        self.add_facts(rhs, negate);
862        let rhs_added: HashSet<_> = self.state.low_s.difference(&baseline).copied().collect();
863        self.state.low_s = baseline;
864        self.state.low_s.extend(lhs_added.intersection(&rhs_added).copied());
865    }
866
867    fn add_comparison_fact(
868        &mut self,
869        lhs: &'hir hir::Expr<'hir>,
870        op: BinOpKind,
871        rhs: &'hir hir::Expr<'hir>,
872        negate: bool,
873    ) {
874        let op = if negate { negate_comparison(op) } else { op };
875        for (candidate, bound, op) in [(lhs, rhs, op), (rhs, lhs, reverse_comparison(op))] {
876            let Some(value) = self.current_value(candidate) else { continue };
877            let Some(bound) = self.const_value(bound) else { continue };
878            let proves_low = match op {
879                BinOpKind::Lt => bound <= SECP256K1_HALF_ORDER + U256::from(1),
880                BinOpKind::Le => bound <= SECP256K1_HALF_ORDER,
881                BinOpKind::Eq => bound <= SECP256K1_HALF_ORDER,
882                _ => false,
883            };
884            if proves_low {
885                self.state.low_s.insert(value);
886            }
887        }
888    }
889
890    fn pending_recovery(&self, expr: &'hir hir::Expr<'hir>) -> Option<PendingRecovery> {
891        let expr = expr.peel_parens();
892        let ExprKind::Call(callee, args, _) = &expr.kind else { return None };
893        if !is_ecrecover_builtin(self.gcx, callee) || args.len() != 4 {
894            return None;
895        }
896        let signature = args.exprs().nth(3)?;
897        (!self.is_proven_low_s(signature))
898            .then(|| PendingRecovery { signature: self.current_value(signature), span: expr.span })
899    }
900
901    fn join_all(&mut self, states: Vec<FlowState>) -> Option<FlowState> {
902        states.into_iter().reduce(|left, right| self.join(left, right))
903    }
904
905    fn visit_loop_stmts(
906        &mut self,
907        stmts: &'hir [hir::Stmt<'hir>],
908        exits: &mut Vec<FlowState>,
909    ) -> Option<FlowState> {
910        for stmt in stmts {
911            self.visit_loop_stmt(stmt, exits)?;
912        }
913        Some(self.snapshot())
914    }
915
916    fn visit_loop_stmt(
917        &mut self,
918        stmt: &'hir hir::Stmt<'hir>,
919        exits: &mut Vec<FlowState>,
920    ) -> Option<FlowState> {
921        match &stmt.kind {
922            StmtKind::Break | StmtKind::Continue => {
923                exits.push(self.snapshot());
924                None
925            }
926            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
927                self.visit_loop_stmts(block.stmts, exits)
928            }
929            StmtKind::If(condition, then_stmt, else_stmt) => {
930                let constant = self.const_bool(condition);
931                let _ = self.visit_expr(condition);
932                if let Some(value) = constant {
933                    self.assume(condition, !value);
934                    return if value {
935                        self.visit_loop_stmt(then_stmt, exits)
936                    } else if let Some(else_stmt) = else_stmt {
937                        self.visit_loop_stmt(else_stmt, exits)
938                    } else {
939                        Some(self.snapshot())
940                    };
941                }
942                let baseline = self.snapshot();
943
944                self.assume(condition, false);
945                let after_then = self.visit_loop_stmt(then_stmt, exits);
946
947                self.restore(baseline);
948                self.assume(condition, true);
949                let after_else = if let Some(else_stmt) = else_stmt {
950                    self.visit_loop_stmt(else_stmt, exits)
951                } else {
952                    Some(self.snapshot())
953                };
954
955                let joined = match (after_then, after_else) {
956                    (Some(then_state), Some(else_state)) => self.join(then_state, else_state),
957                    (Some(state), None) | (None, Some(state)) => state,
958                    (None, None) => return None,
959                };
960                self.restore(joined.clone());
961                Some(joined)
962            }
963            StmtKind::Try(stmt_try) => {
964                let _ = self.visit_expr(&stmt_try.expr);
965                let after_call = self.snapshot();
966                let mut fallthrough = Vec::new();
967                for clause in stmt_try.clauses {
968                    // Argument expressions execute in the caller even when the external call
969                    // reverts, so caught paths must retain their effects. Keeping the call's
970                    // effects too is conservative for mutable state.
971                    self.restore(after_call.clone());
972                    if let Some(state) = self.visit_loop_stmts(clause.block.stmts, exits) {
973                        fallthrough.push(state);
974                    }
975                }
976                let joined = self.join_all(fallthrough)?;
977                self.restore(joined.clone());
978                Some(joined)
979            }
980            _ => {
981                let _ = self.visit_stmt(stmt);
982                (!self.stmt_always_exits(stmt)).then(|| self.snapshot())
983            }
984        }
985    }
986}
987
988impl<'hir> Visit<'hir> for Analyzer<'hir> {
989    type BreakValue = Never;
990
991    fn hir(&self) -> &'hir hir::Hir<'hir> {
992        self.hir
993    }
994
995    fn visit_stmt(&mut self, stmt: &'hir hir::Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
996        match &stmt.kind {
997            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
998                for stmt in block.stmts {
999                    let _ = self.visit_stmt(stmt);
1000                    if self.stmt_always_exits(stmt) {
1001                        break;
1002                    }
1003                }
1004                return ControlFlow::Continue(());
1005            }
1006            StmtKind::If(condition, then_stmt, else_stmt) => {
1007                let constant = self.const_bool(condition);
1008                let _ = self.visit_expr(condition);
1009                if let Some(value) = constant {
1010                    self.assume(condition, !value);
1011                    if value {
1012                        let _ = self.visit_stmt(then_stmt);
1013                    } else if let Some(else_stmt) = else_stmt {
1014                        let _ = self.visit_stmt(else_stmt);
1015                    }
1016                    return ControlFlow::Continue(());
1017                }
1018                let baseline = self.snapshot();
1019
1020                self.assume(condition, false);
1021                let _ = self.visit_stmt(then_stmt);
1022                let then_exits = self.stmt_always_exits(then_stmt);
1023                let after_then = self.snapshot();
1024
1025                self.restore(baseline);
1026                self.assume(condition, true);
1027                let else_exits = if let Some(else_stmt) = else_stmt {
1028                    let _ = self.visit_stmt(else_stmt);
1029                    self.stmt_always_exits(else_stmt)
1030                } else {
1031                    false
1032                };
1033                let after_else = self.snapshot();
1034
1035                let joined = match (then_exits, else_exits) {
1036                    (true, false) => after_else,
1037                    (false, true) => after_then,
1038                    _ => self.join(after_then, after_else),
1039                };
1040                self.restore(joined);
1041                return ControlFlow::Continue(());
1042            }
1043            StmtKind::Loop(block, source) => {
1044                let baseline = self.snapshot();
1045                self.invalidate_loop_carried(block, *source);
1046                let mut exits = Vec::new();
1047                if let Some(fallthrough) = self.visit_loop_stmts(block.stmts, &mut exits) {
1048                    exits.push(fallthrough);
1049                }
1050                let joined = self.join_all(exits).unwrap_or(baseline);
1051                self.restore(joined);
1052                return ControlFlow::Continue(());
1053            }
1054            StmtKind::Try(stmt_try) => {
1055                let _ = self.visit_expr(&stmt_try.expr);
1056                let after_call = self.snapshot();
1057                let mut fallthrough = Vec::new();
1058                for clause in stmt_try.clauses {
1059                    self.restore(after_call.clone());
1060                    for stmt in clause.block.stmts {
1061                        let _ = self.visit_stmt(stmt);
1062                        if self.stmt_always_exits(stmt) {
1063                            break;
1064                        }
1065                    }
1066                    if !clause.block.stmts.iter().any(|stmt| self.stmt_always_exits(stmt)) {
1067                        fallthrough.push(self.snapshot());
1068                    }
1069                }
1070                let joined = fallthrough
1071                    .into_iter()
1072                    .reduce(|left, right| self.join(left, right))
1073                    .unwrap_or(after_call);
1074                self.restore(joined);
1075                return ControlFlow::Continue(());
1076            }
1077            StmtKind::DeclSingle(var) => {
1078                let init = self.hir.variable(*var).initializer;
1079                if let Some(init) = init {
1080                    let mut calls = HashSet::new();
1081                    self.collect_result_calls(init, &mut calls);
1082                    let targets: HashMap<_, _> =
1083                        calls.into_iter().map(|call| (call, *var)).collect();
1084                    let recoveries = self.visit_stored_expr(init, true, &targets);
1085                    self.assign_var(*var, Some(init));
1086                    for (var, recovery) in recoveries {
1087                        self.record_pending(var, recovery);
1088                    }
1089                } else {
1090                    self.assign_var(*var, None);
1091                }
1092                return ControlFlow::Continue(());
1093            }
1094            StmtKind::DeclMulti(vars, init) => {
1095                let mut targets = HashMap::new();
1096                if let ExprKind::Tuple(exprs) = &init.peel_parens().kind {
1097                    for (var, expr) in vars.iter().zip(exprs.iter()) {
1098                        if let (Some(var), Some(expr)) = (var, expr) {
1099                            let mut calls = HashSet::new();
1100                            self.collect_result_calls(expr, &mut calls);
1101                            targets.extend(calls.into_iter().map(|call| (call, *var)));
1102                        }
1103                    }
1104                }
1105                let recoveries = self.visit_stored_expr(init, !targets.is_empty(), &targets);
1106                if let ExprKind::Tuple(exprs) = &init.peel_parens().kind {
1107                    for (var, expr) in vars.iter().zip(exprs.iter()) {
1108                        if let Some(var) = var {
1109                            self.assign_var(*var, *expr);
1110                        }
1111                    }
1112                } else {
1113                    for var in vars.iter().flatten() {
1114                        self.assign_var(*var, None);
1115                    }
1116                }
1117                for (var, recovery) in recoveries {
1118                    self.record_pending(var, recovery);
1119                }
1120                return ControlFlow::Continue(());
1121            }
1122            StmtKind::Expr(expr) => {
1123                self.visit_discarded_expr(expr);
1124                return ControlFlow::Continue(());
1125            }
1126            StmtKind::Err(_) | StmtKind::AssemblyBlock(_) => {
1127                // Inline assembly is opaque and may observe any local before changing it. Flush
1128                // deferred recoveries before discarding facts so assembly cannot hide a warning.
1129                self.use_all_pending();
1130                self.state = FlowState::default();
1131            }
1132            StmtKind::Return(None) => {
1133                self.use_return_values();
1134            }
1135            _ => {}
1136        }
1137        self.walk_stmt(stmt)
1138    }
1139
1140    fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
1141        if matches!(expr.kind, ExprKind::Ident(_))
1142            && let Some(value) = self.current_value(expr)
1143        {
1144            self.use_value(value);
1145        }
1146        if let ExprKind::Binary(lhs, op, rhs) = &expr.kind
1147            && matches!(op.kind, BinOpKind::And | BinOpKind::Or)
1148        {
1149            let constant = self.const_bool(lhs);
1150            let _ = self.visit_expr(lhs);
1151            if let Some(value) = constant {
1152                let short_circuits = matches!(op.kind, BinOpKind::And) != value;
1153                if !short_circuits {
1154                    self.assume(lhs, !value);
1155                    let _ = self.visit_expr(rhs);
1156                }
1157                return ControlFlow::Continue(());
1158            }
1159            let skipped_rhs = self.snapshot();
1160            self.assume(lhs, op.kind == BinOpKind::Or);
1161            let _ = self.visit_expr(rhs);
1162            let ran_rhs = self.snapshot();
1163            let joined = self.join(skipped_rhs, ran_rhs);
1164            self.restore(joined);
1165            return ControlFlow::Continue(());
1166        }
1167        if let ExprKind::Ternary(condition, then_expr, else_expr) = &expr.kind {
1168            let constant = self.const_bool(condition);
1169            let _ = self.visit_expr(condition);
1170            if let Some(value) = constant {
1171                self.assume(condition, !value);
1172                let _ = self.visit_expr(if value { then_expr } else { else_expr });
1173                return ControlFlow::Continue(());
1174            }
1175            let baseline = self.snapshot();
1176            self.assume(condition, false);
1177            let _ = self.visit_expr(then_expr);
1178            let after_then = self.snapshot();
1179            self.restore(baseline);
1180            self.assume(condition, true);
1181            let _ = self.visit_expr(else_expr);
1182            let after_else = self.snapshot();
1183            let joined = self.join(after_then, after_else);
1184            self.restore(joined);
1185            return ControlFlow::Continue(());
1186        }
1187
1188        match &expr.kind {
1189            ExprKind::Call(callee, args, _) if is_require_or_assert(callee) => {
1190                let result = self.walk_expr(expr);
1191                if let Some(condition) = args.exprs().next() {
1192                    self.assume(condition, false);
1193                }
1194                return result;
1195            }
1196            ExprKind::Assign(lhs, op, rhs) => {
1197                if op.is_none() {
1198                    let target = self.deferable_target(lhs);
1199                    let mut targets = HashMap::new();
1200                    let mut observable = HashSet::new();
1201                    self.collect_recovery_targets(lhs, rhs, &mut targets, &mut observable);
1202                    let result_is_stored = self.stored_results.contains(&expr.peel_parens().id);
1203                    self.visit_assignment_lhs(lhs);
1204                    let deferred_calls = self.deferred_calls.clone();
1205                    self.deferred_calls.retain(|call| !observable.contains(call));
1206                    let recoveries = self.visit_stored_expr(
1207                        rhs,
1208                        !targets.is_empty() || target.is_some(),
1209                        &targets,
1210                    );
1211                    self.deferred_calls = deferred_calls;
1212                    self.assign_lhs(lhs, Some(rhs));
1213                    for (var, recovery) in recoveries {
1214                        self.record_pending(var, recovery);
1215                    }
1216                    if let Some(target) = target
1217                        && !result_is_stored
1218                    {
1219                        self.use_value(self.state.value(target));
1220                    }
1221                    return ControlFlow::Continue(());
1222                }
1223                let result = self.walk_expr(expr);
1224                self.assign_lhs(lhs, None);
1225                return result;
1226            }
1227            ExprKind::Delete(target) => {
1228                self.visit_assignment_lhs(target);
1229                self.mark_deleted(target);
1230                return ControlFlow::Continue(());
1231            }
1232            ExprKind::Unary(op, target)
1233                if matches!(
1234                    op.kind,
1235                    UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec
1236                ) =>
1237            {
1238                let result = self.walk_expr(expr);
1239                self.invalidate(target);
1240                return result;
1241            }
1242            _ => {}
1243        }
1244
1245        let result = self.walk_expr(expr);
1246        if let ExprKind::Call(callee, _, _) = &expr.kind
1247            && call_may_mutate_state(self.gcx, self.hir, callee)
1248        {
1249            self.invalidate_mutable_state();
1250        }
1251        if let Some(recovery) = self.pending_recovery(expr) {
1252            if self.deferred_calls.contains(&expr.peel_parens().id) {
1253                self.captured_recoveries.insert(expr.peel_parens().id, recovery);
1254            } else {
1255                self.emit_hit(recovery.span);
1256            }
1257        }
1258        result
1259    }
1260}
1261
1262fn underlying_var(expr: &hir::Expr<'_>) -> Option<hir::VariableId> {
1263    match &expr.peel_parens().kind {
1264        ExprKind::Ident(reses) => reses.iter().find_map(|res| match res {
1265            Res::Item(ItemId::Variable(var)) => Some(*var),
1266            _ => None,
1267        }),
1268        ExprKind::Call(callee, args, _)
1269            if is_transparent_signature_cast(callee) && args.len() == 1 =>
1270        {
1271            args.exprs().next().and_then(underlying_var)
1272        }
1273        _ => None,
1274    }
1275}
1276
1277fn collect_lhs_vars(expr: &hir::Expr<'_>, vars: &mut HashSet<hir::VariableId>) {
1278    if let ExprKind::Tuple(exprs) = &expr.peel_parens().kind {
1279        for expr in exprs.iter().flatten() {
1280            collect_lhs_vars(expr, vars);
1281        }
1282    } else if let Some(var) = underlying_var(expr) {
1283        vars.insert(var);
1284    }
1285}
1286
1287fn for_loop_next_expr<'hir>(block: &'hir hir::Block<'hir>) -> Option<&'hir hir::Expr<'hir>> {
1288    let [stmt] = block.stmts else { return None };
1289    let stmt = match &stmt.kind {
1290        StmtKind::If(_, then_stmt, _) => *then_stmt,
1291        _ => stmt,
1292    };
1293    let StmtKind::Block(inner) = &stmt.kind else { return None };
1294    if inner.span != block.span {
1295        return None;
1296    }
1297    let [_, next] = inner.stmts else { return None };
1298    let StmtKind::Expr(next) = &next.kind else { return None };
1299    Some(*next)
1300}
1301
1302fn merge_loop_effect_paths(
1303    left: Option<LoopEffects>,
1304    right: Option<LoopEffects>,
1305) -> Option<LoopEffects> {
1306    match (left, right) {
1307        (Some(mut left), Some(right)) => {
1308            left.merge(right);
1309            Some(left)
1310        }
1311        (Some(effects), None) | (None, Some(effects)) => Some(effects),
1312        (None, None) => None,
1313    }
1314}
1315
1316fn is_transparent_signature_cast(callee: &hir::Expr<'_>) -> bool {
1317    matches!(
1318        &callee.peel_parens().kind,
1319        ExprKind::Type(hir::Type {
1320            kind: TypeKind::Elementary(
1321                ElementaryType::UInt(size) | ElementaryType::FixedBytes(size)
1322            ),
1323            ..
1324        }) if size.bits() == 256
1325    )
1326}
1327
1328fn is_ecrecover_builtin(gcx: Gcx<'_>, callee: &hir::Expr<'_>) -> bool {
1329    if let Some(resolved) = gcx.resolved_callee(callee.peel_parens().id) {
1330        return matches!(resolved.res, Res::Builtin(Builtin::EcRecover));
1331    }
1332    matches!(
1333        &callee.peel_parens().kind,
1334        ExprKind::Ident(reses)
1335            if reses.iter().any(|res| matches!(res, Res::Builtin(Builtin::EcRecover)))
1336    )
1337}
1338
1339const fn negate_comparison(op: BinOpKind) -> BinOpKind {
1340    match op {
1341        BinOpKind::Lt => BinOpKind::Ge,
1342        BinOpKind::Le => BinOpKind::Gt,
1343        BinOpKind::Gt => BinOpKind::Le,
1344        BinOpKind::Ge => BinOpKind::Lt,
1345        BinOpKind::Eq => BinOpKind::Ne,
1346        BinOpKind::Ne => BinOpKind::Eq,
1347        _ => op,
1348    }
1349}
1350
1351const fn reverse_comparison(op: BinOpKind) -> BinOpKind {
1352    match op {
1353        BinOpKind::Lt => BinOpKind::Gt,
1354        BinOpKind::Le => BinOpKind::Ge,
1355        BinOpKind::Gt => BinOpKind::Lt,
1356        BinOpKind::Ge => BinOpKind::Le,
1357        _ => op,
1358    }
1359}
1360
1361fn call_may_mutate_state(gcx: Gcx<'_>, hir: &hir::Hir<'_>, callee: &hir::Expr<'_>) -> bool {
1362    let callee = callee.peel_parens();
1363    if matches!(callee.kind, ExprKind::Type(_)) {
1364        return false;
1365    }
1366    if let Some(ty) = gcx.type_of_expr(callee.id)
1367        && let TyKind::Fn(function) = ty.peel_refs().kind
1368    {
1369        return function.state_mutability > StateMutability::View;
1370    }
1371    match &callee.kind {
1372        ExprKind::Ident(reses) => !reses.iter().all(|res| match res {
1373            Res::Builtin(_) => true,
1374            Res::Item(ItemId::Function(function)) => {
1375                hir.function(*function).state_mutability <= StateMutability::View
1376            }
1377            _ => false,
1378        }),
1379        _ => true,
1380    }
1381}
1382
1383fn constant_bool(gcx: Gcx<'_>, expr: &hir::Expr<'_>) -> Option<bool> {
1384    match gcx.try_eval_const_value(expr).ok()? {
1385        ConstValue::Bool(value) => Some(*value),
1386        _ => None,
1387    }
1388}
1389
1390fn expr_has_fact_side_effect(gcx: Gcx<'_>, hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
1391    match &expr.peel_parens().kind {
1392        ExprKind::Assign(..) | ExprKind::Delete(_) => true,
1393        ExprKind::Unary(op, inner) => {
1394            matches!(
1395                op.kind,
1396                UnOpKind::PreInc | UnOpKind::PreDec | UnOpKind::PostInc | UnOpKind::PostDec
1397            ) || expr_has_fact_side_effect(gcx, hir, inner)
1398        }
1399        ExprKind::Array(exprs) => {
1400            exprs.iter().any(|expr| expr_has_fact_side_effect(gcx, hir, expr))
1401        }
1402        ExprKind::Binary(lhs, op, rhs) => {
1403            if expr_has_fact_side_effect(gcx, hir, lhs) {
1404                return true;
1405            }
1406            let short_circuits = matches!(op.kind, BinOpKind::And | BinOpKind::Or)
1407                && constant_bool(gcx, lhs)
1408                    .is_some_and(|value| matches!(op.kind, BinOpKind::And) != value);
1409            !short_circuits && expr_has_fact_side_effect(gcx, hir, rhs)
1410        }
1411        ExprKind::Call(callee, args, options) => {
1412            call_may_mutate_state(gcx, hir, callee)
1413                || expr_has_fact_side_effect(gcx, hir, callee)
1414                || args.exprs().any(|expr| expr_has_fact_side_effect(gcx, hir, expr))
1415                || options.is_some_and(|options| {
1416                    options.args.iter().any(|arg| expr_has_fact_side_effect(gcx, hir, &arg.value))
1417                })
1418        }
1419        ExprKind::Index(base, index) => {
1420            expr_has_fact_side_effect(gcx, hir, base)
1421                || index.is_some_and(|expr| expr_has_fact_side_effect(gcx, hir, expr))
1422        }
1423        ExprKind::Slice(base, start, end) => {
1424            expr_has_fact_side_effect(gcx, hir, base)
1425                || start.is_some_and(|expr| expr_has_fact_side_effect(gcx, hir, expr))
1426                || end.is_some_and(|expr| expr_has_fact_side_effect(gcx, hir, expr))
1427        }
1428        ExprKind::Member(base, _) | ExprKind::Payable(base) | ExprKind::YulMember(base, _) => {
1429            expr_has_fact_side_effect(gcx, hir, base)
1430        }
1431        ExprKind::Ternary(condition, then_expr, else_expr) => {
1432            if expr_has_fact_side_effect(gcx, hir, condition) {
1433                return true;
1434            }
1435            match constant_bool(gcx, condition) {
1436                Some(true) => expr_has_fact_side_effect(gcx, hir, then_expr),
1437                Some(false) => expr_has_fact_side_effect(gcx, hir, else_expr),
1438                None => {
1439                    expr_has_fact_side_effect(gcx, hir, then_expr)
1440                        || expr_has_fact_side_effect(gcx, hir, else_expr)
1441                }
1442            }
1443        }
1444        ExprKind::Tuple(exprs) => {
1445            exprs.iter().flatten().any(|expr| expr_has_fact_side_effect(gcx, hir, expr))
1446        }
1447        ExprKind::Lit(_)
1448        | ExprKind::Ident(_)
1449        | ExprKind::New(_)
1450        | ExprKind::TypeCall(_)
1451        | ExprKind::Type(_)
1452        | ExprKind::Err(_) => false,
1453    }
1454}