Skip to main content

foundry_evm_symbolic/runtime/solver/fallback/
mod.rs

1//! Bounded fallback model generation for hard arithmetic constraints.
2
3use super::*;
4
5impl SymBoolExpr {
6    pub(crate) fn contains_hard_arith(&self) -> bool {
7        self.visit_bool(is_hard_arith_node)
8    }
9
10    fn contains_symbolic_hash(&self) -> bool {
11        self.visit_bool(|expr| matches!(expr.kind(), SymExprKind::Hash { .. }))
12    }
13}
14
15impl SymExpr {
16    #[cfg(test)]
17    pub(crate) fn contains_hard_arith(&self) -> bool {
18        self.visit_bool(is_hard_arith_node)
19    }
20
21    fn contains_var(&self) -> bool {
22        self.visit_bool(|expr| {
23            matches!(
24                expr.kind(),
25                SymExprKind::Var(_) | SymExprKind::Keccak { .. } | SymExprKind::Hash { .. }
26            )
27        })
28    }
29}
30
31fn is_hard_arith_node(expr: &SymExpr) -> bool {
32    match expr.kind() {
33        SymExprKind::BinOp(SymBinOp::Mul, left, right) => {
34            left.contains_var() && right.contains_var()
35        }
36        SymExprKind::BinOp(
37            SymBinOp::UDiv | SymBinOp::URem | SymBinOp::SDiv | SymBinOp::SRem,
38            left,
39            right,
40        ) => left.contains_var() || right.contains_var(),
41        SymExprKind::TernOp(_, left, right, modulus) => {
42            left.contains_var() || right.contains_var() || modulus.contains_var()
43        }
44        _ => false,
45    }
46}
47
48/// Returns whether local hard-arithmetic search should run before asking the solver.
49pub(crate) fn constraints_prefer_hard_arith_fallback_first(
50    cx: &SymCx,
51    constraints: &[SymBoolExpr],
52) -> bool {
53    if !constraints.iter().any(SymBoolExpr::contains_hard_arith)
54        || constraints.iter().any(SymBoolExpr::contains_symbolic_hash)
55    {
56        return false;
57    }
58
59    let mut vars = SymbolicVars::default();
60    for constraint in constraints {
61        collect_bool_fallback_vars(constraint, &mut vars);
62    }
63    let vars = fallback_search_vars(cx, vars, constraints);
64    !vars.is_empty() && vars.len() <= HARD_ARITH_FALLBACK_MAX_VARS
65}
66
67pub(crate) fn hard_arith_fallback_model(
68    cx: &SymCx,
69    constraints: &[SymBoolExpr],
70) -> Option<SymbolicModel> {
71    if !constraints.iter().any(SymBoolExpr::contains_hard_arith)
72        || constraints.iter().any(SymBoolExpr::contains_symbolic_hash)
73    {
74        return None;
75    }
76
77    let mut vars = SymbolicVars::default();
78    let mut constants = HashSet::<U256>::default();
79    for constraint in constraints {
80        collect_bool_fallback_vars(constraint, &mut vars);
81        collect_bool_constants(constraint, &mut constants);
82    }
83    let mut constants = constants.into_iter().collect::<Vec<_>>();
84    constants.sort_unstable();
85    let vars = fallback_search_vars(cx, vars, constraints);
86    if vars.is_empty() || vars.len() > HARD_ARITH_FALLBACK_MAX_VARS {
87        return None;
88    }
89
90    let candidates = vars
91        .iter()
92        .map(|var| fallback_candidates_for_var(var, constraints, &constants))
93        .collect::<Option<Vec<_>>>()?;
94    let searched_vars = vars.iter().copied().collect::<SymbolicVars>();
95    let constraint_vars = constraints
96        .iter()
97        .map(|constraint| {
98            let mut vars = SymbolicVars::default();
99            constraint.collect_vars(&mut vars);
100            vars
101        })
102        .collect::<Vec<_>>();
103    let mut model = SymbolicModel::default();
104    let mut assignments = 0usize;
105    let search = FallbackSearch {
106        constraints,
107        constraint_vars: &constraint_vars,
108        searched_vars: &searched_vars,
109        vars: &vars,
110        candidates: &candidates,
111    };
112    search.model(0, &mut model, &mut assignments)
113}
114
115// Constructive checked-multiply modeling is optional. Bound repeated support scans to keep a miss
116// from consuming more work than the solver fallback it is intended to avoid.
117const MAX_CHECKED_MUL_SUPPORT_VISITS: usize = 256;
118
119/// Constructs and validates a concrete model for a checked-multiply guard branch.
120///
121/// Solidity's guard is `x == 0 || (x * y) / x == y`. The assignments below represent its
122/// semantic cases directly: the zero disjunct, a nonzero exact product, and wrapping products in
123/// either operand order. Simple support constraints are completed first so an exact operand value
124/// from the path is preserved instead of being overwritten by the semantic default. This does not
125/// perform the generic bounded candidate search, and a model is returned only when it satisfies
126/// every original constraint.
127pub(super) fn checked_mul_guard_branch_model(
128    cx: &SymCx,
129    constraints: &[SymBoolExpr],
130    original_constraints: &[SymBoolExpr],
131    replayable_storage: &SymbolicVars,
132) -> Option<SymbolicModel> {
133    let mut eval_vars = SymbolicVars::default();
134    for constraint in original_constraints {
135        constraint.collect_eval_vars(&mut eval_vars);
136    }
137    if eval_vars
138        .iter()
139        .any(|var| !cx.is_replayable_input(*var) && !replayable_storage.contains(var))
140    {
141        return None;
142    }
143
144    let mut remaining_support_visits = MAX_CHECKED_MUL_SUPPORT_VISITS;
145    let mut candidates = Vec::new();
146    let mut seen = HashSet::<&SymBoolExpr>::default();
147    let mut pending = Vec::new();
148    for constraint in constraints {
149        if !seen.insert(constraint) {
150            continue;
151        }
152        if remaining_support_visits == 0 {
153            return None;
154        }
155        remaining_support_visits -= 1;
156        if let Some(candidate) = checked_mul_guard_branch(constraint) {
157            candidates.push(candidate);
158        }
159        match constraint.kind() {
160            SymBoolExprKind::Not(inner) => pending.push(inner),
161            SymBoolExprKind::And(values) => pending.extend(values.iter()),
162            SymBoolExprKind::Const(_) | SymBoolExprKind::Cmp(_, _, _) => {}
163        }
164    }
165
166    let mut nested = Vec::new();
167    while let Some(constraint) = pending.pop() {
168        if !seen.insert(constraint) {
169            continue;
170        }
171        if remaining_support_visits == 0 {
172            return None;
173        }
174        remaining_support_visits -= 1;
175        nested.push(constraint);
176        match constraint.kind() {
177            SymBoolExprKind::Not(inner) => pending.push(inner),
178            SymBoolExprKind::And(values) => pending.extend(values.iter()),
179            SymBoolExprKind::Const(_) | SymBoolExprKind::Cmp(_, _, _) => {}
180        }
181    }
182    for constraint in nested.into_iter().rev() {
183        if let Some(candidate) = checked_mul_guard_branch(constraint) {
184            candidates.push(candidate);
185        }
186    }
187
188    for (zero_operand, expected, guard_is_true) in candidates {
189        let assignments = if guard_is_true {
190            [(U256::ZERO, U256::ZERO), (U256::ONE, U256::ONE)]
191        } else {
192            [(U256::MAX, U256::from(2)), (U256::from(2), U256::MAX)]
193        };
194        for (zero_default, expected_default) in assignments {
195            let seed_orders = [
196                [(&zero_operand, zero_default), (&expected, expected_default)],
197                [(&expected, expected_default), (&zero_operand, zero_default)],
198            ];
199            for seeds in seed_orders {
200                let mut model = SymbolicModel::default();
201                if !propagate_fallback_support_constraints(
202                    constraints,
203                    &mut model,
204                    &mut remaining_support_visits,
205                ) {
206                    if remaining_support_visits == 0 {
207                        return None;
208                    }
209                    continue;
210                }
211                let mut valid = true;
212                for (operand, default) in seeds {
213                    let assigned = match operand.eval_model_if_complete(&model) {
214                        Ok(Some(_)) => true,
215                        Ok(None) => operand.assign_model_value(&mut model, default),
216                        Err(_) => false,
217                    };
218                    if !assigned {
219                        valid = false;
220                        break;
221                    }
222                    if !propagate_fallback_support_constraints(
223                        constraints,
224                        &mut model,
225                        &mut remaining_support_visits,
226                    ) {
227                        if remaining_support_visits == 0 {
228                            return None;
229                        }
230                        valid = false;
231                        break;
232                    }
233                }
234                if valid {
235                    if eval_vars.iter().all(|var| model.contains_name(*var)) {
236                        let valid = original_constraints.iter().all(|constraint| {
237                            charge_support_constraint(constraint, &mut remaining_support_visits)
238                                && constraint.eval_model(&model).unwrap_or(false)
239                        });
240                        if valid {
241                            return Some(model);
242                        }
243                        if remaining_support_visits == 0 {
244                            return None;
245                        }
246                        continue;
247                    }
248                    if complete_fallback_support_model(
249                        constraints,
250                        &mut model,
251                        &mut remaining_support_visits,
252                    ) && complete_model_with_zeroes(
253                        original_constraints,
254                        &mut model,
255                        &mut remaining_support_visits,
256                    ) {
257                        let valid = original_constraints.iter().all(|constraint| {
258                            charge_support_constraint(constraint, &mut remaining_support_visits)
259                                && constraint.eval_model(&model).unwrap_or(false)
260                        });
261                        if valid {
262                            return Some(model);
263                        }
264                    }
265                    if remaining_support_visits == 0 {
266                        return None;
267                    }
268                }
269            }
270        }
271    }
272    None
273}
274
275fn checked_mul_guard_branch(constraint: &SymBoolExpr) -> Option<(SymExpr, SymExpr, bool)> {
276    match constraint.kind() {
277        SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
278            checked_mul_guard_word_comparison(left, right)
279                .map(|(zero_operand, expected)| (zero_operand, expected, false))
280        }
281        SymBoolExprKind::Not(inner) => match inner.kind() {
282            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
283                checked_mul_guard_word_comparison(left, right)
284                    .map(|(zero_operand, expected)| (zero_operand, expected, true))
285            }
286            SymBoolExprKind::And(values) => checked_mul_guard_conjunction(values)
287                .map(|(zero_operand, expected)| (zero_operand, expected, true)),
288            _ => None,
289        },
290        SymBoolExprKind::And(values) => checked_mul_guard_conjunction(values)
291            .map(|(zero_operand, expected)| (zero_operand, expected, false)),
292        _ => None,
293    }
294}
295
296fn checked_mul_guard_word_comparison(
297    left: &SymExpr,
298    right: &SymExpr,
299) -> Option<(SymExpr, SymExpr)> {
300    let guard_word = if right.as_const().is_some_and(|value| value.is_zero()) {
301        left
302    } else if left.as_const().is_some_and(|value| value.is_zero()) {
303        right
304    } else {
305        return None;
306    };
307    let SymExprKind::BinOp(SymBinOp::Or, left, right) = guard_word.kind() else {
308        return None;
309    };
310
311    for (quotient_word, zero_word) in [(left, right), (right, left)] {
312        let Some(quotient_matches) = quotient_word.bool_word_condition() else {
313            continue;
314        };
315        let Some(zero_condition) = zero_word.bool_word_condition() else {
316            continue;
317        };
318        let Some((zero_operand, expected, quotient_zero_condition)) =
319            checked_mul_guard_operands(&quotient_matches)
320        else {
321            continue;
322        };
323        if zero_condition == quotient_zero_condition {
324            return Some((zero_operand, expected));
325        }
326    }
327    None
328}
329
330fn checked_mul_guard_conjunction(values: &[SymBoolExpr]) -> Option<(SymExpr, SymExpr)> {
331    for value in values {
332        let SymBoolExprKind::Not(quotient_matches) = value.kind() else {
333            continue;
334        };
335        let Some((zero_operand, expected, zero_condition)) =
336            checked_mul_guard_operands(quotient_matches)
337        else {
338            continue;
339        };
340        let contains_negated_zero_condition = values.iter().any(
341            |value| matches!(value.kind(), SymBoolExprKind::Not(inner) if inner == &zero_condition),
342        );
343        if contains_negated_zero_condition {
344            return Some((zero_operand, expected));
345        }
346    }
347    None
348}
349
350fn checked_mul_guard_operands(condition: &SymBoolExpr) -> Option<(SymExpr, SymExpr, SymBoolExpr)> {
351    let SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) = condition.kind() else {
352        return None;
353    };
354    for (guarded_quotient, expected) in [(left, right), (right, left)] {
355        let SymExprKind::Ite(zero_condition, zero, quotient) = guarded_quotient.kind() else {
356            continue;
357        };
358        if !zero.as_const().is_some_and(|value| value.is_zero()) {
359            continue;
360        }
361        let Some(zero_operand) = zero_condition.zero_check_operand() else {
362            continue;
363        };
364        let Some((numerator, denominator)) = quotient.udiv_operands() else {
365            continue;
366        };
367        if denominator != zero_operand {
368            continue;
369        }
370        let SymExprKind::BinOp(SymBinOp::Mul, product_left, product_right) = numerator.kind()
371        else {
372            continue;
373        };
374        if (product_left == denominator && product_right == expected)
375            || (product_right == denominator && product_left == expected)
376        {
377            return Some((zero_operand.clone(), expected.clone(), zero_condition.clone()));
378        }
379    }
380    None
381}
382
383fn fallback_search_vars(
384    cx: &SymCx,
385    vars: SymbolicVars,
386    constraints: &[SymBoolExpr],
387) -> Vec<Symbol> {
388    if vars.len() <= HARD_ARITH_FALLBACK_MAX_VARS {
389        return vars.into_iter().collect();
390    }
391
392    let hard_arith_vars = hard_arith_fallback_vars(constraints);
393    if !hard_arith_vars.is_empty() && hard_arith_vars.len() <= HARD_ARITH_FALLBACK_MAX_VARS {
394        let mut vars = hard_arith_vars;
395        add_zero_invalid_support_vars(&mut vars, constraints);
396        return vars.into_iter().collect();
397    }
398
399    vars.into_iter()
400        .filter(|var| {
401            let var = cx.symbol_name(*var);
402            var.starts_with("calldata")
403                || var.starts_with("sequence")
404                || var.starts_with("create_address")
405                || var.starts_with("create2_address")
406                || !var.contains('_')
407        })
408        .collect()
409}
410
411fn hard_arith_fallback_vars(constraints: &[SymBoolExpr]) -> SymbolicVars {
412    let mut vars = SymbolicVars::default();
413    for constraint in constraints {
414        collect_bool_hard_arith_vars(constraint, &mut vars);
415    }
416    vars
417}
418
419fn add_zero_invalid_support_vars(vars: &mut SymbolicVars, constraints: &[SymBoolExpr]) {
420    let zero_model = SymbolicModel::default();
421    for constraint in constraints {
422        if constraint.eval_model(&zero_model).unwrap_or(false) {
423            continue;
424        }
425        // Scalar bounds can be completed constructively. Spending a search slot on them can
426        // miss a valid endpoint (e.g. int256::MAX + 1) that is outside the candidate budget.
427        let (inner, inverted) = match constraint.kind() {
428            SymBoolExprKind::Not(inner) => (inner, true),
429            _ => (constraint, false),
430        };
431        if let SymBoolExprKind::Cmp(op, left, right) = inner.kind()
432            && support_cmp_op(*op, inverted)
433                .is_some_and(|op| !matches!(op, SymCmpOp::Slt | SymCmpOp::Sgt))
434            && [(left, right), (right, left)].into_iter().any(|(variable, bound)| {
435                if !matches!(variable.kind(), SymExprKind::Var(_)) {
436                    return false;
437                }
438                let mut dependencies = SymbolicVars::default();
439                bound.collect_eval_vars(&mut dependencies);
440                dependencies.is_subset(vars)
441            })
442        {
443            continue;
444        }
445
446        let mut constraint_vars = SymbolicVars::default();
447        constraint.collect_vars(&mut constraint_vars);
448        let missing =
449            constraint_vars.iter().filter(|var| !vars.contains(*var)).copied().collect::<Vec<_>>();
450        if vars.len() + missing.len() > HARD_ARITH_FALLBACK_MAX_VARS {
451            continue;
452        }
453        vars.extend(missing);
454    }
455}
456
457fn fallback_candidates_for_var(
458    var: &Symbol,
459    constraints: &[SymBoolExpr],
460    constants: &[U256],
461) -> Option<Vec<U256>> {
462    let hints = MaskHints::for_var(var, constraints);
463    if (hints.one & hints.zero) != U256::ZERO {
464        return None;
465    }
466
467    let mut candidates = HashSet::<U256>::default();
468    for candidate in [
469        U256::ZERO,
470        U256::from(1),
471        U256::from(2),
472        U256::from(3),
473        U256::MAX,
474        U256::MAX - U256::from(1),
475        U256::MAX - U256::from(2),
476    ] {
477        push_fallback_candidate(&mut candidates, candidate, hints);
478    }
479
480    for constant in constants.iter().copied() {
481        push_fallback_candidate(&mut candidates, constant, hints);
482        push_fallback_candidate(&mut candidates, constant.wrapping_add(U256::from(1)), hints);
483        push_fallback_candidate(&mut candidates, constant.wrapping_sub(U256::from(1)), hints);
484        if candidates.len() >= HARD_ARITH_FALLBACK_MAX_CANDIDATES_PER_VAR {
485            break;
486        }
487    }
488
489    for bit in 0..256 {
490        let power = U256::from(1) << bit;
491        push_fallback_candidate(&mut candidates, power, hints);
492        if candidates.len() >= HARD_ARITH_FALLBACK_MAX_CANDIDATES_PER_VAR {
493            break;
494        }
495    }
496
497    let mut candidates = candidates.into_iter().collect::<Vec<_>>();
498    candidates.sort_unstable();
499    candidates.truncate(HARD_ARITH_FALLBACK_MAX_CANDIDATES_PER_VAR);
500    Some(candidates)
501}
502
503struct FallbackSearch<'a> {
504    constraints: &'a [SymBoolExpr],
505    constraint_vars: &'a [SymbolicVars],
506    searched_vars: &'a SymbolicVars,
507    vars: &'a [Symbol],
508    candidates: &'a [Vec<U256>],
509}
510
511impl FallbackSearch<'_> {
512    fn model(
513        &self,
514        index: usize,
515        model: &mut SymbolicModel,
516        assignments: &mut usize,
517    ) -> Option<SymbolicModel> {
518        if index == self.vars.len() {
519            *assignments += 1;
520            if *assignments > HARD_ARITH_FALLBACK_MAX_ASSIGNMENTS {
521                return None;
522            }
523            let mut completed = model.clone();
524            let mut remaining_support_visits = usize::MAX;
525            if complete_fallback_support_model(
526                self.constraints,
527                &mut completed,
528                &mut remaining_support_visits,
529            ) {
530                return Some(completed);
531            }
532            // Greedy completion can choose one endpoint before seeing a tighter bound. Retry
533            // from the search assignment after intersecting all currently evaluable bounds.
534            let mut completed = model.clone();
535            return (seed_bounded_support_vars(self.constraints, &mut completed)
536                && complete_fallback_support_model(
537                    self.constraints,
538                    &mut completed,
539                    &mut remaining_support_visits,
540                ))
541            .then_some(completed);
542        }
543
544        for candidate in &self.candidates[index] {
545            model.insert(self.vars[index], *candidate);
546            if fallback_partial_model_satisfies_known_constraints(
547                self.constraints,
548                self.constraint_vars,
549                self.searched_vars,
550                model,
551            ) && let Some(model) = self.model(index + 1, model, assignments)
552            {
553                return Some(model);
554            }
555            if *assignments > HARD_ARITH_FALLBACK_MAX_ASSIGNMENTS {
556                return None;
557            }
558        }
559        model.remove(&self.vars[index]);
560        None
561    }
562}
563
564#[cfg(test)]
565fn fallback_model_satisfies_all_constraints(
566    constraints: &[SymBoolExpr],
567    model: &(impl SymbolicModelLookup + ?Sized),
568) -> bool {
569    eval_model_constraints(constraints, model)
570}
571
572/// Seeds only unassigned scalar variables; every resulting witness is still fully validated.
573fn seed_bounded_support_vars(constraints: &[SymBoolExpr], model: &mut SymbolicModel) -> bool {
574    let mut bounds: HashMap<Symbol, (U256, U256)> = HashMap::default();
575    for constraint in constraints {
576        let (constraint, inverted) = match constraint.kind() {
577            SymBoolExprKind::Not(inner) => (inner, true),
578            _ => (constraint, false),
579        };
580        let SymBoolExprKind::Cmp(op, left, right) = constraint.kind() else { continue };
581        let Some(mut op) = support_cmp_op(*op, inverted) else { continue };
582        if matches!(op, SymCmpOp::Slt | SymCmpOp::Sgt) {
583            continue;
584        }
585        let (var, known) = if let SymExprKind::Var(var) = left.kind()
586            && !model.contains_name(*var)
587            && let Ok(Some(value)) = right.eval_model_if_complete(model)
588        {
589            (*var, value)
590        } else if let SymExprKind::Var(var) = right.kind()
591            && !model.contains_name(*var)
592            && let Ok(Some(value)) = left.eval_model_if_complete(model)
593        {
594            op = match op {
595                SymCmpOp::Ult => SymCmpOp::Ugt,
596                SymCmpOp::Ule => SymCmpOp::Uge,
597                SymCmpOp::Ugt => SymCmpOp::Ult,
598                SymCmpOp::Uge => SymCmpOp::Ule,
599                other => other,
600            };
601            (*var, value)
602        } else {
603            continue;
604        };
605        let Some(value) = support_target_for_known_right(op, known) else { return false };
606        let (lower, upper) = bounds.entry(var).or_insert((U256::ZERO, U256::MAX));
607        match op {
608            SymCmpOp::Eq => {
609                *lower = (*lower).max(value);
610                *upper = (*upper).min(value);
611            }
612            SymCmpOp::Ule | SymCmpOp::Ult => *upper = (*upper).min(value),
613            SymCmpOp::Uge | SymCmpOp::Ugt => *lower = (*lower).max(value),
614            SymCmpOp::Slt | SymCmpOp::Sgt => continue,
615        }
616        if lower > upper {
617            return false;
618        }
619    }
620    if bounds.is_empty() {
621        return false;
622    }
623    for (var, (lower, _)) in bounds {
624        model.insert(var, lower);
625    }
626    true
627}
628
629fn complete_fallback_support_model(
630    constraints: &[SymBoolExpr],
631    model: &mut SymbolicModel,
632    remaining_support_visits: &mut usize,
633) -> bool {
634    for _ in 0..constraints.len() {
635        let Some(mut changed) =
636            complete_support_constraints_once(constraints, model, remaining_support_visits)
637        else {
638            return false;
639        };
640        if changed {
641            continue;
642        }
643        // Default checked-add bases to zero only after exact/lower-bound completions had a chance
644        // to assign a stronger value required by another constraint.
645        for constraint in constraints {
646            if !charge_support_constraint(constraint, remaining_support_visits) {
647                return false;
648            }
649            match constraint.eval_model_if_complete(model) {
650                Ok(Some(true)) => {}
651                Ok(Some(false)) | Err(_) => return false,
652                Ok(None) => {
653                    changed |= complete_default_support_constraint(constraint, model);
654                }
655            }
656        }
657        if !changed {
658            break;
659        }
660    }
661    constraints.iter().all(|constraint| {
662        charge_support_constraint(constraint, remaining_support_visits)
663            && constraint.eval_model(model).unwrap_or(false)
664    })
665}
666
667fn propagate_fallback_support_constraints(
668    constraints: &[SymBoolExpr],
669    model: &mut SymbolicModel,
670    remaining_support_visits: &mut usize,
671) -> bool {
672    for _ in 0..constraints.len() {
673        match complete_support_constraints_once(constraints, model, remaining_support_visits) {
674            Some(true) => {}
675            Some(false) => return true,
676            None => return false,
677        }
678    }
679    true
680}
681
682fn complete_support_constraints_once(
683    constraints: &[SymBoolExpr],
684    model: &mut SymbolicModel,
685    remaining_support_visits: &mut usize,
686) -> Option<bool> {
687    let mut changed = false;
688    for constraint in constraints {
689        if !charge_support_constraint(constraint, remaining_support_visits) {
690            return None;
691        }
692        match constraint.eval_model_if_complete(model) {
693            Ok(Some(true)) => {}
694            Ok(Some(false)) | Err(_) => return None,
695            Ok(None) => changed |= complete_support_constraint(constraint, model),
696        }
697    }
698    Some(changed)
699}
700
701fn charge_support_constraint(
702    constraint: &SymBoolExpr,
703    remaining_support_visits: &mut usize,
704) -> bool {
705    if *remaining_support_visits == 0 {
706        return false;
707    }
708    *remaining_support_visits -= 1;
709    !constraint
710        .visit_exprs(&mut |_| {
711            if *remaining_support_visits == 0 {
712                return ControlFlow::Break(());
713            }
714            *remaining_support_visits -= 1;
715            ControlFlow::Continue(())
716        })
717        .is_break()
718}
719
720fn complete_model_with_zeroes(
721    constraints: &[SymBoolExpr],
722    model: &mut SymbolicModel,
723    remaining_support_visits: &mut usize,
724) -> bool {
725    let mut vars = SymbolicVars::default();
726    for constraint in constraints {
727        if !charge_support_constraint(constraint, remaining_support_visits) {
728            return false;
729        }
730        constraint.collect_eval_vars(&mut vars);
731    }
732    for var in vars {
733        model.entry(var).or_default();
734    }
735    true
736}
737
738fn complete_support_constraint(constraint: &SymBoolExpr, model: &mut SymbolicModel) -> bool {
739    complete_support_bool(constraint, model, false, false)
740}
741
742fn complete_default_support_constraint(
743    constraint: &SymBoolExpr,
744    model: &mut SymbolicModel,
745) -> bool {
746    complete_support_bool(constraint, model, false, true)
747}
748
749fn complete_support_bool(
750    constraint: &SymBoolExpr,
751    model: &mut SymbolicModel,
752    inverted: bool,
753    defaults_only: bool,
754) -> bool {
755    match constraint.kind() {
756        SymBoolExprKind::Const(_) => false,
757        SymBoolExprKind::Not(value) => {
758            complete_support_bool(value, model, !inverted, defaults_only)
759        }
760        SymBoolExprKind::And(values) if !inverted => {
761            let mut changed = false;
762            for value in values.iter() {
763                changed |= complete_support_bool(value, model, false, defaults_only);
764            }
765            changed
766        }
767        SymBoolExprKind::Cmp(op, left, right) => {
768            let Some(op) = support_cmp_op(*op, inverted) else {
769                return false;
770            };
771            if defaults_only {
772                complete_default_support_comparison(op, left, right, model)
773            } else {
774                complete_support_comparison(op, left, right, model)
775            }
776        }
777        SymBoolExprKind::And(_) => false,
778    }
779}
780
781const fn support_cmp_op(op: SymCmpOp, inverted: bool) -> Option<SymCmpOp> {
782    if !inverted {
783        return Some(op);
784    }
785
786    match op {
787        SymCmpOp::Ult => Some(SymCmpOp::Uge),
788        SymCmpOp::Ugt => Some(SymCmpOp::Ule),
789        SymCmpOp::Ule => Some(SymCmpOp::Ugt),
790        SymCmpOp::Uge => Some(SymCmpOp::Ult),
791        SymCmpOp::Eq | SymCmpOp::Slt | SymCmpOp::Sgt => None,
792    }
793}
794
795fn complete_support_comparison(
796    op: SymCmpOp,
797    left: &SymExpr,
798    right: &SymExpr,
799    model: &mut SymbolicModel,
800) -> bool {
801    if complete_checked_sub_guard(op, left, right, model) {
802        return true;
803    }
804    if let Ok(Some(value)) = left.eval_model_if_complete(model)
805        && let Some(target) = support_target_for_known_left(op, value)
806    {
807        return right.assign_model_value(model, target);
808    }
809    if let Ok(Some(value)) = right.eval_model_if_complete(model)
810        && let Some(target) = support_target_for_known_right(op, value)
811    {
812        return left.assign_model_value(model, target);
813    }
814    false
815}
816
817fn complete_default_support_comparison(
818    op: SymCmpOp,
819    left: &SymExpr,
820    right: &SymExpr,
821    model: &mut SymbolicModel,
822) -> bool {
823    complete_checked_add_guard(op, left, right, model)
824}
825
826fn complete_checked_sub_guard(
827    op: SymCmpOp,
828    left: &SymExpr,
829    right: &SymExpr,
830    model: &mut SymbolicModel,
831) -> bool {
832    match op {
833        SymCmpOp::Uge => assign_checked_sub_minuend(left, right, model),
834        SymCmpOp::Ule => assign_checked_sub_minuend(right, left, model),
835        _ => false,
836    }
837}
838
839fn assign_checked_sub_minuend(
840    minuend: &SymExpr,
841    sub_expr: &SymExpr,
842    model: &mut SymbolicModel,
843) -> bool {
844    let SymExprKind::BinOp(SymBinOp::Sub, sub_minuend, amount) = sub_expr.kind() else {
845        return false;
846    };
847    if sub_minuend != minuend {
848        return false;
849    }
850    let Ok(Some(amount)) = amount.eval_model_if_complete(model) else {
851        return false;
852    };
853    minuend.assign_model_value(model, amount)
854}
855
856fn complete_checked_add_guard(
857    op: SymCmpOp,
858    left: &SymExpr,
859    right: &SymExpr,
860    model: &mut SymbolicModel,
861) -> bool {
862    match op {
863        SymCmpOp::Uge => assign_checked_add_base(left, right, model),
864        SymCmpOp::Ule => assign_checked_add_base(right, left, model),
865        _ => false,
866    }
867}
868
869fn assign_checked_add_base(sum: &SymExpr, base: &SymExpr, model: &mut SymbolicModel) -> bool {
870    let SymExprKind::BinOp(SymBinOp::Add, left, right) = sum.kind() else {
871        return false;
872    };
873    if left == base && right.eval_model_if_complete(model).ok().flatten().is_some() {
874        return base.assign_model_value(model, U256::ZERO);
875    }
876    if right == base && left.eval_model_if_complete(model).ok().flatten().is_some() {
877        return base.assign_model_value(model, U256::ZERO);
878    }
879    false
880}
881
882fn support_target_for_known_left(op: SymCmpOp, value: U256) -> Option<U256> {
883    match op {
884        SymCmpOp::Eq | SymCmpOp::Ule | SymCmpOp::Uge => Some(value),
885        SymCmpOp::Ult => value.checked_add(U256::from(1)),
886        SymCmpOp::Ugt => value.checked_sub(U256::from(1)),
887        SymCmpOp::Slt | SymCmpOp::Sgt => None,
888    }
889}
890
891fn support_target_for_known_right(op: SymCmpOp, value: U256) -> Option<U256> {
892    match op {
893        SymCmpOp::Eq | SymCmpOp::Ule | SymCmpOp::Uge => Some(value),
894        SymCmpOp::Ult => value.checked_sub(U256::from(1)),
895        SymCmpOp::Ugt => value.checked_add(U256::from(1)),
896        SymCmpOp::Slt | SymCmpOp::Sgt => None,
897    }
898}
899
900fn fallback_partial_model_satisfies_known_constraints(
901    constraints: &[SymBoolExpr],
902    constraint_vars: &[SymbolicVars],
903    searched_vars: &SymbolicVars,
904    model: &SymbolicModel,
905) -> bool {
906    constraints.iter().zip(constraint_vars).all(|(constraint, vars)| {
907        !vars.is_subset(searched_vars)
908            || !vars.iter().all(|var| model.contains_name(*var))
909            || constraint.eval_model(model).unwrap_or(false)
910    })
911}
912
913fn collect_bool_fallback_vars(expr: &SymBoolExpr, vars: &mut SymbolicVars) {
914    let _ = expr.visit_exprs(&mut |expr| {
915        if let Some(var) = expr.kind().get_eval_var() {
916            vars.insert(var);
917        }
918        ControlFlow::<()>::Continue(())
919    });
920}
921
922fn collect_bool_hard_arith_vars(expr: &SymBoolExpr, vars: &mut SymbolicVars) {
923    let _ = expr.visit_exprs(&mut |expr| {
924        if is_hard_arith_node(expr) {
925            expr.collect_eval_vars(vars);
926        }
927        ControlFlow::<()>::Continue(())
928    });
929}
930
931pub(crate) fn fallback_single_var_model(constraints: &[SymBoolExpr]) -> Option<SymbolicModel> {
932    let mut vars = SymbolicVars::default();
933    let mut constants = HashSet::<U256>::default();
934    for constraint in constraints {
935        constraint.collect_vars(&mut vars);
936        collect_bool_constants(constraint, &mut constants);
937    }
938    let mut constants = constants.into_iter().collect::<Vec<_>>();
939    constants.sort_unstable();
940
941    let var = if vars.len() == 1 { *vars.iter().next()? } else { return None };
942    let hints = MaskHints::for_var(&var, constraints);
943    if (hints.one & hints.zero) != U256::ZERO {
944        return None;
945    }
946
947    let mut model = SymbolicModel::default();
948    let mut remaining_support_visits = usize::MAX;
949    if complete_fallback_support_model(constraints, &mut model, &mut remaining_support_visits)
950        && model.len() == 1
951        && model.contains_key(&var)
952    {
953        return Some(model);
954    }
955
956    for candidate in [
957        U256::ZERO,
958        U256::from(1),
959        U256::from(2),
960        U256::MAX,
961        U256::MAX - U256::from(1),
962        U256::MAX - U256::from(2),
963    ] {
964        let mut model = SymbolicModel::default();
965        model.insert(var, (candidate | hints.one) & !hints.zero);
966        if eval_model_constraints(constraints, &model) {
967            return Some(model);
968        }
969    }
970
971    let mut candidates = HashSet::<U256>::default();
972    for constant in constants.iter().copied() {
973        push_fallback_candidate(&mut candidates, constant, hints);
974        push_fallback_candidate(&mut candidates, constant.wrapping_add(U256::from(1)), hints);
975        push_fallback_candidate(&mut candidates, constant.wrapping_sub(U256::from(1)), hints);
976    }
977
978    for bit in 0..256 {
979        let power = U256::from(1) << bit;
980        push_fallback_candidate(&mut candidates, power, hints);
981        for constant in constants.iter().copied().take(64) {
982            push_fallback_candidate(&mut candidates, power | constant, hints);
983            push_fallback_candidate(&mut candidates, power.wrapping_add(constant), hints);
984        }
985    }
986
987    let mut candidates = candidates.into_iter().collect::<Vec<_>>();
988    candidates.sort_unstable();
989    for candidate in candidates {
990        let mut model = SymbolicModel::default();
991        model.insert(var, candidate);
992        if eval_model_constraints(constraints, &model) {
993            return Some(model);
994        }
995    }
996
997    None
998}
999
1000pub(crate) fn fallback_two_var_model(constraints: &[SymBoolExpr]) -> Option<SymbolicModel> {
1001    if constraints.iter().any(SymBoolExpr::contains_hard_arith) {
1002        return None;
1003    }
1004
1005    let mut vars = SymbolicVars::default();
1006    for constraint in constraints {
1007        collect_bool_fallback_vars(constraint, &mut vars);
1008        if vars.len() > 2 {
1009            return None;
1010        }
1011    }
1012    if vars.len() != 2 {
1013        return None;
1014    }
1015    if constraints.iter().any(SymBoolExpr::contains_symbolic_hash)
1016        || constraints.iter().any(SymBoolExpr::contains_gasleft)
1017    {
1018        return None;
1019    }
1020    if !constraints_have_two_var_relation(constraints, &vars)
1021        || !constraints_bind_each_search_var(constraints, &vars)
1022    {
1023        return None;
1024    }
1025
1026    let mut constants = HashSet::<U256>::default();
1027    for constraint in constraints {
1028        collect_bool_constants(constraint, &mut constants);
1029    }
1030    let mut constants = constants.into_iter().collect::<Vec<_>>();
1031    constants.sort_unstable();
1032    let vars = vars.into_iter().collect::<Vec<_>>();
1033    let candidates = vars
1034        .iter()
1035        .map(|var| fallback_candidates_for_var(var, constraints, &constants))
1036        .collect::<Option<Vec<_>>>()?;
1037    let searched_vars = vars.iter().copied().collect::<SymbolicVars>();
1038    let constraint_vars = constraints
1039        .iter()
1040        .map(|constraint| {
1041            let mut vars = SymbolicVars::default();
1042            constraint.collect_vars(&mut vars);
1043            vars
1044        })
1045        .collect::<Vec<_>>();
1046    let search = FallbackSearch {
1047        constraints,
1048        constraint_vars: &constraint_vars,
1049        searched_vars: &searched_vars,
1050        vars: &vars,
1051        candidates: &candidates,
1052    };
1053    let mut model = SymbolicModel::default();
1054    let mut assignments = 0usize;
1055    search.model(0, &mut model, &mut assignments)
1056}
1057
1058fn constraints_have_two_var_relation(
1059    constraints: &[SymBoolExpr],
1060    searched_vars: &SymbolicVars,
1061) -> bool {
1062    constraints
1063        .iter()
1064        .any(|constraint| bool_expr_has_two_var_relation(constraint, searched_vars, false))
1065}
1066
1067fn bool_expr_has_two_var_relation(
1068    expr: &SymBoolExpr,
1069    searched_vars: &SymbolicVars,
1070    inverted: bool,
1071) -> bool {
1072    match expr.kind() {
1073        SymBoolExprKind::Const(_) => false,
1074        SymBoolExprKind::Not(expr) => {
1075            bool_expr_has_two_var_relation(expr, searched_vars, !inverted)
1076        }
1077        SymBoolExprKind::And(exprs) if !inverted => {
1078            exprs.iter().any(|expr| bool_expr_has_two_var_relation(expr, searched_vars, false))
1079        }
1080        SymBoolExprKind::And(_) => false,
1081        SymBoolExprKind::Cmp(_, left, right) => {
1082            let mut vars = SymbolicVars::default();
1083            collect_expr_fallback_vars(left, &mut vars);
1084            collect_expr_fallback_vars(right, &mut vars);
1085            vars.len() == 2 && vars.is_subset(searched_vars)
1086        }
1087    }
1088}
1089
1090fn constraints_bind_each_search_var(
1091    constraints: &[SymBoolExpr],
1092    searched_vars: &SymbolicVars,
1093) -> bool {
1094    searched_vars.iter().all(|var| {
1095        constraints.iter().any(|constraint| bool_expr_binds_single_var(constraint, *var, false))
1096    })
1097}
1098
1099fn bool_expr_binds_single_var(expr: &SymBoolExpr, bound_var: Symbol, inverted: bool) -> bool {
1100    match expr.kind() {
1101        SymBoolExprKind::Const(_) => false,
1102        SymBoolExprKind::Not(expr) => bool_expr_binds_single_var(expr, bound_var, !inverted),
1103        SymBoolExprKind::And(exprs) if !inverted => {
1104            exprs.iter().any(|expr| bool_expr_binds_single_var(expr, bound_var, false))
1105        }
1106        SymBoolExprKind::And(_) => false,
1107        SymBoolExprKind::Cmp(_, left, right) => {
1108            let mut vars = SymbolicVars::default();
1109            collect_expr_fallback_vars(left, &mut vars);
1110            collect_expr_fallback_vars(right, &mut vars);
1111            vars.len() == 1
1112                && vars.contains(&bound_var)
1113                && (expr_contains_const(left) || expr_contains_const(right))
1114        }
1115    }
1116}
1117
1118fn collect_expr_fallback_vars(expr: &SymExpr, vars: &mut SymbolicVars) {
1119    let _ = expr.visit(&mut |expr| {
1120        if let Some(var) = expr.kind().get_eval_var() {
1121            vars.insert(var);
1122        }
1123        ControlFlow::<()>::Continue(())
1124    });
1125}
1126
1127fn expr_contains_const(expr: &SymExpr) -> bool {
1128    expr.visit_bool(|expr| matches!(expr.kind(), SymExprKind::Const(_)))
1129}
1130
1131fn push_fallback_candidate(candidates: &mut HashSet<U256>, candidate: U256, hints: MaskHints) {
1132    candidates.insert((candidate | hints.one) & !hints.zero);
1133}
1134
1135fn collect_bool_constants(expr: &SymBoolExpr, constants: &mut HashSet<U256>) {
1136    let _ = expr.visit_exprs(&mut |expr| {
1137        if let SymExprKind::Const(value) = expr.kind() {
1138            constants.insert(*value);
1139        }
1140        ControlFlow::<()>::Continue(())
1141    });
1142}
1143
1144#[derive(Clone, Copy, Debug, Default)]
1145struct MaskHints {
1146    one: U256,
1147    zero: U256,
1148}
1149
1150impl MaskHints {
1151    fn for_var(var: &Symbol, constraints: &[SymBoolExpr]) -> Self {
1152        let mut hints = Self::default();
1153        for constraint in constraints {
1154            hints.apply_bool(var, constraint, false);
1155        }
1156        hints
1157    }
1158
1159    fn apply_bool(&mut self, var: &Symbol, expr: &SymBoolExpr, inverted: bool) {
1160        match expr.kind() {
1161            SymBoolExprKind::Const(_) => {}
1162            SymBoolExprKind::Not(value) => self.apply_bool(var, value, !inverted),
1163            SymBoolExprKind::And(values) if !inverted => {
1164                for value in values.iter() {
1165                    self.apply_bool(var, value, false);
1166                }
1167            }
1168            SymBoolExprKind::Cmp(SymCmpOp::Eq, left, right) => {
1169                self.apply_equality(var, left, right, inverted)
1170            }
1171            SymBoolExprKind::Cmp(_, _, _) | SymBoolExprKind::And(_) => {}
1172        }
1173    }
1174
1175    fn apply_equality(&mut self, var: &Symbol, left: &SymExpr, right: &SymExpr, inverted: bool) {
1176        if let Some(mask) =
1177            zero_mask_equality(var, left, right).or_else(|| zero_mask_equality(var, right, left))
1178        {
1179            if inverted {
1180                if is_single_bit(mask) {
1181                    self.one |= mask;
1182                }
1183            } else {
1184                self.zero |= mask;
1185            }
1186        }
1187    }
1188}
1189
1190fn is_single_bit(value: U256) -> bool {
1191    !value.is_zero() && (value & (value - U256::from(1))).is_zero()
1192}
1193
1194fn zero_mask_equality(var: &Symbol, masked: &SymExpr, zero: &SymExpr) -> Option<U256> {
1195    if !zero.as_const().is_some_and(|value| value.is_zero()) {
1196        return None;
1197    }
1198    match masked.kind() {
1199        SymExprKind::BinOp(SymBinOp::And, left, right)
1200            if left.kind().get_var().is_some_and(|name| &name == var) =>
1201        {
1202            right.as_const()
1203        }
1204        _ => None,
1205    }
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210    use super::*;
1211
1212    fn replayable_input(cx: &mut SymCx, name: &str) -> SymExpr {
1213        let symbol = cx.intern(name);
1214        cx.mark_replayable_input(symbol);
1215        SymExpr::get_var(cx, symbol)
1216    }
1217
1218    fn checked_mul_guard_word(
1219        cx: &mut SymCx,
1220        zero_operand: &SymExpr,
1221        expected: &SymExpr,
1222    ) -> SymExpr {
1223        let zero = SymExpr::zero(cx);
1224        let operand_is_zero = SymBoolExpr::eq(cx, zero_operand.clone(), zero.clone());
1225        let product = SymExpr::binop(cx, SymBinOp::Mul, zero_operand.clone(), expected.clone());
1226        let quotient = SymExpr::binop(cx, SymBinOp::UDiv, product, zero_operand.clone());
1227        let checked_product = SymExpr::ite(cx, operand_is_zero.clone(), zero, quotient);
1228        let operand_is_zero_word = SymExpr::bool_word(cx, operand_is_zero);
1229        let product_matches_expected = SymBoolExpr::eq(cx, checked_product, expected.clone());
1230        let product_matches_expected_word = SymExpr::bool_word(cx, product_matches_expected);
1231        SymExpr::binop(cx, SymBinOp::Or, operand_is_zero_word, product_matches_expected_word)
1232    }
1233
1234    #[test]
1235    fn checked_mul_guard_branch_model_preserves_exact_operand_constraints() {
1236        let mut cx = SymCx::new();
1237        let x = replayable_input(&mut cx, "x");
1238        let y = replayable_input(&mut cx, "y");
1239        let guard = checked_mul_guard_word(&mut cx, &x, &y);
1240        let zero = SymExpr::zero(&mut cx);
1241        let guard_is_false = SymBoolExpr::eq(&mut cx, guard, zero);
1242        let guard_is_true = guard_is_false.clone().not(&mut cx);
1243
1244        let seven = SymExpr::constant(&mut cx, U256::from(7));
1245        let y_is_seven = SymBoolExpr::eq(&mut cx, y.clone(), seven);
1246        let true_constraints = [guard_is_true, y_is_seven];
1247        let true_model = checked_mul_guard_branch_model(
1248            &cx,
1249            &true_constraints,
1250            &true_constraints,
1251            &SymbolicVars::default(),
1252        )
1253        .expect("true guard branch model");
1254        assert_eq!(x.eval_model(&true_model).unwrap(), U256::ZERO);
1255        assert_eq!(y.eval_model(&true_model).unwrap(), U256::from(7));
1256        assert!(fallback_model_satisfies_all_constraints(&true_constraints, &true_model));
1257
1258        let three = SymExpr::constant(&mut cx, U256::from(3));
1259        let y_is_three = SymBoolExpr::eq(&mut cx, y.clone(), three);
1260        let false_constraints = [guard_is_false, y_is_three];
1261        let false_model = checked_mul_guard_branch_model(
1262            &cx,
1263            &false_constraints,
1264            &false_constraints,
1265            &SymbolicVars::default(),
1266        )
1267        .expect("false guard branch model");
1268        assert_eq!(x.eval_model(&false_model).unwrap(), U256::MAX);
1269        assert_eq!(y.eval_model(&false_model).unwrap(), U256::from(3));
1270        assert!(fallback_model_satisfies_all_constraints(&false_constraints, &false_model));
1271    }
1272
1273    #[test]
1274    fn checked_mul_guard_branch_model_matches_nested_boolean_guard() {
1275        let mut cx = SymCx::new();
1276        let x = replayable_input(&mut cx, "x");
1277        let y = replayable_input(&mut cx, "y");
1278        let zero = SymExpr::zero(&mut cx);
1279        let x_is_zero = SymBoolExpr::eq(&mut cx, x.clone(), zero.clone());
1280        let product = SymExpr::binop(&mut cx, SymBinOp::Mul, x.clone(), y.clone());
1281        let quotient = SymExpr::binop(&mut cx, SymBinOp::UDiv, product.clone(), x.clone());
1282        let guarded_quotient = SymExpr::ite(&mut cx, x_is_zero.clone(), zero, quotient);
1283        let quotient_matches = SymBoolExpr::eq(&mut cx, guarded_quotient, y.clone());
1284        let quotient_mismatches = quotient_matches.not(&mut cx);
1285        let x_is_nonzero = x_is_zero.not(&mut cx);
1286        let guard_is_false = SymBoolExpr::and(&mut cx, vec![quotient_mismatches, x_is_nonzero]);
1287        let max = SymExpr::constant(&mut cx, U256::MAX);
1288        let product_is_not_max = SymBoolExpr::eq(&mut cx, product, max).not(&mut cx);
1289
1290        let guard_is_true = guard_is_false.clone().not(&mut cx);
1291        let nested_false_branch =
1292            SymBoolExpr::and(&mut cx, vec![guard_is_true, product_is_not_max.clone()]).not(&mut cx);
1293        let false_constraints = [nested_false_branch.clone()];
1294        let false_model = checked_mul_guard_branch_model(
1295            &cx,
1296            &false_constraints,
1297            &false_constraints,
1298            &SymbolicVars::default(),
1299        )
1300        .expect("nested false guard branch model");
1301        assert_eq!(x.eval_model(&false_model).unwrap(), U256::MAX);
1302        assert_eq!(y.eval_model(&false_model).unwrap(), U256::from(2));
1303        assert!(fallback_model_satisfies_all_constraints(&false_constraints, &false_model));
1304
1305        let guard_word = checked_mul_guard_word(&mut cx, &x, &y);
1306        let zero = SymExpr::zero(&mut cx);
1307        let word_guard_is_false = SymBoolExpr::eq(&mut cx, guard_word, zero);
1308        let normalized = [nested_false_branch, word_guard_is_false.clone()];
1309        let original = [word_guard_is_false, normalized[0].clone()];
1310        let combined_model =
1311            checked_mul_guard_branch_model(&cx, &normalized, &original, &SymbolicVars::default())
1312                .expect("combined word and nested guard model");
1313        assert!(fallback_model_satisfies_all_constraints(&original, &combined_model));
1314
1315        let guarded_nonmax_product =
1316            SymBoolExpr::and(&mut cx, vec![guard_is_false, product_is_not_max]).not(&mut cx);
1317        let true_constraints = [guarded_nonmax_product];
1318        let true_model = checked_mul_guard_branch_model(
1319            &cx,
1320            &true_constraints,
1321            &true_constraints,
1322            &SymbolicVars::default(),
1323        )
1324        .expect("nested true guard branch model");
1325        assert_eq!(x.eval_model(&true_model).unwrap(), U256::ZERO);
1326        assert_eq!(y.eval_model(&true_model).unwrap(), U256::ZERO);
1327        assert!(fallback_model_satisfies_all_constraints(&true_constraints, &true_model));
1328    }
1329
1330    #[test]
1331    fn checked_mul_guard_branch_model_completes_original_model_symbols() {
1332        let mut cx = SymCx::new();
1333        let x = replayable_input(&mut cx, "x");
1334        let y = replayable_input(&mut cx, "y");
1335        let guard = checked_mul_guard_word(&mut cx, &x, &y);
1336        let zero = SymExpr::zero(&mut cx);
1337        let guard_is_true = SymBoolExpr::eq(&mut cx, guard, zero).not(&mut cx);
1338        let slot_symbol = cx.intern("slot");
1339        let slot = SymExpr::get_var(&mut cx, slot_symbol);
1340        let one = SymExpr::one(&mut cx);
1341        let slot_is_not_one = SymBoolExpr::eq(&mut cx, slot, one).not(&mut cx);
1342        let normalized = [guard_is_true.clone()];
1343        let original = [guard_is_true, slot_is_not_one];
1344        let replayable_storage = [slot_symbol].into_iter().collect();
1345
1346        let model =
1347            checked_mul_guard_branch_model(&cx, &normalized, &original, &replayable_storage)
1348                .expect("completed guard branch model");
1349
1350        assert_eq!(model.get(&slot_symbol), Some(&U256::ZERO));
1351        assert!(fallback_model_satisfies_all_constraints(&original, &model));
1352    }
1353
1354    #[test]
1355    fn checked_mul_guard_branch_model_rejects_symbolic_hash_assignments() {
1356        let mut cx = SymCx::new();
1357        let x = replayable_input(&mut cx, "x");
1358        let y = replayable_input(&mut cx, "y");
1359        let guard = checked_mul_guard_word(&mut cx, &x, &y);
1360        let zero = SymExpr::zero(&mut cx);
1361        let guard_is_true = SymBoolExpr::eq(&mut cx, guard, zero.clone()).not(&mut cx);
1362        let y_is_zero = SymBoolExpr::eq(&mut cx, y.clone(), zero.clone());
1363        let hash_symbol = cx.intern("sha256_y");
1364        let hash = SymExpr::hash_symbol(&mut cx, hash_symbol, "sha256", vec![y]);
1365        let hash_is_zero = SymBoolExpr::eq(&mut cx, hash, zero);
1366        let constraints = [guard_is_true, y_is_zero, hash_is_zero];
1367
1368        assert!(
1369            checked_mul_guard_branch_model(
1370                &cx,
1371                &constraints,
1372                &constraints,
1373                &SymbolicVars::default(),
1374            )
1375            .is_none()
1376        );
1377    }
1378
1379    #[test]
1380    fn checked_mul_guard_branch_model_rejects_gasleft_assignments() {
1381        let mut cx = SymCx::new();
1382        let x = replayable_input(&mut cx, "x");
1383        let y = replayable_input(&mut cx, "y");
1384        let guard = checked_mul_guard_word(&mut cx, &x, &y);
1385        let zero = SymExpr::zero(&mut cx);
1386        let guard_is_true = SymBoolExpr::eq(&mut cx, guard, zero.clone()).not(&mut cx);
1387        let gas_left = SymExpr::gas_left(&mut cx, 0);
1388        let gas_is_zero = SymBoolExpr::eq(&mut cx, gas_left, zero);
1389        let constraints = [guard_is_true, gas_is_zero];
1390
1391        assert!(
1392            checked_mul_guard_branch_model(
1393                &cx,
1394                &constraints,
1395                &constraints,
1396                &SymbolicVars::default(),
1397            )
1398            .is_none()
1399        );
1400    }
1401
1402    #[test]
1403    fn checked_mul_guard_branch_model_rejects_opaque_var_assignments() {
1404        for name in ["create_address_opaque", "vmRandomUint_0", "svm_0"] {
1405            let mut cx = SymCx::new();
1406            let x = replayable_input(&mut cx, "x");
1407            let y = replayable_input(&mut cx, "y");
1408            let guard = checked_mul_guard_word(&mut cx, &x, &y);
1409            let zero = SymExpr::zero(&mut cx);
1410            let guard_is_true = SymBoolExpr::eq(&mut cx, guard, zero.clone()).not(&mut cx);
1411            let opaque = SymExpr::var(&mut cx, name);
1412            let opaque_is_zero = SymBoolExpr::eq(&mut cx, opaque, zero);
1413            let constraints = [guard_is_true, opaque_is_zero];
1414
1415            assert!(
1416                checked_mul_guard_branch_model(
1417                    &cx,
1418                    &constraints,
1419                    &constraints,
1420                    &SymbolicVars::default(),
1421                )
1422                .is_none(),
1423                "accepted opaque model symbol {name}"
1424            );
1425        }
1426    }
1427
1428    #[test]
1429    fn checked_mul_guard_branch_model_propagates_relational_operand_constraints() {
1430        let mut cx = SymCx::new();
1431        let x = replayable_input(&mut cx, "x");
1432        let y = replayable_input(&mut cx, "y");
1433        let guard = checked_mul_guard_word(&mut cx, &x, &y);
1434        let zero = SymExpr::zero(&mut cx);
1435        let guard_is_false = SymBoolExpr::eq(&mut cx, guard, zero);
1436        let guard_is_true = guard_is_false.clone().not(&mut cx);
1437
1438        let seven = SymExpr::constant(&mut cx, U256::from(7));
1439        let x_plus_seven = SymExpr::binop(&mut cx, SymBinOp::Add, x.clone(), seven);
1440        let y_is_x_plus_seven = SymBoolExpr::eq(&mut cx, y.clone(), x_plus_seven);
1441        let true_constraints = [guard_is_true, y_is_x_plus_seven];
1442        let true_model = checked_mul_guard_branch_model(
1443            &cx,
1444            &true_constraints,
1445            &true_constraints,
1446            &SymbolicVars::default(),
1447        )
1448        .expect("true relational model");
1449        assert_eq!(x.eval_model(&true_model).unwrap(), U256::ZERO);
1450        assert_eq!(y.eval_model(&true_model).unwrap(), U256::from(7));
1451        assert!(fallback_model_satisfies_all_constraints(&true_constraints, &true_model));
1452
1453        let operands_are_equal = SymBoolExpr::eq(&mut cx, x.clone(), y.clone());
1454        let false_constraints = [guard_is_false, operands_are_equal];
1455        let false_model = checked_mul_guard_branch_model(
1456            &cx,
1457            &false_constraints,
1458            &false_constraints,
1459            &SymbolicVars::default(),
1460        )
1461        .expect("false relational model");
1462        assert_eq!(x.eval_model(&false_model).unwrap(), U256::MAX);
1463        assert_eq!(y.eval_model(&false_model).unwrap(), U256::MAX);
1464        assert!(fallback_model_satisfies_all_constraints(&false_constraints, &false_model));
1465    }
1466
1467    #[test]
1468    fn checked_mul_guard_branch_model_stops_at_shared_support_budget() {
1469        let mut cx = SymCx::new();
1470        let first_x = replayable_input(&mut cx, "first_x");
1471        let first_y = replayable_input(&mut cx, "first_y");
1472        let first_guard = checked_mul_guard_word(&mut cx, &first_x, &first_y);
1473        let zero = SymExpr::zero(&mut cx);
1474        let first_guard_is_true = SymBoolExpr::eq(&mut cx, first_guard, zero.clone()).not(&mut cx);
1475
1476        let second_x = replayable_input(&mut cx, "second_x");
1477        let second_y = replayable_input(&mut cx, "second_y");
1478        let second_guard = checked_mul_guard_word(&mut cx, &second_x, &second_y);
1479        let second_guard_is_false = SymBoolExpr::eq(&mut cx, second_guard, zero);
1480
1481        let one = SymExpr::one(&mut cx);
1482        let second_x_plus_one = SymExpr::binop(&mut cx, SymBinOp::Add, second_x.clone(), one);
1483        let x_relation = SymBoolExpr::eq(&mut cx, first_x.clone(), second_x_plus_one);
1484        let y_relation = SymBoolExpr::eq(&mut cx, first_y.clone(), second_y.clone());
1485        let mut constraints =
1486            vec![first_guard_is_true, second_guard_is_false, x_relation, y_relation];
1487        for _ in 0..8 {
1488            constraints.push(SymBoolExpr::constant(&mut cx, true));
1489        }
1490
1491        let expected = [
1492            (cx.intern("first_x"), U256::ZERO),
1493            (cx.intern("first_y"), U256::from(2)),
1494            (cx.intern("second_x"), U256::MAX),
1495            (cx.intern("second_y"), U256::from(2)),
1496        ]
1497        .into_iter()
1498        .collect::<SymbolicModel>();
1499        assert!(fallback_model_satisfies_all_constraints(&constraints, &expected));
1500
1501        assert!(
1502            checked_mul_guard_branch_model(
1503                &cx,
1504                &constraints,
1505                &constraints,
1506                &SymbolicVars::default(),
1507            )
1508            .is_none()
1509        );
1510    }
1511
1512    #[test]
1513    fn checked_mul_guard_branch_model_stops_at_shared_expression_budget() {
1514        let mut cx = SymCx::new();
1515        let x = replayable_input(&mut cx, "x");
1516        let y = replayable_input(&mut cx, "y");
1517        let guard = checked_mul_guard_word(&mut cx, &x, &y);
1518        let zero = SymExpr::zero(&mut cx);
1519        let guard_is_true = SymBoolExpr::eq(&mut cx, guard, zero.clone()).not(&mut cx);
1520
1521        let source = replayable_input(&mut cx, "source");
1522        let mut shared = source;
1523        for _ in 0..9 {
1524            shared = SymExpr::binop(&mut cx, SymBinOp::Add, shared.clone(), shared);
1525        }
1526        let support = SymBoolExpr::eq(&mut cx, shared, zero);
1527        let original = [guard_is_true, support.clone()];
1528        let normalized = normalize_constraints_for_solver(&mut cx, &original);
1529
1530        assert!(normalized.contains(&support));
1531        assert!(
1532            checked_mul_guard_branch_model(&cx, &normalized, &original, &SymbolicVars::default(),)
1533                .is_none()
1534        );
1535    }
1536
1537    #[test]
1538    fn fallback_completes_signed_global_bounds_after_rounded_conversion() {
1539        let mut cx = SymCx::new();
1540        let balance = SymExpr::var(&mut cx, "storage_balance");
1541        let rate = SymExpr::var(&mut cx, "storage_rate");
1542        let credits = SymExpr::var(&mut cx, "storage_credits");
1543        let supply = SymExpr::var(&mut cx, "storage_supply");
1544        let account = SymExpr::var(&mut cx, "calldata_0");
1545        let state = SymExpr::var(&mut cx, "storage_state");
1546        let fixed = SymExpr::var(&mut cx, "storage_fixed");
1547        let scale_value = U256::from(1_000_000_000_000_000_000u64);
1548        let scale = SymExpr::constant(&mut cx, scale_value);
1549        let one = SymExpr::one(&mut cx);
1550        let zero = SymExpr::zero(&mut cx);
1551        let max = U256::MAX >> 1;
1552        let max_word = SymExpr::constant(&mut cx, max);
1553        let product = SymExpr::binop(&mut cx, SymBinOp::Mul, balance.clone(), rate.clone());
1554        let rounded = SymExpr::binop(&mut cx, SymBinOp::Add, product, scale.clone());
1555        let rounded = SymExpr::binop(&mut cx, SymBinOp::Sub, rounded, one.clone());
1556        let rounded = SymExpr::binop(&mut cx, SymBinOp::UDiv, rounded, scale.clone());
1557        let room = SymExpr::binop(&mut cx, SymBinOp::Sub, max_word, rounded);
1558        let mask = SymExpr::constant(&mut cx, U256::from(255));
1559        let masked_state = SymExpr::binop(&mut cx, SymBinOp::And, state, mask);
1560        let mut constraints = vec![
1561            SymBoolExpr::eq(&mut cx, balance.clone(), zero.clone()).not(&mut cx),
1562            SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ugt, &supply, max),
1563            SymBoolExpr::cmp(&mut cx, SymCmpOp::Ule, credits.clone(), room.clone()),
1564            SymBoolExpr::eq(&mut cx, fixed, scale),
1565            SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ult, &balance, U256::from(u128::MAX)),
1566            SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ult, &account, U256::ONE << 160),
1567            SymBoolExpr::eq(&mut cx, masked_state, one),
1568            SymBoolExpr::cmp_word_const(
1569                &mut cx,
1570                SymCmpOp::Ule,
1571                &rate,
1572                scale_value * U256::from(1_000_000_000),
1573            ),
1574            SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Ule, &credits, max),
1575            SymBoolExpr::cmp(&mut cx, SymCmpOp::Ule, balance, supply),
1576            SymBoolExpr::eq(&mut cx, account, zero).not(&mut cx),
1577            SymBoolExpr::cmp_word_const(&mut cx, SymCmpOp::Uge, &rate, scale_value),
1578        ];
1579        for overflow in [false, true] {
1580            constraints[2] = SymBoolExpr::cmp(
1581                &mut cx,
1582                if overflow { SymCmpOp::Ugt } else { SymCmpOp::Ule },
1583                credits.clone(),
1584                room.clone(),
1585            );
1586            for _ in 0..2 {
1587                let normalized = normalize_constraints_for_solver(&mut cx, &constraints);
1588                let model =
1589                    hard_arith_fallback_model(&cx, &normalized).expect("bounded global witness");
1590                assert!(fallback_model_satisfies_all_constraints(&constraints, &model));
1591                constraints.reverse();
1592            }
1593        }
1594    }
1595
1596    #[test]
1597    fn hard_arith_fallback_ignores_unrelated_abi_vars() {
1598        let mut cx = SymCx::new();
1599        let amount = SymExpr::var(&mut cx, "sequence_0_0_0_1");
1600        let zero = SymExpr::zero(&mut cx);
1601        let scale = SymExpr::constant(&mut cx, U256::from(1_000_000));
1602        let product = SymExpr::binop(&mut cx, SymBinOp::Mul, scale.clone(), amount.clone());
1603        let div = SymExpr::binop(&mut cx, SymBinOp::UDiv, product, amount.clone());
1604        let amount_is_zero = SymBoolExpr::eq(&mut cx, amount, zero);
1605        let guarded_zero = SymExpr::zero(&mut cx);
1606        let guarded_div = SymExpr::ite(&mut cx, amount_is_zero.clone(), guarded_zero, div);
1607        let overflow_branch = SymBoolExpr::eq(&mut cx, guarded_div, scale).not(&mut cx);
1608
1609        let address_bound = U256::from(1) << 160;
1610        let mut constraints = vec![amount_is_zero.not(&mut cx), overflow_branch];
1611        for idx in 0..6 {
1612            let abi_word = SymExpr::var(&mut cx, &format!("sequence_0_0_0_addr_{idx}"));
1613            constraints.push(SymBoolExpr::cmp_word_const(
1614                &mut cx,
1615                SymCmpOp::Ult,
1616                &abi_word,
1617                address_bound,
1618            ));
1619        }
1620
1621        assert!(constraints_prefer_hard_arith_fallback_first(&cx, &constraints));
1622        let model = hard_arith_fallback_model(&cx, &constraints).expect("fallback model");
1623        assert!(model.contains_name(cx.symbol("sequence_0_0_0_1")));
1624        assert!(constraints.iter().all(|constraint| constraint.eval_model(&model).unwrap()));
1625    }
1626
1627    #[test]
1628    fn hard_arith_fallback_keeps_prior_path_vars_needed_by_zero_model() {
1629        let mut cx = SymCx::new();
1630        let setup_amount = SymExpr::var(&mut cx, "sequence_0_0_0_1");
1631        let borrow_amount = SymExpr::var(&mut cx, "sequence_2_2_0_1");
1632        let zero = SymExpr::zero(&mut cx);
1633        let scale = SymExpr::constant(&mut cx, U256::from(1_000_000));
1634        let product = SymExpr::binop(&mut cx, SymBinOp::Mul, scale.clone(), borrow_amount.clone());
1635        let quotient = SymExpr::binop(&mut cx, SymBinOp::UDiv, product, borrow_amount.clone());
1636
1637        let constraints = vec![
1638            SymBoolExpr::eq(&mut cx, setup_amount, zero.clone()).not(&mut cx),
1639            SymBoolExpr::eq(&mut cx, borrow_amount, zero).not(&mut cx),
1640            SymBoolExpr::eq(&mut cx, quotient, scale),
1641        ];
1642
1643        assert!(constraints_prefer_hard_arith_fallback_first(&cx, &constraints));
1644        let model = hard_arith_fallback_model(&cx, &constraints).expect("fallback model");
1645        assert!(model.contains_name(cx.symbol("sequence_0_0_0_1")));
1646        assert!(model.contains_name(cx.symbol("sequence_2_2_0_1")));
1647        assert!(constraints.iter().all(|constraint| constraint.eval_model(&model).unwrap()));
1648    }
1649
1650    #[test]
1651    fn hard_arith_fallback_completes_checked_storage_guards() {
1652        let mut cx = SymCx::new();
1653        let amount = SymExpr::var(&mut cx, "sequence_0_0_0_1");
1654        let from_balance = SymExpr::var(&mut cx, "storage_from_balance");
1655        let to_balance = SymExpr::var(&mut cx, "storage_to_balance");
1656        let zero = SymExpr::zero(&mut cx);
1657        let scale = SymExpr::constant(&mut cx, U256::from(1_000_000));
1658        let product = SymExpr::binop(&mut cx, SymBinOp::Mul, scale.clone(), amount.clone());
1659        let quotient = SymExpr::binop(&mut cx, SymBinOp::UDiv, product, amount.clone());
1660
1661        let debited = SymExpr::binop(&mut cx, SymBinOp::Sub, from_balance.clone(), amount.clone());
1662        let credited = SymExpr::binop(&mut cx, SymBinOp::Add, to_balance.clone(), amount.clone());
1663        let mut constraints = vec![
1664            SymBoolExpr::eq(&mut cx, amount, zero).not(&mut cx),
1665            SymBoolExpr::eq(&mut cx, quotient, scale),
1666            SymBoolExpr::cmp(&mut cx, SymCmpOp::Ult, from_balance, debited).not(&mut cx),
1667            SymBoolExpr::cmp(&mut cx, SymCmpOp::Ult, credited, to_balance).not(&mut cx),
1668        ];
1669
1670        let address_bound = U256::from(1) << 160;
1671        for idx in 0..6 {
1672            let abi_word = SymExpr::var(&mut cx, &format!("sequence_0_0_0_addr_{idx}"));
1673            constraints.push(SymBoolExpr::cmp_word_const(
1674                &mut cx,
1675                SymCmpOp::Ult,
1676                &abi_word,
1677                address_bound,
1678            ));
1679        }
1680
1681        assert!(constraints_prefer_hard_arith_fallback_first(&cx, &constraints));
1682        let model = hard_arith_fallback_model(&cx, &constraints).expect("fallback model");
1683        assert!(model.contains_name(cx.symbol("sequence_0_0_0_1")));
1684        assert!(model.contains_name(cx.symbol("storage_from_balance")));
1685        assert!(model.contains_name(cx.symbol("storage_to_balance")));
1686        assert!(constraints.iter().all(|constraint| constraint.eval_model(&model).unwrap()));
1687    }
1688}