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