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
35const 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 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 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 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}