Skip to main content

foundry_evm_symbolic/executor/
run.rs

1use super::*;
2
3impl SymbolicExecutor {
4    /// Creates a symbolic executor from Foundry's symbolic configuration.
5    ///
6    /// The configured solver command is not executed here. Solver availability is
7    /// checked by [`Self::run`] so construction remains cheap and side-effect free.
8    ///
9    /// The executor owns an isolated solver backend and symbolic world overlay. Create
10    /// a fresh executor when a caller needs independent solver query accounting.
11    pub fn new(config: SymbolicConfig) -> Self {
12        let solver = SmtLibSubprocessSolver::from_config(&config);
13        Self { config, solver: Box::new(solver) }
14    }
15
16    /// Returns staged solver portfolio diagnostics collected by this executor.
17    pub fn portfolio_diagnostics(&self) -> Option<PortfolioDiagnostics> {
18        self.solver.portfolio_diagnostics().cloned()
19    }
20
21    /// Defers verbose solver diagnostics until the caller explicitly takes them.
22    pub fn capture_diagnostics(&mut self) {
23        self.solver.capture_diagnostics();
24    }
25
26    /// Returns and clears deferred verbose solver diagnostics.
27    pub fn take_diagnostics(&mut self) -> Option<String> {
28        self.solver.take_diagnostics()
29    }
30
31    /// Registers a callback invoked after each solver query for live progress rendering.
32    pub fn set_query_observer(&mut self, observer: impl Fn(usize) + Send + Sync + 'static) {
33        self.solver.set_query_observer(Some(Box::new(observer)));
34    }
35
36    /// Executes one function symbolically against an already-deployed test contract.
37    ///
38    /// The input executor supplies the deployed bytecode, storage backend, caller, and
39    /// target address established by the normal forge test setup flow. This method
40    /// does not mutate the concrete executor and does not replay failures itself; when
41    /// it returns [`SymbolicRunResult::Counterexample`], callers should replay the
42    /// returned arguments through the concrete executor before reporting the failure.
43    ///
44    /// Unsupported opcodes, unsupported ABI types, missing solver support, and resource
45    /// limit exhaustion are reported as [`SymbolicRunResult::Incomplete`].
46    ///
47    /// Ordinary Solidity `require` reverts prune the current path. Assertion failures,
48    /// forge-std assertion reverts, and DSTest failure signals are reported as
49    /// counterexample candidates when the failing path is satisfiable.
50    pub fn run<FEN: FoundryEvmNetwork>(
51        &mut self,
52        input: SymbolicRunInput<'_, FEN>,
53    ) -> SymbolicRunResult {
54        if let Err(err) = self.solver.check_available() {
55            return SymbolicRunResult::Incomplete {
56                kind: err.stop_reason(),
57                reason: err.to_string(),
58                stats: SymbolicStats::default(),
59            };
60        }
61
62        match self.run_inner(input) {
63            Ok(result) => result,
64            Err(err) => SymbolicRunResult::Incomplete {
65                kind: err.stop_reason(),
66                reason: err.to_string(),
67                stats: self.solver.stats(),
68            },
69        }
70    }
71
72    /// Executes a bounded symbolic invariant call sequence.
73    ///
74    /// Each sequence step chooses from the concrete target functions and senders supplied by
75    /// Foundry's invariant target discovery. Arguments are generated through the same symbolic ABI
76    /// model used by stateless symbolic tests, and the symbolic world state is preserved between
77    /// steps. Returned counterexamples must still be replayed by the caller before reporting.
78    ///
79    /// The configured invariant depth limits the number of target calls explored before the
80    /// invariant is checked. A depth of zero checks only the invariant against setup state.
81    pub fn run_invariant<FEN: FoundryEvmNetwork>(
82        &mut self,
83        input: SymbolicInvariantRunInput<'_, FEN>,
84    ) -> SymbolicInvariantRunResult {
85        if let Err(err) = self.solver.check_available() {
86            return SymbolicInvariantRunResult::Incomplete {
87                kind: err.stop_reason(),
88                reason: err.to_string(),
89                stats: SymbolicStats::default(),
90            };
91        }
92
93        match self.run_invariant_inner(input) {
94            Ok(result) => result,
95            Err(err) => SymbolicInvariantRunResult::Incomplete {
96                kind: err.stop_reason(),
97                reason: err.to_string(),
98                stats: self.solver.stats(),
99            },
100        }
101    }
102
103    /// Runs the `run_inner` symbolic executor helper.
104    pub(super) fn run_inner<FEN: FoundryEvmNetwork>(
105        &mut self,
106        input: SymbolicRunInput<'_, FEN>,
107    ) -> Result<SymbolicRunResult, SymbolicError> {
108        let heuristic_witness_baseline = self.solver.heuristic_witnesses();
109        let account = input
110            .executor
111            .backend()
112            .basic_ref(input.target)
113            .map_err(|err| SymbolicError::Backend(err.to_string()))?
114            .ok_or(SymbolicError::MissingAccount(input.target))?;
115        let code =
116            account.code.ok_or(SymbolicError::MissingCode(input.target))?.original_bytes().to_vec();
117        let code = SymCode::concrete(code);
118        let jumpdests = analyze_jumpdests(&code);
119        let mut worklist = VecDeque::new();
120        for calldata in SymbolicCalldata::variants(input.function, &self.config)? {
121            let mut root = PathState::new(
122                input.target,
123                input.sender,
124                input.value,
125                calldata,
126                input.ffi_enabled,
127            );
128            root.apply_executor_env(input.executor);
129            root.world.set_storage_layout(self.config.storage_layout);
130            root.world.clear_transient_storage();
131            worklist.push_back(root);
132        }
133        let mut completed_paths = 0usize;
134        let mut reverted_paths = 0usize;
135        let mut normal_paths = 0usize;
136        let path_limit = self.config.path_width() as usize;
137        let depth_limit = self.config.execution_depth() as usize;
138
139        while let Some(mut state) = pop_worklist(&mut worklist, self.config.exploration_order) {
140            if completed_paths >= path_limit {
141                debug!(completed_paths, path_limit, "symbolic path limit reached");
142                return Ok(SymbolicRunResult::Incomplete {
143                    kind: SymbolicStopReason::Stuck,
144                    reason: format!("symbolic path limit exceeded ({path_limit})"),
145                    stats: self.stats_with_paths(completed_paths),
146                });
147            }
148            let _path_span =
149                trace_span!("symbolic_path", completed_paths, worklist_size = worklist.len())
150                    .entered();
151            trace!(completed_paths, worklist_size = worklist.len(), "exploring symbolic path");
152
153            loop {
154                if state.depth >= depth_limit {
155                    debug!(depth = state.depth, depth_limit, "symbolic depth limit reached");
156                    return Ok(SymbolicRunResult::Incomplete {
157                        kind: SymbolicStopReason::Stuck,
158                        reason: format!("symbolic depth limit exceeded ({depth_limit})"),
159                        stats: self.stats_with_paths(completed_paths),
160                    });
161                }
162                state.depth += 1;
163
164                let Some(op) = code.opcode(state.pc)? else {
165                    if !state.expectations_satisfied() {
166                        let (args, calldata_bytes) = self.materialize_stateless_counterexample(
167                            state.root_calldata.as_ref().ok_or_else(|| {
168                                SymbolicError::Unsupported("missing root symbolic calldata")
169                            })?,
170                            input.function,
171                            &state,
172                        )?;
173                        return Ok(SymbolicRunResult::Counterexample {
174                            args,
175                            calldata: calldata_bytes,
176                            stats: self.stats_with_paths(completed_paths + 1),
177                        });
178                    }
179                    completed_paths += 1;
180                    break;
181                };
182
183                let _step_span = trace_span!("symbolic_step", pc = state.pc - 1, op).entered();
184                match self.step(
185                    input.executor,
186                    &code,
187                    &jumpdests,
188                    &mut state,
189                    &mut worklist,
190                    &mut completed_paths,
191                    op,
192                )? {
193                    StepOutcome::Continue => {}
194                    StepOutcome::Halt => {
195                        if !state.expectations_satisfied() {
196                            let (args, calldata_bytes) = self
197                                .materialize_stateless_counterexample(
198                                    state.root_calldata.as_ref().ok_or_else(|| {
199                                        SymbolicError::Unsupported("missing root symbolic calldata")
200                                    })?,
201                                    input.function,
202                                    &state,
203                                )?;
204                            return Ok(SymbolicRunResult::Counterexample {
205                                args,
206                                calldata: calldata_bytes,
207                                stats: self.stats_with_paths(completed_paths + 1),
208                            });
209                        }
210                        completed_paths += 1;
211                        normal_paths += 1;
212                        break;
213                    }
214                    StepOutcome::Revert => {
215                        completed_paths += 1;
216                        reverted_paths += 1;
217                        break;
218                    }
219                    StepOutcome::AssumeRejected => break,
220                    StepOutcome::Forked => break,
221                    StepOutcome::Failure => {
222                        let (args, calldata_bytes) = self.materialize_stateless_counterexample(
223                            state.root_calldata.as_ref().ok_or_else(|| {
224                                SymbolicError::Unsupported("missing root symbolic calldata")
225                            })?,
226                            input.function,
227                            &state,
228                        )?;
229                        return Ok(SymbolicRunResult::Counterexample {
230                            args,
231                            calldata: calldata_bytes,
232                            stats: self.stats_with_paths(completed_paths + 1),
233                        });
234                    }
235                }
236            }
237        }
238
239        if normal_paths == 0 && reverted_paths > 0 {
240            debug!(completed_paths, "all symbolic paths reverted");
241            return Ok(SymbolicRunResult::Incomplete {
242                kind: SymbolicStopReason::RevertAll,
243                reason: "all symbolic paths reverted".to_string(),
244                stats: self.stats_with_paths(completed_paths),
245            });
246        }
247
248        if self.heuristic_witnesses_used_since(heuristic_witness_baseline) {
249            return Ok(SymbolicRunResult::Incomplete {
250                kind: SymbolicStopReason::Timeout,
251                reason: Self::hard_arith_heuristic_incomplete_reason(),
252                stats: self.stats_with_paths(completed_paths),
253            });
254        }
255
256        debug!(completed_paths, "symbolic execution safe");
257        Ok(SymbolicRunResult::Safe(self.stats_with_paths(completed_paths)))
258    }
259
260    /// Runs the `materialize_stateless_counterexample` symbolic executor helper.
261    pub(super) fn materialize_stateless_counterexample(
262        &mut self,
263        calldata: &SymbolicCalldata,
264        function: &Function,
265        state: &PathState,
266    ) -> Result<(Vec<DynSolValue>, Bytes), SymbolicError> {
267        debug!(
268            constraint_count = state.constraints.len(),
269            "materializing counterexample from solver model"
270        );
271        let model = self.solver.model(&state.constraints)?;
272        let args = calldata.model_to_args(&model)?;
273        let calldata_bytes = Bytes::from(function.abi_encode_input(&args)?);
274        Ok((args, calldata_bytes))
275    }
276
277    /// Runs the `run_invariant_inner` symbolic executor helper.
278    pub(super) fn run_invariant_inner<FEN: FoundryEvmNetwork>(
279        &mut self,
280        input: SymbolicInvariantRunInput<'_, FEN>,
281    ) -> Result<SymbolicInvariantRunResult, SymbolicError> {
282        let heuristic_witness_baseline = self.solver.heuristic_witnesses();
283        if input.targets.is_empty() {
284            return Err(SymbolicError::Unsupported("symbolic invariant has no targets"));
285        }
286
287        let senders =
288            if input.senders.is_empty() { vec![input.sender] } else { input.senders.clone() };
289        let mut completed_paths = 0usize;
290        let mut initial_state =
291            PathState::empty(input.invariant_address, input.sender, input.ffi_enabled);
292        initial_state.apply_executor_env(input.executor);
293        initial_state.world.set_storage_layout(self.config.storage_layout);
294        let initial = SequencePath { state: initial_state, steps: Vec::new() };
295
296        for outcome in self.execute_invariant_check(
297            input.executor,
298            initial.state.clone(),
299            input.invariant_address,
300            input.sender,
301            input.invariant,
302            input.after_invariant,
303            &mut completed_paths,
304        )? {
305            if outcome.failed {
306                let sequence = self.materialize_sequence(&initial.steps, &outcome.state)?;
307                return Ok(SymbolicInvariantRunResult::Counterexample {
308                    sequence,
309                    stats: self.stats_with_paths(completed_paths),
310                });
311            }
312        }
313
314        let path_limit = self.config.path_width() as usize;
315        let mut frontier = vec![initial];
316        for depth in 0..input.depth {
317            let mut next_frontier = Vec::new();
318            for sequence in frontier {
319                for (target_idx, target) in input.targets.iter().enumerate() {
320                    for (sender_idx, sender) in senders.iter().copied().enumerate() {
321                        let prefix = format!("sequence_{depth}_{target_idx}_{sender_idx}");
322                        let calldatas = SymbolicCalldata::variants_with_prefix(
323                            &target.function,
324                            &self.config,
325                            prefix,
326                        )?;
327                        for calldata in calldatas {
328                            let step = SequenceStepTemplate {
329                                sender,
330                                address: target.address,
331                                contract_name: target.contract_name.clone(),
332                                function: target.function.clone(),
333                                calldata,
334                            };
335                            let outcomes = self.execute_sequence_call(
336                                input.executor,
337                                sequence.state.clone(),
338                                target.address,
339                                sender,
340                                &target.function,
341                                step.calldata.call_data(),
342                                step.calldata.constraints.clone(),
343                                &mut completed_paths,
344                            )?;
345
346                            for outcome in outcomes {
347                                let mut steps = sequence.steps.clone();
348                                steps.push(step.clone());
349
350                                match outcome.status {
351                                    TopLevelCallStatus::Failure => {
352                                        let sequence =
353                                            self.materialize_sequence(&steps, &outcome.state)?;
354                                        return Ok(SymbolicInvariantRunResult::Counterexample {
355                                            sequence,
356                                            stats: self.stats_with_paths(completed_paths),
357                                        });
358                                    }
359                                    TopLevelCallStatus::Revert => {
360                                        if input.fail_on_revert {
361                                            let sequence =
362                                                self.materialize_sequence(&steps, &outcome.state)?;
363                                            return Ok(
364                                                SymbolicInvariantRunResult::Counterexample {
365                                                    sequence,
366                                                    stats: self.stats_with_paths(completed_paths),
367                                                },
368                                            );
369                                        }
370                                        let mut reverted_state = sequence.state.clone();
371                                        reverted_state.world.clear_transient_storage();
372                                        for invariant_outcome in self.execute_invariant_check(
373                                            input.executor,
374                                            reverted_state,
375                                            input.invariant_address,
376                                            input.sender,
377                                            input.invariant,
378                                            input.after_invariant,
379                                            &mut completed_paths,
380                                        )? {
381                                            if invariant_outcome.failed {
382                                                let sequence = self.materialize_sequence(
383                                                    &steps,
384                                                    &invariant_outcome.state,
385                                                )?;
386                                                return Ok(
387                                                    SymbolicInvariantRunResult::Counterexample {
388                                                        sequence,
389                                                        stats: self
390                                                            .stats_with_paths(completed_paths),
391                                                    },
392                                                );
393                                            }
394                                            next_frontier.push(SequencePath {
395                                                state: invariant_outcome.state,
396                                                steps: steps.clone(),
397                                            });
398                                        }
399                                    }
400                                    TopLevelCallStatus::Success => {
401                                        for invariant_outcome in self.execute_invariant_check(
402                                            input.executor,
403                                            outcome.state.clone(),
404                                            input.invariant_address,
405                                            input.sender,
406                                            input.invariant,
407                                            input.after_invariant,
408                                            &mut completed_paths,
409                                        )? {
410                                            if invariant_outcome.failed {
411                                                let sequence = self.materialize_sequence(
412                                                    &steps,
413                                                    &invariant_outcome.state,
414                                                )?;
415                                                return Ok(
416                                                    SymbolicInvariantRunResult::Counterexample {
417                                                        sequence,
418                                                        stats: self
419                                                            .stats_with_paths(completed_paths),
420                                                    },
421                                                );
422                                            }
423                                            next_frontier.push(SequencePath {
424                                                state: invariant_outcome.state,
425                                                steps: steps.clone(),
426                                            });
427                                        }
428                                    }
429                                }
430
431                                if completed_paths >= path_limit {
432                                    return Ok(SymbolicInvariantRunResult::Incomplete {
433                                        kind: SymbolicStopReason::Stuck,
434                                        reason: format!(
435                                            "symbolic path limit exceeded ({path_limit})"
436                                        ),
437                                        stats: self.stats_with_paths(completed_paths),
438                                    });
439                                }
440                            }
441                        }
442                    }
443                }
444            }
445
446            if next_frontier.is_empty() {
447                break;
448            }
449            frontier = next_frontier;
450        }
451
452        if self.heuristic_witnesses_used_since(heuristic_witness_baseline) {
453            return Ok(SymbolicInvariantRunResult::Incomplete {
454                kind: SymbolicStopReason::Timeout,
455                reason: Self::hard_arith_heuristic_incomplete_reason(),
456                stats: self.stats_with_paths(completed_paths),
457            });
458        }
459
460        Ok(SymbolicInvariantRunResult::Safe(self.stats_with_paths(completed_paths)))
461    }
462
463    /// Implements the `stats_with_paths` symbolic executor helper.
464    pub(super) fn stats_with_paths(&self, paths: usize) -> SymbolicStats {
465        let mut stats = self.solver.stats();
466        stats.paths = paths;
467        stats
468    }
469
470    /// Returns whether this run used a hard-arithmetic heuristic witness.
471    fn heuristic_witnesses_used_since(&self, baseline: usize) -> bool {
472        self.solver.heuristic_witnesses() > baseline
473    }
474
475    /// Returns the incomplete reason used when heuristic witnesses cannot certify safety.
476    fn hard_arith_heuristic_incomplete_reason() -> String {
477        "hard arithmetic heuristic witness used; no replayed counterexample found".to_string()
478    }
479}