Skip to main content

evm_fork_cache/
errors.rs

1//! Simulation error types and revert-reason decoding.
2//!
3//! Every EVM revert is either one of the two Solidity built-ins —
4//! `Error(string)` (from `require`/`revert("msg")`) and `Panic(uint256)` (from
5//! overflow, division-by-zero, etc.) — or a contract-defined *custom error*
6//! identified by a 4-byte selector. This module decodes the two built-ins
7//! natively and lets callers register any number of their own custom Solidity
8//! errors with a [`RevertDecoder`].
9//!
10//! Application-specific selectors therefore live in the application, not in this
11//! generic layer: define them with `sol!` and register them once.
12//!
13//! Note that [`Panic(uint256)`](RevertReason::Panic) codes that exceed
14//! `u64::MAX` are dropped to `None` during decoding (and so surface as
15//! [`RevertReason::Unknown`]). This is benign: real compiler-emitted panic
16//! codes are single-byte constants (e.g. `0x11`, `0x32`).
17//!
18//! ```
19//! use alloy_sol_types::{SolError, sol};
20//! use evm_fork_cache::errors::{RevertDecoder, RevertReason};
21//!
22//! sol! {
23//!     #[derive(Debug)]
24//!     error Unauthorized(address caller);
25//! }
26//!
27//! let decoder = RevertDecoder::new().with_error::<Unauthorized>();
28//!
29//! // 4-byte selector of `Unauthorized`, with no parameter bytes.
30//! let raw = alloy_primitives::Bytes::from(Unauthorized::SELECTOR.to_vec());
31//! match decoder.decode(&raw) {
32//!     RevertReason::Custom(err) => assert_eq!(err.name, "Unauthorized(address)"),
33//!     other => panic!("expected a custom error, got {other}"),
34//! }
35//! ```
36
37use std::collections::HashMap;
38use std::fmt;
39use std::path::PathBuf;
40use std::sync::{Arc, OnceLock};
41use std::{borrow::Cow, io};
42
43use alloy_primitives::{Address, B256, Bytes, FixedBytes, U256};
44use alloy_sol_types::SolError;
45use tracing::warn;
46
47/// 4-byte selector of the standard Solidity `Error(string)` revert
48/// (`0x08c379a0`), emitted by `require`/`revert("msg")`.
49pub const ERROR_SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0];
50
51/// 4-byte selector of the standard Solidity `Panic(uint256)` revert
52/// (`0x4e487b71`), emitted on overflow, division-by-zero, etc.
53pub const PANIC_SELECTOR: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71];
54
55/// A decoded contract-defined custom error.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct CustomRevert {
58    /// Human-readable signature, e.g. `"Unauthorized(address)"`.
59    pub name: Cow<'static, str>,
60    /// The error's 4-byte selector (the first 4 bytes of [`data`](Self::data)),
61    /// the `keccak256` prefix of [`name`](Self::name).
62    pub selector: FixedBytes<4>,
63    /// Debug-formatted decoded parameters, when the body decoded successfully.
64    ///
65    /// `None` if only the selector matched but the ABI-encoded parameters could
66    /// not be decoded (e.g. truncated revert data).
67    pub params: Option<String>,
68    /// Raw revert bytes (selector followed by ABI-encoded parameters).
69    pub data: Bytes,
70}
71
72impl fmt::Display for CustomRevert {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match &self.params {
75            Some(params) => write!(f, "{params}"),
76            None => write!(f, "{}", self.name),
77        }
78    }
79}
80
81/// A decoded EVM revert reason.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum RevertReason {
84    /// The call reverted with no return data (e.g. a bare `revert()` or `assert`
85    /// in older Solidity, or an empty `require`).
86    Empty,
87    /// Standard Solidity `Error(string)` revert (e.g. `require(cond, "msg")`).
88    Error(String),
89    /// Standard Solidity `Panic(uint256)` revert (e.g. arithmetic overflow).
90    Panic(u64),
91    /// A contract-defined custom error whose selector was registered on the
92    /// decoder via [`RevertDecoder::with_error`], [`RevertDecoder::register`],
93    /// or [`RevertDecoder::register_raw`].
94    Custom(CustomRevert),
95    /// A selector that matched no built-in or registered custom error.
96    Unknown {
97        /// The 4-byte selector (right-padded with zeros if fewer than 4 bytes
98        /// of revert data were returned).
99        selector: FixedBytes<4>,
100        /// Raw revert bytes.
101        data: Bytes,
102    },
103}
104
105impl fmt::Display for RevertReason {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            RevertReason::Empty => write!(f, "<empty revert>"),
109            RevertReason::Error(msg) => write!(f, "Error({msg:?})"),
110            RevertReason::Panic(code) => write!(f, "Panic({code:#x})"),
111            RevertReason::Custom(custom) => write!(f, "{custom}"),
112            RevertReason::Unknown { selector, data } => {
113                write!(f, "Unknown(selector={selector}, data_len={})", data.len())
114            }
115        }
116    }
117}
118
119type DecodeFn = Arc<dyn Fn(&Bytes) -> Option<String> + Send + Sync>;
120
121#[derive(Clone)]
122struct CustomErrorDecoder {
123    name: Cow<'static, str>,
124    decode: DecodeFn,
125}
126
127/// Error returned when registering a custom error selector that already exists.
128///
129/// A [`RevertDecoder`] keeps the first decoder registered for a selector so a
130/// later registration cannot silently change how existing revert data decodes.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct DuplicateSelectorError {
133    /// The 4-byte selector that was already registered.
134    pub selector: FixedBytes<4>,
135    /// Signature/name of the existing registration that will be kept.
136    pub existing: Cow<'static, str>,
137    /// Signature/name of the attempted duplicate registration.
138    pub attempted: Cow<'static, str>,
139}
140
141impl fmt::Display for DuplicateSelectorError {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        write!(
144            f,
145            "duplicate custom error selector {}: keeping {}, ignoring {}",
146            self.selector, self.existing, self.attempted
147        )
148    }
149}
150
151impl std::error::Error for DuplicateSelectorError {}
152
153/// Decodes raw EVM revert data into a [`RevertReason`].
154///
155/// The two standard Solidity built-ins — `Error(string)` and `Panic(uint256)` —
156/// are always recognized. Register additional contract-defined custom errors
157/// with [`with_error`](RevertDecoder::with_error),
158/// [`register`](RevertDecoder::register), or
159/// [`register_raw`](RevertDecoder::register_raw). Duplicate custom-error
160/// selectors keep the first registration; use
161/// [`try_register`](RevertDecoder::try_register) or
162/// [`try_register_raw`](RevertDecoder::try_register_raw) when collisions should
163/// be handled as errors instead of warnings.
164///
165/// The decoder is cheap to [`Clone`] and is `Send + Sync`, so a configured
166/// decoder can be shared across parallel simulations.
167#[derive(Clone, Default)]
168pub struct RevertDecoder {
169    custom: HashMap<[u8; 4], CustomErrorDecoder>,
170}
171
172impl fmt::Debug for RevertDecoder {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        let mut names: Vec<&str> = self.custom.values().map(|d| d.name.as_ref()).collect();
175        names.sort_unstable();
176        f.debug_struct("RevertDecoder")
177            .field("custom_errors", &names)
178            .finish()
179    }
180}
181
182impl RevertDecoder {
183    /// Create a decoder that recognizes only the standard Solidity built-ins
184    /// (`Error(string)` and `Panic(uint256)`) and no custom errors.
185    ///
186    /// ```
187    /// use evm_fork_cache::errors::RevertDecoder;
188    ///
189    /// let decoder = RevertDecoder::new();
190    /// assert!(decoder.is_empty());
191    /// ```
192    pub fn new() -> Self {
193        Self::default()
194    }
195
196    /// Register a `sol!`-generated custom error type for decoding, consuming and
197    /// returning `self` for builder-style chaining.
198    ///
199    /// If the selector is already registered, the first registration is kept
200    /// and a warning is emitted. Use [`try_register`](Self::try_register) when
201    /// duplicate selectors should fail configuration.
202    ///
203    /// ```
204    /// use alloy_sol_types::sol;
205    /// use evm_fork_cache::errors::RevertDecoder;
206    ///
207    /// sol! {
208    ///     #[derive(Debug)]
209    ///     error SlippageExceeded(uint256 wanted, uint256 got);
210    ///     #[derive(Debug)]
211    ///     error Paused();
212    /// }
213    ///
214    /// let decoder = RevertDecoder::new()
215    ///     .with_error::<SlippageExceeded>()
216    ///     .with_error::<Paused>();
217    /// assert_eq!(decoder.len(), 2);
218    /// ```
219    pub fn with_error<E>(mut self) -> Self
220    where
221        E: SolError + fmt::Debug + 'static,
222    {
223        self.register::<E>();
224        self
225    }
226
227    /// Register a `sol!`-generated custom error type for decoding.
228    ///
229    /// If an error with the same selector is already registered, the first
230    /// registration is kept and a warning is emitted. Use
231    /// [`try_register`](Self::try_register) to surface duplicates as errors.
232    pub fn register<E>(&mut self) -> &mut Self
233    where
234        E: SolError + fmt::Debug + 'static,
235    {
236        if let Err(err) = self.try_register::<E>() {
237            warn_duplicate_selector(&err);
238        }
239        self
240    }
241
242    /// Register a `sol!`-generated custom error type for decoding, returning an
243    /// error when another custom error already owns the same selector.
244    pub fn try_register<E>(&mut self) -> Result<&mut Self, DuplicateSelectorError>
245    where
246        E: SolError + fmt::Debug + 'static,
247    {
248        let decode: DecodeFn =
249            Arc::new(|data: &Bytes| E::abi_decode(data).ok().map(|err| format!("{err:?}")));
250        self.insert_custom_error(
251            E::SELECTOR,
252            CustomErrorDecoder {
253                name: Cow::Borrowed(E::SIGNATURE),
254                decode,
255            },
256        )
257    }
258
259    /// Register a custom error by raw selector, name, and parameter decoder.
260    ///
261    /// Use this when there is no `sol!`-generated type to hand — for example
262    /// when the selector and signature come from an ABI loaded at runtime. The
263    /// `decode` closure receives the full revert bytes (selector included) and
264    /// returns the formatted parameters, or `None` if it cannot decode them.
265    ///
266    /// If the closure returns `None`, the selector still matches: the decode
267    /// yields a [`RevertReason::Custom`] whose
268    /// [`params`](CustomRevert::params) is `None`. If an error with the same
269    /// selector is already registered, the first registration is kept and a
270    /// warning is emitted. Use [`try_register_raw`](Self::try_register_raw) to
271    /// surface duplicates as errors.
272    ///
273    /// ```
274    /// use alloy_primitives::Bytes;
275    /// use evm_fork_cache::errors::{RevertDecoder, RevertReason};
276    ///
277    /// let mut decoder = RevertDecoder::new();
278    /// // A closure that decodes the parameters when there is a payload byte,
279    /// // and otherwise reports a decode failure by returning `None`.
280    /// decoder.register_raw([0xde, 0xad, 0xbe, 0xef], "MyError(uint256)", |data| {
281    ///     (data.len() > 4).then(|| format!("payload {} bytes", data.len() - 4))
282    /// });
283    ///
284    /// // Selector plus a payload byte: the closure decodes the parameters.
285    /// let with_params = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00]);
286    /// match decoder.decode(&with_params) {
287    ///     RevertReason::Custom(custom) => {
288    ///         assert_eq!(custom.name, "MyError(uint256)");
289    ///         assert_eq!(custom.params.as_deref(), Some("payload 1 bytes"));
290    ///     }
291    ///     other => panic!("expected Custom, got {other}"),
292    /// }
293    ///
294    /// // Bare selector: the closure returns `None`, but the selector still
295    /// // matches, so the result is a `Custom` with `params == None`.
296    /// let bare = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]);
297    /// match decoder.decode(&bare) {
298    ///     RevertReason::Custom(custom) => assert!(custom.params.is_none()),
299    ///     other => panic!("expected Custom, got {other}"),
300    /// }
301    /// ```
302    pub fn register_raw(
303        &mut self,
304        selector: [u8; 4],
305        name: impl Into<Cow<'static, str>>,
306        decode: impl Fn(&Bytes) -> Option<String> + Send + Sync + 'static,
307    ) -> &mut Self {
308        if let Err(err) = self.try_register_raw(selector, name, decode) {
309            warn_duplicate_selector(&err);
310        }
311        self
312    }
313
314    /// Register a custom error by raw selector, name, and parameter decoder,
315    /// returning an error when another custom error already owns the selector.
316    ///
317    /// The `decode` closure receives the full revert bytes (selector included)
318    /// and returns formatted parameters, or `None` if the selector matched but
319    /// the parameter payload could not be decoded.
320    pub fn try_register_raw(
321        &mut self,
322        selector: [u8; 4],
323        name: impl Into<Cow<'static, str>>,
324        decode: impl Fn(&Bytes) -> Option<String> + Send + Sync + 'static,
325    ) -> Result<&mut Self, DuplicateSelectorError> {
326        self.insert_custom_error(
327            selector,
328            CustomErrorDecoder {
329                name: name.into(),
330                decode: Arc::new(decode),
331            },
332        )
333    }
334
335    /// Number of registered custom errors. The two Solidity built-ins are
336    /// always recognized and are not counted, so a freshly
337    /// [`new`](RevertDecoder::new) decoder reports `0`.
338    pub fn len(&self) -> usize {
339        self.custom.len()
340    }
341
342    /// Returns `true` if no custom errors are registered. The built-ins are
343    /// always recognized regardless, so this is `true` for a freshly
344    /// [`new`](RevertDecoder::new) decoder.
345    pub fn is_empty(&self) -> bool {
346        self.custom.is_empty()
347    }
348
349    /// Decode raw EVM revert data into a [`RevertReason`].
350    ///
351    /// Resolution order: the two Solidity built-ins (`Error(string)` and
352    /// `Panic(uint256)`), then registered custom errors by selector, then
353    /// [`RevertReason::Unknown`] for anything else. Empty input decodes to
354    /// [`RevertReason::Empty`], and data shorter than 4 bytes decodes to
355    /// [`RevertReason::Unknown`] with the selector right-padded with zeros.
356    ///
357    /// ```
358    /// use alloy_primitives::{Bytes, U256};
359    /// use alloy_sol_types::{Panic, SolError, sol};
360    /// use evm_fork_cache::errors::{RevertDecoder, RevertReason, ERROR_SELECTOR};
361    ///
362    /// sol! {
363    ///     #[derive(Debug)]
364    ///     error Custom();
365    /// }
366    ///
367    /// let decoder = RevertDecoder::new().with_error::<Custom>();
368    ///
369    /// // Built-in `Error(string)` decodes natively, without registration.
370    /// // Layout: selector | offset(0x20) | length | utf8 bytes (padded).
371    /// let mut bytes = ERROR_SELECTOR.to_vec();
372    /// bytes.extend_from_slice(&{ let mut o = [0u8; 32]; o[31] = 0x20; o }); // offset
373    /// bytes.extend_from_slice(&{ let mut l = [0u8; 32]; l[31] = 2; l });    // length 2
374    /// bytes.extend_from_slice(b"hi");
375    /// bytes.extend_from_slice(&[0u8; 30]);                                  // pad to 32
376    /// assert_eq!(decoder.decode(&Bytes::from(bytes)), RevertReason::Error("hi".into()));
377    ///
378    /// // Built-in `Panic(uint256)` decodes natively too.
379    /// let panic = Bytes::from(Panic { code: U256::from(0x11) }.abi_encode());
380    /// assert_eq!(decoder.decode(&panic), RevertReason::Panic(0x11));
381    ///
382    /// // A registered selector resolves to `Custom`.
383    /// let raw = Bytes::from(Custom::SELECTOR.to_vec());
384    /// match decoder.decode(&raw) {
385    ///     RevertReason::Custom(err) => assert_eq!(err.name, "Custom()"),
386    ///     other => panic!("expected Custom, got {other}"),
387    /// }
388    ///
389    /// // An unregistered selector falls through to `Unknown`.
390    /// let unknown = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]);
391    /// assert!(matches!(decoder.decode(&unknown), RevertReason::Unknown { .. }));
392    /// ```
393    pub fn decode(&self, data: &Bytes) -> RevertReason {
394        if data.is_empty() {
395            return RevertReason::Empty;
396        }
397        if data.len() < 4 {
398            // Too short for a selector; surface the raw bytes as Unknown with a
399            // right-padded selector so nothing is silently discarded.
400            let mut selector = [0u8; 4];
401            selector[..data.len()].copy_from_slice(&data[..]);
402            return RevertReason::Unknown {
403                selector: FixedBytes::from(selector),
404                data: data.clone(),
405            };
406        }
407
408        let selector: [u8; 4] = data[..4].try_into().expect("length checked >= 4");
409
410        if selector == ERROR_SELECTOR
411            && let Some(message) = decode_solidity_error_string(data)
412        {
413            return RevertReason::Error(message);
414        }
415        if selector == PANIC_SELECTOR
416            && let Some(code) = decode_solidity_panic(data)
417        {
418            return RevertReason::Panic(code);
419        }
420        if let Some(entry) = self.custom.get(&selector) {
421            return RevertReason::Custom(CustomRevert {
422                name: entry.name.clone(),
423                selector: FixedBytes::from(selector),
424                params: (entry.decode)(data),
425                data: data.clone(),
426            });
427        }
428
429        RevertReason::Unknown {
430            selector: FixedBytes::from(selector),
431            data: data.clone(),
432        }
433    }
434
435    fn insert_custom_error(
436        &mut self,
437        selector: [u8; 4],
438        decoder: CustomErrorDecoder,
439    ) -> Result<&mut Self, DuplicateSelectorError> {
440        if let Some(existing) = self.custom.get(&selector) {
441            return Err(DuplicateSelectorError {
442                selector: FixedBytes::from(selector),
443                existing: existing.name.clone(),
444                attempted: decoder.name,
445            });
446        }
447
448        self.custom.insert(selector, decoder);
449        Ok(self)
450    }
451}
452
453fn warn_duplicate_selector(err: &DuplicateSelectorError) {
454    warn!(
455        selector = %err.selector,
456        existing = err.existing.as_ref(),
457        attempted = err.attempted.as_ref(),
458        "duplicate custom error selector registration ignored; keeping first registration"
459    );
460}
461
462/// Decode revert data using only the standard Solidity built-ins.
463///
464/// For application-specific custom errors, build a [`RevertDecoder`] and call
465/// [`RevertDecoder::decode`].
466pub fn decode_revert_reason(data: &Bytes) -> RevertReason {
467    static STANDARD: OnceLock<RevertDecoder> = OnceLock::new();
468    STANDARD.get_or_init(RevertDecoder::new).decode(data)
469}
470
471/// Decode the `uint256` payload of a standard `Panic(uint256)` revert.
472///
473/// Delegates to alloy's built-in decoder (which validates the ABI encoding) and
474/// returns `None` for codes that do not fit in a `u64`. Real compiler-emitted
475/// panic codes are single-byte constants (e.g. `0x11` for arithmetic overflow,
476/// `0x32` for out-of-bounds array access).
477fn decode_solidity_panic(data: &Bytes) -> Option<u64> {
478    alloy_sol_types::Panic::abi_decode(data)
479        .ok()
480        .and_then(|panic| u64::try_from(panic.code).ok())
481}
482
483/// Decode the string payload of a standard `Error(string)` revert.
484///
485/// Delegates to alloy's built-in decoder, which follows the ABI offset and
486/// validates the length rather than assuming a fixed in-memory layout — so it
487/// stays correct on non-standard or adversarial revert data.
488fn decode_solidity_error_string(data: &Bytes) -> Option<String> {
489    alloy_sol_types::Revert::abi_decode(data)
490        .ok()
491        .map(|revert| revert.reason)
492}
493
494/// A structured simulation revert with its decoded reason.
495#[derive(Debug, Clone)]
496pub struct SimulationError {
497    /// Gas consumed before the revert.
498    pub gas_used: u64,
499    /// Raw revert data returned by the EVM (the bytes that were decoded into
500    /// [`reason`](Self::reason)).
501    pub revert_data: Bytes,
502    /// The revert reason decoded from [`revert_data`](Self::revert_data).
503    pub reason: RevertReason,
504}
505
506impl SimulationError {
507    /// Create a simulation error from raw revert data, decoding with the
508    /// standard Solidity built-ins only.
509    pub fn from_revert(gas_used: u64, output: Bytes) -> Self {
510        let reason = decode_revert_reason(&output);
511        Self {
512            gas_used,
513            revert_data: output,
514            reason,
515        }
516    }
517
518    /// Create a simulation error from raw revert data, decoding custom errors
519    /// with the supplied [`RevertDecoder`].
520    pub fn from_revert_with(gas_used: u64, output: Bytes, decoder: &RevertDecoder) -> Self {
521        let reason = decoder.decode(&output);
522        Self {
523            gas_used,
524            revert_data: output,
525            reason,
526        }
527    }
528
529    /// The decoded revert reason. Equivalent to borrowing the public
530    /// [`reason`](Self::reason) field.
531    pub fn reason(&self) -> &RevertReason {
532        &self.reason
533    }
534
535    /// The `Error(string)` message, if this was a standard string revert.
536    pub fn revert_message(&self) -> Option<&str> {
537        match &self.reason {
538            RevertReason::Error(message) => Some(message.as_str()),
539            _ => None,
540        }
541    }
542
543    /// The panic code, if this was a standard `Panic(uint256)` revert.
544    pub fn panic_code(&self) -> Option<u64> {
545        match self.reason {
546            RevertReason::Panic(code) => Some(code),
547            _ => None,
548        }
549    }
550
551    /// The decoded custom error, if a registered custom error matched.
552    pub fn custom_error(&self) -> Option<&CustomRevert> {
553        match &self.reason {
554            RevertReason::Custom(custom) => Some(custom),
555            _ => None,
556        }
557    }
558
559    /// The 4-byte selector of the revert, if any (custom or unknown).
560    pub fn selector(&self) -> Option<FixedBytes<4>> {
561        match &self.reason {
562            RevertReason::Custom(custom) => Some(custom.selector),
563            RevertReason::Unknown { selector, .. } => Some(*selector),
564            _ => None,
565        }
566    }
567
568    /// `true` if the call reverted with no return data, i.e. the reason is
569    /// [`RevertReason::Empty`].
570    pub fn is_empty_revert(&self) -> bool {
571        matches!(self.reason, RevertReason::Empty)
572    }
573}
574
575impl fmt::Display for SimulationError {
576    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577        write!(
578            f,
579            "SimulationError(gas_used={}, reason={})",
580            self.gas_used, self.reason
581        )
582    }
583}
584
585impl std::error::Error for SimulationError {}
586
587/// Error raised when validating or refreshing the block-execution context
588/// (`NUMBER` / `BASEFEE` / `COINBASE` / `PREVRANDAO` / `GASLIMIT` opcodes).
589///
590/// Under strict [`BlockContextRequirements`](crate::cache::BlockContextRequirements)
591/// a missing header field surfaces loudly as one of these variants instead of
592/// silently defaulting the corresponding EVM block-env field.
593#[derive(Debug, thiserror::Error)]
594pub enum BlockContextError {
595    /// The block header could not be fetched (the provider errored or returned
596    /// no block) while strict requirements demanded one at construction.
597    #[error("block-context header fetch failed: {0}")]
598    FetchFailed(String),
599    /// A header was available but a required block-context field was absent.
600    ///
601    /// `field` is the lowercased field token (e.g. `"basefee"`, `"prevrandao"`).
602    #[error("required block-context field missing: {field}")]
603    MissingField {
604        /// The lowercased name of the missing field.
605        field: &'static str,
606    },
607}
608
609/// Error returned when crate-managed synchronous RPC bridges cannot enter the
610/// required tokio runtime context.
611#[derive(Debug, Clone, thiserror::Error)]
612pub enum RuntimeError {
613    /// A current-thread runtime was active, but the crate needs
614    /// `tokio::task::block_in_place`, which is available only on multi-thread
615    /// runtimes.
616    #[error(
617        "evm-fork-cache RPC operations require a multi-thread tokio runtime; \
618         found a current-thread runtime (block_in_place is not supported there). \
619         Build the runtime with `tokio::runtime::Builder::new_multi_thread()` \
620         or annotate with `#[tokio::main(flavor = \"multi_thread\")]`"
621    )]
622    CurrentThreadRuntime,
623    /// No usable runtime handle was available.
624    #[error(
625        "evm-fork-cache RPC operations require a running multi-thread tokio runtime: {details}"
626    )]
627    MissingRuntime {
628        /// The runtime lookup error rendered by tokio.
629        details: String,
630    },
631}
632
633/// Error returned by direct RPC call callbacks installed on [`EvmCache`].
634///
635/// [`EvmCache`]: crate::cache::EvmCache
636#[derive(Debug, thiserror::Error)]
637pub enum RpcError {
638    /// Runtime precondition failure before the RPC call could be made.
639    #[error(transparent)]
640    Runtime(#[from] RuntimeError),
641    /// The provider rejected or failed the RPC request.
642    #[error("RPC provider call {operation} failed: {details}")]
643    Provider {
644        /// JSON-RPC method or high-level operation name.
645        operation: &'static str,
646        /// Provider/transport error text. Alloy's provider error type is generic
647        /// over the transport, so this boundary preserves it as display text.
648        details: String,
649    },
650    /// Caller-provided callback failure.
651    #[error("custom RPC callback failed: {message}")]
652    Custom {
653        /// Caller-provided error text.
654        message: String,
655    },
656}
657
658impl RpcError {
659    /// Build a provider-failure error from any displayable provider error.
660    pub fn provider(operation: &'static str, source: impl fmt::Display) -> Self {
661        Self::Provider {
662            operation,
663            details: source.to_string(),
664        }
665    }
666
667    /// Build a custom-callback error.
668    pub fn custom(message: impl Into<String>) -> Self {
669        Self::Custom {
670            message: message.into(),
671        }
672    }
673}
674
675/// Error returned by storage/proof batch callbacks.
676///
677/// `Clone` lets a batch-level failure (one `eth_call` or one JSON-RPC batch)
678/// be reported once per affected slot without re-stringifying the source.
679#[derive(Debug, Clone, thiserror::Error)]
680pub enum StorageFetchError {
681    /// Runtime precondition failure before a fetch could be made.
682    #[error(transparent)]
683    Runtime(#[from] RuntimeError),
684    /// The provider rejected or failed an RPC request.
685    #[error("storage/provider request {operation} failed: {details}")]
686    Provider {
687        /// JSON-RPC method or high-level operation name.
688        operation: &'static str,
689        /// Provider/transport error text. Alloy's provider error type is generic
690        /// over the transport, so this boundary preserves it as display text.
691        details: String,
692    },
693    /// JSON-RPC batch serialization failed before sending.
694    #[error("failed to serialize storage batch request: {details}")]
695    Serialization {
696        /// Serialization error text.
697        details: String,
698    },
699    /// Sending the JSON-RPC batch failed before per-call waiters could resolve.
700    #[error("failed to send storage batch request: {details}")]
701    BatchSend {
702        /// Send error text.
703        details: String,
704    },
705    /// Caller-provided callback failure.
706    #[error("custom storage/proof fetcher failed: {message}")]
707    Custom {
708        /// Caller-provided error text.
709        message: String,
710    },
711}
712
713impl StorageFetchError {
714    /// Build a provider-failure error from any displayable provider error.
715    pub fn provider(operation: &'static str, source: impl fmt::Display) -> Self {
716        Self::Provider {
717            operation,
718            details: source.to_string(),
719        }
720    }
721
722    /// Build a batch-send error.
723    pub fn batch_send(source: impl fmt::Display) -> Self {
724        Self::BatchSend {
725            details: source.to_string(),
726        }
727    }
728
729    /// Build a serialization error.
730    pub fn serialization(source: impl fmt::Display) -> Self {
731        Self::Serialization {
732            details: source.to_string(),
733        }
734    }
735
736    /// Build a custom-callback error.
737    pub fn custom(message: impl Into<String>) -> Self {
738        Self::Custom {
739            message: message.into(),
740        }
741    }
742}
743
744/// Result type returned by storage/proof batch callbacks.
745pub type StorageFetchResult<T> = Result<T, StorageFetchError>;
746
747/// Persistence failure for crate-owned on-disk cache files.
748#[derive(Debug, thiserror::Error)]
749pub enum PersistenceError {
750    /// bincode serialization failed.
751    #[error("failed to serialize {label}: {source}")]
752    Serialize {
753        /// Human-readable cache payload label.
754        label: &'static str,
755        /// bincode serialization failure.
756        #[source]
757        source: bincode::Error,
758    },
759    /// A parent directory could not be created.
760    #[error("failed to create directory {path:?}: {source}")]
761    CreateDir {
762        /// Directory path.
763        path: PathBuf,
764        /// Filesystem error.
765        #[source]
766        source: io::Error,
767    },
768    /// A file write failed.
769    #[error("failed to write {path:?}: {source}")]
770    Write {
771        /// File path.
772        path: PathBuf,
773        /// Filesystem error.
774        #[source]
775        source: io::Error,
776    },
777}
778
779impl PersistenceError {
780    /// Build a bincode serialization error.
781    pub(crate) fn serialize(label: &'static str, source: bincode::Error) -> Self {
782        Self::Serialize { label, source }
783    }
784
785    /// Build a directory creation error.
786    pub(crate) fn create_dir(path: impl Into<PathBuf>, source: io::Error) -> Self {
787        Self::CreateDir {
788            path: path.into(),
789            source,
790        }
791    }
792
793    /// Build a file write error.
794    pub(crate) fn write(path: impl Into<PathBuf>, source: io::Error) -> Self {
795        Self::Write {
796            path: path.into(),
797            source,
798        }
799    }
800}
801
802/// General cache-operation error for APIs that execute or mutate local fork
803/// state but do not classify EVM reverts as [`SimError`].
804#[derive(Debug, thiserror::Error)]
805pub enum CacheError {
806    /// Runtime precondition failure.
807    #[error(transparent)]
808    Runtime(#[from] RuntimeError),
809    /// Direct RPC callback failure.
810    #[error(transparent)]
811    Rpc(#[from] RpcError),
812    /// On-disk persistence failure.
813    #[error(transparent)]
814    Persistence(#[from] PersistenceError),
815    /// A storage verification/reconciliation path had no batch fetcher.
816    #[error("storage verification requires a storage batch fetcher")]
817    MissingStorageBatchFetcher,
818    /// Code-seed verification had pending seeds but no account-fields fetcher.
819    #[error("code-seed verification requires an account fields fetcher")]
820    MissingAccountFieldsFetcher,
821    /// Reconciliation could not fetch any requested slot, so it cannot prove the
822    /// local state fresh.
823    #[error(
824        "reconcile could not fetch any of the {requested} requested slot(s) \
825         (no usable storage fetcher / provider unreachable)"
826    )]
827    ReconcileFetchFailed {
828        /// Number of requested slots.
829        requested: usize,
830    },
831    /// Account fetch failed.
832    #[error("failed to fetch account {address}: {details}")]
833    AccountFetch {
834        /// Account address.
835        address: Address,
836        /// Backend/provider error text.
837        details: String,
838    },
839    /// A canonical code seed contradicts code already fetched from the chain.
840    ///
841    /// Chain-fetched state is authoritative over templates: the seed is
842    /// rejected and the cached code is left untouched. If the caller believes
843    /// the chain has moved (e.g. a redeploy), purge the account first and
844    /// re-seed.
845    #[error(
846        "code seed for {address} conflicts with chain-fetched code \
847         (cached hash {cached}, seeded hash {seeded})"
848    )]
849    CodeSeedConflict {
850        /// Account address.
851        address: Address,
852        /// Code hash of the RPC-origin code already in the cache.
853        cached: B256,
854        /// Code hash of the rejected seed.
855        seeded: B256,
856    },
857    /// A code seed/etch was given empty bytes; claiming an address is
858    /// code-less is not a supported seed (that is what verification's
859    /// `codeless` classification reports).
860    #[error("cannot seed/etch empty code at {address}")]
861    CodeSeedEmpty {
862        /// Account address.
863        address: Address,
864    },
865    /// Storage read failed.
866    #[error("storage read failed for {address} slot {slot}: {details}")]
867    StorageRead {
868        /// Contract address.
869        address: Address,
870        /// Storage slot.
871        slot: U256,
872        /// Backend/provider error text.
873        details: String,
874    },
875    /// Storage insert failed.
876    #[error("storage insert failed for {address} slot {slot}: {details}")]
877    StorageInsert {
878        /// Contract address.
879        address: Address,
880        /// Storage slot.
881        slot: U256,
882        /// Backend error text.
883        details: String,
884    },
885    /// Transaction environment construction failed.
886    #[error("failed to build transaction environment: {details}")]
887    TxEnv {
888        /// Builder error text.
889        details: String,
890    },
891    /// revm returned a host/database transaction error.
892    #[error("failed to transact: {details}")]
893    Transact {
894        /// revm/database error text.
895        details: String,
896    },
897    /// A helper that requires a successful EVM call observed a revert or halt.
898    #[error("EVM call did not succeed: {result}")]
899    CallNotSuccessful {
900        /// Debug rendering of the execution result.
901        result: String,
902    },
903    /// A debug/trace RPC response could not be parsed into the cache's typed
904    /// block state-diff representation.
905    #[error("failed to parse block state trace: {details}")]
906    TraceParse {
907        /// Parser error text.
908        details: String,
909    },
910    /// ABI or helper-specific decode failure.
911    #[error("failed to decode {what}: {details}")]
912    Decode {
913        /// Human-readable decode target.
914        what: &'static str,
915        /// Decoder error text.
916        details: String,
917    },
918    /// A typed Solidity call executed but did not succeed.
919    #[error("Solidity call {signature} from {from:?} to {to:?} did not succeed: {result}")]
920    SolCallFailed {
921        /// Solidity function signature.
922        signature: &'static str,
923        /// Call sender.
924        from: Address,
925        /// Call target.
926        to: Address,
927        /// Debug rendering of the execution result.
928        result: String,
929    },
930    /// A typed Solidity call returned malformed data.
931    #[error(
932        "failed to decode Solidity call {signature} return data from {from:?} to {to:?}: \
933         output_len={output_len}, error: {details}"
934    )]
935    SolCallDecode {
936        /// Solidity function signature.
937        signature: &'static str,
938        /// Call sender.
939        from: Address,
940        /// Call target.
941        to: Address,
942        /// Return-data length in bytes.
943        output_len: usize,
944        /// Decoder error text.
945        details: String,
946    },
947    /// Deployment succeeded without a created address.
948    #[error("contract deployment succeeded but no address returned")]
949    DeploymentMissingAddress,
950    /// Deployment reverted.
951    #[error("contract deployment reverted: 0x{output_hex}")]
952    DeploymentReverted {
953        /// Hex-encoded revert output.
954        output_hex: String,
955    },
956    /// Deployment halted.
957    #[error("contract deployment halted: {reason}")]
958    DeploymentHalted {
959        /// Debug rendering of the halt reason.
960        reason: String,
961    },
962    /// Source account did not contain bytecode for an override.
963    #[error("no bytecode found at source address {source_address}")]
964    MissingSourceBytecode {
965        /// Source address.
966        source_address: Address,
967    },
968    /// Runtime bytecode was required but absent or empty.
969    #[error("{role} account {address} has no runtime bytecode")]
970    MissingRuntimeCode {
971        /// Account role in the operation.
972        role: &'static str,
973        /// Account address.
974        address: Address,
975    },
976    /// Target account was required but absent.
977    #[error(
978        "target account {target} not found; use override_or_create_account_code for synthetic targets"
979    )]
980    MissingTargetAccount {
981        /// Target address.
982        target: Address,
983    },
984    /// Target account fetch failed while validating an override target.
985    #[error("failed to fetch target account {target}: {details}")]
986    TargetAccountFetch {
987        /// Target address.
988        target: Address,
989        /// Backend/provider error text.
990        details: String,
991    },
992}
993
994impl CacheError {
995    /// Convert a transaction-builder error into [`CacheError::TxEnv`].
996    pub fn tx_env(source: impl fmt::Debug) -> Self {
997        Self::TxEnv {
998            details: format!("{source:?}"),
999        }
1000    }
1001
1002    /// Convert a revm host/database transaction error into
1003    /// [`CacheError::Transact`].
1004    pub fn transact(source: impl fmt::Debug) -> Self {
1005        Self::Transact {
1006            details: format!("{source:?}"),
1007        }
1008    }
1009}
1010
1011/// Result type returned by cache APIs.
1012pub type CacheResult<T, E = CacheError> = Result<T, E>;
1013
1014/// Error for immutable snapshot-overlay execution helpers that do not classify
1015/// reverts as [`SimError`].
1016#[derive(Debug, thiserror::Error)]
1017pub enum OverlayError {
1018    /// Transaction environment construction failed.
1019    #[error("failed to build transaction environment: {details}")]
1020    TxEnv {
1021        /// Builder error text.
1022        details: String,
1023    },
1024    /// revm returned a host/database transaction error.
1025    #[error("failed to transact: {details}")]
1026    Transact {
1027        /// revm/database error text.
1028        details: String,
1029    },
1030    /// A typed Solidity call ([`EvmOverlay::call_sol`](crate::cache::EvmOverlay::call_sol))
1031    /// did not `Success` (it reverted or halted).
1032    #[error("Solidity call {signature} from {from:?} to {to:?} did not succeed: {result}")]
1033    SolCallFailed {
1034        /// The call's Solidity signature.
1035        signature: &'static str,
1036        /// Caller address.
1037        from: Address,
1038        /// Callee address.
1039        to: Address,
1040        /// Debug rendering of the non-`Success` result.
1041        result: String,
1042    },
1043    /// A typed Solidity call succeeded but its return data could not be decoded.
1044    #[error(
1045        "failed to decode return of {signature} from {from:?} to {to:?} ({output_len} bytes): {details}"
1046    )]
1047    SolCallDecode {
1048        /// The call's Solidity signature.
1049        signature: &'static str,
1050        /// Caller address.
1051        from: Address,
1052        /// Callee address.
1053        to: Address,
1054        /// Length of the undecodable return data.
1055        output_len: usize,
1056        /// Decoder error text.
1057        details: String,
1058    },
1059}
1060
1061impl OverlayError {
1062    /// Convert a transaction-builder error into [`OverlayError::TxEnv`].
1063    pub fn tx_env(source: impl fmt::Debug) -> Self {
1064        Self::TxEnv {
1065            details: format!("{source:?}"),
1066        }
1067    }
1068
1069    /// Convert a revm host/database transaction error into
1070    /// [`OverlayError::Transact`].
1071    pub fn transact(source: impl fmt::Debug) -> Self {
1072        Self::Transact {
1073            details: format!("{source:?}"),
1074        }
1075    }
1076}
1077
1078/// Result type returned by overlay APIs that return raw [`ExecutionResult`]
1079/// values instead of classifying reverts.
1080///
1081/// [`ExecutionResult`]: revm::context::result::ExecutionResult
1082pub type OverlayResult<T, E = OverlayError> = Result<T, E>;
1083
1084/// Host-side failure for simulation entry points.
1085#[derive(Debug, thiserror::Error)]
1086pub enum SimHostError {
1087    /// Transaction environment construction failed.
1088    #[error("failed to build transaction environment: {details}")]
1089    TxEnv {
1090        /// Builder error text.
1091        details: String,
1092    },
1093    /// revm returned a host/database transaction error.
1094    #[error("failed to transact: {details}")]
1095    Transact {
1096        /// revm/database error text.
1097        details: String,
1098    },
1099    /// Database read failed outside a transaction execution.
1100    #[error("database operation failed: {details}")]
1101    Database {
1102        /// Database error text.
1103        details: String,
1104    },
1105    /// Cache helper failed.
1106    #[error(transparent)]
1107    Cache(#[from] CacheError),
1108    /// Overlay helper failed.
1109    #[error(transparent)]
1110    Overlay(#[from] OverlayError),
1111}
1112
1113impl SimHostError {
1114    /// Convert a transaction-builder error into [`SimHostError::TxEnv`].
1115    pub fn tx_env(source: impl fmt::Debug) -> Self {
1116        Self::TxEnv {
1117            details: format!("{source:?}"),
1118        }
1119    }
1120
1121    /// Convert a revm host/database transaction error into
1122    /// [`SimHostError::Transact`].
1123    pub fn transact(source: impl fmt::Debug) -> Self {
1124        Self::Transact {
1125            details: format!("{source:?}"),
1126        }
1127    }
1128
1129    /// Convert a database error into [`SimHostError::Database`].
1130    pub fn database(source: impl fmt::Debug) -> Self {
1131        Self::Database {
1132            details: format!("{source:?}"),
1133        }
1134    }
1135}
1136
1137/// Multicall3 helper failure.
1138#[derive(Debug, thiserror::Error)]
1139pub enum MulticallError {
1140    /// Underlying EVM cache call failed.
1141    #[error(transparent)]
1142    Cache(#[from] CacheError),
1143    /// The aggregate3 call reverted or halted.
1144    #[error("Multicall3 aggregate call failed: {result}")]
1145    AggregateFailed {
1146        /// Debug rendering of the execution result.
1147        result: String,
1148    },
1149    /// A per-call result marked `success = false`.
1150    #[error("multicall result indicates the call failed")]
1151    CallFailed,
1152    /// ABI decode failure.
1153    #[error("failed to decode multicall result: {details}")]
1154    Decode {
1155        /// Decoder error text.
1156        details: String,
1157    },
1158}
1159
1160/// Result type returned by Multicall3 helpers.
1161pub type MulticallResult<T> = Result<T, MulticallError>;
1162
1163/// Deployment and Foundry-artifact loading failure.
1164#[derive(Debug, thiserror::Error)]
1165pub enum DeployError {
1166    /// Artifact file could not be read.
1167    #[error("failed to read Foundry artifact at {path}: {source}")]
1168    ReadArtifact {
1169        /// Artifact path.
1170        path: PathBuf,
1171        /// Filesystem error.
1172        #[source]
1173        source: io::Error,
1174    },
1175    /// Artifact JSON was invalid.
1176    #[error("failed to parse Foundry artifact JSON at {path}: {source}")]
1177    ParseArtifact {
1178        /// Artifact path.
1179        path: PathBuf,
1180        /// JSON parse error.
1181        #[source]
1182        source: serde_json::Error,
1183    },
1184    /// Artifact had no bytecode field.
1185    #[error("artifact {path} has no `bytecode` field")]
1186    MissingBytecodeField {
1187        /// Artifact path.
1188        path: PathBuf,
1189    },
1190    /// Artifact bytecode was not a supported string shape.
1191    #[error("artifact {path} has no `bytecode.object` string")]
1192    MissingBytecodeObject {
1193        /// Artifact path.
1194        path: PathBuf,
1195    },
1196    /// Bytecode was empty.
1197    #[error("empty bytecode")]
1198    EmptyBytecode,
1199    /// Bytecode still contains unresolved Foundry library placeholders.
1200    #[error("bytecode contains unresolved library placeholders")]
1201    UnresolvedLibraryPlaceholders,
1202    /// Hex bytecode could not be decoded.
1203    #[error("invalid hex bytecode: {details}")]
1204    InvalidHex {
1205        /// Hex decoder error text.
1206        details: String,
1207    },
1208    /// Underlying cache operation failed.
1209    #[error(transparent)]
1210    Cache(#[from] CacheError),
1211    /// Artifact deployment failed, with path context.
1212    #[error("deploying Foundry artifact {path} failed: {source}")]
1213    ArtifactDeploy {
1214        /// Artifact path.
1215        path: PathBuf,
1216        /// Cache operation failure.
1217        #[source]
1218        source: CacheError,
1219    },
1220    /// Target validation failed before etching.
1221    #[error("validating target contract {target} failed: {source}")]
1222    TargetValidation {
1223        /// Target address.
1224        target: Address,
1225        /// Cache operation failure.
1226        #[source]
1227        source: CacheError,
1228    },
1229    /// Runtime bytecode etch failed.
1230    #[error("etching runtime bytecode at {target} failed: {source}")]
1231    EtchRuntime {
1232        /// Target address.
1233        target: Address,
1234        /// Cache operation failure.
1235        #[source]
1236        source: CacheError,
1237    },
1238}
1239
1240/// Result type returned by deployment helpers.
1241pub type DeployResult<T> = Result<T, DeployError>;
1242
1243/// Access-list pricing query failure.
1244#[derive(Debug, thiserror::Error)]
1245pub enum AccessListError {
1246    /// Provider/oracle query failed.
1247    #[error("failed to query {operation}: {details}")]
1248    Query {
1249        /// Operation being queried.
1250        operation: &'static str,
1251        /// Provider/transport error text.
1252        details: String,
1253    },
1254}
1255
1256impl AccessListError {
1257    /// Build a query error from any displayable provider error.
1258    pub fn query(operation: &'static str, source: impl fmt::Display) -> Self {
1259        Self::Query {
1260            operation,
1261            details: source.to_string(),
1262        }
1263    }
1264}
1265
1266/// Result type returned by access-list pricing helpers.
1267pub type AccessListResult<T> = Result<T, AccessListError>;
1268
1269/// Error returned by speculative freshness orchestration.
1270#[derive(Debug, thiserror::Error)]
1271pub enum FreshnessError {
1272    /// The validation handle was already consumed.
1273    #[error("validation handle already consumed")]
1274    ValidationHandleConsumed,
1275    /// The background validation task failed to join.
1276    #[error("validation task failed: {source}")]
1277    ValidationTaskFailed {
1278        /// Tokio join failure.
1279        #[source]
1280        source: tokio::task::JoinError,
1281    },
1282    /// An optimistic simulation failed before validation was spawned.
1283    #[error(transparent)]
1284    Overlay(#[from] OverlayError),
1285}
1286
1287/// Result type returned by freshness APIs.
1288pub type FreshnessResult<T> = Result<T, FreshnessError>;
1289
1290/// Result type returned by simulation entry points: `Ok(T)` on success, or a
1291/// [`SimError`] distinguishing a transaction-level revert, an EVM halt, and a
1292/// host-side failure.
1293pub type SimulationResult<T> = Result<T, SimError>;
1294
1295/// Error returned by simulation entry points.
1296///
1297/// Distinguishes the three outcomes a caller must branch on: a transaction-level
1298/// [`Revert`](SimError::Revert) (with a decoded reason), an EVM
1299/// [`Halt`](SimError::Halt) (e.g. out of gas), and a host-side
1300/// [`Other`](SimError::Other) failure (RPC, database, ABI encoding).
1301///
1302/// Note that when a revert decodes to [`RevertReason::Panic`], panic codes
1303/// exceeding `u64::MAX` are dropped to `None` and so surface as
1304/// [`RevertReason::Unknown`] rather than `Panic`. This is benign: real
1305/// compiler-emitted panic codes are single-byte constants.
1306#[derive(Debug, thiserror::Error)]
1307pub enum SimError {
1308    /// The transaction reverted; carries the decoded revert.
1309    #[error("transaction reverted: {0}")]
1310    Revert(#[source] Box<SimulationError>),
1311    /// The EVM halted without returning revert data (e.g. out of gas, stack
1312    /// overflow). `reason` is the debug rendering of revm's halt reason.
1313    #[error("transaction halted: {reason} (gas used {gas_used})")]
1314    Halt {
1315        /// Debug rendering of the EVM halt reason.
1316        reason: String,
1317        /// Gas consumed before the halt.
1318        gas_used: u64,
1319    },
1320    /// An unexpected host-side error (RPC, database, ABI encoding).
1321    #[error("{0}")]
1322    Other(#[source] SimHostError),
1323}
1324
1325impl SimError {
1326    /// `true` if this is a transaction-level revert, i.e. the
1327    /// [`Revert`](SimError::Revert) variant.
1328    pub fn is_revert(&self) -> bool {
1329        matches!(self, SimError::Revert(_))
1330    }
1331
1332    /// `true` if the EVM halted without returning revert data (e.g. out of
1333    /// gas), i.e. the [`Halt`](SimError::Halt) variant.
1334    pub fn is_halt(&self) -> bool {
1335        matches!(self, SimError::Halt { .. })
1336    }
1337
1338    /// The decoded [`SimulationError`] if this is a
1339    /// [`Revert`](SimError::Revert), or `None` for a
1340    /// [`Halt`](SimError::Halt) or [`Other`](SimError::Other) error.
1341    pub fn as_revert(&self) -> Option<&SimulationError> {
1342        match self {
1343            SimError::Revert(e) => Some(e),
1344            _ => None,
1345        }
1346    }
1347}
1348
1349impl From<SimHostError> for SimError {
1350    fn from(e: SimHostError) -> Self {
1351        SimError::Other(e)
1352    }
1353}
1354
1355impl From<CacheError> for SimError {
1356    fn from(e: CacheError) -> Self {
1357        SimError::Other(e.into())
1358    }
1359}
1360
1361impl From<OverlayError> for SimError {
1362    fn from(e: OverlayError) -> Self {
1363        SimError::Other(e.into())
1364    }
1365}
1366
1367impl From<SimulationError> for SimError {
1368    fn from(e: SimulationError) -> Self {
1369        SimError::Revert(Box::new(e))
1370    }
1371}
1372
1373#[cfg(test)]
1374mod tests {
1375    use super::*;
1376    use alloy_primitives::{Address, U256};
1377    use alloy_sol_types::sol;
1378
1379    sol! {
1380        #[derive(Debug)]
1381        error Unauthorized(address caller);
1382        #[derive(Debug)]
1383        error Paused();
1384        #[derive(Debug)]
1385        error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
1386    }
1387
1388    /// Build ABI-encoded revert data for a standard `Error(string)` revert.
1389    /// Layout: selector(4) | offset(32) | length(32) | utf8 bytes (padded).
1390    fn encode_solidity_error(message: &str) -> Bytes {
1391        let bytes = message.as_bytes();
1392        let mut out = Vec::new();
1393        out.extend_from_slice(&ERROR_SELECTOR);
1394
1395        // Offset to the string data (always 0x20 from the start of args).
1396        let mut offset = [0u8; 32];
1397        offset[31] = 0x20;
1398        out.extend_from_slice(&offset);
1399
1400        // String length (assumed < 256 for these test fixtures).
1401        let mut length = [0u8; 32];
1402        length[31] = bytes.len() as u8;
1403        out.extend_from_slice(&length);
1404
1405        // String bytes, right-padded to a 32-byte boundary.
1406        out.extend_from_slice(bytes);
1407        let pad = (32 - (bytes.len() % 32)) % 32;
1408        out.extend(std::iter::repeat_n(0u8, pad));
1409
1410        Bytes::from(out)
1411    }
1412
1413    #[test]
1414    fn decodes_solidity_error_string() {
1415        let data = encode_solidity_error("transfer amount exceeds balance");
1416        let reason = decode_revert_reason(&data);
1417        assert_eq!(
1418            reason,
1419            RevertReason::Error("transfer amount exceeds balance".to_string())
1420        );
1421
1422        let err = SimulationError::from_revert(21_000, data);
1423        assert_eq!(
1424            err.revert_message(),
1425            Some("transfer amount exceeds balance")
1426        );
1427        assert!(err.panic_code().is_none());
1428        assert!(err.custom_error().is_none());
1429    }
1430
1431    #[test]
1432    fn decodes_panic_uint256() {
1433        // selector(4) | uint256 panic code (0x11 = arithmetic overflow).
1434        let mut data = PANIC_SELECTOR.to_vec();
1435        let mut code = [0u8; 32];
1436        code[31] = 0x11;
1437        data.extend_from_slice(&code);
1438        let data = Bytes::from(data);
1439
1440        let reason = decode_revert_reason(&data);
1441        assert_eq!(reason, RevertReason::Panic(0x11));
1442
1443        let err = SimulationError::from_revert(0, data);
1444        assert_eq!(err.panic_code(), Some(0x11));
1445        assert!(err.revert_message().is_none());
1446    }
1447
1448    #[test]
1449    fn standard_decoder_does_not_recognize_custom_errors() {
1450        // A registered-only selector is Unknown to the standard decoder.
1451        let data = Bytes::from(Paused::SELECTOR.to_vec());
1452        match decode_revert_reason(&data) {
1453            RevertReason::Unknown { selector, .. } => {
1454                assert_eq!(selector.as_slice(), &Paused::SELECTOR);
1455            }
1456            other => panic!("expected Unknown, got {other}"),
1457        }
1458    }
1459
1460    #[test]
1461    fn decodes_registered_custom_error_without_params() {
1462        let decoder = RevertDecoder::new().with_error::<Paused>();
1463        let data = Bytes::from(Paused::SELECTOR.to_vec());
1464
1465        match decoder.decode(&data) {
1466            RevertReason::Custom(custom) => {
1467                assert_eq!(custom.name, "Paused()");
1468                assert_eq!(custom.selector.as_slice(), &Paused::SELECTOR);
1469                assert_eq!(custom.params.as_deref(), Some("Paused"));
1470            }
1471            other => panic!("expected Custom, got {other}"),
1472        }
1473    }
1474
1475    #[test]
1476    fn decodes_registered_custom_error_with_params() {
1477        let decoder = RevertDecoder::new()
1478            .with_error::<Unauthorized>()
1479            .with_error::<ERC20InsufficientBalance>();
1480
1481        let caller = Address::repeat_byte(0xAB);
1482        let data = Bytes::from(Unauthorized { caller }.abi_encode());
1483        let custom = match decoder.decode(&data) {
1484            RevertReason::Custom(custom) => custom,
1485            other => panic!("expected Custom, got {other}"),
1486        };
1487        assert_eq!(custom.name, "Unauthorized(address)");
1488        let params = custom.params.expect("params should decode");
1489        // The Debug rendering of the decoded struct includes the address.
1490        assert!(params.contains(&format!("{caller:?}")), "got {params}");
1491
1492        // The IERC6093 standard error decodes through the same mechanism.
1493        let data = Bytes::from(
1494            ERC20InsufficientBalance {
1495                sender: caller,
1496                balance: U256::from(1u64),
1497                needed: U256::from(2u64),
1498            }
1499            .abi_encode(),
1500        );
1501        match decoder.decode(&data) {
1502            RevertReason::Custom(custom) => {
1503                assert_eq!(
1504                    custom.name,
1505                    "ERC20InsufficientBalance(address,uint256,uint256)"
1506                );
1507            }
1508            other => panic!("expected Custom, got {other}"),
1509        }
1510    }
1511
1512    #[test]
1513    fn register_raw_decodes_by_selector() {
1514        let mut decoder = RevertDecoder::new();
1515        decoder.register_raw([0xde, 0xad, 0xbe, 0xef], "MyError(uint256)", |data| {
1516            Some(format!("raw {} bytes", data.len()))
1517        });
1518
1519        let data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00]);
1520        match decoder.decode(&data) {
1521            RevertReason::Custom(custom) => {
1522                assert_eq!(custom.name, "MyError(uint256)");
1523                assert_eq!(custom.params.as_deref(), Some("raw 5 bytes"));
1524            }
1525            other => panic!("expected Custom, got {other}"),
1526        }
1527    }
1528
1529    #[test]
1530    fn unknown_blob_is_classified_as_unknown() {
1531        let data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00, 0x01, 0x02, 0x03]);
1532        match decode_revert_reason(&data) {
1533            RevertReason::Unknown {
1534                selector,
1535                data: blob,
1536            } => {
1537                assert_eq!(selector.as_slice(), &[0xde, 0xad, 0xbe, 0xef]);
1538                assert_eq!(blob.len(), 8);
1539            }
1540            other => panic!("expected Unknown, got {other}"),
1541        }
1542
1543        let err = SimulationError::from_revert(0, data);
1544        assert_eq!(
1545            err.selector().map(|s| s.to_vec()),
1546            Some(vec![0xde, 0xad, 0xbe, 0xef])
1547        );
1548        assert!(!err.is_empty_revert());
1549    }
1550
1551    #[test]
1552    fn empty_revert_data_decodes_to_empty() {
1553        let reason = decode_revert_reason(&Bytes::new());
1554        assert_eq!(reason, RevertReason::Empty);
1555
1556        let err = SimulationError::from_revert(0, Bytes::new());
1557        assert!(err.is_empty_revert());
1558        assert!(err.selector().is_none());
1559    }
1560
1561    #[test]
1562    fn data_shorter_than_selector_is_unknown_with_padded_selector() {
1563        let data = Bytes::from(vec![0x01, 0x02, 0x03]);
1564        match decode_revert_reason(&data) {
1565            RevertReason::Unknown { selector, .. } => {
1566                assert_eq!(selector.as_slice(), &[0x01, 0x02, 0x03, 0x00]);
1567            }
1568            other => panic!("expected Unknown, got {other}"),
1569        }
1570    }
1571}