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, FuzzedCases,
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 cases: Vec<FuzzedCases>,
40 pub reverts: usize,
42 pub last_run_inputs: Vec<BasicTxDetails>,
45 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
47 pub line_coverage: Option<HitMaps>,
49 pub metrics: HashMap<String, InvariantMetrics>,
51 pub failed_corpus_replays: usize,
53 pub optimization_best_value: Option<I256>,
56 pub optimization_best_sequence: Vec<BasicTxDetails>,
58}
59
60pub(crate) fn invariant_preflight_check<FEN: FoundryEvmNetwork>(
64 invariant_contract: &InvariantContract<'_>,
65 invariant_config: &InvariantConfig,
66 targeted_contracts: &FuzzRunIdentifiedContracts,
67 executor: &Executor<FEN>,
68 calldata: &[BasicTxDetails],
69 invariant_failures: &mut InvariantFailures,
70) -> Result<()> {
71 assert_invariants(
72 invariant_contract,
73 invariant_config,
74 targeted_contracts,
75 executor,
76 calldata,
77 invariant_failures,
78 )?;
79 Ok(())
80}
81
82pub(crate) fn is_assertion_failure<FEN: FoundryEvmNetwork>(
86 call_result: &RawCallResult<FEN>,
87) -> bool {
88 if !call_result.reverted {
89 return false;
90 }
91
92 is_assert_panic(call_result.result.as_ref())
93 || matches!(call_result.exit_reason, Some(InstructionResult::InvalidFEOpcode))
94 || is_revert_assertion_failure(call_result.result.as_ref())
95 || is_cheatcode_assert_revert(call_result)
96}
97
98fn is_assert_panic(data: &[u8]) -> bool {
99 Panic::abi_decode(data).is_ok_and(|panic| panic == PanicKind::Assert.into())
100}
101
102fn is_revert_assertion_failure(data: &[u8]) -> bool {
103 Revert::abi_decode(data).is_ok_and(|revert| revert.reason.contains(ASSERTION_FAILED_PREFIX))
104}
105
106fn is_cheatcode_assert_revert<FEN: FoundryEvmNetwork>(call_result: &RawCallResult<FEN>) -> bool {
107 fn decoded_cheatcode_message(data: &[u8]) -> Option<String> {
108 Vm::VmErrors::abi_decode(data).ok().map(|error| error.to_string())
109 }
110
111 call_result.reverter == Some(CHEATCODE_ADDRESS)
112 && decoded_cheatcode_message(call_result.result.as_ref())
113 .is_some_and(|message| message.starts_with(ASSERTION_FAILED_PREFIX))
114}
115
116fn logged_assertion_failure<FEN: FoundryEvmNetwork>(call_result: &RawCallResult<FEN>) -> bool {
117 call_result
118 .logs
119 .iter()
120 .filter_map(decode_console_log)
121 .any(|msg| msg.starts_with(ASSERTION_FAILED_PREFIX))
122}
123
124pub(crate) fn did_fail_on_assert<FEN: FoundryEvmNetwork>(
129 call_result: &RawCallResult<FEN>,
130 state_changeset: &StateChangeset,
131) -> bool {
132 is_assertion_failure(call_result)
133 || call_result.has_state_snapshot_failure
134 || Executor::<FEN>::has_pending_global_failure(state_changeset)
135 || logged_assertion_failure(call_result)
136}
137
138pub(crate) fn assert_invariants<'a, FEN: FoundryEvmNetwork>(
144 invariant_contract: &InvariantContract<'a>,
145 invariant_config: &InvariantConfig,
146 targeted_contracts: &FuzzRunIdentifiedContracts,
147 executor: &Executor<FEN>,
148 calldata: &[BasicTxDetails],
149 invariant_failures: &mut InvariantFailures,
150) -> Result<Option<&'a Function>> {
151 let inner_sequence = invariant_inner_sequence(executor);
152 let mut first_broken: Option<&'a Function> = None;
153 let ctx = InvariantRunCtx {
154 contract: invariant_contract,
155 config: invariant_config,
156 targeted_contracts,
157 calldata,
158 };
159
160 for (invariant, fail_on_revert) in &invariant_contract.invariant_fns {
161 if invariant_failures.has_failure(invariant) {
163 continue;
164 }
165
166 let (call_result, success) = call_invariant_function(
167 executor,
168 invariant_contract.address,
169 invariant.abi_encode_input(&[])?.into(),
170 )?;
171 if !success {
172 let case =
173 ctx.failed_case(invariant, *fail_on_revert, false, call_result, &inner_sequence);
174 invariant_failures.record_failure(invariant, InvariantFuzzError::BrokenInvariant(case));
175 if first_broken.is_none() {
176 first_broken = Some(*invariant);
177 }
178 }
179 }
180
181 Ok(first_broken)
182}
183
184fn invariant_inner_sequence<FEN: FoundryEvmNetwork>(
186 executor: &Executor<FEN>,
187) -> Vec<Option<BasicTxDetails>> {
188 let mut seq = vec![];
189 if let Some(fuzzer) = &executor.inspector().fuzzer
190 && let Some(call_generator) = &fuzzer.call_generator
191 {
192 seq.extend(call_generator.last_sequence.read().iter().cloned());
193 }
194 seq
195}
196
197#[derive(Debug)]
199pub(crate) struct ContinueOutcome<'a> {
200 pub continues: bool,
202 pub broken: Option<&'a Function>,
205}
206
207#[allow(clippy::too_many_arguments)]
216pub(crate) fn can_continue<'a, FEN: FoundryEvmNetwork>(
217 invariant_contract: &InvariantContract<'a>,
218 invariant_test: &mut InvariantTest,
219 invariant_run: &mut InvariantTestRun<FEN>,
220 invariant_config: &InvariantConfig,
221 call_result: RawCallResult<FEN>,
222 state_changeset: &StateChangeset,
223 handler_target: Address,
224 handler_selector: Selector,
225 pre_merge_edges_hash: Option<B256>,
226) -> Result<ContinueOutcome<'a>> {
227 let is_optimization = invariant_contract.is_optimization();
228 let mut broken: Option<&'a Function> = None;
229
230 let handlers_succeeded = || {
235 invariant_test.targeted_contracts.targets().keys().all(|address| {
236 invariant_run.executor.is_success_handler_gate(
237 *address,
238 false,
239 Cow::Borrowed(state_changeset),
240 )
241 })
242 };
243
244 if !call_result.reverted && handlers_succeeded() {
245 if let Some(traces) = call_result.traces {
246 invariant_run.run_traces.push(traces);
247 }
248
249 if is_optimization {
250 let (inv_result, success) = call_invariant_function(
252 &invariant_run.executor,
253 invariant_contract.address,
254 invariant_contract.anchor().abi_encode_input(&[])?.into(),
255 )?;
256 if success
257 && inv_result.result.len() >= 32
258 && let Some(value) = I256::try_from_be_slice(&inv_result.result[..32])
259 {
260 invariant_test.update_optimization_value(value, &invariant_run.inputs);
261 if invariant_run.optimization_value.is_none_or(|prev| value > prev) {
264 invariant_run.optimization_value = Some(value);
265 invariant_run.optimization_prefix_len = invariant_run.inputs.len();
266 }
267 }
268 } else {
269 broken = assert_invariants(
271 invariant_contract,
272 invariant_config,
273 &invariant_test.targeted_contracts,
274 &invariant_run.executor,
275 &invariant_run.inputs,
276 &mut invariant_test.test_data.failures,
277 )?;
278 }
279 } else {
280 let is_assert_failure = did_fail_on_assert(&call_result, state_changeset);
281 let reverted = call_result.reverted;
282
283 if reverted {
284 invariant_test.test_data.failures.reverts += 1;
285 }
286
287 if is_assert_failure {
288 record_handler_assertion_bug(
291 invariant_contract,
292 invariant_config,
293 &invariant_test.targeted_contracts,
294 &mut invariant_test.test_data.failures,
295 &mut invariant_run.inputs,
296 handler_target,
297 handler_selector,
298 pre_merge_edges_hash,
299 call_result,
300 reverted,
301 is_optimization,
302 );
303
304 let continues = invariant_test
306 .test_data
307 .failures
308 .can_continue(invariant_contract.invariant_fns.len());
309 return Ok(ContinueOutcome { continues, broken: None });
310 }
311
312 let failing_invariants: Vec<_> = invariant_contract
315 .invariant_fns
316 .iter()
317 .filter(|(invariant, fail_on_revert)| {
318 *fail_on_revert && !invariant_test.test_data.failures.has_failure(invariant)
319 })
320 .collect();
321
322 if let Some((first_invariant, _)) = failing_invariants.first() {
323 broken = Some(*first_invariant);
324 let base = InvariantRunCtx {
328 contract: invariant_contract,
329 config: invariant_config,
330 targeted_contracts: &invariant_test.targeted_contracts,
331 calldata: &invariant_run.inputs,
332 }
333 .failed_case(
334 first_invariant,
335 invariant_config.fail_on_revert,
336 is_assert_failure,
337 call_result,
338 &[],
339 );
340
341 for (invariant, fail_on_revert) in failing_invariants {
342 let mut data = base.clone();
343 data.fail_on_revert = *fail_on_revert;
344 data.calldata = invariant.selector().to_vec().into();
345 data.test_error = TestError::Fail(
346 format!("{}, reason: {}", invariant.name, data.revert_reason).into(),
347 invariant_run.inputs.clone(),
348 );
349 invariant_test.test_data.failures.record_failure(
352 invariant,
353 if is_assert_failure {
354 InvariantFuzzError::BrokenInvariant(data)
355 } else {
356 InvariantFuzzError::Revert(data)
357 },
358 );
359 }
360 }
361
362 if reverted && !is_optimization && !invariant_config.has_delay() {
363 invariant_run.inputs.pop();
367 }
368 }
369
370 let continues =
371 invariant_test.test_data.failures.can_continue(invariant_contract.invariant_fns.len());
372 Ok(ContinueOutcome { continues, broken })
373}
374
375pub(crate) fn assert_after_invariant<'a, FEN: FoundryEvmNetwork>(
380 invariant_contract: &InvariantContract<'a>,
381 invariant_test: &mut InvariantTest,
382 invariant_run: &InvariantTestRun<FEN>,
383 invariant_config: &InvariantConfig,
384) -> Result<Option<&'a Function>> {
385 let (call_result, success) =
386 call_after_invariant_function(&invariant_run.executor, invariant_contract.address)?;
387 if success {
389 return Ok(None);
390 }
391 let anchor = invariant_contract.anchor();
394 let case_data = 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(anchor, invariant_config.fail_on_revert, false, call_result, &[]);
401 invariant_test.set_error(anchor, InvariantFuzzError::BrokenInvariant(case_data));
402 Ok(Some(anchor))
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use alloy_primitives::Bytes;
409 use foundry_evm_core::evm::EthEvmNetwork;
410
411 fn panic_payload(code: u8) -> Bytes {
412 let mut payload = vec![0_u8; 36];
413 payload[..4].copy_from_slice(&[0x4e, 0x48, 0x7b, 0x71]);
414 payload[35] = code;
415 payload.into()
416 }
417
418 #[test]
419 fn detects_assert_panic_code() {
420 let call_result = RawCallResult::<EthEvmNetwork> {
421 reverted: true,
422 result: panic_payload(0x01),
423 ..Default::default()
424 };
425 assert!(is_assertion_failure(&call_result));
426 }
427
428 #[test]
429 fn ignores_non_assert_panic_code() {
430 let call_result = RawCallResult::<EthEvmNetwork> {
431 reverted: true,
432 result: panic_payload(0x11),
433 ..Default::default()
434 };
435 assert!(!is_assertion_failure(&call_result));
436 }
437
438 #[test]
439 fn detects_legacy_invalid_opcode_assert() {
440 let call_result = RawCallResult::<EthEvmNetwork> {
441 reverted: true,
442 exit_reason: Some(InstructionResult::InvalidFEOpcode),
443 ..Default::default()
444 };
445 assert!(is_assertion_failure(&call_result));
446 }
447
448 #[test]
449 fn detects_vm_assert_revert() {
450 let call_result = RawCallResult::<EthEvmNetwork> {
451 reverted: true,
452 result: Vm::CheatcodeError { message: format!("{ASSERTION_FAILED_PREFIX}: 1 != 2") }
453 .abi_encode()
454 .into(),
455 reverter: Some(CHEATCODE_ADDRESS),
456 ..Default::default()
457 };
458 assert!(is_assertion_failure(&call_result));
459 }
460
461 #[test]
462 fn detects_assertion_failure_revert_reason() {
463 let call_result = RawCallResult::<EthEvmNetwork> {
464 reverted: true,
465 result: Revert { reason: format!("{ASSERTION_FAILED_PREFIX}: expected") }
466 .abi_encode()
467 .into(),
468 ..Default::default()
469 };
470 assert!(is_assertion_failure(&call_result));
471 }
472
473 #[test]
474 fn ignores_empty_cheatcode_revert() {
475 let call_result = RawCallResult::<EthEvmNetwork> {
476 reverted: true,
477 result: Bytes::new(),
478 reverter: Some(CHEATCODE_ADDRESS),
479 ..Default::default()
480 };
481 assert!(!is_assertion_failure(&call_result));
482 }
483}