foundry_evm/executors/invariant/
result.rs1use 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>> {
190 let inner_sequence = invariant_inner_sequence(executor);
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 !success {
211 let case =
212 ctx.failed_case(invariant, *fail_on_revert, false, call_result, &inner_sequence);
213 invariant_failures.record_failure(invariant, InvariantFuzzError::BrokenInvariant(case));
214 if first_broken.is_none() {
215 first_broken = Some(*invariant);
216 }
217 }
218 }
219
220 Ok(first_broken)
221}
222
223fn invariant_inner_sequence<FEN: FoundryEvmNetwork>(
225 executor: &Executor<FEN>,
226) -> Vec<Option<BasicTxDetails>> {
227 let mut seq = vec![];
228 if let Some(fuzzer) = &executor.inspector().fuzzer
229 && let Some(call_generator) = &fuzzer.call_generator
230 {
231 seq.extend(call_generator.last_sequence.read().iter().cloned());
232 }
233 seq
234}
235
236#[derive(Debug)]
238pub(crate) struct ContinueOutcome<'a> {
239 pub continues: bool,
241 pub broken: Option<&'a Function>,
244}
245
246#[allow(clippy::too_many_arguments)]
255pub(crate) fn can_continue<'a, FEN: FoundryEvmNetwork>(
256 invariant_contract: &InvariantContract<'a>,
257 invariant_test: &mut InvariantTest,
258 invariant_run: &mut InvariantTestRun<FEN>,
259 invariant_config: &InvariantConfig,
260 call_result: RawCallResult<FEN>,
261 state_changeset: &StateChangeset,
262 handler_target: Address,
263 handler_selector: Selector,
264 pre_merge_edges_hash: Option<B256>,
265) -> Result<ContinueOutcome<'a>> {
266 let is_optimization = invariant_contract.is_optimization();
267 let mut broken: Option<&'a Function> = None;
268
269 let handlers_succeeded = || {
274 invariant_test.targeted_contracts.targets().keys().all(|address| {
275 invariant_run.executor.is_success_handler_gate(
276 *address,
277 false,
278 Cow::Borrowed(state_changeset),
279 )
280 })
281 };
282
283 if !call_result.reverted && handlers_succeeded() {
284 if let Some(traces) = call_result.traces {
285 invariant_run.run_traces.push(traces);
286 }
287
288 if is_optimization {
289 let (inv_result, success) = call_invariant_function(
291 &invariant_run.executor,
292 invariant_contract.address,
293 invariant_contract.anchor().abi_encode_input(&[])?.into(),
294 )?;
295 if success
296 && inv_result.result.len() >= 32
297 && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
298 {
299 invariant_test.update_optimization_value(value, &invariant_run.inputs);
300 if invariant_run.optimization_value.is_none_or(|prev| value > prev) {
303 invariant_run.optimization_value = Some(value);
304 invariant_run.optimization_prefix_len = invariant_run.inputs.len();
305 }
306 }
307 } else {
308 broken = assert_invariants(
310 invariant_contract,
311 invariant_config,
312 &invariant_test.targeted_contracts,
313 &invariant_run.executor,
314 &invariant_run.inputs,
315 &mut invariant_test.test_data.failures,
316 )?;
317 }
318 } else {
319 let is_assert_failure = did_fail_on_assert(&call_result, state_changeset);
320 let reverted = call_result.reverted;
321
322 if reverted {
323 invariant_test.test_data.failures.reverts += 1;
324 }
325
326 if is_assert_failure {
327 record_handler_assertion_bug(
330 invariant_contract,
331 invariant_config,
332 &invariant_test.targeted_contracts,
333 &mut invariant_test.test_data.failures,
334 &mut invariant_run.inputs,
335 handler_target,
336 handler_selector,
337 pre_merge_edges_hash,
338 call_result,
339 reverted,
340 is_optimization,
341 );
342
343 let continues = invariant_test
345 .test_data
346 .failures
347 .can_continue(invariant_contract.invariant_fns.len());
348 return Ok(ContinueOutcome { continues, broken: None });
349 }
350
351 let failing_invariants: Vec<_> = invariant_contract
354 .invariant_fns
355 .iter()
356 .filter(|(invariant, fail_on_revert)| {
357 *fail_on_revert && !invariant_test.test_data.failures.has_failure(invariant)
358 })
359 .collect();
360
361 if let Some((first_invariant, _)) = failing_invariants.first() {
362 broken = Some(*first_invariant);
363 let base = InvariantRunCtx {
367 contract: invariant_contract,
368 config: invariant_config,
369 targeted_contracts: &invariant_test.targeted_contracts,
370 calldata: &invariant_run.inputs,
371 }
372 .failed_case(
373 first_invariant,
374 invariant_config.fail_on_revert,
375 is_assert_failure,
376 call_result,
377 &[],
378 );
379
380 for (invariant, fail_on_revert) in failing_invariants {
381 let mut data = base.clone();
382 data.fail_on_revert = *fail_on_revert;
383 data.calldata = invariant.selector().to_vec().into();
384 data.test_error = TestError::Fail(
385 format!("{}, reason: {}", invariant.name, data.revert_reason).into(),
386 invariant_run.inputs.clone(),
387 );
388 invariant_test.test_data.failures.record_failure(
391 invariant,
392 if is_assert_failure {
393 InvariantFuzzError::BrokenInvariant(data)
394 } else {
395 InvariantFuzzError::Revert(data)
396 },
397 );
398 }
399 }
400
401 if reverted && !is_optimization && !invariant_config.has_delay() {
402 invariant_run.inputs.pop();
406 }
407 }
408
409 let continues =
410 invariant_test.test_data.failures.can_continue(invariant_contract.invariant_fns.len());
411 Ok(ContinueOutcome { continues, broken })
412}
413
414pub(crate) fn assert_after_invariant<'a, FEN: FoundryEvmNetwork>(
419 invariant_contract: &InvariantContract<'a>,
420 invariant_test: &mut InvariantTest,
421 invariant_run: &InvariantTestRun<FEN>,
422 invariant_config: &InvariantConfig,
423) -> Result<Option<&'a Function>> {
424 let (call_result, success) =
425 call_after_invariant_function(&invariant_run.executor, invariant_contract.address)?;
426 if success {
428 return Ok(None);
429 }
430 let anchor = invariant_contract.anchor();
433 let case_data = InvariantRunCtx {
434 contract: invariant_contract,
435 config: invariant_config,
436 targeted_contracts: &invariant_test.targeted_contracts,
437 calldata: &invariant_run.inputs,
438 }
439 .failed_case(anchor, invariant_config.fail_on_revert, false, call_result, &[]);
440 invariant_test
441 .test_data
442 .failures
443 .record_failure(anchor, InvariantFuzzError::BrokenInvariant(case_data));
444 Ok(Some(anchor))
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use alloy_primitives::Bytes;
451 use foundry_evm_core::evm::EthEvmNetwork;
452
453 fn panic_payload(code: u8) -> Bytes {
454 let mut payload = vec![0_u8; 36];
455 payload[..4].copy_from_slice(&[0x4e, 0x48, 0x7b, 0x71]);
456 payload[35] = code;
457 payload.into()
458 }
459
460 #[test]
461 fn detects_assert_panic_code() {
462 let call_result = RawCallResult::<EthEvmNetwork> {
463 reverted: true,
464 result: panic_payload(0x01),
465 ..Default::default()
466 };
467 assert!(is_assertion_failure(&call_result));
468 }
469
470 #[test]
471 fn ignores_non_assert_panic_code() {
472 let call_result = RawCallResult::<EthEvmNetwork> {
473 reverted: true,
474 result: panic_payload(0x11),
475 ..Default::default()
476 };
477 assert!(!is_assertion_failure(&call_result));
478 }
479
480 #[test]
481 fn detects_legacy_invalid_opcode_assert() {
482 let call_result = RawCallResult::<EthEvmNetwork> {
483 reverted: true,
484 exit_reason: Some(InstructionResult::InvalidFEOpcode),
485 ..Default::default()
486 };
487 assert!(is_assertion_failure(&call_result));
488 }
489
490 #[test]
491 fn detects_vm_assert_revert() {
492 let call_result = RawCallResult::<EthEvmNetwork> {
493 reverted: true,
494 result: Vm::CheatcodeError { message: format!("{ASSERTION_FAILED_PREFIX}: 1 != 2") }
495 .abi_encode()
496 .into(),
497 reverter: Some(CHEATCODE_ADDRESS),
498 ..Default::default()
499 };
500 assert!(is_assertion_failure(&call_result));
501 }
502
503 #[test]
504 fn detects_assertion_failure_revert_reason() {
505 let call_result = RawCallResult::<EthEvmNetwork> {
506 reverted: true,
507 result: Revert { reason: format!("{ASSERTION_FAILED_PREFIX}: expected") }
508 .abi_encode()
509 .into(),
510 ..Default::default()
511 };
512 assert!(is_assertion_failure(&call_result));
513 }
514
515 #[test]
516 fn ignores_empty_cheatcode_revert() {
517 let call_result = RawCallResult::<EthEvmNetwork> {
518 reverted: true,
519 result: Bytes::new(),
520 reverter: Some(CHEATCODE_ADDRESS),
521 ..Default::default()
522 };
523 assert!(!is_assertion_failure(&call_result));
524 }
525}