1use crate::{
2 cli::{
3 ui::logger::LogWriter,
4 ziggy::ZiggyConfig,
5 },
6 contract::{
7 payload::PayloadCrafter,
8 remote::{
9 ContractSetup,
10 FullContractResponse,
11 },
12 runtime::{
13 RuntimeOrigin,
14 Timestamp,
15 },
16 selectors::database::SelectorDatabase,
17 },
18 cover::coverage::InputCoverage,
19 fuzzer::{
20 environment::EnvironmentBuilder,
21 fuzz::FuzzingMode::{
22 ExecuteOneInput,
23 Fuzz,
24 },
25 manager::CampaignManager,
26 parser::{
27 try_parse_input,
28 OneInput,
29 MIN_SEED_LEN,
30 },
31 },
32 EmptyResult,
33 ResultOf,
34};
35use anyhow::Context;
36use frame_support::__private::BasicExternalities;
37use sp_core::hexdisplay::AsBytesRef;
38use std::{
39 fs,
40 path::PathBuf,
41};
42
43pub const MAX_MESSAGES_PER_EXEC: usize = 1; pub enum FuzzingMode {
46 ExecuteOneInput(PathBuf),
47 Fuzz,
48}
49
50#[derive(Clone)]
51pub struct Fuzzer {
52 pub ziggy_config: ZiggyConfig,
53 pub setup: ContractSetup,
54}
55
56impl Fuzzer {
57 pub fn new(ziggy_config: anyhow::Result<ZiggyConfig>) -> ResultOf<Self> {
58 let config = ziggy_config?;
59 Ok(Self {
60 ziggy_config: config.to_owned(),
61 setup: ContractSetup::initialize_wasm(config)?,
62 })
63 }
64
65 pub fn execute_harness(self, mode: FuzzingMode) -> EmptyResult {
66 match mode {
67 Fuzz => {
68 let manager = self.clone().init_fuzzer()?;
69 ziggy::fuzz!(|data: &[u8]| {
70 self.harness(manager.to_owned(), data);
71 });
72 }
73 ExecuteOneInput(seed_path) => {
74 let manager = self
75 .to_owned()
76 .init_fuzzer()
77 .context("Couldn't grap the transcoder and the invariant manager")?;
78
79 let data = fs::read(seed_path).context("Couldn't read the seed")?;
80 self.harness(manager, data.as_bytes_ref());
81 }
82 }
83
84 Ok(())
85 }
86
87 pub fn init_fuzzer(self) -> ResultOf<CampaignManager> {
88 let contract_bridge = self.setup.clone();
89
90 let invariants = PayloadCrafter::extract_invariants(&contract_bridge.json_specs)
91 .context("No invariants found, check your contract")?;
92
93 let conf = self.ziggy_config.config();
94 let messages = PayloadCrafter::extract_all(conf.instrumented_contract().to_path_buf())
95 .context("Couldn't extract all the messages selectors")?;
96
97 let payable_messages = PayloadCrafter::extract_payables(&contract_bridge.json_specs)
98 .context("Couldn't fetch payable messages")?;
99
100 let mut database = SelectorDatabase::new();
101
102 database.add_invariants(invariants);
103 database.add_messages(messages);
104 database.add_payables(payable_messages);
105
106 let env_builder = EnvironmentBuilder::new(database.clone());
107
108 env_builder
109 .build_env(self.ziggy_config.to_owned())
110 .context("Couldn't create corpus entries and dict")?;
111
112 if conf.verbose {
113 println!(
114 "\n🚀 Now fuzzing `{}` ({})!\n",
115 &contract_bridge.path_to_specs.as_os_str().to_str().unwrap(),
116 &contract_bridge.contract_address
117 );
118 }
119
120 CampaignManager::new(database, contract_bridge.clone(), conf.to_owned())
121 }
122
123 fn execute_messages(
124 &self,
125 input: &OneInput,
126 chain: &mut BasicExternalities,
127 coverage: &mut InputCoverage,
128 ) -> Vec<FullContractResponse> {
129 let mut responses = Vec::new();
130
131 chain.execute_with(|| {
132 for message in &input.messages {
133 let transfer_value = if message.is_payable {
134 message.value_token
135 } else {
136 0
137 };
138
139 let result: FullContractResponse = self.setup.clone().call(
140 &message.payload,
141 message.origin.into(),
142 transfer_value,
143 self.ziggy_config.config().clone(),
144 );
145
146 coverage.add_cov(result.clone().debug_message());
147 responses.push(result);
148 }
149 });
150
151 responses
152 }
153
154 pub fn harness(&self, manager: CampaignManager, input: &[u8]) {
155 if input.len() < MIN_SEED_LEN {
156 return;
157 }
158
159 let maybe_parsed_input: Option<OneInput> = try_parse_input(input, manager.to_owned());
160
161 let parsed_input = match maybe_parsed_input {
162 None => {
163 return;
164 }
165 Some(parsed) => parsed,
166 };
167
168 let mut chain = BasicExternalities::new(self.setup.genesis.clone());
169 chain.execute_with(|| {
170 Timestamp::set(RuntimeOrigin::none(), 3000).unwrap();
171 });
172
173 let mut coverage = InputCoverage::new();
174 let all_msg_responses = self.execute_messages(&parsed_input, &mut chain, &mut coverage);
175
176 let cov = coverage.messages_coverage();
177 #[cfg(not(fuzzing))]
181 {
182 parsed_input.pretty_print(all_msg_responses.clone());
183
184 println!("[🚧UPDATE] Adding to the coverage file...");
185 coverage
186 .save(&manager.config().fuzz_output.unwrap_or_default())
187 .expect("🙅 Cannot save the coverage");
188 let debug = coverage.concatened_trace();
189 println!("[🚧COVERAGE] Caught identifiers {cov:?}",);
190 println!("[🚧DEBUG TRACE] Fetched the following trace: {debug:?}\n",);
191 }
192 coverage.redirect_coverage(cov);
194
195 if self.ziggy_config.config().show_ui {
197 let seeder = LogWriter::new(parsed_input.to_owned(), coverage.to_owned());
198 if LogWriter::should_save() {
199 seeder.save(self.clone().ziggy_config.fuzz_output()).expect(
200 "\nYou should run `fuzz` at least once and have a valid `output` directory\n",
201 );
202 }
203 }
204
205 chain.execute_with(|| {
207 manager.check_invariants(
208 &all_msg_responses,
209 &parsed_input,
210 manager.config().catch_trapped_contract,
211 )
212 });
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use crate::{
219 cli::{
220 config::Configuration,
221 ziggy::ZiggyConfig,
222 },
223 contract::{
224 payload::PayloadCrafter,
225 selectors::database::SelectorDatabase,
226 },
227 fuzzer::{
228 environment::EnvironmentBuilder,
229 fuzz::Fuzzer,
230 manager::CampaignManager,
231 },
232 instrumenter::path::InstrumentedPath,
233 EmptyResult,
234 };
235 use contract_transcode::ContractMessageTranscoder;
236
237 use std::{
238 fs,
239 path::{
240 Path,
241 PathBuf,
242 },
243 sync::Mutex,
244 };
245 use tempfile::tempdir;
246
247 fn create_test_config() -> ZiggyConfig {
248 let config = Configuration {
249 verbose: true,
250 cores: Some(1),
251 use_honggfuzz: false,
252 fuzz_output: Some(tempdir().unwrap().into_path()),
253 instrumented_contract_path: Some(InstrumentedPath::from("sample/dns")),
254 show_ui: false,
255 max_messages_per_exec: Some(4),
256 ..Default::default()
257 };
258 ZiggyConfig::new_with_contract(config, PathBuf::from("sample/dns")).unwrap()
259 }
260
261 #[test]
262 fn test_database_and_envbuilder() -> EmptyResult {
263 let config = create_test_config();
264 let contract_bridge = Fuzzer::new(Ok(config.clone()))?.setup;
265
266 let invariants = PayloadCrafter::extract_invariants(&contract_bridge.json_specs).unwrap();
267
268 let messages = PayloadCrafter::extract_all(config.contract_path()?.clone())?
269 .into_iter()
270 .filter(|s| !invariants.contains(s))
271 .collect();
272
273 let mut database = SelectorDatabase::new();
274 database.add_invariants(invariants);
275 database.add_messages(messages);
276
277 let manager = CampaignManager::new(
278 database.clone(),
279 contract_bridge.clone(),
280 config.config().to_owned(),
281 )?;
282
283 let env_builder = EnvironmentBuilder::new(database);
284
285 env_builder.build_env(config.clone())?;
286
287 let x = manager.database();
288 let get_unique_messages = x.clone().get_unique_messages()?.len();
289
290 assert_eq!(
291 fs::read_dir(config.clone().fuzz_output().join("phink").join("corpus"))
292 .expect("Failed to read directory")
293 .count(),
294 get_unique_messages
295 );
296 assert_eq!(get_unique_messages, 5 + 1); let inv_counter = x.clone().invariants()?.len();
299 assert_eq!(inv_counter, 1);
300
301 assert_eq!(x.clone().messages()?.len(), get_unique_messages);
302
303 let dict_path = config.fuzz_output().join("phink").join("selectors.dict");
304 let dict: String = fs::read_to_string(dict_path.clone())?;
305 assert!(dict.contains("********"));
306 assert!(dict.contains("# Dictionary file for selector"));
307
308 Ok(())
309 }
310 #[test]
311 fn test_decode_constructor() {
312 let metadata_path =
313 Path::new("sample/multi-contract-caller/target/ink/multi_contract_caller.json");
314 let transcoder = Mutex::new(
315 ContractMessageTranscoder::load(metadata_path)
316 .expect("Failed to load `ContractMessageTranscoder`"),
317 );
318
319 let encoded_bytes =
320 hex::decode("9BAE9D5E5C1100007B000000ACAC0000CC5B763F7AA51000F4BD3F32F51151FF017FD22F9404D0308AFBDB3DE6F2E030E23910AC7DCDBB41BC52F1F2F923E49BAF32E9587DCD4D43D50408B62431D7B79C1A506DBEC4785423DDF36E66E2BEBA6CFEFCDD4F5708DFA3388E48").unwrap();
321 let result = transcoder
322 .lock()
323 .unwrap()
324 .decode_contract_constructor(&mut &encoded_bytes[..])
325 .unwrap();
326 let expected = "new { init_value: 4444, version: 123, accumulator_code_hash: 0xacac0000cc5b763f7aa51000f4bd3f32f51151ff017fd22f9404d0308afbdb3d, adder_code_hash: 0xe6f2e030e23910ac7dcdbb41bc52f1f2f923e49baf32e9587dcd4d43d50408b6, subber_code_hash: 0x2431d7b79c1a506dbec4785423ddf36e66e2beba6cfefcdd4f5708dfa3388e48 }";
328 assert_eq!(result.to_string(), expected);
329 }
330
331 #[test]
332 fn test_parse_input() {
333 let metadata_path = Path::new("sample/dns/target/ink/dns.json");
334 let transcoder = Mutex::new(
335 ContractMessageTranscoder::load(metadata_path)
336 .expect("Failed to load `ContractMessageTranscoder`"),
337 );
338
339 let encoded_bytes =
340 hex::decode("229b553f9400000000000000000027272727272727272700002727272727272727272727")
341 .expect("Failed to decode hex string");
342
343 assert!(
344 transcoder
345 .lock()
346 .unwrap()
347 .decode_contract_message(&mut &encoded_bytes[..])
348 .is_ok(),
349 "Failed to decode contract message"
350 );
351
352 let binding = transcoder.lock().unwrap();
353 let messages = binding.metadata().spec().messages();
354 assert!(!messages.is_empty(), "There should be some messages here");
355 }
356
357 #[test]
358 fn test_parse_dummy() {
359 let metadata_path = Path::new("sample/dummy/target/ink/dummy.json");
360 let transcoder = Mutex::new(
361 ContractMessageTranscoder::load(metadata_path)
362 .expect("Failed to load `ContractMessageTranscoder`"),
363 );
364
365 let encoded_bytes = hex::decode("fa80c2f600").expect("Failed to decode hex string");
366
367 assert!(
368 transcoder
369 .lock()
370 .unwrap()
371 .decode_contract_message(&mut &encoded_bytes[..])
372 .is_ok(),
373 "Failed to decode contract message"
374 );
375
376 let binding = transcoder.lock().unwrap();
377 let messages = binding.metadata().spec().messages();
378 assert!(!messages.is_empty(), "There should be some messages here");
379 }
380}