1use super::{
2 InvariantFailures, InvariantFuzzError, InvariantMetrics, InvariantTest, InvariantTestRun,
3 call_after_invariant_function, call_invariant_function,
4 error::{InvariantRunCtx, record_handler_assertion_bug},
5};
6use crate::executors::{Executor, RawCallResult};
7use alloy_json_abi::Function;
8use alloy_primitives::{Address, B256, I256, Selector};
9use alloy_sol_types::{Panic, PanicKind, Revert, SolError, SolInterface};
10use eyre::Result;
11use foundry_config::InvariantConfig;
12use foundry_evm_core::{
13 abi::Vm,
14 constants::CHEATCODE_ADDRESS,
15 decode::{ASSERTION_FAILED_PREFIX, decode_console_log},
16 evm::FoundryEvmNetwork,
17 utils::StateChangeset,
18};
19use foundry_evm_coverage::HitMaps;
20use foundry_evm_fuzz::{
21 BasicTxDetails,
22 invariant::{FuzzRunIdentifiedContracts, InvariantContract},
23};
24use proptest::test_runner::TestError;
25use revm::interpreter::InstructionResult;
26use revm_inspectors::tracing::CallTraceArena;
27use std::{borrow::Cow, collections::HashMap};
28
29#[derive(Debug)]
31pub struct InvariantFuzzTestResult {
32 pub errors: HashMap<String, InvariantFuzzError>,
34 pub handler_errors: HashMap<(Address, Selector), InvariantFuzzError>,
37 pub runs: usize,
39 pub calls: usize,
41 pub reverts: usize,
43 pub last_run_inputs: Vec<BasicTxDetails>,
46 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
48 pub line_coverage: Option<HitMaps>,
50 pub metrics: HashMap<String, InvariantMetrics>,
52 pub failed_corpus_replays: usize,
54 pub workers: usize,
56 pub fork_block_number: Option<u64>,
58 pub optimization_best_value: Option<I256>,
61 pub optimization_best_sequence: Vec<BasicTxDetails>,
63}
64
65impl InvariantFuzzTestResult {
66 #[expect(clippy::too_many_arguments)]
67 pub(crate) fn new(
68 errors: HashMap<String, InvariantFuzzError>,
69 handler_errors: HashMap<(Address, Selector), InvariantFuzzError>,
70 runs: usize,
71 calls: usize,
72 reverts: usize,
73 last_run_inputs: Vec<BasicTxDetails>,
74 gas_report_traces: Vec<Vec<CallTraceArena>>,
75 line_coverage: Option<HitMaps>,
76 metrics: HashMap<String, InvariantMetrics>,
77 failed_corpus_replays: usize,
78 workers: usize,
79 optimization_best_value: Option<I256>,
80 optimization_best_sequence: Vec<BasicTxDetails>,
81 ) -> Self {
82 let mut failure_blocks = errors
83 .values()
84 .chain(handler_errors.values())
85 .map(InvariantFuzzError::fork_block_number);
86 let fork_block_number = failure_blocks
87 .next()
88 .flatten()
89 .filter(|first| failure_blocks.all(|block| block == Some(*first)));
90
91 Self {
92 errors,
93 handler_errors,
94 runs,
95 calls,
96 reverts,
97 last_run_inputs,
98 gas_report_traces,
99 line_coverage,
100 metrics,
101 failed_corpus_replays,
102 workers,
103 fork_block_number,
104 optimization_best_value,
105 optimization_best_sequence,
106 }
107 }
108}
109
110pub(crate) fn invariant_preflight_check<FEN: FoundryEvmNetwork>(
114 invariant_contract: &InvariantContract<'_>,
115 invariant_config: &InvariantConfig,
116 targeted_contracts: &FuzzRunIdentifiedContracts,
117 executor: &Executor<FEN>,
118 calldata: &[BasicTxDetails],
119 invariant_failures: &mut InvariantFailures,
120) -> Result<()> {
121 assert_invariants(
122 invariant_contract,
123 invariant_config,
124 targeted_contracts,
125 executor,
126 calldata,
127 invariant_failures,
128 )?;
129 Ok(())
130}
131
132pub(crate) fn is_assertion_failure<FEN: FoundryEvmNetwork>(
136 call_result: &RawCallResult<FEN>,
137) -> bool {
138 if !call_result.reverted {
139 return false;
140 }
141
142 is_assert_panic(call_result.result.as_ref())
143 || matches!(call_result.exit_reason, Some(InstructionResult::InvalidFEOpcode))
144 || is_revert_assertion_failure(call_result.result.as_ref())
145 || is_cheatcode_assert_revert(call_result)
146}
147
148fn is_assert_panic(data: &[u8]) -> bool {
149 Panic::abi_decode(data).is_ok_and(|panic| panic == PanicKind::Assert.into())
150}
151
152fn is_revert_assertion_failure(data: &[u8]) -> bool {
153 Revert::abi_decode(data).is_ok_and(|revert| revert.reason.contains(ASSERTION_FAILED_PREFIX))
154}
155
156fn is_cheatcode_assert_revert<FEN: FoundryEvmNetwork>(call_result: &RawCallResult<FEN>) -> bool {
157 call_result.reverter == Some(CHEATCODE_ADDRESS)
158 && Vm::VmErrors::abi_decode(call_result.result.as_ref())
159 .ok()
160 .map(|error| error.to_string())
161 .is_some_and(|message| message.starts_with(ASSERTION_FAILED_PREFIX))
162}
163
164fn logged_assertion_failure<FEN: FoundryEvmNetwork>(call_result: &RawCallResult<FEN>) -> bool {
165 call_result
166 .logs
167 .iter()
168 .filter_map(decode_console_log)
169 .any(|msg| msg.starts_with(ASSERTION_FAILED_PREFIX))
170}
171
172pub fn did_fail_on_assert<FEN: FoundryEvmNetwork>(
177 call_result: &RawCallResult<FEN>,
178 state_changeset: &StateChangeset,
179) -> bool {
180 is_assertion_failure(call_result)
181 || call_result.has_state_snapshot_failure
182 || Executor::<FEN>::has_pending_global_failure(state_changeset)
183 || logged_assertion_failure(call_result)
184}
185
186pub(crate) fn assert_invariants<'a, FEN: FoundryEvmNetwork>(
192 invariant_contract: &InvariantContract<'a>,
193 invariant_config: &InvariantConfig,
194 targeted_contracts: &FuzzRunIdentifiedContracts,
195 executor: &Executor<FEN>,
196 calldata: &[BasicTxDetails],
197 invariant_failures: &mut InvariantFailures,
198) -> Result<(Option<&'a Function>, bool)> {
199 let mut inner_sequence = None;
200 let mut first_broken: Option<&'a Function> = None;
201 let ctx = InvariantRunCtx {
202 contract: invariant_contract,
203 config: invariant_config,
204 targeted_contracts,
205 calldata,
206 };
207
208 for (idx, (invariant, fail_on_revert)) in invariant_contract.invariant_fns.iter().enumerate() {
209 if invariant_failures.has_failure(invariant) {
211 continue;
212 }
213
214 let (call_result, success) = call_invariant_function(
215 executor,
216 invariant_contract.address,
217 invariant_contract.invariant_calldata(idx),
218 )?;
219 if call_result.execution_cancelled {
220 return Ok((first_broken, true));
221 }
222 if !success {
223 let inner_sequence =
224 inner_sequence.get_or_insert_with(|| invariant_inner_sequence(executor));
225 let case =
226 ctx.failed_case(invariant, *fail_on_revert, false, call_result, inner_sequence);
227 invariant_failures.record_failure(invariant, InvariantFuzzError::BrokenInvariant(case));
228 if first_broken.is_none() {
229 first_broken = Some(*invariant);
230 }
231 }
232 }
233
234 Ok((first_broken, false))
235}
236
237fn invariant_inner_sequence<FEN: FoundryEvmNetwork>(
239 executor: &Executor<FEN>,
240) -> Vec<Option<BasicTxDetails>> {
241 let mut seq = vec![];
242 if let Some(fuzzer) = &executor.inspector().fuzzer
243 && let Some(call_generator) = &fuzzer.call_generator
244 {
245 seq.extend(call_generator.last_sequence.read().iter().cloned());
246 }
247 seq
248}
249
250#[derive(Debug)]
252pub(crate) struct ContinueOutcome {
253 pub continues: bool,
255 pub cancelled: bool,
257}
258
259#[allow(clippy::too_many_arguments)]
268pub(crate) fn can_continue<'a, FEN: FoundryEvmNetwork>(
269 invariant_contract: &InvariantContract<'a>,
270 invariant_test: &mut InvariantTest,
271 invariant_run: &mut InvariantTestRun<FEN>,
272 invariant_config: &InvariantConfig,
273 call_result: RawCallResult<FEN>,
274 state_changeset: &StateChangeset,
275 handler_target: Address,
276 handler_selector: Selector,
277 assertion_failure: bool,
278 pre_merge_edges_hash: Option<B256>,
279) -> Result<ContinueOutcome> {
280 let is_optimization = invariant_contract.is_optimization();
281
282 let handlers_succeeded = || {
287 if !invariant_run.executor.legacy_assertions() {
288 return invariant_run.executor.is_success_handler_gate(
289 invariant_contract.address,
290 false,
291 Cow::Borrowed(state_changeset),
292 );
293 }
294
295 invariant_test.targeted_contracts.targets().keys().all(|address| {
296 invariant_run.executor.is_success_handler_gate(
297 *address,
298 false,
299 Cow::Borrowed(state_changeset),
300 )
301 })
302 };
303
304 if !call_result.reverted && handlers_succeeded() {
305 if let Some(traces) = call_result.traces {
306 invariant_run.run_traces.push(traces);
307 }
308
309 if is_optimization {
310 let (inv_result, success) = call_invariant_function(
312 &invariant_run.executor,
313 invariant_contract.address,
314 invariant_contract.anchor_calldata(),
315 )?;
316 if inv_result.execution_cancelled {
317 return Ok(ContinueOutcome { continues: true, cancelled: true });
318 }
319 if success
320 && inv_result.result.len() >= 32
321 && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
322 {
323 if invariant_run.optimization_value.is_none_or(|prev| value > prev) {
326 invariant_run.optimization_value = Some(value);
327 invariant_run.optimization_prefix_len = invariant_run.inputs.len();
328 }
329 }
330 } else {
331 let (_, cancelled) = assert_invariants(
333 invariant_contract,
334 invariant_config,
335 &invariant_test.targeted_contracts,
336 &invariant_run.executor,
337 &invariant_run.inputs,
338 &mut invariant_test.test_data.failures,
339 )?;
340 if cancelled {
341 return Ok(ContinueOutcome { continues: true, cancelled: true });
342 }
343 }
344 } else {
345 let is_assert_failure = assertion_failure;
346 let reverted = call_result.reverted;
347
348 if reverted {
349 invariant_test.test_data.failures.reverts += 1;
350 }
351
352 if is_assert_failure {
353 record_handler_assertion_bug(
356 invariant_contract,
357 invariant_config,
358 &invariant_test.targeted_contracts,
359 &mut invariant_test.test_data.failures,
360 &mut invariant_run.inputs,
361 handler_target,
362 handler_selector,
363 pre_merge_edges_hash,
364 call_result,
365 reverted,
366 is_optimization,
367 );
368
369 let continues = invariant_test
371 .test_data
372 .failures
373 .can_continue(invariant_contract.invariant_fns.len());
374 return Ok(ContinueOutcome { continues, cancelled: false });
375 }
376
377 let failing_invariants: Vec<_> = invariant_contract
380 .invariant_fns
381 .iter()
382 .filter(|(invariant, fail_on_revert)| {
383 *fail_on_revert && !invariant_test.test_data.failures.has_failure(invariant)
384 })
385 .collect();
386
387 if let Some((first_invariant, _)) = failing_invariants.first() {
388 let base = InvariantRunCtx {
392 contract: invariant_contract,
393 config: invariant_config,
394 targeted_contracts: &invariant_test.targeted_contracts,
395 calldata: &invariant_run.inputs,
396 }
397 .failed_case(
398 first_invariant,
399 invariant_config.fail_on_revert,
400 is_assert_failure,
401 call_result,
402 &[],
403 );
404
405 for (invariant, fail_on_revert) in failing_invariants {
406 let mut data = base.clone();
407 data.fail_on_revert = *fail_on_revert;
408 data.calldata = invariant.selector().to_vec().into();
409 data.test_error = TestError::Fail(
410 format!("{}, reason: {}", invariant.name, data.revert_reason).into(),
411 invariant_run.inputs.clone(),
412 );
413 invariant_test.test_data.failures.record_failure(
416 invariant,
417 if is_assert_failure {
418 InvariantFuzzError::BrokenInvariant(data)
419 } else {
420 InvariantFuzzError::Revert(data)
421 },
422 );
423 }
424 }
425
426 if reverted && !is_optimization && !invariant_config.has_delay() {
427 invariant_run.inputs.pop();
431 }
432 }
433
434 let continues =
435 invariant_test.test_data.failures.can_continue(invariant_contract.invariant_fns.len());
436 Ok(ContinueOutcome { continues, cancelled: false })
437}
438
439pub(crate) fn assert_after_invariant<'a, FEN: FoundryEvmNetwork>(
444 invariant_contract: &InvariantContract<'a>,
445 invariant_test: &mut InvariantTest,
446 invariant_run: &InvariantTestRun<FEN>,
447 invariant_config: &InvariantConfig,
448) -> Result<(Option<&'a Function>, bool)> {
449 let (call_result, success) =
450 call_after_invariant_function(&invariant_run.executor, invariant_contract.address)?;
451 if call_result.execution_cancelled {
452 return Ok((None, true));
453 }
454 if success {
456 return Ok((None, false));
457 }
458 let anchor = invariant_contract.anchor();
461 let case_data = InvariantRunCtx {
462 contract: invariant_contract,
463 config: invariant_config,
464 targeted_contracts: &invariant_test.targeted_contracts,
465 calldata: &invariant_run.inputs,
466 }
467 .failed_case(anchor, invariant_config.fail_on_revert, false, call_result, &[]);
468 invariant_test
469 .test_data
470 .failures
471 .record_failure(anchor, InvariantFuzzError::BrokenInvariant(case_data));
472 Ok((Some(anchor), false))
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use crate::executors::{EarlyExit, ExecutorBuilder};
479 use alloy_dyn_abi::JsonAbiExt;
480 use alloy_primitives::{Bytes, U256};
481 use alloy_sol_types::SolCall;
482 use foundry_cheatcodes::{CheatsConfig, Vm::expectRevert_0Call};
483 use foundry_config::Config;
484 use foundry_evm_core::{
485 backend::Backend,
486 constants::CALLER,
487 evm::{EthEvmNetwork, EvmEnvFor, TxEnvFor},
488 opts::EvmOpts,
489 };
490 use foundry_evm_fuzz::invariant::TargetedContracts;
491 use revm::bytecode::Bytecode;
492 use std::sync::Arc;
493
494 fn panic_payload(code: u8) -> Bytes {
495 let mut payload = vec![0_u8; 36];
496 payload[..4].copy_from_slice(&[0x4e, 0x48, 0x7b, 0x71]);
497 payload[35] = code;
498 payload.into()
499 }
500
501 #[test]
502 fn cancellation_does_not_record_call_end_rewrite_as_invariant_failure() {
503 let cheats_config =
504 Arc::new(CheatsConfig::new(&Config::default(), EvmOpts::default(), None, None, false));
505 let backend = Backend::<EthEvmNetwork>::spawn(None).unwrap();
506 let mut executor = ExecutorBuilder::default()
507 .inspectors(|stack| stack.cheatcodes(cheats_config))
508 .gas_limit(1 << 24)
509 .build(
510 EvmEnvFor::<EthEvmNetwork>::default(),
511 TxEnvFor::<EthEvmNetwork>::default(),
512 backend,
513 Default::default(),
514 );
515 let invariant_address = Address::repeat_byte(0x11);
516 executor
517 .set_code(
518 invariant_address,
519 Bytecode::new_raw(Bytes::from_static(&[0x5b, 0x60, 0x00, 0x56])),
520 )
521 .unwrap();
522 let expect_result = executor
523 .transact_raw(
524 CALLER,
525 CHEATCODE_ADDRESS,
526 expectRevert_0Call {}.abi_encode().into(),
527 U256::ZERO,
528 )
529 .unwrap();
530 assert!(!expect_result.reverted);
531
532 let early_exit = EarlyExit::new(false);
533 executor.inspector_mut().set_early_exit(early_exit.clone());
534 early_exit.record_ctrl_c();
535
536 let invariant = Function::parse("invariant_ok() view returns (bool)").unwrap();
537 let mut abi = alloy_json_abi::JsonAbi::new();
538 abi.functions.entry(invariant.name.clone()).or_default().push(invariant.clone());
539 let invariant_contract = InvariantContract::new(
540 invariant_address,
541 "InvariantTest",
542 vec![(&invariant, false)],
543 0,
544 false,
545 &abi,
546 );
547
548 let (_, success) = call_invariant_function(
549 &executor.clone(),
550 invariant_address,
551 invariant.abi_encode_input(&[]).unwrap().into(),
552 )
553 .unwrap();
554 assert!(!success, "pending expectRevert should rewrite the interrupted call");
555
556 let targets = FuzzRunIdentifiedContracts::new(TargetedContracts::new(), false);
557 let mut failures = InvariantFailures::new();
558 let broken = assert_invariants(
559 &invariant_contract,
560 &InvariantConfig::default(),
561 &targets,
562 &executor,
563 &[],
564 &mut failures,
565 )
566 .unwrap();
567
568 assert!(broken.0.is_none());
569 assert!(broken.1);
570 assert_eq!(failures.invariant_count(), 0);
571 }
572
573 #[test]
574 fn detects_assert_panic_code() {
575 let call_result = RawCallResult::<EthEvmNetwork> {
576 reverted: true,
577 result: panic_payload(0x01),
578 ..Default::default()
579 };
580 assert!(is_assertion_failure(&call_result));
581 }
582
583 #[test]
584 fn ignores_non_assert_panic_code() {
585 let call_result = RawCallResult::<EthEvmNetwork> {
586 reverted: true,
587 result: panic_payload(0x11),
588 ..Default::default()
589 };
590 assert!(!is_assertion_failure(&call_result));
591 }
592
593 #[test]
594 fn detects_legacy_invalid_opcode_assert() {
595 let call_result = RawCallResult::<EthEvmNetwork> {
596 reverted: true,
597 exit_reason: Some(InstructionResult::InvalidFEOpcode),
598 ..Default::default()
599 };
600 assert!(is_assertion_failure(&call_result));
601 }
602
603 #[test]
604 fn detects_vm_assert_revert() {
605 let call_result = RawCallResult::<EthEvmNetwork> {
606 reverted: true,
607 result: Vm::CheatcodeError { message: format!("{ASSERTION_FAILED_PREFIX}: 1 != 2") }
608 .abi_encode()
609 .into(),
610 reverter: Some(CHEATCODE_ADDRESS),
611 ..Default::default()
612 };
613 assert!(is_assertion_failure(&call_result));
614 }
615
616 #[test]
617 fn detects_assertion_failure_revert_reason() {
618 let call_result = RawCallResult::<EthEvmNetwork> {
619 reverted: true,
620 result: Revert { reason: format!("{ASSERTION_FAILED_PREFIX}: expected") }
621 .abi_encode()
622 .into(),
623 ..Default::default()
624 };
625 assert!(is_assertion_failure(&call_result));
626 }
627
628 #[test]
629 fn ignores_empty_cheatcode_revert() {
630 let call_result = RawCallResult::<EthEvmNetwork> {
631 reverted: true,
632 result: Bytes::new(),
633 reverter: Some(CHEATCODE_ADDRESS),
634 ..Default::default()
635 };
636 assert!(!is_assertion_failure(&call_result));
637 }
638}