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