foundry_evm_symbolic/runtime/solver/
hard_arith_fallback.rs1use super::*;
2
3pub(crate) fn bool_contains_hard_arith(expr: &BoolExpr) -> bool {
5 match expr {
6 BoolExpr::Const(_) => false,
7 BoolExpr::Not(value) => bool_contains_hard_arith(value),
8 BoolExpr::And(values) => values.iter().any(bool_contains_hard_arith),
9 BoolExpr::Eq(left, right) | BoolExpr::Cmp(_, left, right) => {
10 expr_contains_hard_arith(left) || expr_contains_hard_arith(right)
11 }
12 }
13}
14
15pub(crate) fn expr_contains_hard_arith(expr: &Expr) -> bool {
17 match expr {
18 Expr::Const(_)
19 | Expr::Var(_)
20 | Expr::GasLeft(_)
21 | Expr::Keccak { .. }
22 | Expr::Hash { .. } => false,
23 Expr::Not(value) => expr_contains_hard_arith(value),
24 Expr::Op(ExprOp::Mul, left, right) => expr_contains_var(left) && expr_contains_var(right),
25 Expr::Op(ExprOp::UDiv | ExprOp::URem | ExprOp::SDiv | ExprOp::SRem, left, right) => {
26 expr_contains_var(left) || expr_contains_var(right)
27 }
28 Expr::Op(_, left, right) => {
29 expr_contains_hard_arith(left) || expr_contains_hard_arith(right)
30 }
31 Expr::Ite(cond, left, right) => {
32 bool_contains_hard_arith(cond)
33 || expr_contains_hard_arith(left)
34 || expr_contains_hard_arith(right)
35 }
36 }
37}
38
39pub(crate) fn expr_contains_symbolic_hash(expr: &Expr) -> bool {
41 match expr {
42 Expr::Hash { .. } => true,
43 Expr::Keccak { len, bytes, .. } => {
44 expr_contains_symbolic_hash(len) || bytes.iter().any(expr_contains_symbolic_hash)
45 }
46 Expr::Const(_) | Expr::Var(_) | Expr::GasLeft(_) => false,
47 Expr::Not(value) => expr_contains_symbolic_hash(value),
48 Expr::Op(_, left, right) => {
49 expr_contains_symbolic_hash(left) || expr_contains_symbolic_hash(right)
50 }
51 Expr::Ite(cond, left, right) => {
52 bool_contains_symbolic_hash(cond)
53 || expr_contains_symbolic_hash(left)
54 || expr_contains_symbolic_hash(right)
55 }
56 }
57}
58
59pub(crate) fn bool_contains_symbolic_hash(expr: &BoolExpr) -> bool {
61 match expr {
62 BoolExpr::Const(_) => false,
63 BoolExpr::Not(value) => bool_contains_symbolic_hash(value),
64 BoolExpr::And(values) => values.iter().any(bool_contains_symbolic_hash),
65 BoolExpr::Eq(left, right) | BoolExpr::Cmp(_, left, right) => {
66 expr_contains_symbolic_hash(left) || expr_contains_symbolic_hash(right)
67 }
68 }
69}
70
71pub(crate) fn expr_contains_var(expr: &Expr) -> bool {
73 match expr {
74 Expr::Const(_) => false,
75 Expr::Var(_) | Expr::Keccak { .. } | Expr::Hash { .. } => true,
76 Expr::GasLeft(_) => false,
77 Expr::Not(value) => expr_contains_var(value),
78 Expr::Op(_, left, right) => expr_contains_var(left) || expr_contains_var(right),
79 Expr::Ite(cond, left, right) => {
80 bool_contains_var(cond) || expr_contains_var(left) || expr_contains_var(right)
81 }
82 }
83}
84
85pub(crate) fn bool_contains_var(expr: &BoolExpr) -> bool {
87 match expr {
88 BoolExpr::Const(_) => false,
89 BoolExpr::Not(value) => bool_contains_var(value),
90 BoolExpr::And(values) => values.iter().any(bool_contains_var),
91 BoolExpr::Eq(left, right) | BoolExpr::Cmp(_, left, right) => {
92 expr_contains_var(left) || expr_contains_var(right)
93 }
94 }
95}
96
97pub(crate) fn constraints_prefer_hard_arith_fallback_first(constraints: &[BoolExpr]) -> bool {
99 if !constraints.iter().any(bool_contains_hard_arith)
100 || constraints.iter().any(bool_contains_symbolic_hash)
101 {
102 return false;
103 }
104
105 let mut vars = BTreeSet::new();
106 for constraint in constraints {
107 collect_bool_fallback_vars(constraint, &mut vars);
108 }
109 let vars = fallback_search_vars(vars);
110 !vars.is_empty() && vars.len() <= HARD_ARITH_FALLBACK_MAX_VARS
111}
112
113pub(crate) fn hard_arith_fallback_model(
115 constraints: &[BoolExpr],
116) -> Option<BTreeMap<String, U256>> {
117 if !constraints.iter().any(bool_contains_hard_arith)
118 || constraints.iter().any(bool_contains_symbolic_hash)
119 {
120 return None;
121 }
122
123 let mut vars = BTreeSet::new();
124 let mut constants = BTreeSet::new();
125 for constraint in constraints {
126 collect_bool_fallback_vars(constraint, &mut vars);
127 collect_bool_constants(constraint, &mut constants);
128 }
129 let vars = fallback_search_vars(vars);
130 if vars.is_empty() || vars.len() > HARD_ARITH_FALLBACK_MAX_VARS {
131 return None;
132 }
133
134 let candidates = vars
135 .iter()
136 .map(|var| fallback_candidates_for_var(var, constraints, &constants))
137 .collect::<Option<Vec<_>>>()?;
138 let searched_vars = vars.iter().cloned().collect::<BTreeSet<_>>();
139 let constraint_vars = constraints
140 .iter()
141 .map(|constraint| {
142 let mut vars = BTreeSet::new();
143 constraint.collect_vars(&mut vars);
144 vars
145 })
146 .collect::<Vec<_>>();
147 let mut model = BTreeMap::new();
148 let mut assignments = 0usize;
149 let search = FallbackSearch {
150 constraints,
151 constraint_vars: &constraint_vars,
152 searched_vars: &searched_vars,
153 vars: &vars,
154 candidates: &candidates,
155 };
156 search.model(0, &mut model, &mut assignments)
157}
158
159pub(crate) fn fallback_search_vars(vars: BTreeSet<String>) -> Vec<String> {
161 if vars.len() <= HARD_ARITH_FALLBACK_MAX_VARS {
162 return vars.into_iter().collect();
163 }
164
165 vars.into_iter()
166 .filter(|var| {
167 var.starts_with("calldata")
168 || var.starts_with("sequence")
169 || var.starts_with("create_address")
170 || var.starts_with("create2_address")
171 || !var.contains('_')
172 })
173 .collect()
174}
175
176pub(crate) fn fallback_candidates_for_var(
178 var: &str,
179 constraints: &[BoolExpr],
180 constants: &BTreeSet<U256>,
181) -> Option<Vec<U256>> {
182 let hints = MaskHints::for_var(var, constraints);
183 if (hints.one & hints.zero) != U256::ZERO {
184 return None;
185 }
186
187 let mut candidates = BTreeSet::new();
188 for candidate in [
189 U256::ZERO,
190 U256::from(1),
191 U256::from(2),
192 U256::from(3),
193 U256::MAX,
194 U256::MAX - U256::from(1),
195 U256::MAX - U256::from(2),
196 ] {
197 push_fallback_candidate(&mut candidates, candidate, hints);
198 }
199
200 for constant in constants.iter().copied() {
201 push_fallback_candidate(&mut candidates, constant, hints);
202 push_fallback_candidate(&mut candidates, constant.wrapping_add(U256::from(1)), hints);
203 push_fallback_candidate(&mut candidates, constant.wrapping_sub(U256::from(1)), hints);
204 if candidates.len() >= HARD_ARITH_FALLBACK_MAX_CANDIDATES_PER_VAR {
205 break;
206 }
207 }
208
209 for bit in 0..256 {
210 let power = U256::from(1) << bit;
211 push_fallback_candidate(&mut candidates, power, hints);
212 if candidates.len() >= HARD_ARITH_FALLBACK_MAX_CANDIDATES_PER_VAR {
213 break;
214 }
215 }
216
217 Some(candidates.into_iter().take(HARD_ARITH_FALLBACK_MAX_CANDIDATES_PER_VAR).collect())
218}
219
220struct FallbackSearch<'a> {
222 constraints: &'a [BoolExpr],
223 constraint_vars: &'a [BTreeSet<String>],
224 searched_vars: &'a BTreeSet<String>,
225 vars: &'a [String],
226 candidates: &'a [Vec<U256>],
227}
228
229impl FallbackSearch<'_> {
230 fn model(
232 &self,
233 index: usize,
234 model: &mut BTreeMap<String, U256>,
235 assignments: &mut usize,
236 ) -> Option<BTreeMap<String, U256>> {
237 if index == self.vars.len() {
238 *assignments += 1;
239 if *assignments > HARD_ARITH_FALLBACK_MAX_ASSIGNMENTS {
240 return None;
241 }
242 return fallback_model_satisfies_all_constraints(self.constraints, model)
243 .then(|| model.clone());
244 }
245
246 for candidate in &self.candidates[index] {
247 model.insert(self.vars[index].clone(), *candidate);
248 if fallback_partial_model_satisfies_known_constraints(
249 self.constraints,
250 self.constraint_vars,
251 self.searched_vars,
252 model,
253 ) && let Some(model) = self.model(index + 1, model, assignments)
254 {
255 return Some(model);
256 }
257 if *assignments > HARD_ARITH_FALLBACK_MAX_ASSIGNMENTS {
258 return None;
259 }
260 }
261 model.remove(&self.vars[index]);
262 None
263 }
264}
265
266pub(crate) fn fallback_model_satisfies_all_constraints(
268 constraints: &[BoolExpr],
269 model: &BTreeMap<String, U256>,
270) -> bool {
271 constraints.iter().all(|constraint| eval_bool_expr(constraint, model).unwrap_or(false))
272}
273
274pub(crate) fn fallback_partial_model_satisfies_known_constraints(
276 constraints: &[BoolExpr],
277 constraint_vars: &[BTreeSet<String>],
278 searched_vars: &BTreeSet<String>,
279 model: &BTreeMap<String, U256>,
280) -> bool {
281 constraints.iter().zip(constraint_vars).all(|(constraint, vars)| {
282 !vars.is_subset(searched_vars)
283 || !vars.iter().all(|var| model.contains_key(var))
284 || eval_bool_expr(constraint, model).unwrap_or(false)
285 })
286}
287
288pub(crate) fn collect_bool_fallback_vars(expr: &BoolExpr, vars: &mut BTreeSet<String>) {
290 match expr {
291 BoolExpr::Const(_) => {}
292 BoolExpr::Not(value) => collect_bool_fallback_vars(value, vars),
293 BoolExpr::And(values) => {
294 for value in values {
295 collect_bool_fallback_vars(value, vars);
296 }
297 }
298 BoolExpr::Eq(left, right) | BoolExpr::Cmp(_, left, right) => {
299 collect_expr_fallback_vars(left, vars);
300 collect_expr_fallback_vars(right, vars);
301 }
302 }
303}
304
305pub(crate) fn collect_expr_fallback_vars(expr: &Expr, vars: &mut BTreeSet<String>) {
307 match expr {
308 Expr::Const(_) | Expr::GasLeft(_) | Expr::Hash { .. } => {}
309 Expr::Var(var) => {
310 vars.insert(var.clone());
311 }
312 Expr::Keccak { len, bytes, .. } => {
313 collect_expr_fallback_vars(len, vars);
314 for byte in bytes {
315 collect_expr_fallback_vars(byte, vars);
316 }
317 }
318 Expr::Not(value) => collect_expr_fallback_vars(value, vars),
319 Expr::Op(_, left, right) => {
320 collect_expr_fallback_vars(left, vars);
321 collect_expr_fallback_vars(right, vars);
322 }
323 Expr::Ite(cond, left, right) => {
324 collect_bool_fallback_vars(cond, vars);
325 collect_expr_fallback_vars(left, vars);
326 collect_expr_fallback_vars(right, vars);
327 }
328 }
329}
330
331#[cfg(test)]
333pub(crate) fn fallback_single_var_model(
334 constraints: &[BoolExpr],
335) -> Option<BTreeMap<String, U256>> {
336 let mut vars = BTreeSet::new();
337 let mut constants = BTreeSet::new();
338 for constraint in constraints {
339 constraint.collect_vars(&mut vars);
340 collect_bool_constants(constraint, &mut constants);
341 }
342
343 let var = if vars.len() == 1 { vars.iter().next()?.clone() } else { return None };
344 let hints = MaskHints::for_var(&var, constraints);
345 if (hints.one & hints.zero) != U256::ZERO {
346 return None;
347 }
348
349 let mut candidates = BTreeSet::new();
350 for candidate in [
351 U256::ZERO,
352 U256::from(1),
353 U256::from(2),
354 U256::MAX,
355 U256::MAX - U256::from(1),
356 U256::MAX - U256::from(2),
357 ] {
358 push_fallback_candidate(&mut candidates, candidate, hints);
359 }
360
361 for constant in constants.iter().copied() {
362 push_fallback_candidate(&mut candidates, constant, hints);
363 push_fallback_candidate(&mut candidates, constant.wrapping_add(U256::from(1)), hints);
364 push_fallback_candidate(&mut candidates, constant.wrapping_sub(U256::from(1)), hints);
365 }
366
367 for bit in 0..256 {
368 let power = U256::from(1) << bit;
369 push_fallback_candidate(&mut candidates, power, hints);
370 for constant in constants.iter().copied().take(64) {
371 push_fallback_candidate(&mut candidates, power | constant, hints);
372 push_fallback_candidate(&mut candidates, power.wrapping_add(constant), hints);
373 }
374 }
375
376 for candidate in candidates {
377 let model = BTreeMap::from([(var.clone(), candidate)]);
378 if constraints.iter().all(|constraint| eval_bool_expr(constraint, &model).unwrap_or(false))
379 {
380 return Some(model);
381 }
382 }
383
384 None
385}
386
387pub(crate) fn push_fallback_candidate(
389 candidates: &mut BTreeSet<U256>,
390 candidate: U256,
391 hints: MaskHints,
392) {
393 candidates.insert((candidate | hints.one) & !hints.zero);
394}
395
396pub(crate) fn collect_bool_constants(expr: &BoolExpr, constants: &mut BTreeSet<U256>) {
398 match expr {
399 BoolExpr::Const(_) => {}
400 BoolExpr::Not(value) => collect_bool_constants(value, constants),
401 BoolExpr::And(values) => {
402 for value in values {
403 collect_bool_constants(value, constants);
404 }
405 }
406 BoolExpr::Eq(left, right) | BoolExpr::Cmp(_, left, right) => {
407 collect_expr_constants(left, constants);
408 collect_expr_constants(right, constants);
409 }
410 }
411}
412
413pub(crate) fn collect_expr_constants(expr: &Expr, constants: &mut BTreeSet<U256>) {
415 match expr {
416 Expr::Const(value) => {
417 constants.insert(*value);
418 }
419 Expr::Var(_) | Expr::GasLeft(_) | Expr::Keccak { .. } | Expr::Hash { .. } => {}
420 Expr::Not(value) => collect_expr_constants(value, constants),
421 Expr::Op(_, left, right) => {
422 collect_expr_constants(left, constants);
423 collect_expr_constants(right, constants);
424 }
425 Expr::Ite(cond, left, right) => {
426 collect_bool_constants(cond, constants);
427 collect_expr_constants(left, constants);
428 collect_expr_constants(right, constants);
429 }
430 }
431}
432
433#[derive(Clone, Copy, Debug, Default)]
434pub(crate) struct MaskHints {
435 pub(crate) one: U256,
436 pub(crate) zero: U256,
437}
438
439impl MaskHints {
440 pub(crate) fn for_var(var: &str, constraints: &[BoolExpr]) -> Self {
442 let mut hints = Self::default();
443 for constraint in constraints {
444 hints.apply_bool(var, constraint, false);
445 }
446 hints
447 }
448
449 pub(crate) fn apply_bool(&mut self, var: &str, expr: &BoolExpr, inverted: bool) {
451 match expr {
452 BoolExpr::Const(_) => {}
453 BoolExpr::Not(value) => self.apply_bool(var, value, !inverted),
454 BoolExpr::And(values) if !inverted => {
455 for value in values {
456 self.apply_bool(var, value, false);
457 }
458 }
459 BoolExpr::Eq(left, right) => self.apply_equality(var, left, right, inverted),
460 BoolExpr::Cmp(_, _, _) | BoolExpr::And(_) => {}
461 }
462 }
463
464 pub(crate) fn apply_equality(&mut self, var: &str, left: &Expr, right: &Expr, inverted: bool) {
466 if let Some(mask) =
467 zero_mask_equality(var, left, right).or_else(|| zero_mask_equality(var, right, left))
468 {
469 if inverted {
470 self.one |= mask;
471 } else {
472 self.zero |= mask;
473 }
474 }
475 }
476}
477
478pub(crate) fn zero_mask_equality(var: &str, masked: &Expr, zero: &Expr) -> Option<U256> {
480 if !matches!(zero, Expr::Const(value) if value.is_zero()) {
481 return None;
482 }
483 match masked {
484 Expr::Op(ExprOp::And, left, right) => match (left.as_ref(), right.as_ref()) {
485 (Expr::Var(name), Expr::Const(mask)) | (Expr::Const(mask), Expr::Var(name))
486 if name == var =>
487 {
488 Some(*mask)
489 }
490 _ => None,
491 },
492 _ => None,
493 }
494}