1use 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
48pub(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
115const MAX_CHECKED_MUL_SUPPORT_VISITS: usize = 256;
118
119pub(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("ient_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 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 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
572fn 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 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}