Skip to main content

foundry_config/
fuzz.rs

1//! Configuration for fuzz testing.
2
3use alloy_primitives::U256;
4use foundry_compilers::utils::canonicalized;
5use serde::{Deserialize, Deserializer, Serialize, de::Error};
6use std::path::PathBuf;
7
8/// Contains for fuzz testing
9#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
10pub struct FuzzConfig {
11    /// The number of test cases that must execute for each property test
12    #[serde(deserialize_with = "deserialize_fuzz_runs")]
13    pub runs: u32,
14    /// Optional 1-based fuzz run to execute.
15    pub run: Option<u32>,
16    /// Optional fuzz worker ID to pair with `run`.
17    pub worker: Option<u32>,
18    /// Fails the fuzzed test if a revert occurs.
19    pub fail_on_revert: bool,
20    /// The maximum number of test case rejections allowed,
21    /// encountered during usage of `vm.assume` cheatcode.
22    pub max_test_rejects: u32,
23    /// Optional seed for the fuzzing RNG algorithm
24    pub seed: Option<U256>,
25    /// The fuzz dictionary configuration
26    #[serde(flatten)]
27    pub dictionary: FuzzDictionaryConfig,
28    /// Number of runs to execute and include in the gas report.
29    pub gas_report_samples: u32,
30    /// The fuzz corpus configuration.
31    #[serde(flatten)]
32    pub corpus: FuzzCorpusConfig,
33    /// Path where fuzz failures are recorded and replayed.
34    pub failure_persist_dir: Option<PathBuf>,
35    /// show `console.log` in fuzz test, defaults to `false`
36    pub show_logs: bool,
37    /// Optional timeout (in seconds) for each property test
38    pub timeout: Option<u32>,
39}
40
41impl Default for FuzzConfig {
42    fn default() -> Self {
43        Self {
44            runs: 256,
45            run: None,
46            worker: None,
47            fail_on_revert: true,
48            max_test_rejects: 65536,
49            seed: None,
50            dictionary: FuzzDictionaryConfig::default(),
51            gas_report_samples: 256,
52            corpus: FuzzCorpusConfig { payable_value_weight: 0, ..Default::default() },
53            failure_persist_dir: None,
54            show_logs: false,
55            timeout: None,
56        }
57    }
58}
59
60impl FuzzConfig {
61    /// Creates fuzz configuration to write failures in `{PROJECT_ROOT}/cache/fuzz` dir.
62    pub fn new(cache_dir: PathBuf) -> Self {
63        Self { failure_persist_dir: Some(cache_dir), ..Default::default() }
64    }
65}
66
67/// Contains for fuzz testing
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
69pub struct FuzzDictionaryConfig {
70    /// The weight of the dictionary
71    #[serde(deserialize_with = "crate::deserialize_stringified_percent")]
72    pub dictionary_weight: u32,
73    /// The flag indicating whether to include values from storage
74    pub include_storage: bool,
75    /// The flag indicating whether to include push bytes values
76    pub include_push_bytes: bool,
77    /// How many addresses to record at most.
78    /// Once the fuzzer exceeds this limit, it will start evicting random entries
79    ///
80    /// This limit is put in place to prevent memory blowup.
81    #[serde(
82        deserialize_with = "crate::deserialize_usize_or_max",
83        serialize_with = "crate::serialize_usize_or_max"
84    )]
85    pub max_fuzz_dictionary_addresses: usize,
86    /// How many values to record at most.
87    /// Once the fuzzer exceeds this limit, it will start evicting random entries
88    /// The dictionary always retains a zero seed, so the effective minimum is one.
89    #[serde(
90        deserialize_with = "crate::deserialize_usize_or_max",
91        serialize_with = "crate::serialize_usize_or_max"
92    )]
93    pub max_fuzz_dictionary_values: usize,
94    /// How many literal values to seed from the AST, at most.
95    ///
96    /// This value is independent from the max amount of addresses and values.
97    #[serde(
98        deserialize_with = "crate::deserialize_usize_or_max",
99        serialize_with = "crate::serialize_usize_or_max"
100    )]
101    pub max_fuzz_dictionary_literals: usize,
102}
103
104impl Default for FuzzDictionaryConfig {
105    fn default() -> Self {
106        const MB: usize = 1024 * 1024;
107
108        Self {
109            dictionary_weight: 40,
110            include_storage: true,
111            include_push_bytes: true,
112            max_fuzz_dictionary_addresses: 300 * MB / 20,
113            max_fuzz_dictionary_values: 300 * MB / 32,
114            max_fuzz_dictionary_literals: 200 * MB / 32,
115        }
116    }
117}
118
119#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
120pub struct FuzzCorpusConfig {
121    // Path to corpus directory, enabled coverage guided fuzzing mode.
122    // If not set then sequences producing new coverage are not persisted and mutated.
123    pub corpus_dir: Option<PathBuf>,
124    // Path to fuzz branch frontier artifacts for symbolic follow-up.
125    pub frontier_dir: Option<PathBuf>,
126    // Maximum number of branch frontier records to write for one fuzz test.
127    pub frontier_limit: usize,
128    // Whether corpus to use gzip file compression and decompression.
129    pub corpus_gzip: bool,
130    // Number of mutations until entry marked as eligible to be flushed from in-memory corpus.
131    // Mutations will be performed at least `corpus_min_mutations` times.
132    pub corpus_min_mutations: usize,
133    // Number of corpus that won't be evicted from memory.
134    pub corpus_min_size: usize,
135    /// Whether to collect and display edge coverage metrics.
136    pub show_edge_coverage: bool,
137    /// Whether EVM edge coverage should use collision-free dense IDs.
138    pub evm_edge_coverage_collision_free: bool,
139    /// Whether EVM edge coverage IDs should include call-frame depth.
140    pub evm_edge_coverage_include_call_depth: bool,
141    /// Whether to collect edge coverage from native Rust crates compiled with
142    /// SanitizerCoverage instrumentation (e.g. precompile implementations).
143    /// Requires building forge with a `RUSTC_WRAPPER` that injects sancov flags.
144    pub sancov_edges: bool,
145    /// Whether to capture comparison operands from sancov-instrumented crates
146    /// and inject them into the fuzz dictionary. Independent of `sancov_edges`.
147    pub sancov_trace_cmp: bool,
148    /// Percent chance of generating fresh transaction input instead of continuing from corpus
149    /// input during coverage-guided campaigns.
150    #[serde(deserialize_with = "crate::deserialize_stringified_percent")]
151    pub corpus_random_sequence_weight: u32,
152    /// Percent chance that generated payable calls carry non-zero `msg.value`.
153    #[serde(deserialize_with = "crate::deserialize_stringified_percent")]
154    pub payable_value_weight: u32,
155    /// Weights for coverage-guided corpus mutation strategies.
156    #[serde(flatten)]
157    pub mutation_weights: FuzzCorpusMutationWeights,
158}
159
160impl FuzzCorpusConfig {
161    pub const DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT: u32 = 10;
162    pub const ENSEMBLE_CORPUS_RANDOM_SEQUENCE_WEIGHT: u32 = 50;
163    pub const DEFAULT_FRONTIER_LIMIT: usize = 256;
164
165    pub fn with_test(&mut self, contract: &str, test: &str) {
166        if let Some(corpus_dir) = &self.corpus_dir {
167            self.corpus_dir = Some(canonicalized(corpus_dir.join(contract).join(test)));
168        }
169        if let Some(frontier_dir) = &self.frontier_dir {
170            self.frontier_dir = Some(canonicalized(frontier_dir.join(contract).join(test)));
171        }
172    }
173
174    /// Whether any edge coverage (EVM or sancov) should be collected.
175    pub const fn collect_edge_coverage(&self) -> bool {
176        self.corpus_dir.is_some() || self.show_edge_coverage || self.sancov_edges
177    }
178
179    /// Whether the EVM `EdgeCovInspector` should be enabled.
180    ///
181    /// Disabled when sancov edge coverage is active — sancov provides the
182    /// coverage signal and EVM hits from the Solidity handler would dilute it.
183    /// Trace-cmp-only mode keeps EVM edges enabled since trace-cmp only
184    /// contributes dictionary entries, not edge coverage.
185    pub const fn collect_evm_edge_coverage(&self) -> bool {
186        !self.sancov_edges && (self.corpus_dir.is_some() || self.show_edge_coverage)
187    }
188
189    /// Whether EVM comparison operand capture is enabled.
190    ///
191    /// EVM comparison operands for corpus mutation are only useful for coverage-guided fuzzing, so
192    /// they are derived from corpus mode and disabled when sancov edge coverage is active.
193    /// Frontier capture still records EVM comparison sites because those artifacts target
194    /// Solidity bytecode branches, not the active coverage guidance source.
195    pub fn collect_evm_cmp_log(&self) -> bool {
196        self.capture_branch_frontiers()
197            || (!self.sancov_edges
198                && self.corpus_dir.is_some()
199                && self.mutation_weights.effective().mutation_weight_cmp > 0)
200    }
201
202    /// Whether fuzz branch frontier artifacts should be captured.
203    pub const fn capture_branch_frontiers(&self) -> bool {
204        self.frontier_dir.is_some() && self.frontier_limit > 0
205    }
206
207    /// Whether EVM edge coverage should use collision-free dense IDs.
208    pub const fn evm_edge_coverage_collision_free(&self) -> bool {
209        self.evm_edge_coverage_collision_free
210    }
211
212    /// Whether EVM edge coverage IDs should include call-frame depth.
213    pub const fn evm_edge_coverage_include_call_depth(&self) -> bool {
214        self.evm_edge_coverage_include_call_depth
215    }
216
217    /// Whether sancov edge coverage collection is enabled.
218    pub const fn collect_sancov_edges(&self) -> bool {
219        self.sancov_edges
220    }
221
222    /// Whether sancov trace-cmp capture is enabled.
223    pub const fn collect_sancov_trace_cmp(&self) -> bool {
224        self.sancov_trace_cmp
225    }
226
227    /// Whether either sancov coverage mode is active.
228    pub const fn sancov_active(&self) -> bool {
229        self.sancov_edges || self.sancov_trace_cmp
230    }
231
232    /// Whether coverage guided fuzzing is enabled.
233    pub const fn is_coverage_guided(&self) -> bool {
234        self.corpus_dir.is_some()
235    }
236}
237
238impl Default for FuzzCorpusConfig {
239    fn default() -> Self {
240        Self {
241            corpus_dir: None,
242            frontier_dir: None,
243            frontier_limit: Self::DEFAULT_FRONTIER_LIMIT,
244            corpus_gzip: true,
245            corpus_min_mutations: 5,
246            corpus_min_size: 0,
247            show_edge_coverage: false,
248            evm_edge_coverage_collision_free: true,
249            evm_edge_coverage_include_call_depth: false,
250            sancov_edges: false,
251            sancov_trace_cmp: false,
252            corpus_random_sequence_weight: Self::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT,
253            payable_value_weight: 15,
254            mutation_weights: FuzzCorpusMutationWeights::default(),
255        }
256    }
257}
258
259#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
260pub struct FuzzCorpusMutationWeights {
261    /// Weight for splicing two corpus sequences.
262    pub mutation_weight_splice: u32,
263    /// Weight for repeating part of a corpus sequence.
264    pub mutation_weight_repeat: u32,
265    /// Weight for interleaving two corpus sequences.
266    pub mutation_weight_interleave: u32,
267    /// Weight for replacing a corpus sequence prefix with generated calls.
268    pub mutation_weight_prefix: u32,
269    /// Weight for replacing a corpus sequence suffix with generated calls.
270    pub mutation_weight_suffix: u32,
271    /// Weight for ABI-aware argument mutation.
272    pub mutation_weight_abi: u32,
273    /// Weight for comparison-operand guided argument mutation.
274    pub mutation_weight_cmp: u32,
275}
276
277impl FuzzCorpusMutationWeights {
278    pub const fn total(&self) -> u64 {
279        self.mutation_weight_splice as u64
280            + self.mutation_weight_repeat as u64
281            + self.mutation_weight_interleave as u64
282            + self.mutation_weight_prefix as u64
283            + self.mutation_weight_suffix as u64
284            + self.mutation_weight_abi as u64
285            + self.mutation_weight_cmp as u64
286    }
287
288    /// Returns defaults if every configured weight is zero.
289    pub fn effective(self) -> Self {
290        if self.total() == 0 { Self::default() } else { self }
291    }
292}
293
294impl Default for FuzzCorpusMutationWeights {
295    fn default() -> Self {
296        Self {
297            mutation_weight_splice: 1,
298            mutation_weight_repeat: 1,
299            mutation_weight_interleave: 1,
300            mutation_weight_prefix: 1,
301            mutation_weight_suffix: 1,
302            mutation_weight_abi: 1,
303            mutation_weight_cmp: 1,
304        }
305    }
306}
307
308fn deserialize_fuzz_runs<'de, D>(deserializer: D) -> Result<u32, D::Error>
309where
310    D: Deserializer<'de>,
311{
312    let runs = u32::deserialize(deserializer)?;
313    if runs == 0 {
314        return Err(D::Error::custom("`fuzz.runs` must be greater than 0"));
315    }
316    Ok(runs)
317}