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    /// Fails the fuzzed test if a revert occurs.
14    pub fail_on_revert: bool,
15    /// The maximum number of test case rejections allowed,
16    /// encountered during usage of `vm.assume` cheatcode.
17    pub max_test_rejects: u32,
18    /// Optional seed for the fuzzing RNG algorithm
19    pub seed: Option<U256>,
20    /// The fuzz dictionary configuration
21    #[serde(flatten)]
22    pub dictionary: FuzzDictionaryConfig,
23    /// Number of runs to execute and include in the gas report.
24    pub gas_report_samples: u32,
25    /// The fuzz corpus configuration.
26    #[serde(flatten)]
27    pub corpus: FuzzCorpusConfig,
28    /// Path where fuzz failures are recorded and replayed.
29    pub failure_persist_dir: Option<PathBuf>,
30    /// show `console.log` in fuzz test, defaults to `false`
31    pub show_logs: bool,
32    /// Optional timeout (in seconds) for each property test
33    pub timeout: Option<u32>,
34}
35
36impl Default for FuzzConfig {
37    fn default() -> Self {
38        Self {
39            runs: 256,
40            fail_on_revert: true,
41            max_test_rejects: 65536,
42            seed: None,
43            dictionary: FuzzDictionaryConfig::default(),
44            gas_report_samples: 256,
45            corpus: FuzzCorpusConfig::default(),
46            failure_persist_dir: None,
47            show_logs: false,
48            timeout: None,
49        }
50    }
51}
52
53impl FuzzConfig {
54    /// Creates fuzz configuration to write failures in `{PROJECT_ROOT}/cache/fuzz` dir.
55    pub fn new(cache_dir: PathBuf) -> Self {
56        Self { failure_persist_dir: Some(cache_dir), ..Default::default() }
57    }
58}
59
60/// Contains for fuzz testing
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
62pub struct FuzzDictionaryConfig {
63    /// The weight of the dictionary
64    #[serde(deserialize_with = "crate::deserialize_stringified_percent")]
65    pub dictionary_weight: u32,
66    /// The flag indicating whether to include values from storage
67    pub include_storage: bool,
68    /// The flag indicating whether to include push bytes values
69    pub include_push_bytes: bool,
70    /// How many addresses to record at most.
71    /// Once the fuzzer exceeds this limit, it will start evicting random entries
72    ///
73    /// This limit is put in place to prevent memory blowup.
74    #[serde(deserialize_with = "crate::deserialize_usize_or_max")]
75    pub max_fuzz_dictionary_addresses: usize,
76    /// How many values to record at most.
77    /// Once the fuzzer exceeds this limit, it will start evicting random entries
78    #[serde(deserialize_with = "crate::deserialize_usize_or_max")]
79    pub max_fuzz_dictionary_values: usize,
80    /// How many literal values to seed from the AST, at most.
81    ///
82    /// This value is independent from the max amount of addresses and values.
83    #[serde(deserialize_with = "crate::deserialize_usize_or_max")]
84    pub max_fuzz_dictionary_literals: usize,
85}
86
87impl Default for FuzzDictionaryConfig {
88    fn default() -> Self {
89        const MB: usize = 1024 * 1024;
90
91        Self {
92            dictionary_weight: 40,
93            include_storage: true,
94            include_push_bytes: true,
95            max_fuzz_dictionary_addresses: 300 * MB / 20,
96            max_fuzz_dictionary_values: 300 * MB / 32,
97            max_fuzz_dictionary_literals: 200 * MB / 32,
98        }
99    }
100}
101
102#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
103pub struct FuzzCorpusConfig {
104    // Path to corpus directory, enabled coverage guided fuzzing mode.
105    // If not set then sequences producing new coverage are not persisted and mutated.
106    pub corpus_dir: Option<PathBuf>,
107    // Whether corpus to use gzip file compression and decompression.
108    pub corpus_gzip: bool,
109    // Number of mutations until entry marked as eligible to be flushed from in-memory corpus.
110    // Mutations will be performed at least `corpus_min_mutations` times.
111    pub corpus_min_mutations: usize,
112    // Number of corpus that won't be evicted from memory.
113    pub corpus_min_size: usize,
114    /// Whether to collect and display edge coverage metrics.
115    pub show_edge_coverage: bool,
116}
117
118impl FuzzCorpusConfig {
119    pub fn with_test(&mut self, contract: &str, test: &str) {
120        if let Some(corpus_dir) = &self.corpus_dir {
121            self.corpus_dir = Some(canonicalized(corpus_dir.join(contract).join(test)));
122        }
123    }
124
125    /// Whether edge coverage should be collected and displayed.
126    pub fn collect_edge_coverage(&self) -> bool {
127        self.corpus_dir.is_some() || self.show_edge_coverage
128    }
129
130    /// Whether coverage guided fuzzing is enabled.
131    pub fn is_coverage_guided(&self) -> bool {
132        self.corpus_dir.is_some()
133    }
134}
135
136impl Default for FuzzCorpusConfig {
137    fn default() -> Self {
138        Self {
139            corpus_dir: None,
140            corpus_gzip: true,
141            corpus_min_mutations: 5,
142            corpus_min_size: 0,
143            show_edge_coverage: false,
144        }
145    }
146}