Skip to main content

foundry_evm/executors/invariant/
error.rs

1use super::InvariantContract;
2use crate::executors::RawCallResult;
3use alloy_primitives::{Address, Bytes};
4use foundry_config::InvariantConfig;
5use foundry_evm_core::{
6    decode::{ASSERTION_FAILED_PREFIX, EMPTY_REVERT_DATA, RevertDecoder},
7    evm::FoundryEvmNetwork,
8};
9use foundry_evm_fuzz::{BasicTxDetails, Reason, invariant::FuzzRunIdentifiedContracts};
10use proptest::test_runner::TestError;
11
12/// Stores information about failures and reverts of the invariant tests.
13#[derive(Clone, Default)]
14pub struct InvariantFailures {
15    /// Total number of reverts.
16    pub reverts: usize,
17    /// The latest revert reason of a run.
18    pub revert_reason: Option<String>,
19    /// Maps a broken invariant to its specific error.
20    pub error: Option<InvariantFuzzError>,
21}
22
23impl InvariantFailures {
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    pub fn into_inner(self) -> (usize, Option<InvariantFuzzError>) {
29        (self.reverts, self.error)
30    }
31}
32
33#[derive(Clone, Debug)]
34pub enum InvariantFuzzError {
35    Revert(FailedInvariantCaseData),
36    BrokenInvariant(FailedInvariantCaseData),
37    MaxAssumeRejects(u32),
38}
39
40impl InvariantFuzzError {
41    pub fn revert_reason(&self) -> Option<String> {
42        match self {
43            Self::BrokenInvariant(case_data) | Self::Revert(case_data) => {
44                (!case_data.revert_reason.is_empty()).then(|| case_data.revert_reason.clone())
45            }
46            Self::MaxAssumeRejects(allowed) => {
47                Some(format!("`vm.assume` rejected too many inputs ({allowed} allowed)"))
48            }
49        }
50    }
51}
52
53#[derive(Clone, Debug)]
54pub struct FailedInvariantCaseData {
55    /// The proptest error occurred as a result of a test case.
56    pub test_error: TestError<Vec<BasicTxDetails>>,
57    /// The return reason of the offending call.
58    pub return_reason: Reason,
59    /// The revert string of the offending call.
60    pub revert_reason: String,
61    /// Address of the invariant asserter.
62    pub addr: Address,
63    /// Function calldata for invariant check.
64    pub calldata: Bytes,
65    /// Inner fuzzing Sequence coming from overriding calls.
66    pub inner_sequence: Vec<Option<BasicTxDetails>>,
67    /// Shrink run limit
68    pub shrink_run_limit: u32,
69    /// Fail on revert, used to check sequence when shrinking.
70    pub fail_on_revert: bool,
71    /// Whether this failure originated from a handler assertion.
72    pub assertion_failure: bool,
73}
74
75impl FailedInvariantCaseData {
76    pub fn new<FEN: FoundryEvmNetwork>(
77        invariant_contract: &InvariantContract<'_>,
78        invariant_config: &InvariantConfig,
79        targeted_contracts: &FuzzRunIdentifiedContracts,
80        calldata: &[BasicTxDetails],
81        call_result: RawCallResult<FEN>,
82        inner_sequence: &[Option<BasicTxDetails>],
83    ) -> Self {
84        // Collect abis of fuzzed and invariant contracts to decode custom error.
85        let revert_reason = RevertDecoder::new()
86            .with_abis(targeted_contracts.targets.lock().values().map(|c| &c.abi))
87            .with_abi(invariant_contract.abi)
88            .decode(call_result.result.as_ref(), call_result.exit_reason);
89        // Non-reverting assertion failures surface through Foundry's failure flags instead of
90        // revert data. Use a stable fallback so invariant output is not blank.
91        let revert_reason =
92            if !call_result.reverted && matches!(revert_reason.as_str(), "" | EMPTY_REVERT_DATA) {
93                ASSERTION_FAILED_PREFIX.to_string()
94            } else {
95                revert_reason
96            };
97
98        let func = invariant_contract.invariant_function;
99        debug_assert!(func.inputs.is_empty());
100        let origin = func.name.as_str();
101        Self {
102            test_error: TestError::Fail(
103                format!("{origin}, reason: {revert_reason}").into(),
104                calldata.to_vec(),
105            ),
106            return_reason: "".into(),
107            revert_reason,
108            addr: invariant_contract.address,
109            calldata: func.selector().to_vec().into(),
110            inner_sequence: inner_sequence.to_vec(),
111            shrink_run_limit: invariant_config.shrink_run_limit,
112            fail_on_revert: invariant_config.fail_on_revert,
113            assertion_failure: false,
114        }
115    }
116
117    /// Marks this case as assertion-originated and normalizes empty decoded revert data from
118    /// non-reverting assertion paths into a stable user-facing message.
119    pub fn with_assertion_failure(mut self, assertion_failure: bool) -> Self {
120        self.assertion_failure = assertion_failure;
121        if assertion_failure && matches!(self.revert_reason.as_str(), "" | EMPTY_REVERT_DATA) {
122            self.revert_reason = ASSERTION_FAILED_PREFIX.to_string();
123        }
124        self
125    }
126}