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    /// 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(
75        deserialize_with = "crate::deserialize_usize_or_max",
76        serialize_with = "crate::serialize_usize_or_max"
77    )]
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(
82        deserialize_with = "crate::deserialize_usize_or_max",
83        serialize_with = "crate::serialize_usize_or_max"
84    )]
85    pub max_fuzz_dictionary_values: usize,
86    /// How many literal values to seed from the AST, at most.
87    ///
88    /// This value is independent from the max amount of addresses and values.
89    #[serde(
90        deserialize_with = "crate::deserialize_usize_or_max",
91        serialize_with = "crate::serialize_usize_or_max"
92    )]
93    pub max_fuzz_dictionary_literals: usize,
94}
95
96impl Default for FuzzDictionaryConfig {
97    fn default() -> Self {
98        const MB: usize = 1024 * 1024;
99
100        Self {
101            dictionary_weight: 40,
102            include_storage: true,
103            include_push_bytes: true,
104            max_fuzz_dictionary_addresses: 300 * MB / 20,
105            max_fuzz_dictionary_values: 300 * MB / 32,
106            max_fuzz_dictionary_literals: 200 * MB / 32,
107        }
108    }
109}
110
111#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
112pub struct FuzzCorpusConfig {
113    // Path to corpus directory, enabled coverage guided fuzzing mode.
114    // If not set then sequences producing new coverage are not persisted and mutated.
115    pub corpus_dir: Option<PathBuf>,
116    // Whether corpus to use gzip file compression and decompression.
117    pub corpus_gzip: bool,
118    // Number of mutations until entry marked as eligible to be flushed from in-memory corpus.
119    // Mutations will be performed at least `corpus_min_mutations` times.
120    pub corpus_min_mutations: usize,
121    // Number of corpus that won't be evicted from memory.
122    pub corpus_min_size: usize,
123    /// Whether to collect and display edge coverage metrics.
124    pub show_edge_coverage: bool,
125}
126
127impl FuzzCorpusConfig {
128    pub fn with_test(&mut self, contract: &str, test: &str) {
129        if let Some(corpus_dir) = &self.corpus_dir {
130            self.corpus_dir = Some(canonicalized(corpus_dir.join(contract).join(test)));
131        }
132    }
133
134    /// Whether edge coverage should be collected and displayed.
135    pub fn collect_edge_coverage(&self) -> bool {
136        self.corpus_dir.is_some() || self.show_edge_coverage
137    }
138
139    /// Whether coverage guided fuzzing is enabled.
140    pub fn is_coverage_guided(&self) -> bool {
141        self.corpus_dir.is_some()
142    }
143}
144
145impl Default for FuzzCorpusConfig {
146    fn default() -> Self {
147        Self {
148            corpus_dir: None,
149            corpus_gzip: true,
150            corpus_min_mutations: 5,
151            corpus_min_size: 0,
152            show_edge_coverage: false,
153        }
154    }
155}