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