Skip to main content

foundry_evm_symbolic/executor/
constraints.rs

1use super::*;
2use foundry_evm::revm::interpreter::instructions::i256::i256_cmp;
3
4impl SymbolicExecutor {
5    pub(super) fn handle_assume(
6        &mut self,
7        state: &mut PathState,
8        condition_offset: usize,
9    ) -> Result<CheatcodeOutcome, SymbolicError> {
10        let cond = state.memory.load_word(&mut self.cx, condition_offset)?;
11        let cond = cond.nonzero_bool(&mut self.cx);
12        if state.invariant_predicate && cond.as_const() != Some(true) {
13            // A predicate must cover every reachable state. Restricting its input domain would
14            // hide rejected states, including when accepted values remain on this same path.
15            let rejected = cond.not(&mut self.cx);
16            let (_, rejected_sat) = self.constraints_with_condition(state, rejected)?;
17            if rejected_sat {
18                return Err(SymbolicError::Unsupported(
19                    "vm.assume may reject an invariant predicate",
20                ));
21            }
22            return Ok(CheatcodeOutcome::Continue(Vec::new()));
23        }
24        self.assume_condition(state, cond)
25    }
26
27    pub(super) fn handle_skip(
28        &mut self,
29        state: &mut PathState,
30        condition_offset: usize,
31    ) -> Result<CheatcodeOutcome, SymbolicError> {
32        let cond = state.memory.load_word(&mut self.cx, condition_offset)?;
33        let cond = cond.nonzero_bool(&mut self.cx).not(&mut self.cx);
34        self.assume_condition(state, cond)
35    }
36
37    pub(super) fn assume_condition(
38        &mut self,
39        state: &mut PathState,
40        condition: SymBoolExpr,
41    ) -> Result<CheatcodeOutcome, SymbolicError> {
42        match condition.as_const() {
43            Some(true) => Ok(CheatcodeOutcome::Continue(Vec::new())),
44            Some(false) => Ok(CheatcodeOutcome::AssumeRejected),
45            None => {
46                state.constraints.push(condition);
47                if self.is_sat_with_state(state, &state.constraints)? {
48                    Ok(CheatcodeOutcome::Continue(Vec::new()))
49                } else {
50                    Ok(CheatcodeOutcome::AssumeRejected)
51                }
52            }
53        }
54    }
55
56    pub(super) fn solver_upper_bound_usize(
57        &mut self,
58        state: &PathState,
59        expr: &SymExpr,
60        max: usize,
61        reason: &'static str,
62    ) -> Result<usize, SymbolicError> {
63        if let Some(bound) =
64            state.upper_bound_usize(&mut self.cx, expr).filter(|bound| *bound <= max)
65        {
66            return Ok(bound);
67        }
68        let mut above_max = state.constraints.clone();
69        above_max.push(SymBoolExpr::cmp_word_const(
70            &mut self.cx,
71            SymCmpOp::Ugt,
72            expr,
73            U256::from(max),
74        ));
75        if self.is_sat_with_state(state, &above_max)? {
76            return Err(SymbolicError::Unsupported(reason));
77        }
78
79        let mut low = 0usize;
80        let mut high = max;
81        while low < high {
82            let mid = low + (high - low) / 2;
83            let mut above_mid = state.constraints.clone();
84            above_mid.push(SymBoolExpr::cmp_word_const(
85                &mut self.cx,
86                SymCmpOp::Ugt,
87                expr,
88                U256::from(mid),
89            ));
90            if self.is_sat_with_state(state, &above_mid)? {
91                low = mid + 1;
92            } else {
93                high = mid;
94            }
95        }
96        Ok(low)
97    }
98
99    pub(super) fn assume_expr_at_least(
100        &mut self,
101        state: &mut PathState,
102        expr: &SymExpr,
103        min: usize,
104    ) -> Result<bool, SymbolicError> {
105        let condition =
106            SymBoolExpr::cmp_word_const(&mut self.cx, SymCmpOp::Uge, expr, U256::from(min));
107        match condition.as_const() {
108            Some(value) => Ok(value),
109            None => {
110                let mut constraints = state.constraints.clone();
111                constraints.push(condition);
112                if self.is_sat_with_state(state, &constraints)? {
113                    state.constraints = constraints;
114                    Ok(true)
115                } else {
116                    Ok(false)
117                }
118            }
119        }
120    }
121
122    /// Proves that every feasible value is at least `min` without restricting the path.
123    pub(super) fn proves_expr_at_least(
124        &mut self,
125        state: &PathState,
126        expr: &SymExpr,
127        min: usize,
128    ) -> Result<bool, SymbolicError> {
129        if state.lower_bound_usize(expr) >= min {
130            return Ok(true);
131        }
132
133        let mut below_min = state.constraints.clone();
134        below_min.push(SymBoolExpr::cmp_word_const(
135            &mut self.cx,
136            SymCmpOp::Ult,
137            expr,
138            U256::from(min),
139        ));
140        Ok(!self.is_sat_with_state(state, &below_min)?)
141    }
142
143    /// Resolves a path-constant word and proves that no alternate value is feasible.
144    pub(super) fn constrained_word_with_solver(
145        &mut self,
146        state: &PathState,
147        expr: &SymExpr,
148    ) -> Result<Option<U256>, SymbolicError> {
149        if let Some(value) = state.constrained_word(&mut self.cx, expr) {
150            return Ok(Some(value));
151        }
152        if expr.contains_gasleft() {
153            return Err(SymbolicError::Unsupported("GAS/gasleft() not modeled"));
154        }
155
156        let replayable_storage = state.world.replay_storage_symbols();
157        let model = self.solver.model_with_replayable_storage(
158            &mut self.cx,
159            &state.constraints,
160            &replayable_storage,
161        )?;
162        let value = expr.eval_model(&model)?;
163        let differs = SymBoolExpr::eq_word_const(&mut self.cx, expr, value).not(&mut self.cx);
164        let mut constraints = state.constraints.clone();
165        constraints.push(differs);
166        if self.is_sat_with_state(state, &constraints)? { Ok(None) } else { Ok(Some(value)) }
167    }
168
169    /// Rejects symbolic integer bit widths outside the EVM word size.
170    pub(super) fn validate_symbolic_integer_bits(
171        bits: U256,
172        context: &'static str,
173    ) -> Result<(), SymbolicError> {
174        if bits <= U256::from(256) { Ok(()) } else { Err(SymbolicError::Unsupported(context)) }
175    }
176
177    /// Handles `vm.bound` for unsigned or signed (`int256`) ranges.
178    pub(super) fn handle_bound(
179        &mut self,
180        state: &mut PathState,
181        args_offset: usize,
182        signed: bool,
183    ) -> Result<CheatcodeOutcome, SymbolicError> {
184        let value = read_abi_word_arg(&mut self.cx, &state.memory, args_offset, 0)?;
185        let min = read_abi_word_arg(&mut self.cx, &state.memory, args_offset, 1)?;
186        let max = read_abi_word_arg(&mut self.cx, &state.memory, args_offset, 2)?;
187        let order = |a: &U256, b: &U256| if signed { i256_cmp(a, b) } else { a.cmp(b) };
188
189        if let (Some(value), Some(min), Some(max)) =
190            (value.as_const(), min.as_const(), max.as_const())
191        {
192            if !order(&min, &max).is_lt()
193                || order(&value, &min).is_lt()
194                || order(&value, &max).is_gt()
195            {
196                return Ok(CheatcodeOutcome::Failure);
197            }
198            let bounded = if value == min { max } else { min };
199            return Ok(CheatcodeOutcome::Continue(vec![SymExpr::constant(&mut self.cx, bounded)]));
200        }
201
202        if let (Some(min), Some(max)) = (min.as_const(), max.as_const())
203            && !order(&min, &max).is_lt()
204        {
205            return Ok(CheatcodeOutcome::Failure);
206        }
207        let (Some(min_word), Some(max_word)) = (min.as_const(), max.as_const()) else {
208            return Err(SymbolicError::Unsupported("symbolic vm.bound range"));
209        };
210
211        // Signed bounds are expressed as `!(x < min) && !(x > max)`.
212        let range_conditions = |cx: &mut SymCx, word: &SymExpr, as_consts: bool| {
213            let cmp = |cx: &mut SymCx, op, bound| {
214                if as_consts {
215                    SymBoolExpr::cmp_word_const(cx, op, word, bound)
216                } else {
217                    let bound = SymExpr::constant(cx, bound);
218                    SymBoolExpr::cmp_word_expr(cx, op, word, bound)
219                }
220            };
221            if signed {
222                let below_min = cmp(cx, SymCmpOp::Slt, min_word).not(cx);
223                let above_max = cmp(cx, SymCmpOp::Sgt, max_word).not(cx);
224                [below_min, above_max]
225            } else {
226                [cmp(cx, SymCmpOp::Uge, min_word), cmp(cx, SymCmpOp::Ule, max_word)]
227            }
228        };
229        let in_range = range_conditions(&mut self.cx, &value, false);
230        let in_range = SymBoolExpr::and(&mut self.cx, in_range.into());
231        let (_, in_range_sat) = self.constraints_with_condition(state, in_range.clone())?;
232        if !in_range_sat {
233            return Ok(CheatcodeOutcome::Failure);
234        }
235        let out_of_range = in_range.not(&mut self.cx);
236        let (out_of_range_constraints, out_of_range_sat) =
237            self.constraints_with_condition(state, out_of_range)?;
238        if out_of_range_sat {
239            state.constraints = out_of_range_constraints;
240            return Ok(CheatcodeOutcome::Failure);
241        }
242
243        let bounded =
244            state.fresh_word(&mut self.cx, if signed { "vmBoundInt" } else { "vmBoundUint" });
245        state.constraints.extend(range_conditions(&mut self.cx, &bounded, true));
246        let same_value = SymBoolExpr::eq(&mut self.cx, bounded.clone(), value);
247        state.constraints.push(same_value.not(&mut self.cx));
248        Ok(CheatcodeOutcome::Continue(vec![bounded]))
249    }
250}