Skip to main content

foundry_evm_symbolic/
lib.rs

1//! Foundry's symbolic EVM executor.
2
3#![warn(unused_crate_dependencies)]
4
5use alloy_dyn_abi::{DynSolType, DynSolValue, JsonAbiExt};
6use alloy_json_abi::Function;
7use alloy_primitives::{
8    Address, B256, Bytes, I256, Keccak256, U256, hex, keccak256,
9    map::{HashMap, HashSet, IndexSet},
10};
11use alloy_signer::SignerSync;
12use alloy_signer_local::{
13    PrivateKeySigner,
14    coins_bip39::{English, Wordlist},
15};
16use alloy_sol_types::SolCall;
17use base64::prelude::*;
18use foundry_cheatcodes_spec::{SymbolicVm, Vm};
19use foundry_config::{SymbolicConfig, SymbolicExplorationOrder, SymbolicStorageLayout};
20use foundry_evm::{
21    constants::{CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS},
22    core::{backend::DatabaseExt, evm::FoundryEvmNetwork},
23    executors::Executor,
24    revm::{
25        bytecode::{Bytecode, JumpTable, eip7702::EIP7702_BYTECODE_LEN, opcode},
26        context::{Block, Cfg, Transaction},
27        database::DatabaseRef,
28        precompile::{PrecompileSpecId, Precompiles, kzg_point_evaluation},
29        primitives::hardfork::SpecId,
30    },
31};
32use serde::{Deserialize, Serialize};
33use std::{
34    collections::VecDeque,
35    fmt::{self, Write as _},
36    io::Write,
37    ops::{ControlFlow, Deref, DerefMut},
38    path::{Path, PathBuf},
39    process::{Command, Stdio},
40    sync::{
41        Arc,
42        atomic::{AtomicBool, Ordering},
43        mpsc,
44    },
45    thread,
46    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
47};
48use thiserror::Error;
49use tracing::{debug, trace, trace_span, warn};
50
51mod abi;
52mod consts;
53mod executor;
54mod runtime;
55
56pub(crate) use consts::*;
57pub use runtime::{ExecutorView, SymbolicBranchTarget, SymbolicError, SymbolicRunInput};
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60enum SymbolicVmCheatcode {
61    CreateAddress,
62    CreateBool,
63    CreateBytes,
64    CreateBytesSized,
65    CreateBytesFixed(usize),
66    CreateCalldata,
67    CreateInt,
68    CreateIntBits(usize),
69    CreateString,
70    CreateStringSized,
71    CreateUint,
72    CreateUintBits(usize),
73    EnableSymbolicStorage,
74    SnapshotStorage,
75    SnapshotState,
76}
77
78impl SymbolicVmCheatcode {
79    fn from_selector(selector: [u8; 4]) -> Option<Self> {
80        match selector {
81            SymbolicVm::createAddressCall::SELECTOR => Some(Self::CreateAddress),
82            SymbolicVm::createBoolCall::SELECTOR => Some(Self::CreateBool),
83            SymbolicVm::createBytes_0Call::SELECTOR => Some(Self::CreateBytes),
84            SymbolicVm::createBytes_1Call::SELECTOR => Some(Self::CreateBytesSized),
85            SymbolicVm::createCalldataCall::SELECTOR => Some(Self::CreateCalldata),
86            SymbolicVm::createIntCall::SELECTOR => Some(Self::CreateInt),
87            SymbolicVm::createString_0Call::SELECTOR => Some(Self::CreateString),
88            SymbolicVm::createString_1Call::SELECTOR => Some(Self::CreateStringSized),
89            SymbolicVm::createUintCall::SELECTOR => Some(Self::CreateUint),
90            SymbolicVm::enableSymbolicStorageCall::SELECTOR
91            | Vm::setArbitraryStorage_0Call::SELECTOR => Some(Self::EnableSymbolicStorage),
92            SymbolicVm::snapshotStorageCall::SELECTOR => Some(Self::SnapshotStorage),
93            Vm::snapshotStateCall::SELECTOR => Some(Self::SnapshotState),
94            _ => {
95                let name = SymbolicVm::SymbolicVmCalls::name_by_selector(selector)?;
96                if let Some(bits) = name.strip_prefix("createUint") {
97                    bits.parse().ok().map(Self::CreateUintBits)
98                } else if let Some(bits) = name.strip_prefix("createInt") {
99                    bits.parse().ok().map(Self::CreateIntBits)
100                } else if let Some(bytes) = name.strip_prefix("createBytes") {
101                    bytes.parse().ok().map(Self::CreateBytesFixed)
102                } else {
103                    None
104                }
105            }
106        }
107    }
108
109    const fn min_input_words(self) -> usize {
110        match self {
111            Self::CreateUint
112            | Self::CreateInt
113            | Self::CreateBytesSized
114            | Self::CreateStringSized
115            | Self::EnableSymbolicStorage
116            | Self::SnapshotStorage => 1,
117            Self::CreateAddress
118            | Self::CreateBool
119            | Self::CreateBytes
120            | Self::CreateBytesFixed(_)
121            | Self::CreateCalldata
122            | Self::CreateIntBits(_)
123            | Self::CreateString
124            | Self::CreateUintBits(_)
125            | Self::SnapshotState => 0,
126        }
127    }
128}
129
130/// Outcome of a symbolic test execution.
131///
132/// The forge runner treats `Safe` as a passing symbolic test, `Counterexample` as a
133/// candidate failure that must be replayed concretely, and `Incomplete` as a failing
134/// test because the symbolic engine could not prove the property with the supported
135/// semantics and configured resource limits.
136#[derive(Clone, Debug)]
137pub enum SymbolicRunResult {
138    /// All explored paths completed without a feasible failure.
139    Safe {
140        /// Execution counters collected during the run.
141        stats: SymbolicStats,
142        /// One concrete successful input, when requested by the caller.
143        success_input: Option<SymbolicConcreteInput>,
144    },
145    /// A feasible failure was found.
146    Counterexample {
147        /// ABI-typed argument values extracted from the solver model.
148        args: Vec<DynSolValue>,
149        /// ABI-encoded calldata for the failing invocation.
150        calldata: Bytes,
151        /// Execution counters collected before the counterexample was returned.
152        stats: SymbolicStats,
153    },
154    /// Execution was intentionally stopped because V1 semantics were insufficient.
155    Incomplete {
156        /// Category describing why symbolic execution stopped before proving the test.
157        kind: SymbolicStopReason,
158        /// Human-readable explanation of the unsupported construct or exhausted limit.
159        reason: String,
160        /// Execution counters collected before execution stopped.
161        stats: SymbolicStats,
162    },
163}
164
165/// One concrete symbolic input materialized from a solver model.
166#[derive(Clone, Debug)]
167pub struct SymbolicConcreteInput {
168    /// ABI-typed argument values extracted from the solver model.
169    pub args: Vec<DynSolValue>,
170    /// ABI-encoded calldata for replay.
171    pub calldata: Bytes,
172}
173
174/// Result of best-effort symbolic exploration toward one branch target.
175#[derive(Debug)]
176pub struct SymbolicBranchTargetSearchResult {
177    /// Concrete inputs whose completed root path reached the requested branch outcome.
178    pub candidates: Vec<SymbolicConcreteInput>,
179    /// Underlying execution result, retained so callers can report incomplete exploration.
180    pub execution: SymbolicRunResult,
181}
182
183/// A concrete invariant target selected from Foundry's invariant discovery.
184#[derive(Clone, Debug)]
185pub struct SymbolicInvariantTarget {
186    /// Address that receives the sequence call.
187    pub address: Address,
188    /// Human-readable contract identifier used in counterexample rendering.
189    pub contract_name: Option<String>,
190    /// ABI function invoked with symbolic arguments.
191    pub function: Function,
192}
193
194/// Input for best-effort invariant candidate search after one symbolic handler call.
195pub struct SymbolicInvariantCandidateInput<'a> {
196    /// Read-only view of the concrete Foundry executor containing the replayed invariant frontier
197    /// prefix.
198    pub executor: ExecutorView<'a>,
199    /// Address of the deployed invariant test contract.
200    pub invariant_address: Address,
201    /// Invariant functions checked independently after the handler call.
202    pub invariants: &'a [&'a Function],
203    /// Optional campaign hook checked from the unchanged post-handler state.
204    pub after_invariant: Option<&'a Function>,
205    /// Concrete handler target selected from the captured frontier.
206    pub target: &'a SymbolicInvariantTarget,
207    /// Sender of the captured handler call.
208    pub handler_sender: Address,
209    /// Whether symbolic `vm.ffi` calls are allowed to execute subprocesses.
210    pub ffi_enabled: bool,
211}
212
213/// One unconfirmed symbolic input produced by invariant candidate search.
214#[derive(Clone, Debug)]
215pub struct SymbolicInvariantCandidate {
216    /// Index within [`SymbolicInvariantCandidateInput::invariants`] predicted to fail.
217    pub invariant_idx: usize,
218    /// Concrete handler call extracted from the solver model.
219    pub step: SymbolicInvariantStep,
220    /// Concrete setup-storage values needed to replay the candidate.
221    pub storage: Vec<SymbolicStorageAssignment>,
222}
223
224/// An execution or solver limitation encountered during best-effort candidate search.
225#[derive(Clone, Debug, PartialEq, Eq)]
226pub struct SymbolicInvariantSearchLimitation {
227    /// Category describing why part of the search could not complete.
228    pub kind: SymbolicStopReason,
229    /// Human-readable description of the limitation.
230    pub reason: String,
231}
232
233impl From<SymbolicError> for SymbolicInvariantSearchLimitation {
234    fn from(error: SymbolicError) -> Self {
235        Self { kind: error.stop_reason(), reason: error.to_string() }
236    }
237}
238
239/// Result of best-effort invariant candidate search after one symbolic handler call.
240#[derive(Clone, Debug)]
241pub struct SymbolicInvariantCandidateSearchResult {
242    /// Unconfirmed candidates that must be replayed concretely by the caller.
243    pub candidates: Vec<SymbolicInvariantCandidate>,
244    /// First encountered search limitation, unless a later error exhausts the search.
245    /// `None` is not a proof of safety.
246    pub limitation: Option<SymbolicInvariantSearchLimitation>,
247}
248
249/// Input for bounded symbolic invariant execution.
250pub struct SymbolicInvariantRunInput<'a> {
251    /// Read-only view of the concrete Foundry executor used as the source of deployed bytecode and
252    /// backend state.
253    pub executor: ExecutorView<'a>,
254    /// Address of the deployed invariant test contract.
255    pub invariant_address: Address,
256    /// Default sender used when invariant targeting does not configure senders.
257    pub sender: Address,
258    /// Invariant function checked after each symbolic sequence step.
259    pub invariant: &'a Function,
260    /// Optional `afterInvariant` hook to execute after a passing invariant check.
261    pub after_invariant: Option<&'a Function>,
262    /// Concrete target/selector set discovered by Foundry invariant targeting.
263    pub targets: Vec<SymbolicInvariantTarget>,
264    /// Concrete sender set discovered by Foundry invariant targeting.
265    pub senders: Vec<Address>,
266    /// Sender addresses excluded by Foundry invariant targeting.
267    pub excluded_senders: Vec<Address>,
268    /// Maximum number of sequence calls to execute.
269    pub depth: usize,
270    /// Concrete invariant check interval. `0` means only check at sequence end.
271    pub check_interval: u32,
272    /// Whether ordinary target-call reverts should be reported as failures.
273    pub fail_on_revert: bool,
274    /// Whether symbolic `vm.ffi` calls are allowed to execute subprocesses.
275    pub ffi_enabled: bool,
276}
277
278/// One concrete storage value required to replay a symbolic invariant candidate.
279#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
280pub struct SymbolicStorageAssignment {
281    /// Account whose storage slot should be initialized.
282    pub address: Address,
283    /// Concrete storage slot.
284    pub slot: U256,
285    /// Concrete value extracted from the solver model.
286    pub value: U256,
287}
288
289/// Outcome of bounded symbolic invariant execution.
290#[derive(Clone, Debug)]
291pub enum SymbolicInvariantRunResult {
292    /// No feasible invariant failure was found within the configured sequence depth.
293    Safe(SymbolicStats),
294    /// A feasible invariant or handler failure was found.
295    Counterexample {
296        /// Which part of the invariant run produced the failure.
297        kind: SymbolicInvariantCounterexampleKind,
298        /// Concrete sequence extracted from the solver model.
299        sequence: Vec<SymbolicInvariantStep>,
300        /// Concrete setup-storage values needed for replay.
301        storage: Vec<SymbolicStorageAssignment>,
302        /// Execution counters collected before the counterexample was returned.
303        stats: SymbolicStats,
304    },
305    /// Execution stopped before proving the invariant.
306    Incomplete {
307        /// Category describing why symbolic execution stopped.
308        kind: SymbolicStopReason,
309        /// Human-readable explanation of the unsupported construct or exhausted limit.
310        reason: String,
311        /// Execution counters collected before execution stopped.
312        stats: SymbolicStats,
313    },
314}
315
316/// Part of a symbolic invariant run that produced a replayable counterexample.
317#[derive(Clone, Copy, Debug, PartialEq, Eq)]
318pub enum SymbolicInvariantCounterexampleKind {
319    /// An `invariant_*` or `afterInvariant` check failed.
320    Predicate,
321    /// A fuzzed target/handler call failed with an assertion.
322    Handler,
323}
324
325/// One concrete step in a symbolic invariant counterexample sequence.
326#[derive(Clone, Debug)]
327pub struct SymbolicInvariantStep {
328    /// Sender used for the call.
329    pub sender: Address,
330    /// Target address called by the sequence step.
331    pub address: Address,
332    /// Human-readable contract identifier, when known.
333    pub contract_name: Option<String>,
334    /// ABI function name.
335    pub function_name: String,
336    /// ABI function signature.
337    pub signature: String,
338    /// ABI-typed arguments extracted from the solver model.
339    pub args: Vec<DynSolValue>,
340    /// ABI-encoded calldata for replay.
341    pub calldata: Bytes,
342}
343
344/// High-level reason a symbolic run stopped without a proof or replayed counterexample.
345#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
346pub enum SymbolicStopReason {
347    /// The executor reached a supported-but-incomplete semantic boundary.
348    Stuck,
349    /// Every explored execution path ended in an ordinary revert.
350    RevertAll,
351    /// The solver timed out or returned `unknown`.
352    Timeout,
353    /// An internal engine, backend, or solver process error occurred.
354    Error,
355}
356
357/// Symbolic execution counters.
358#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
359pub struct SymbolicStats {
360    /// Number of explored symbolic paths.
361    pub paths: usize,
362    /// Number of normalized solver queries issued during the run.
363    pub solver_queries: usize,
364    /// Number of queries sent to the SMT backend after local fast paths.
365    #[serde(default)]
366    pub smt_queries: usize,
367    /// Number of satisfiability checks requested by the executor.
368    #[serde(default)]
369    pub sat_queries: usize,
370    /// Number of concrete model requests requested by the executor.
371    #[serde(default)]
372    pub model_queries: usize,
373    /// Number of satisfiability checks served from the normalized cache.
374    #[serde(default)]
375    pub sat_cache_hits: usize,
376    /// Number of model requests served from the normalized model cache.
377    #[serde(default)]
378    pub model_cache_hits: usize,
379    /// Number of satisfiable witnesses produced by local hard-arithmetic search.
380    #[serde(default)]
381    pub heuristic_witnesses: usize,
382    /// Wall-clock time spent waiting on backend solver subprocesses, in milliseconds.
383    #[serde(default)]
384    pub solver_time_ms: u64,
385    /// Total SMT-LIB input bytes sent to backend solver subprocesses.
386    #[serde(default)]
387    pub smt_input_bytes: u64,
388    /// Largest single SMT-LIB query input sent to a backend solver subprocess, in bytes.
389    #[serde(default)]
390    pub smt_max_query_bytes: u64,
391    /// Wall-clock time spent building SMT-LIB query strings, in milliseconds.
392    #[serde(default)]
393    pub smt_build_time_ms: u64,
394    /// Longest single backend solver subprocess query, in milliseconds.
395    #[serde(default)]
396    pub smt_max_query_time_ms: u64,
397}
398
399/// SMT-LIB-backed symbolic executor.
400///
401/// This executor is intentionally separate from the concrete revm executor used by
402/// Foundry. It consumes bytecode and state from an existing [`Executor`], explores
403/// symbolic branches, and returns either a proof result, a counterexample candidate,
404/// or an incomplete result.
405pub struct SymbolicExecutor {
406    config: SymbolicConfig,
407    cx: runtime::SymCx,
408    solver: runtime::SmtLibSubprocessSolver,
409    deferred_incomplete: Option<DeferredIncomplete>,
410    deadline: Option<Instant>,
411    nested_deferred_mode: DeferredPathMode,
412    stateless_retry_safe: bool,
413}
414
415#[derive(Clone, Copy, Debug, PartialEq, Eq)]
416enum DeferredPathMode {
417    Skip,
418    Yield,
419    Drain,
420}
421
422#[derive(Clone, Copy, Debug, PartialEq, Eq)]
423enum DeferredIncomplete {
424    Unsupported(&'static str),
425    SolverUnknown,
426    HardArithmetic,
427}