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