foundry_evm/executors/invariant/
error.rs1use super::InvariantContract;
2use crate::{
3 executors::RawCallResult,
4 inspectors::{EdgeCovHit, EdgeCoverage},
5};
6use alloy_json_abi::Function;
7use alloy_primitives::{Address, B256, Bytes, Selector, keccak256};
8use foundry_config::InvariantConfig;
9use foundry_evm_core::{
10 decode::{ASSERTION_FAILED_PREFIX, EMPTY_REVERT_DATA, RevertDecoder},
11 evm::FoundryEvmNetwork,
12};
13use foundry_evm_fuzz::{BasicTxDetails, Reason, invariant::FuzzRunIdentifiedContracts};
14use proptest::test_runner::TestError;
15use std::{collections::HashMap, fmt};
16
17#[derive(Clone, Debug)]
21pub struct HandlerAssertionFailure {
22 pub reverter: Address,
24 pub selector: Selector,
26 pub call_sequence: Vec<BasicTxDetails>,
28 pub original_sequence_len: usize,
30 pub revert_reason: String,
32 pub edge_fingerprint: B256,
35}
36
37impl HandlerAssertionFailure {
38 pub fn from_replayed_sequence(
40 call_sequence: Vec<BasicTxDetails>,
41 edge_fingerprint: B256,
42 revert_reason: String,
43 ) -> Self {
44 let last = call_sequence.last().expect("replayed sequence is non-empty");
45 let reverter = last.call_details.target;
46 let selector_bytes: [u8; 4] =
47 last.call_details.calldata.get(..4).and_then(|s| s.try_into().ok()).unwrap_or_default();
48 let original_sequence_len = call_sequence.len();
49 Self {
50 reverter,
51 selector: Selector::from(selector_bytes),
52 call_sequence,
53 original_sequence_len,
54 revert_reason,
55 edge_fingerprint,
56 }
57 }
58}
59
60pub struct InvariantRunCtx<'a> {
62 pub contract: &'a InvariantContract<'a>,
64 pub config: &'a InvariantConfig,
66 pub targeted_contracts: &'a FuzzRunIdentifiedContracts,
68 pub calldata: &'a [BasicTxDetails],
70}
71
72impl<'a> InvariantRunCtx<'a> {
73 pub fn failed_case<FEN: FoundryEvmNetwork>(
77 &self,
78 broken_fn: &Function,
79 fail_on_revert: bool,
80 assertion_failure: bool,
81 call_result: RawCallResult<FEN>,
82 inner_sequence: &[Option<BasicTxDetails>],
83 ) -> FailedInvariantCaseData {
84 let revert_reason = self.decode_revert_reason(&call_result, assertion_failure);
85 let origin = broken_fn.name.as_str();
86 FailedInvariantCaseData {
87 test_error: TestError::Fail(
88 format!("{origin}, reason: {revert_reason}").into(),
89 self.calldata.to_vec(),
90 ),
91 return_reason: "".into(),
92 revert_reason,
93 addr: self.contract.address,
94 calldata: broken_fn.selector().to_vec().into(),
95 inner_sequence: inner_sequence.to_vec(),
96 shrink_run_limit: self.config.shrink_run_limit,
97 fail_on_revert,
98 assertion_failure,
99 }
100 }
101
102 pub fn decode_revert_reason<FEN: FoundryEvmNetwork>(
105 &self,
106 call_result: &RawCallResult<FEN>,
107 assertion_failure: bool,
108 ) -> String {
109 let revert_reason = RevertDecoder::new()
110 .with_abis(self.targeted_contracts.targets().values().map(|c| &c.abi))
111 .with_abi(self.contract.abi)
112 .decode(call_result.result.as_ref(), call_result.exit_reason);
113 let needs_fallback = matches!(revert_reason.as_str(), "" | EMPTY_REVERT_DATA);
116 if needs_fallback && (!call_result.reverted || assertion_failure) {
117 ASSERTION_FAILED_PREFIX.to_string()
118 } else {
119 revert_reason
120 }
121 }
122}
123
124pub fn handler_edge_fingerprint(
127 pre_merge_edges_hash: Option<B256>,
128 target: Address,
129 selector: Selector,
130) -> B256 {
131 if let Some(hash) = pre_merge_edges_hash {
132 return hash;
133 }
134 let mut buf = [0u8; 24];
135 buf[..20].copy_from_slice(target.as_slice());
136 buf[20..].copy_from_slice(selector.as_slice());
137 keccak256(buf)
138}
139
140#[expect(clippy::too_many_arguments)]
144pub(crate) fn record_handler_assertion_bug<FEN: FoundryEvmNetwork>(
145 invariant_contract: &InvariantContract<'_>,
146 config: &InvariantConfig,
147 targeted_contracts: &FuzzRunIdentifiedContracts,
148 failures: &mut InvariantFailures,
149 inputs: &mut Vec<BasicTxDetails>,
150 handler_target: Address,
151 handler_selector: Selector,
152 pre_merge_edges_hash: Option<B256>,
153 call_result: RawCallResult<FEN>,
154 call_reverted: bool,
155 is_optimization: bool,
156) {
157 let fingerprint =
158 handler_edge_fingerprint(pre_merge_edges_hash, handler_target, handler_selector);
159
160 if !handler_site_already_minimal(
161 &failures.failures,
162 (handler_target, handler_selector),
163 inputs.len(),
164 ) {
165 let revert_reason = InvariantRunCtx {
167 contract: invariant_contract,
168 config,
169 targeted_contracts,
170 calldata: inputs,
171 }
172 .decode_revert_reason(&call_result, true);
173 let call_sequence = inputs.clone();
174 let original_sequence_len = call_sequence.len();
175 failures.record_handler_failure(HandlerAssertionFailure {
176 reverter: handler_target,
177 selector: handler_selector,
178 call_sequence,
179 original_sequence_len,
180 revert_reason,
181 edge_fingerprint: fingerprint,
182 });
183 }
184
185 if call_reverted && !is_optimization && !config.has_delay() {
188 inputs.pop();
189 }
190}
191
192pub fn handler_site_already_minimal(
195 failures: &HashMap<FailureKey, InvariantFuzzError>,
196 site: (Address, Selector),
197 candidate_len: usize,
198) -> bool {
199 failures
200 .get(&FailureKey::Handler(site.0, site.1))
201 .and_then(InvariantFuzzError::as_handler_assertion)
202 .is_some_and(|existing| existing.call_sequence.len() <= candidate_len)
203}
204
205pub fn snapshot_edge_fingerprint<FEN: FoundryEvmNetwork>(
208 call_result: &RawCallResult<FEN>,
209) -> Option<B256> {
210 let edges = call_result.edge_coverage.as_ref()?;
211 if edges.is_empty() {
212 return None;
213 }
214 match edges {
215 EdgeCoverage::Hash(edges) => Some(keccak256(edges)),
216 EdgeCoverage::CollisionFree(hits) => {
217 let mut sorted: Vec<&EdgeCovHit> = hits.iter().collect();
222 sorted.sort_unstable_by_key(|hit| hit.edge);
223
224 let mut bytes = Vec::with_capacity(sorted.len() * (20 + 8 + 32 + 1 + 8 + 1));
226 for hit in sorted {
227 bytes.extend_from_slice(hit.edge.address.as_slice());
228 bytes.extend_from_slice(&hit.edge.pc.to_le_bytes());
229 bytes.extend_from_slice(&hit.edge.jump_dest.to_be_bytes::<32>());
230 bytes.push(u8::from(hit.edge.depth.is_some()));
233 bytes.extend_from_slice(&hit.edge.depth.unwrap_or(0).to_le_bytes());
234 bytes.push(hit.count);
235 }
236 Some(keccak256(bytes))
237 }
238 }
239}
240
241#[derive(Clone, Debug, Eq, Hash, PartialEq)]
244pub enum FailureKey {
245 Invariant(String),
247 Handler(Address, Selector),
250}
251
252#[derive(Clone, Default)]
257pub struct InvariantFailures {
258 pub reverts: usize,
260 pub(crate) failures: HashMap<FailureKey, InvariantFuzzError>,
264 invariant_count: usize,
266 handler_count: usize,
268}
269
270impl InvariantFailures {
271 pub fn new() -> Self {
272 Self::default()
273 }
274
275 pub fn partition(
277 self,
278 ) -> (HashMap<String, InvariantFuzzError>, HashMap<(Address, Selector), InvariantFuzzError>)
279 {
280 let mut invariant_errors = HashMap::new();
281 let mut handler_errors = HashMap::new();
282 for (key, err) in self.failures {
283 match key {
284 FailureKey::Invariant(name) => {
285 invariant_errors.insert(name, err);
286 }
287 FailureKey::Handler(addr, sel) => {
288 handler_errors.insert((addr, sel), err);
289 }
290 }
291 }
292 (invariant_errors, handler_errors)
293 }
294
295 pub fn record_failure(&mut self, invariant: &Function, failure: InvariantFuzzError) {
296 let prev = self.failures.insert(FailureKey::Invariant(invariant.name.clone()), failure);
297 if prev.is_none() {
298 self.invariant_count += 1;
299 }
300 }
301
302 pub fn has_failure(&self, invariant: &Function) -> bool {
303 self.failures.contains_key(&FailureKey::Invariant(invariant.name.clone()))
304 }
305
306 pub fn get_failure(&self, invariant: &Function) -> Option<&InvariantFuzzError> {
307 self.failures.get(&FailureKey::Invariant(invariant.name.clone()))
308 }
309
310 pub fn broken_reason(&self, invariant: &Function) -> String {
313 self.get_failure(invariant).and_then(|e| e.revert_reason()).unwrap_or_default()
314 }
315
316 pub const fn can_continue(&self, invariants: usize) -> bool {
317 self.invariant_count() < invariants
318 }
319
320 pub const fn invariant_count(&self) -> usize {
322 self.invariant_count
323 }
324
325 pub const fn handler_count(&self) -> usize {
327 self.handler_count
328 }
329
330 pub fn handler_failures_mut(&mut self) -> impl Iterator<Item = &mut InvariantFuzzError> {
331 self.failures.iter_mut().filter_map(|(key, error)| match key {
332 FailureKey::Handler(_, _) => Some(error),
333 FailureKey::Invariant(_) => None,
334 })
335 }
336
337 pub fn record_handler_failure(&mut self, failure: HandlerAssertionFailure) {
340 let site = (failure.reverter, failure.selector);
341 if !handler_site_already_minimal(&self.failures, site, failure.call_sequence.len()) {
342 let prev = self.failures.insert(
343 FailureKey::Handler(site.0, site.1),
344 InvariantFuzzError::HandlerAssertion(failure),
345 );
346 if prev.is_none() {
347 self.handler_count += 1;
348 }
349 }
350 }
351
352 pub fn seed_handler_failure(
355 &mut self,
356 target: Address,
357 selector: Selector,
358 err: InvariantFuzzError,
359 ) {
360 let prev = self.failures.insert(FailureKey::Handler(target, selector), err);
361 if prev.is_none() {
362 self.handler_count += 1;
363 }
364 }
365
366 pub fn has_handler_failure(&self, target: Address, selector: Selector) -> bool {
368 self.failures.contains_key(&FailureKey::Handler(target, selector))
369 }
370}
371
372impl fmt::Display for InvariantFailures {
373 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374 writeln!(f)?;
375 writeln!(f, " ❌ Failures: {}", self.invariant_count())?;
376 Ok(())
377 }
378}
379
380#[derive(Clone, Debug)]
381pub enum InvariantFuzzError {
382 Revert(FailedInvariantCaseData),
384 BrokenInvariant(FailedInvariantCaseData),
386 HandlerAssertion(HandlerAssertionFailure),
389 MaxAssumeRejects(u32),
391}
392
393impl InvariantFuzzError {
394 pub fn revert_reason(&self) -> Option<String> {
395 match self {
396 Self::BrokenInvariant(case_data) | Self::Revert(case_data) => {
397 (!case_data.revert_reason.is_empty()).then(|| case_data.revert_reason.clone())
398 }
399 Self::HandlerAssertion(failure) => {
400 (!failure.revert_reason.is_empty()).then(|| failure.revert_reason.clone())
401 }
402 Self::MaxAssumeRejects(allowed) => {
403 Some(format!("`vm.assume` rejected too many inputs ({allowed} allowed)"))
404 }
405 }
406 }
407
408 pub const fn as_handler_assertion(&self) -> Option<&HandlerAssertionFailure> {
410 match self {
411 Self::HandlerAssertion(failure) => Some(failure),
412 _ => None,
413 }
414 }
415
416 pub const fn as_handler_assertion_mut(&mut self) -> Option<&mut HandlerAssertionFailure> {
418 match self {
419 Self::HandlerAssertion(failure) => Some(failure),
420 _ => None,
421 }
422 }
423}
424
425#[derive(Clone, Debug)]
426pub struct FailedInvariantCaseData {
427 pub test_error: TestError<Vec<BasicTxDetails>>,
429 pub return_reason: Reason,
431 pub revert_reason: String,
433 pub addr: Address,
435 pub calldata: Bytes,
437 pub inner_sequence: Vec<Option<BasicTxDetails>>,
439 pub shrink_run_limit: u32,
441 pub fail_on_revert: bool,
443 pub assertion_failure: bool,
445}