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_dyn_abi::JsonAbiExt;
8use alloy_json_abi::Function;
9use alloy_primitives::{Address, B256, I256, Selector};
10use alloy_sol_types::{Panic, PanicKind, Revert, SolError, SolInterface};
11use eyre::Result;
12use foundry_config::InvariantConfig;
13use foundry_evm_core::{
14 abi::Vm,
15 constants::CHEATCODE_ADDRESS,
16 decode::{ASSERTION_FAILED_PREFIX, decode_console_log},
17 evm::FoundryEvmNetwork,
18 utils::StateChangeset,
19};
20use foundry_evm_coverage::HitMaps;
21use foundry_evm_fuzz::{
22 BasicTxDetails,
23 invariant::{FuzzRunIdentifiedContracts, InvariantContract},
24};
25use proptest::test_runner::TestError;
26use revm::interpreter::InstructionResult;
27use revm_inspectors::tracing::CallTraceArena;
28use std::{borrow::Cow, collections::HashMap};
29
30#[derive(Debug)]
32pub struct InvariantFuzzTestResult {
33 pub errors: HashMap<String, InvariantFuzzError>,
35 pub handler_errors: HashMap<(Address, Selector), InvariantFuzzError>,
38 pub runs: usize,
40 pub calls: usize,
42 pub reverts: usize,
44 pub last_run_inputs: Vec<BasicTxDetails>,
47 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
49 pub line_coverage: Option<HitMaps>,
51 pub metrics: HashMap<String, InvariantMetrics>,
53 pub failed_corpus_replays: usize,
55 pub workers: usize,
57 pub fork_block_number: Option<u64>,
59 pub optimization_best_value: Option<I256>,
62 pub optimization_best_sequence: Vec<BasicTxDetails>,
64}
65
66impl InvariantFuzzTestResult {
67 #[expect(clippy::too_many_arguments)]
68 pub(crate) fn new(
69 errors: HashMap<String, InvariantFuzzError>,
70 handler_errors: HashMap<(Address, Selector), InvariantFuzzError>,
71 runs: usize,
72 calls: usize,
73 reverts: usize,
74 last_run_inputs: Vec<BasicTxDetails>,
75 gas_report_traces: Vec<Vec<CallTraceArena>>,
76 line_coverage: Option<HitMaps>,
77 metrics: HashMap<String, InvariantMetrics>,
78 failed_corpus_replays: usize,
79 workers: usize,
80 optimization_best_value: Option<I256>,
81 optimization_best_sequence: Vec<BasicTxDetails>,
82 ) -> Self {
83 let mut failure_blocks = errors
84 .values()
85 .chain(handler_errors.values())
86 .map(InvariantFuzzError::fork_block_number);
87 let fork_block_number = failure_blocks
88 .next()
89 .flatten()
90 .filter(|first| failure_blocks.all(|block| block == Some(*first)));
91
92 Self {
93 errors,
94 handler_errors,
95 runs,
96 calls,
97 reverts,
98 last_run_inputs,
99 gas_report_traces,
100 line_coverage,
101 metrics,
102 failed_corpus_replays,
103 workers,
104 fork_block_number,
105 optimization_best_value,
106 optimization_best_sequence,
107 }
108 }
109}
110
111pub(crate) fn invariant_preflight_check<FEN: FoundryEvmNetwork>(
115 invariant_contract: &InvariantContract<'_>,
116 invariant_config: &InvariantConfig,
117 targeted_contracts: &FuzzRunIdentifiedContracts,
118 executor: &Executor<FEN>,
119 calldata: &[BasicTxDetails],
120 invariant_failures: &mut InvariantFailures,
121) -> Result<()> {
122 assert_invariants(
123 invariant_contract,
124 invariant_config,
125 targeted_contracts,
126 executor,
127 calldata,
128 invariant_failures,
129 )?;
130 Ok(())
131}
132
133pub(crate) fn is_assertion_failure<FEN: FoundryEvmNetwork>(
137 call_result: &RawCallResult<FEN>,
138) -> bool {
139 if !call_result.reverted {
140 return false;
141 }
142
143 is_assert_panic(call_result.result.as_ref())
144 || matches!(call_result.exit_reason, Some(InstructionResult::InvalidFEOpcode))
145 || is_revert_assertion_failure(call_result.result.as_ref())
146 || is_cheatcode_assert_revert(call_result)
147}
148
149fn is_assert_panic(data: &[u8]) -> bool {
150 Panic::abi_decode(data).is_ok_and(|panic| panic == PanicKind::Assert.into())
151}
152
153fn is_revert_assertion_failure(data: &[u8]) -> bool {
154 Revert::abi_decode(data).is_ok_and(|revert| revert.reason.contains(ASSERTION_FAILED_PREFIX))
155}
156
157fn is_cheatcode_assert_revert<FEN: FoundryEvmNetwork>(call_result: &RawCallResult<FEN>) -> bool {
158 call_result.reverter == Some(CHEATCODE_ADDRESS)
159 && Vm::VmErrors::abi_decode(call_result.result.as_ref())
160 .ok()
161 .map(|error| error.to_string())
162 .is_some_and(|message| message.starts_with(ASSERTION_FAILED_PREFIX))
163}
164
165fn logged_assertion_failure<FEN: FoundryEvmNetwork>(call_result: &RawCallResult<FEN>) -> bool {
166 call_result
167 .logs
168 .iter()
169 .filter_map(decode_console_log)
170 .any(|msg| msg.starts_with(ASSERTION_FAILED_PREFIX))
171}
172
173pub(crate) fn did_fail_on_assert<FEN: FoundryEvmNetwork>(
178 call_result: &RawCallResult<FEN>,
179 state_changeset: &StateChangeset,
180) -> bool {
181 is_assertion_failure(call_result)
182 || call_result.has_state_snapshot_failure
183 || Executor::<FEN>::has_pending_global_failure(state_changeset)
184 || logged_assertion_failure(call_result)
185}
186
187pub(crate) fn assert_invariants<'a, FEN: FoundryEvmNetwork>(
193 invariant_contract: &InvariantContract<'a>,
194 invariant_config: &InvariantConfig,
195 targeted_contracts: &FuzzRunIdentifiedContracts,
196 executor: &Executor<FEN>,
197 calldata: &[BasicTxDetails],
198 invariant_failures: &mut InvariantFailures,
199) -> Result<(Option<&'a Function>, bool)> {
200 let mut inner_sequence = None;
201 let mut first_broken: Option<&'a Function> = None;
202 let ctx = InvariantRunCtx {
203 contract: invariant_contract,
204 config: invariant_config,
205 targeted_contracts,
206 calldata,
207 };
208
209 for (invariant, fail_on_revert) in &invariant_contract.invariant_fns {
210 if invariant_failures.has_failure(invariant) {
212 continue;
213 }
214
215 let (call_result, success) = call_invariant_function(
216 executor,
217 invariant_contract.address,
218 invariant.abi_encode_input(&[])?.into(),
219 )?;
220 if call_result.execution_cancelled {
221 return Ok((first_broken, true));
222 }
223 if !success {
224 let inner_sequence =
225 inner_sequence.get_or_insert_with(|| invariant_inner_sequence(executor));
226 let case =
227 ctx.failed_case(invariant, *fail_on_revert, false, call_result, inner_sequence);
228 invariant_failures.record_failure(invariant, InvariantFuzzError::BrokenInvariant(case));
229 if first_broken.is_none() {
230 first_broken = Some(*invariant);
231 }
232 }
233 }
234
235 Ok((first_broken, false))
236}
237
238fn invariant_inner_sequence<FEN: FoundryEvmNetwork>(
240 executor: &Executor<FEN>,
241) -> Vec<Option<BasicTxDetails>> {
242 let mut seq = vec![];
243 if let Some(fuzzer) = &executor.inspector().fuzzer
244 && let Some(call_generator) = &fuzzer.call_generator
245 {
246 seq.extend(call_generator.last_sequence.read().iter().cloned());
247 }
248 seq
249}
250
251#[derive(Debug)]
253pub(crate) struct ContinueOutcome {
254 pub continues: bool,
256 pub cancelled: bool,
258}
259
260#[allow(clippy::too_many_arguments)]
269pub(crate) fn can_continue<'a, FEN: FoundryEvmNetwork>(
270 invariant_contract: &InvariantContract<'a>,
271 invariant_test: &mut InvariantTest,
272 invariant_run: &mut InvariantTestRun<FEN>,
273 invariant_config: &InvariantConfig,
274 call_result: RawCallResult<FEN>,
275 state_changeset: &StateChangeset,
276 handler_target: Address,
277 handler_selector: Selector,
278 assertion_failure: bool,
279 pre_merge_edges_hash: Option<B256>,
280) -> Result<ContinueOutcome> {
281 let is_optimization = invariant_contract.is_optimization();
282
283 let handlers_succeeded = || {
288 if !invariant_run.executor.legacy_assertions() {
289 return invariant_run.executor.is_success_handler_gate(
290 invariant_contract.address,
291 false,
292 Cow::Borrowed(state_changeset),
293 );
294 }
295
296 invariant_test.targeted_contracts.targets().keys().all(|address| {
297 invariant_run.executor.is_success_handler_gate(
298 *address,
299 false,
300 Cow::Borrowed(state_changeset),
301 )
302 })
303 };
304
305 if !call_result.reverted && handlers_succeeded() {
306 if let Some(traces) = call_result.traces {
307 invariant_run.run_traces.push(traces);
308 }
309
310 if is_optimization {
311 let (inv_result, success) = call_invariant_function(
313 &invariant_run.executor,
314 invariant_contract.address,
315 invariant_contract.anchor().abi_encode_input(&[])?.into(),
316 )?;
317 if inv_result.execution_cancelled {
318 return Ok(ContinueOutcome { continues: true, cancelled: true });
319 }
320 if success
321 && inv_result.result.len() >= 32
322 && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
323 {
324 if invariant_run.optimization_value.is_none_or(|prev| value > prev) {
327 invariant_run.optimization_value = Some(value);
328 invariant_run.optimization_prefix_len = invariant_run.inputs.len();
329 }
330 }
331 } else {
332 let (_, cancelled) = assert_invariants(
334 invariant_contract,
335 invariant_config,
336 &invariant_test.targeted_contracts,
337 &invariant_run.executor,
338 &invariant_run.inputs,
339 &mut invariant_test.test_data.failures,
340 )?;
341 if cancelled {
342 return Ok(ContinueOutcome { continues: true, cancelled: true });
343 }
344 }
345 } else {
346 let is_assert_failure = assertion_failure;
347 let reverted = call_result.reverted;
348
349 if reverted {
350 invariant_test.test_data.failures.reverts += 1;
351 }
352
353 if is_assert_failure {
354 record_handler_assertion_bug(
357 invariant_contract,
358 invariant_config,
359 &invariant_test.targeted_contracts,
360 &mut invariant_test.test_data.failures,
361 &mut invariant_run.inputs,
362 handler_target,
363 handler_selector,
364 pre_merge_edges_hash,
365 call_result,
366 reverted,
367 is_optimization,
368 );
369
370 let continues = invariant_test
372 .test_data
373 .failures
374 .can_continue(invariant_contract.invariant_fns.len());
375 return Ok(ContinueOutcome { continues, cancelled: false });
376 }
377
378 let failing_invariants: Vec<_> = invariant_contract
381 .invariant_fns
382 .iter()
383 .filter(|(invariant, fail_on_revert)| {
384 *fail_on_revert && !invariant_test.test_data.failures.has_failure(invariant)
385 })
386 .collect();
387
388 if let Some((first_invariant, _)) = failing_invariants.first() {
389 let base = InvariantRunCtx {
393 contract: invariant_contract,
394 config: invariant_config,
395 targeted_contracts: &invariant_test.targeted_contracts,
396 calldata: &invariant_run.inputs,
397 }
398 .failed_case(
399 first_invariant,
400 invariant_config.fail_on_revert,
401 is_assert_failure,
402 call_result,
403 &[],
404 );
405
406 for (invariant, fail_on_revert) in failing_invariants {
407 let mut data = base.clone();
408 data.fail_on_revert = *fail_on_revert;
409 data.calldata = invariant.selector().to_vec().into();
410 data.test_error = TestError::Fail(
411 format!("{}, reason: {}", invariant.name, data.revert_reason).into(),
412 invariant_run.inputs.clone(),
413 );
414 invariant_test.test_data.failures.record_failure(
417 invariant,
418 if is_assert_failure {
419 InvariantFuzzError::BrokenInvariant(data)
420 } else {
421 InvariantFuzzError::Revert(data)
422 },
423 );
424 }
425 }
426
427 if reverted && !is_optimization && !invariant_config.has_delay() {
428 invariant_run.inputs.pop();
432 }
433 }
434
435 let continues =
436 invariant_test.test_data.failures.can_continue(invariant_contract.invariant_fns.len());
437 Ok(ContinueOutcome { continues, cancelled: false })
438}
439
440pub(crate) fn assert_after_invariant<'a, FEN: FoundryEvmNetwork>(
445 invariant_contract: &InvariantContract<'a>,
446 invariant_test: &mut InvariantTest,
447 invariant_run: &InvariantTestRun<FEN>,
448 invariant_config: &InvariantConfig,
449) -> Result<(Option<&'a Function>, bool)> {
450 let (call_result, success) =
451 call_after_invariant_function(&invariant_run.executor, invariant_contract.address)?;
452 if call_result.execution_cancelled {
453 return Ok((None, true));
454 }
455 if success {
457 return Ok((None, false));
458 }
459 let anchor = invariant_contract.anchor();
462 let case_data = InvariantRunCtx {
463 contract: invariant_contract,
464 config: invariant_config,
465 targeted_contracts: &invariant_test.targeted_contracts,
466 calldata: &invariant_run.inputs,
467 }
468 .failed_case(anchor, invariant_config.fail_on_revert, false, call_result, &[]);
469 invariant_test
470 .test_data
471 .failures
472 .record_failure(anchor, InvariantFuzzError::BrokenInvariant(case_data));
473 Ok((Some(anchor), false))
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::executors::{EarlyExit, ExecutorBuilder};
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}