foundry_evm/executors/invariant/
error.rs

1use super::{BasicTxDetails, InvariantContract};
2use crate::executors::RawCallResult;
3use alloy_primitives::{Address, Bytes};
4use foundry_config::InvariantConfig;
5use foundry_evm_core::decode::RevertDecoder;
6use foundry_evm_fuzz::{invariant::FuzzRunIdentifiedContracts, Reason};
7use proptest::test_runner::TestError;
8
9/// Stores information about failures and reverts of the invariant tests.
10#[derive(Clone, Default)]
11pub struct InvariantFailures {
12    /// Total number of reverts.
13    pub reverts: usize,
14    /// The latest revert reason of a run.
15    pub revert_reason: Option<String>,
16    /// Maps a broken invariant to its specific error.
17    pub error: Option<InvariantFuzzError>,
18}
19
20impl InvariantFailures {
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    pub fn into_inner(self) -> (usize, Option<InvariantFuzzError>) {
26        (self.reverts, self.error)
27    }
28}
29
30#[derive(Clone, Debug)]
31pub enum InvariantFuzzError {
32    Revert(FailedInvariantCaseData),
33    BrokenInvariant(FailedInvariantCaseData),
34    MaxAssumeRejects(u32),
35}
36
37impl InvariantFuzzError {
38    pub fn revert_reason(&self) -> Option<String> {
39        match self {
40            Self::BrokenInvariant(case_data) | Self::Revert(case_data) => {
41                (!case_data.revert_reason.is_empty()).then(|| case_data.revert_reason.clone())
42            }
43            Self::MaxAssumeRejects(allowed) => {
44                Some(format!("`vm.assume` rejected too many inputs ({allowed} allowed)"))
45            }
46        }
47    }
48}
49
50#[derive(Clone, Debug)]
51pub struct FailedInvariantCaseData {
52    /// The proptest error occurred as a result of a test case.
53    pub test_error: TestError<Vec<BasicTxDetails>>,
54    /// The return reason of the offending call.
55    pub return_reason: Reason,
56    /// The revert string of the offending call.
57    pub revert_reason: String,
58    /// Address of the invariant asserter.
59    pub addr: Address,
60    /// Function calldata for invariant check.
61    pub calldata: Bytes,
62    /// Inner fuzzing Sequence coming from overriding calls.
63    pub inner_sequence: Vec<Option<BasicTxDetails>>,
64    /// Shrink run limit
65    pub shrink_run_limit: u32,
66    /// Fail on revert, used to check sequence when shrinking.
67    pub fail_on_revert: bool,
68}
69
70impl FailedInvariantCaseData {
71    pub fn new(
72        invariant_contract: &InvariantContract<'_>,
73        invariant_config: &InvariantConfig,
74        targeted_contracts: &FuzzRunIdentifiedContracts,
75        calldata: &[BasicTxDetails],
76        call_result: RawCallResult,
77        inner_sequence: &[Option<BasicTxDetails>],
78    ) -> Self {
79        // Collect abis of fuzzed and invariant contracts to decode custom error.
80        let revert_reason = RevertDecoder::new()
81            .with_abis(targeted_contracts.targets.lock().iter().map(|(_, c)| &c.abi))
82            .with_abi(invariant_contract.abi)
83            .decode(call_result.result.as_ref(), Some(call_result.exit_reason));
84
85        let func = invariant_contract.invariant_function;
86        debug_assert!(func.inputs.is_empty());
87        let origin = func.name.as_str();
88        Self {
89            test_error: TestError::Fail(
90                format!("{origin}, reason: {revert_reason}").into(),
91                calldata.to_vec(),
92            ),
93            return_reason: "".into(),
94            revert_reason,
95            addr: invariant_contract.address,
96            calldata: func.selector().to_vec().into(),
97            inner_sequence: inner_sequence.to_vec(),
98            shrink_run_limit: invariant_config.shrink_run_limit,
99            fail_on_revert: invariant_config.fail_on_revert,
100        }
101    }
102}