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::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 #[serde(
88 deserialize_with = "crate::deserialize_usize_or_max",
89 serialize_with = "crate::serialize_usize_or_max"
90 )]
91 pub max_fuzz_dictionary_values: usize,
92 /// How many literal values to seed from the AST, at most.
93 ///
94 /// This value is independent from the max amount of addresses and values.
95 #[serde(
96 deserialize_with = "crate::deserialize_usize_or_max",
97 serialize_with = "crate::serialize_usize_or_max"
98 )]
99 pub max_fuzz_dictionary_literals: usize,
100}
101
102impl Default for FuzzDictionaryConfig {
103 fn default() -> Self {
104 const MB: usize = 1024 * 1024;
105
106 Self {
107 dictionary_weight: 40,
108 include_storage: true,
109 include_push_bytes: true,
110 max_fuzz_dictionary_addresses: 300 * MB / 20,
111 max_fuzz_dictionary_values: 300 * MB / 32,
112 max_fuzz_dictionary_literals: 200 * MB / 32,
113 }
114 }
115}
116
117#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
118pub struct FuzzCorpusConfig {
119 // Path to corpus directory, enabled coverage guided fuzzing mode.
120 // If not set then sequences producing new coverage are not persisted and mutated.
121 pub corpus_dir: Option<PathBuf>,
122 // Whether corpus to use gzip file compression and decompression.
123 pub corpus_gzip: bool,
124 // Number of mutations until entry marked as eligible to be flushed from in-memory corpus.
125 // Mutations will be performed at least `corpus_min_mutations` times.
126 pub corpus_min_mutations: usize,
127 // Number of corpus that won't be evicted from memory.
128 pub corpus_min_size: usize,
129 /// Whether to collect and display edge coverage metrics.
130 pub show_edge_coverage: bool,
131 /// Whether EVM edge coverage should use collision-free dense IDs.
132 pub evm_edge_coverage_collision_free: bool,
133 /// Whether EVM edge coverage IDs should include call-frame depth.
134 pub evm_edge_coverage_include_call_depth: bool,
135 /// Whether to collect edge coverage from native Rust crates compiled with
136 /// SanitizerCoverage instrumentation (e.g. precompile implementations).
137 /// Requires building forge with a `RUSTC_WRAPPER` that injects sancov flags.
138 pub sancov_edges: bool,
139 /// Whether to capture comparison operands from sancov-instrumented crates
140 /// and inject them into the fuzz dictionary. Independent of `sancov_edges`.
141 pub sancov_trace_cmp: bool,
142}
143
144impl FuzzCorpusConfig {
145 pub fn with_test(&mut self, contract: &str, test: &str) {
146 if let Some(corpus_dir) = &self.corpus_dir {
147 self.corpus_dir = Some(canonicalized(corpus_dir.join(contract).join(test)));
148 }
149 }
150
151 /// Whether any edge coverage (EVM or sancov) should be collected.
152 pub const fn collect_edge_coverage(&self) -> bool {
153 self.corpus_dir.is_some() || self.show_edge_coverage || self.sancov_edges
154 }
155
156 /// Whether the EVM `EdgeCovInspector` should be enabled.
157 ///
158 /// Disabled when sancov edge coverage is active — sancov provides the
159 /// coverage signal and EVM hits from the Solidity handler would dilute it.
160 /// Trace-cmp-only mode keeps EVM edges enabled since trace-cmp only
161 /// contributes dictionary entries, not edge coverage.
162 pub const fn collect_evm_edge_coverage(&self) -> bool {
163 !self.sancov_edges && (self.corpus_dir.is_some() || self.show_edge_coverage)
164 }
165
166 /// Whether EVM comparison operand capture is enabled.
167 ///
168 /// EVM comparison operands are only useful for coverage-guided fuzzing, so they are derived
169 /// from corpus mode. Disabled when sancov edge coverage is active because sancov replaces EVM
170 /// bytecode coverage as the guidance signal.
171 pub const fn collect_evm_cmp_log(&self) -> bool {
172 !self.sancov_edges && self.corpus_dir.is_some()
173 }
174
175 /// Whether EVM edge coverage should use collision-free dense IDs.
176 pub const fn evm_edge_coverage_collision_free(&self) -> bool {
177 self.evm_edge_coverage_collision_free
178 }
179
180 /// Whether EVM edge coverage IDs should include call-frame depth.
181 pub const fn evm_edge_coverage_include_call_depth(&self) -> bool {
182 self.evm_edge_coverage_include_call_depth
183 }
184
185 /// Whether sancov edge coverage collection is enabled.
186 pub const fn collect_sancov_edges(&self) -> bool {
187 self.sancov_edges
188 }
189
190 /// Whether sancov trace-cmp capture is enabled.
191 pub const fn collect_sancov_trace_cmp(&self) -> bool {
192 self.sancov_trace_cmp
193 }
194
195 /// Whether either sancov coverage mode is active.
196 pub const fn sancov_active(&self) -> bool {
197 self.sancov_edges || self.sancov_trace_cmp
198 }
199
200 /// Whether coverage guided fuzzing is enabled.
201 pub const fn is_coverage_guided(&self) -> bool {
202 self.corpus_dir.is_some()
203 }
204}
205
206impl Default for FuzzCorpusConfig {
207 fn default() -> Self {
208 Self {
209 corpus_dir: None,
210 corpus_gzip: true,
211 corpus_min_mutations: 5,
212 corpus_min_size: 0,
213 show_edge_coverage: false,
214 evm_edge_coverage_collision_free: true,
215 evm_edge_coverage_include_call_depth: false,
216 sancov_edges: false,
217 sancov_trace_cmp: false,
218 }
219 }
220}