Skip to main content

foundry_evm_symbolic/runtime/solver/
hard_arith_fallback.rs

1use super::*;
2
3/// Returns the `bool_contains_hard_arith` solver helper result.
4pub(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
15/// Returns the `expr_contains_hard_arith` solver helper result.
16pub(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
39/// Returns whether the expression contains symbolic hash variables that local search should avoid.
40pub(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
59/// Returns whether the boolean expression contains symbolic hash variables.
60pub(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
71/// Returns the `expr_contains_var` solver helper result.
72pub(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
85/// Returns the `bool_contains_var` solver helper result.
86pub(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
97/// Returns whether local hard-arithmetic search should run before asking the solver.
98pub(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
113/// Implements the `hard_arith_fallback_model` solver helper.
114pub(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
159/// Selects direct symbolic inputs for bounded fallback search.
160pub(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
176/// Returns deterministic local-search candidates for one symbolic variable.
177pub(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
220/// Holds immutable state for recursive hard-arithmetic fallback search.
221struct 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    /// Searches the bounded candidate product for a satisfying assignment.
231    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
266/// Checks all constraints before returning a hard-arithmetic fallback witness.
267pub(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
274/// Checks constraints that depend only on already-assigned fallback variables.
275pub(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
288/// Collects variables that local hard-arithmetic search can assign directly.
289pub(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
305/// Collects assignable variables from an expression, recursing into recomputable hashes.
306pub(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/// Implements the `fallback_single_var_model` solver helper.
332#[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
387/// Applies the `push_fallback_candidate` solver helper.
388pub(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
396/// Implements the `collect_bool_constants` solver helper.
397pub(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
413/// Implements the `collect_expr_constants` solver helper.
414pub(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    /// Implements the `for_var` solver helper.
441    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    /// Applies the `apply_bool` solver helper.
450    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    /// Applies the `apply_equality` solver helper.
465    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
478/// Implements the `zero_mask_equality` solver helper.
479pub(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}