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