Skip to main content

foundry_evm/executors/invariant/
result.rs

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, 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/// The outcome of an invariant fuzz test
31#[derive(Debug)]
32pub struct InvariantFuzzTestResult {
33    /// Errors recorded per invariant.
34    pub errors: HashMap<String, InvariantFuzzError>,
35    /// Handler-side assertion bugs, keyed by `(reverter, selector)` site (deduped per
36    /// handler function). Each entry is [`InvariantFuzzError::HandlerAssertion`].
37    pub handler_errors: HashMap<(Address, Selector), InvariantFuzzError>,
38    /// Every successful fuzz test case
39    pub cases: Vec<FuzzedCases>,
40    /// Number of reverted fuzz calls
41    pub reverts: usize,
42    /// The entire inputs of the last run of the invariant campaign, used for
43    /// replaying the run for collecting traces.
44    pub last_run_inputs: Vec<BasicTxDetails>,
45    /// Additional traces used for gas report construction.
46    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
47    /// The coverage info collected during the invariant test runs.
48    pub line_coverage: Option<HitMaps>,
49    /// Fuzzed selectors metrics collected during the invariant test runs.
50    pub metrics: HashMap<String, InvariantMetrics>,
51    /// Number of failed replays from persisted corpus.
52    pub failed_corpus_replays: usize,
53    /// For optimization mode (int256 return): the best (maximum) value achieved.
54    /// None means standard invariant check mode.
55    pub optimization_best_value: Option<I256>,
56    /// For optimization mode: the call sequence that produced the best value.
57    pub optimization_best_sequence: Vec<BasicTxDetails>,
58}
59
60/// Given the executor state, asserts that no invariant has been broken. Otherwise, it fills the
61/// external `invariant_failures.failed_invariant` map and returns a generic error.
62/// Either returns the call result if successful, or nothing if there was an error.
63pub(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
82/// Returns true if this call failed due to a Solidity assertion:
83/// - `Panic(0x01)`, or
84/// - legacy invalid opcode assert behavior.
85pub(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
124/// Returns whether the current fuzz call should be treated as an assertion failure.
125///
126/// This covers Solidity `assert`, legacy invalid-opcode assertions, `vm.assert*` reverts, and the
127/// non-reverting `GLOBAL_FAIL_SLOT` path used when `assertions_revert = false`.
128pub(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
138/// Given the executor state, asserts that no invariant has been broken. Otherwise, it fills the
139/// external `invariant_failures.failed_invariant` map.
140///
141/// Returns the first newly-broken invariant in declaration order (if any), so callers can
142/// attribute the failure event without re-scanning `invariant_failures.errors` afterwards.
143pub(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        // We only care about invariants which we haven't broken yet.
162        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
184/// Helper function to initialize invariant inner sequence.
185fn 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/// Outcome of a per-call invariant check.
198#[derive(Debug)]
199pub(crate) struct ContinueOutcome<'a> {
200    /// Whether the invariant campaign should keep running after this call.
201    pub continues: bool,
202    /// First newly-broken invariant produced by this call, in declaration order. Used by the
203    /// executor to record the failure event without re-scanning the failures map.
204    pub broken: Option<&'a Function>,
205}
206
207/// Returns if invariant test can continue and last successful call result of the invariant test
208/// function (if it can continue).
209///
210/// For optimization mode (int256 return), tracks the max value but never fails on invariant.
211/// For check mode, asserts the invariant and fails if broken.
212///
213/// `handler_target` / `handler_selector` identify the just-executed call, used to
214/// attribute handler-side assertion failures.
215#[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    // Use the handler-gate variant so a stale committed `GLOBAL_FAIL_SLOT` from a
231    // previously-recorded handler bug doesn't poison this gate (which would otherwise silently
232    // skip every subsequent `assert_invariants` evaluation under `assertions_revert = false`).
233    // Handler bugs are tracked separately in `failures.broken_handlers`.
234    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            // Optimization mode: call invariant and track max value, never fail.
251            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                // Track the best value and its prefix length for this run
262                // (used for corpus persistence — materialized once at run end).
263                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            // Check mode: assert invariants and fail if broken.
270            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            // Handler-side assertion: deduped by `(reverter, selector)` site, shortest
289            // sequence wins on collision.
290            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            // No invariant predicate broke; `broken = None`.
305            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        // Non-assertion revert: per-invariant `fail_on_revert` still marks affected
313        // invariants as broken.
314        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            // Build a base case_data attributed to the first failing invariant; clone it for
325            // each subsequent broken invariant, retagging name/selector/`fail_on_revert` so
326            // every recorded failure points at its own invariant body.
327            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                // Handler asserts go to `broken_handlers` above; `BrokenInvariant` arm kept
350                // for non-handler-routed assertion paths.
351                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            // If we don't fail test on revert then remove the reverted call from inputs.
364            // Delay-enabled campaigns keep reverted calls so shrinking can preserve their
365            // warp/roll contribution when building the final counterexample.
366            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
375/// Given the executor state, asserts conditions within `afterInvariant` function.
376///
377/// Returns `Some(anchor)` if the hook failed (so the caller can record the failure event
378/// without re-scanning the failures map), or `None` if the hook succeeded.
379pub(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    // Fail the test case if `afterInvariant` doesn't succeed.
388    if success {
389        return Ok(None);
390    }
391    // `afterInvariant` failures are contract-wide (no specific invariant body executed),
392    // so attribute to the campaign anchor.
393    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}