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 const fn from_replayed_sequence(
40 call_sequence: Vec<BasicTxDetails>,
41 reverter: Address,
42 selector: Selector,
43 edge_fingerprint: B256,
44 revert_reason: String,
45 ) -> Self {
46 let original_sequence_len = call_sequence.len();
47 Self {
48 reverter,
49 selector,
50 call_sequence,
51 original_sequence_len,
52 revert_reason,
53 edge_fingerprint,
54 }
55 }
56}
57
58pub struct InvariantRunCtx<'a> {
60 pub contract: &'a InvariantContract<'a>,
62 pub config: &'a InvariantConfig,
64 pub targeted_contracts: &'a FuzzRunIdentifiedContracts,
66 pub calldata: &'a [BasicTxDetails],
68}
69
70impl<'a> InvariantRunCtx<'a> {
71 pub fn failed_case<FEN: FoundryEvmNetwork>(
75 &self,
76 broken_fn: &Function,
77 fail_on_revert: bool,
78 assertion_failure: bool,
79 call_result: RawCallResult<FEN>,
80 inner_sequence: &[Option<BasicTxDetails>],
81 ) -> FailedInvariantCaseData {
82 let revert_reason = self.decode_revert_reason(&call_result, assertion_failure);
83 let origin = broken_fn.name.as_str();
84 FailedInvariantCaseData {
85 test_error: TestError::Fail(
86 format!("{origin}, reason: {revert_reason}").into(),
87 self.calldata.to_vec(),
88 ),
89 return_reason: "".into(),
90 revert_reason,
91 addr: self.contract.address,
92 calldata: broken_fn.selector().to_vec().into(),
93 inner_sequence: inner_sequence.to_vec(),
94 shrink_run_limit: self.config.shrink_run_limit,
95 fail_on_revert,
96 assertion_failure,
97 }
98 }
99
100 pub fn decode_revert_reason<FEN: FoundryEvmNetwork>(
103 &self,
104 call_result: &RawCallResult<FEN>,
105 assertion_failure: bool,
106 ) -> String {
107 let revert_reason = RevertDecoder::new()
108 .with_abis(self.targeted_contracts.targets().values().map(|c| &c.abi))
109 .with_abi(self.contract.abi)
110 .decode(call_result.result.as_ref(), call_result.exit_reason);
111 let needs_fallback = matches!(revert_reason.as_str(), "" | EMPTY_REVERT_DATA);
114 if needs_fallback && (!call_result.reverted || assertion_failure) {
115 ASSERTION_FAILED_PREFIX.to_string()
116 } else {
117 revert_reason
118 }
119 }
120}
121
122pub fn handler_edge_fingerprint(
125 pre_merge_edges_hash: Option<B256>,
126 target: Address,
127 selector: Selector,
128) -> B256 {
129 if let Some(hash) = pre_merge_edges_hash {
130 return hash;
131 }
132 let mut buf = [0u8; 24];
133 buf[..20].copy_from_slice(target.as_slice());
134 buf[20..].copy_from_slice(selector.as_slice());
135 keccak256(buf)
136}
137
138#[expect(clippy::too_many_arguments)]
142pub(crate) fn record_handler_assertion_bug<FEN: FoundryEvmNetwork>(
143 invariant_contract: &InvariantContract<'_>,
144 config: &InvariantConfig,
145 targeted_contracts: &FuzzRunIdentifiedContracts,
146 failures: &mut InvariantFailures,
147 inputs: &mut Vec<BasicTxDetails>,
148 handler_target: Address,
149 handler_selector: Selector,
150 pre_merge_edges_hash: Option<B256>,
151 call_result: RawCallResult<FEN>,
152 call_reverted: bool,
153 is_optimization: bool,
154) {
155 let fingerprint =
156 handler_edge_fingerprint(pre_merge_edges_hash, handler_target, handler_selector);
157
158 if !handler_site_already_minimal(
159 &failures.failures,
160 (handler_target, handler_selector),
161 inputs.len(),
162 ) {
163 let revert_reason = InvariantRunCtx {
165 contract: invariant_contract,
166 config,
167 targeted_contracts,
168 calldata: inputs,
169 }
170 .decode_revert_reason(&call_result, true);
171 let call_sequence = inputs.clone();
172 let original_sequence_len = call_sequence.len();
173 failures.record_handler_failure(HandlerAssertionFailure {
174 reverter: handler_target,
175 selector: handler_selector,
176 call_sequence,
177 original_sequence_len,
178 revert_reason,
179 edge_fingerprint: fingerprint,
180 });
181 }
182
183 if call_reverted && !is_optimization && !config.has_delay() {
186 inputs.pop();
187 }
188}
189
190pub fn handler_site_already_minimal(
193 failures: &HashMap<FailureKey, InvariantFuzzError>,
194 site: (Address, Selector),
195 candidate_len: usize,
196) -> bool {
197 failures
198 .get(&FailureKey::Handler(site.0, site.1))
199 .and_then(InvariantFuzzError::as_handler_assertion)
200 .is_some_and(|existing| existing.call_sequence.len() <= candidate_len)
201}
202
203pub fn snapshot_edge_fingerprint<FEN: FoundryEvmNetwork>(
206 call_result: &RawCallResult<FEN>,
207) -> Option<B256> {
208 let edges = call_result.edge_coverage.as_ref()?;
209 if edges.is_empty() {
210 return None;
211 }
212 match edges {
213 EdgeCoverage::Hash(edges) => Some(keccak256(edges)),
214 EdgeCoverage::CollisionFree(hits) => {
215 let mut sorted: Vec<&EdgeCovHit> = hits.iter().collect();
220 sorted.sort_unstable_by_key(|hit| hit.edge);
221
222 let mut bytes = Vec::with_capacity(sorted.len() * (20 + 8 + 32 + 1 + 8 + 1));
224 for hit in sorted {
225 bytes.extend_from_slice(hit.edge.address.as_slice());
226 bytes.extend_from_slice(&hit.edge.pc.to_le_bytes());
227 bytes.extend_from_slice(&hit.edge.jump_dest.to_be_bytes::<32>());
228 bytes.push(u8::from(hit.edge.depth.is_some()));
231 bytes.extend_from_slice(&hit.edge.depth.unwrap_or(0).to_le_bytes());
232 bytes.push(hit.count);
233 }
234 Some(keccak256(bytes))
235 }
236 }
237}
238
239#[derive(Clone, Debug, Eq, Hash, PartialEq)]
242pub enum FailureKey {
243 Invariant(String),
245 Handler(Address, Selector),
248}
249
250#[derive(Clone, Default)]
255pub struct InvariantFailures {
256 pub reverts: usize,
258 pub(crate) failures: HashMap<FailureKey, InvariantFuzzError>,
262 invariant_count: usize,
264 handler_count: usize,
266 revision: 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 self.revision = self.revision.wrapping_add(1);
298 if prev.is_none() {
299 self.invariant_count += 1;
300 }
301 }
302
303 pub fn has_failure(&self, invariant: &Function) -> bool {
304 self.failures.contains_key(&FailureKey::Invariant(invariant.name.clone()))
305 }
306
307 pub fn get_failure(&self, invariant: &Function) -> Option<&InvariantFuzzError> {
308 self.failures.get(&FailureKey::Invariant(invariant.name.clone()))
309 }
310
311 pub fn broken_reason(&self, invariant: &Function) -> String {
314 self.get_failure(invariant).and_then(|e| e.revert_reason()).unwrap_or_default()
315 }
316
317 pub const fn can_continue(&self, invariants: usize) -> bool {
318 self.invariant_count() < invariants
319 }
320
321 pub const fn invariant_count(&self) -> usize {
323 self.invariant_count
324 }
325
326 pub const fn handler_count(&self) -> usize {
328 self.handler_count
329 }
330
331 pub const fn revision(&self) -> usize {
333 self.revision
334 }
335
336 pub fn handler_failures_mut(&mut self) -> impl Iterator<Item = &mut InvariantFuzzError> {
337 self.failures.iter_mut().filter_map(|(key, error)| match key {
338 FailureKey::Handler(_, _) => Some(error),
339 FailureKey::Invariant(_) => None,
340 })
341 }
342
343 pub fn record_handler_failure(&mut self, failure: HandlerAssertionFailure) {
346 let site = (failure.reverter, failure.selector);
347 if !handler_site_already_minimal(&self.failures, site, failure.call_sequence.len()) {
348 let prev = self.failures.insert(
349 FailureKey::Handler(site.0, site.1),
350 InvariantFuzzError::HandlerAssertion(failure),
351 );
352 self.revision = self.revision.wrapping_add(1);
353 if prev.is_none() {
354 self.handler_count += 1;
355 }
356 }
357 }
358
359 pub fn seed_handler_failure(
362 &mut self,
363 target: Address,
364 selector: Selector,
365 err: InvariantFuzzError,
366 ) {
367 let prev = self.failures.insert(FailureKey::Handler(target, selector), err);
368 self.revision = self.revision.wrapping_add(1);
369 if prev.is_none() {
370 self.handler_count += 1;
371 }
372 }
373
374 pub fn has_handler_failure(&self, target: Address, selector: Selector) -> bool {
376 self.failures.contains_key(&FailureKey::Handler(target, selector))
377 }
378}
379
380impl fmt::Display for InvariantFailures {
381 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
382 writeln!(f)?;
383 writeln!(f, " ❌ Failures: {}", self.invariant_count())?;
384 Ok(())
385 }
386}
387
388#[derive(Clone, Debug)]
389pub enum InvariantFuzzError {
390 Revert(FailedInvariantCaseData),
392 BrokenInvariant(FailedInvariantCaseData),
394 HandlerAssertion(HandlerAssertionFailure),
397 MaxAssumeRejects(u32),
399}
400
401impl InvariantFuzzError {
402 pub fn revert_reason(&self) -> Option<String> {
403 match self {
404 Self::BrokenInvariant(case_data) | Self::Revert(case_data) => {
405 (!case_data.revert_reason.is_empty()).then(|| case_data.revert_reason.clone())
406 }
407 Self::HandlerAssertion(failure) => {
408 (!failure.revert_reason.is_empty()).then(|| failure.revert_reason.clone())
409 }
410 Self::MaxAssumeRejects(allowed) => {
411 Some(format!("`vm.assume` rejected too many inputs ({allowed} allowed)"))
412 }
413 }
414 }
415
416 pub const fn as_handler_assertion(&self) -> Option<&HandlerAssertionFailure> {
418 match self {
419 Self::HandlerAssertion(failure) => Some(failure),
420 _ => None,
421 }
422 }
423
424 pub const fn as_handler_assertion_mut(&mut self) -> Option<&mut HandlerAssertionFailure> {
426 match self {
427 Self::HandlerAssertion(failure) => Some(failure),
428 _ => None,
429 }
430 }
431}
432
433#[derive(Clone, Debug)]
434pub struct FailedInvariantCaseData {
435 pub test_error: TestError<Vec<BasicTxDetails>>,
437 pub return_reason: Reason,
439 pub revert_reason: String,
441 pub addr: Address,
443 pub calldata: Bytes,
445 pub inner_sequence: Vec<Option<BasicTxDetails>>,
447 pub shrink_run_limit: u32,
449 pub fail_on_revert: bool,
451 pub assertion_failure: bool,
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use foundry_evm_fuzz::CallDetails;
459
460 fn handler_failure(sequence_len: usize) -> HandlerAssertionFailure {
461 let tx = BasicTxDetails {
462 warp: None,
463 roll: None,
464 sender: Address::ZERO,
465 call_details: CallDetails {
466 target: Address::ZERO,
467 calldata: Bytes::new(),
468 value: None,
469 },
470 };
471 HandlerAssertionFailure::from_replayed_sequence(
472 vec![tx; sequence_len],
473 Address::ZERO,
474 Selector::ZERO,
475 B256::ZERO,
476 "assertion failed".to_string(),
477 )
478 }
479
480 #[test]
481 fn failure_revision_tracks_shorter_handler_reproducer() {
482 let mut failures = InvariantFailures::new();
483 failures.record_handler_failure(handler_failure(2));
484 let checkpoint = failures.revision();
485
486 failures.record_handler_failure(handler_failure(2));
487 assert_eq!(failures.revision(), checkpoint);
488
489 failures.record_handler_failure(handler_failure(1));
490 assert_ne!(failures.revision(), checkpoint);
491 let failure = failures
492 .failures
493 .get(&FailureKey::Handler(Address::ZERO, Selector::ZERO))
494 .and_then(InvariantFuzzError::as_handler_assertion)
495 .unwrap();
496 assert_eq!(failure.call_sequence.len(), 1);
497 }
498}