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::borrow::Cow;
38use std::collections::HashMap;
39use std::fmt;
40use std::sync::{Arc, OnceLock};
41
42use alloy_primitives::{Bytes, FixedBytes};
43use alloy_sol_types::SolError;
44use tracing::warn;
45
46/// 4-byte selector of the standard Solidity `Error(string)` revert
47/// (`0x08c379a0`), emitted by `require`/`revert("msg")`.
48pub const ERROR_SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0];
49
50/// 4-byte selector of the standard Solidity `Panic(uint256)` revert
51/// (`0x4e487b71`), emitted on overflow, division-by-zero, etc.
52pub const PANIC_SELECTOR: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71];
53
54/// A decoded contract-defined custom error.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct CustomRevert {
57    /// Human-readable signature, e.g. `"Unauthorized(address)"`.
58    pub name: Cow<'static, str>,
59    /// The error's 4-byte selector (the first 4 bytes of [`data`](Self::data)),
60    /// the `keccak256` prefix of [`name`](Self::name).
61    pub selector: FixedBytes<4>,
62    /// Debug-formatted decoded parameters, when the body decoded successfully.
63    ///
64    /// `None` if only the selector matched but the ABI-encoded parameters could
65    /// not be decoded (e.g. truncated revert data).
66    pub params: Option<String>,
67    /// Raw revert bytes (selector followed by ABI-encoded parameters).
68    pub data: Bytes,
69}
70
71impl fmt::Display for CustomRevert {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match &self.params {
74            Some(params) => write!(f, "{params}"),
75            None => write!(f, "{}", self.name),
76        }
77    }
78}
79
80/// A decoded EVM revert reason.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum RevertReason {
83    /// The call reverted with no return data (e.g. a bare `revert()` or `assert`
84    /// in older Solidity, or an empty `require`).
85    Empty,
86    /// Standard Solidity `Error(string)` revert (e.g. `require(cond, "msg")`).
87    Error(String),
88    /// Standard Solidity `Panic(uint256)` revert (e.g. arithmetic overflow).
89    Panic(u64),
90    /// A contract-defined custom error whose selector was registered on the
91    /// decoder via [`RevertDecoder::with_error`], [`RevertDecoder::register`],
92    /// or [`RevertDecoder::register_raw`].
93    Custom(CustomRevert),
94    /// A selector that matched no built-in or registered custom error.
95    Unknown {
96        /// The 4-byte selector (right-padded with zeros if fewer than 4 bytes
97        /// of revert data were returned).
98        selector: FixedBytes<4>,
99        /// Raw revert bytes.
100        data: Bytes,
101    },
102}
103
104impl fmt::Display for RevertReason {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            RevertReason::Empty => write!(f, "<empty revert>"),
108            RevertReason::Error(msg) => write!(f, "Error({msg:?})"),
109            RevertReason::Panic(code) => write!(f, "Panic({code:#x})"),
110            RevertReason::Custom(custom) => write!(f, "{custom}"),
111            RevertReason::Unknown { selector, data } => {
112                write!(f, "Unknown(selector={selector}, data_len={})", data.len())
113            }
114        }
115    }
116}
117
118type DecodeFn = Arc<dyn Fn(&Bytes) -> Option<String> + Send + Sync>;
119
120#[derive(Clone)]
121struct CustomErrorDecoder {
122    name: Cow<'static, str>,
123    decode: DecodeFn,
124}
125
126/// Error returned when registering a custom error selector that already exists.
127///
128/// A [`RevertDecoder`] keeps the first decoder registered for a selector so a
129/// later registration cannot silently change how existing revert data decodes.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct DuplicateSelectorError {
132    /// The 4-byte selector that was already registered.
133    pub selector: FixedBytes<4>,
134    /// Signature/name of the existing registration that will be kept.
135    pub existing: Cow<'static, str>,
136    /// Signature/name of the attempted duplicate registration.
137    pub attempted: Cow<'static, str>,
138}
139
140impl fmt::Display for DuplicateSelectorError {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(
143            f,
144            "duplicate custom error selector {}: keeping {}, ignoring {}",
145            self.selector, self.existing, self.attempted
146        )
147    }
148}
149
150impl std::error::Error for DuplicateSelectorError {}
151
152/// Decodes raw EVM revert data into a [`RevertReason`].
153///
154/// The two standard Solidity built-ins — `Error(string)` and `Panic(uint256)` —
155/// are always recognized. Register additional contract-defined custom errors
156/// with [`with_error`](RevertDecoder::with_error),
157/// [`register`](RevertDecoder::register), or
158/// [`register_raw`](RevertDecoder::register_raw). Duplicate custom-error
159/// selectors keep the first registration; use
160/// [`try_register`](RevertDecoder::try_register) or
161/// [`try_register_raw`](RevertDecoder::try_register_raw) when collisions should
162/// be handled as errors instead of warnings.
163///
164/// The decoder is cheap to [`Clone`] and is `Send + Sync`, so a configured
165/// decoder can be shared across parallel simulations.
166#[derive(Clone, Default)]
167pub struct RevertDecoder {
168    custom: HashMap<[u8; 4], CustomErrorDecoder>,
169}
170
171impl fmt::Debug for RevertDecoder {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        let mut names: Vec<&str> = self.custom.values().map(|d| d.name.as_ref()).collect();
174        names.sort_unstable();
175        f.debug_struct("RevertDecoder")
176            .field("custom_errors", &names)
177            .finish()
178    }
179}
180
181impl RevertDecoder {
182    /// Create a decoder that recognizes only the standard Solidity built-ins
183    /// (`Error(string)` and `Panic(uint256)`) and no custom errors.
184    ///
185    /// ```
186    /// use evm_fork_cache::errors::RevertDecoder;
187    ///
188    /// let decoder = RevertDecoder::new();
189    /// assert!(decoder.is_empty());
190    /// ```
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Register a `sol!`-generated custom error type for decoding, consuming and
196    /// returning `self` for builder-style chaining.
197    ///
198    /// If the selector is already registered, the first registration is kept
199    /// and a warning is emitted. Use [`try_register`](Self::try_register) when
200    /// duplicate selectors should fail configuration.
201    ///
202    /// ```
203    /// use alloy_sol_types::sol;
204    /// use evm_fork_cache::errors::RevertDecoder;
205    ///
206    /// sol! {
207    ///     #[derive(Debug)]
208    ///     error SlippageExceeded(uint256 wanted, uint256 got);
209    ///     #[derive(Debug)]
210    ///     error Paused();
211    /// }
212    ///
213    /// let decoder = RevertDecoder::new()
214    ///     .with_error::<SlippageExceeded>()
215    ///     .with_error::<Paused>();
216    /// assert_eq!(decoder.len(), 2);
217    /// ```
218    pub fn with_error<E>(mut self) -> Self
219    where
220        E: SolError + fmt::Debug + 'static,
221    {
222        self.register::<E>();
223        self
224    }
225
226    /// Register a `sol!`-generated custom error type for decoding.
227    ///
228    /// If an error with the same selector is already registered, the first
229    /// registration is kept and a warning is emitted. Use
230    /// [`try_register`](Self::try_register) to surface duplicates as errors.
231    pub fn register<E>(&mut self) -> &mut Self
232    where
233        E: SolError + fmt::Debug + 'static,
234    {
235        if let Err(err) = self.try_register::<E>() {
236            warn_duplicate_selector(&err);
237        }
238        self
239    }
240
241    /// Register a `sol!`-generated custom error type for decoding, returning an
242    /// error when another custom error already owns the same selector.
243    pub fn try_register<E>(&mut self) -> Result<&mut Self, DuplicateSelectorError>
244    where
245        E: SolError + fmt::Debug + 'static,
246    {
247        let decode: DecodeFn =
248            Arc::new(|data: &Bytes| E::abi_decode(data).ok().map(|err| format!("{err:?}")));
249        self.insert_custom_error(
250            E::SELECTOR,
251            CustomErrorDecoder {
252                name: Cow::Borrowed(E::SIGNATURE),
253                decode,
254            },
255        )
256    }
257
258    /// Register a custom error by raw selector, name, and parameter decoder.
259    ///
260    /// Use this when there is no `sol!`-generated type to hand — for example
261    /// when the selector and signature come from an ABI loaded at runtime. The
262    /// `decode` closure receives the full revert bytes (selector included) and
263    /// returns the formatted parameters, or `None` if it cannot decode them.
264    ///
265    /// If the closure returns `None`, the selector still matches: the decode
266    /// yields a [`RevertReason::Custom`] whose
267    /// [`params`](CustomRevert::params) is `None`. If an error with the same
268    /// selector is already registered, the first registration is kept and a
269    /// warning is emitted. Use [`try_register_raw`](Self::try_register_raw) to
270    /// surface duplicates as errors.
271    ///
272    /// ```
273    /// use alloy_primitives::Bytes;
274    /// use evm_fork_cache::errors::{RevertDecoder, RevertReason};
275    ///
276    /// let mut decoder = RevertDecoder::new();
277    /// // A closure that decodes the parameters when there is a payload byte,
278    /// // and otherwise reports a decode failure by returning `None`.
279    /// decoder.register_raw([0xde, 0xad, 0xbe, 0xef], "MyError(uint256)", |data| {
280    ///     (data.len() > 4).then(|| format!("payload {} bytes", data.len() - 4))
281    /// });
282    ///
283    /// // Selector plus a payload byte: the closure decodes the parameters.
284    /// let with_params = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00]);
285    /// match decoder.decode(&with_params) {
286    ///     RevertReason::Custom(custom) => {
287    ///         assert_eq!(custom.name, "MyError(uint256)");
288    ///         assert_eq!(custom.params.as_deref(), Some("payload 1 bytes"));
289    ///     }
290    ///     other => panic!("expected Custom, got {other}"),
291    /// }
292    ///
293    /// // Bare selector: the closure returns `None`, but the selector still
294    /// // matches, so the result is a `Custom` with `params == None`.
295    /// let bare = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]);
296    /// match decoder.decode(&bare) {
297    ///     RevertReason::Custom(custom) => assert!(custom.params.is_none()),
298    ///     other => panic!("expected Custom, got {other}"),
299    /// }
300    /// ```
301    pub fn register_raw(
302        &mut self,
303        selector: [u8; 4],
304        name: impl Into<Cow<'static, str>>,
305        decode: impl Fn(&Bytes) -> Option<String> + Send + Sync + 'static,
306    ) -> &mut Self {
307        if let Err(err) = self.try_register_raw(selector, name, decode) {
308            warn_duplicate_selector(&err);
309        }
310        self
311    }
312
313    /// Register a custom error by raw selector, name, and parameter decoder,
314    /// returning an error when another custom error already owns the selector.
315    ///
316    /// The `decode` closure receives the full revert bytes (selector included)
317    /// and returns formatted parameters, or `None` if the selector matched but
318    /// the parameter payload could not be decoded.
319    pub fn try_register_raw(
320        &mut self,
321        selector: [u8; 4],
322        name: impl Into<Cow<'static, str>>,
323        decode: impl Fn(&Bytes) -> Option<String> + Send + Sync + 'static,
324    ) -> Result<&mut Self, DuplicateSelectorError> {
325        self.insert_custom_error(
326            selector,
327            CustomErrorDecoder {
328                name: name.into(),
329                decode: Arc::new(decode),
330            },
331        )
332    }
333
334    /// Number of registered custom errors. The two Solidity built-ins are
335    /// always recognized and are not counted, so a freshly
336    /// [`new`](RevertDecoder::new) decoder reports `0`.
337    pub fn len(&self) -> usize {
338        self.custom.len()
339    }
340
341    /// Returns `true` if no custom errors are registered. The built-ins are
342    /// always recognized regardless, so this is `true` for a freshly
343    /// [`new`](RevertDecoder::new) decoder.
344    pub fn is_empty(&self) -> bool {
345        self.custom.is_empty()
346    }
347
348    /// Decode raw EVM revert data into a [`RevertReason`].
349    ///
350    /// Resolution order: the two Solidity built-ins (`Error(string)` and
351    /// `Panic(uint256)`), then registered custom errors by selector, then
352    /// [`RevertReason::Unknown`] for anything else. Empty input decodes to
353    /// [`RevertReason::Empty`], and data shorter than 4 bytes decodes to
354    /// [`RevertReason::Unknown`] with the selector right-padded with zeros.
355    ///
356    /// ```
357    /// use alloy_primitives::{Bytes, U256};
358    /// use alloy_sol_types::{Panic, SolError, sol};
359    /// use evm_fork_cache::errors::{RevertDecoder, RevertReason, ERROR_SELECTOR};
360    ///
361    /// sol! {
362    ///     #[derive(Debug)]
363    ///     error Custom();
364    /// }
365    ///
366    /// let decoder = RevertDecoder::new().with_error::<Custom>();
367    ///
368    /// // Built-in `Error(string)` decodes natively, without registration.
369    /// // Layout: selector | offset(0x20) | length | utf8 bytes (padded).
370    /// let mut bytes = ERROR_SELECTOR.to_vec();
371    /// bytes.extend_from_slice(&{ let mut o = [0u8; 32]; o[31] = 0x20; o }); // offset
372    /// bytes.extend_from_slice(&{ let mut l = [0u8; 32]; l[31] = 2; l });    // length 2
373    /// bytes.extend_from_slice(b"hi");
374    /// bytes.extend_from_slice(&[0u8; 30]);                                  // pad to 32
375    /// assert_eq!(decoder.decode(&Bytes::from(bytes)), RevertReason::Error("hi".into()));
376    ///
377    /// // Built-in `Panic(uint256)` decodes natively too.
378    /// let panic = Bytes::from(Panic { code: U256::from(0x11) }.abi_encode());
379    /// assert_eq!(decoder.decode(&panic), RevertReason::Panic(0x11));
380    ///
381    /// // A registered selector resolves to `Custom`.
382    /// let raw = Bytes::from(Custom::SELECTOR.to_vec());
383    /// match decoder.decode(&raw) {
384    ///     RevertReason::Custom(err) => assert_eq!(err.name, "Custom()"),
385    ///     other => panic!("expected Custom, got {other}"),
386    /// }
387    ///
388    /// // An unregistered selector falls through to `Unknown`.
389    /// let unknown = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]);
390    /// assert!(matches!(decoder.decode(&unknown), RevertReason::Unknown { .. }));
391    /// ```
392    pub fn decode(&self, data: &Bytes) -> RevertReason {
393        if data.is_empty() {
394            return RevertReason::Empty;
395        }
396        if data.len() < 4 {
397            // Too short for a selector; surface the raw bytes as Unknown with a
398            // right-padded selector so nothing is silently discarded.
399            let mut selector = [0u8; 4];
400            selector[..data.len()].copy_from_slice(&data[..]);
401            return RevertReason::Unknown {
402                selector: FixedBytes::from(selector),
403                data: data.clone(),
404            };
405        }
406
407        let selector: [u8; 4] = data[..4].try_into().expect("length checked >= 4");
408
409        if selector == ERROR_SELECTOR
410            && let Some(message) = decode_solidity_error_string(data)
411        {
412            return RevertReason::Error(message);
413        }
414        if selector == PANIC_SELECTOR
415            && let Some(code) = decode_solidity_panic(data)
416        {
417            return RevertReason::Panic(code);
418        }
419        if let Some(entry) = self.custom.get(&selector) {
420            return RevertReason::Custom(CustomRevert {
421                name: entry.name.clone(),
422                selector: FixedBytes::from(selector),
423                params: (entry.decode)(data),
424                data: data.clone(),
425            });
426        }
427
428        RevertReason::Unknown {
429            selector: FixedBytes::from(selector),
430            data: data.clone(),
431        }
432    }
433
434    fn insert_custom_error(
435        &mut self,
436        selector: [u8; 4],
437        decoder: CustomErrorDecoder,
438    ) -> Result<&mut Self, DuplicateSelectorError> {
439        if let Some(existing) = self.custom.get(&selector) {
440            return Err(DuplicateSelectorError {
441                selector: FixedBytes::from(selector),
442                existing: existing.name.clone(),
443                attempted: decoder.name,
444            });
445        }
446
447        self.custom.insert(selector, decoder);
448        Ok(self)
449    }
450}
451
452fn warn_duplicate_selector(err: &DuplicateSelectorError) {
453    warn!(
454        selector = %err.selector,
455        existing = err.existing.as_ref(),
456        attempted = err.attempted.as_ref(),
457        "duplicate custom error selector registration ignored; keeping first registration"
458    );
459}
460
461/// Decode revert data using only the standard Solidity built-ins.
462///
463/// For application-specific custom errors, build a [`RevertDecoder`] and call
464/// [`RevertDecoder::decode`].
465pub fn decode_revert_reason(data: &Bytes) -> RevertReason {
466    static STANDARD: OnceLock<RevertDecoder> = OnceLock::new();
467    STANDARD.get_or_init(RevertDecoder::new).decode(data)
468}
469
470/// Decode the `uint256` payload of a standard `Panic(uint256)` revert.
471///
472/// Delegates to alloy's built-in decoder (which validates the ABI encoding) and
473/// returns `None` for codes that do not fit in a `u64`. Real compiler-emitted
474/// panic codes are single-byte constants (e.g. `0x11` for arithmetic overflow,
475/// `0x32` for out-of-bounds array access).
476fn decode_solidity_panic(data: &Bytes) -> Option<u64> {
477    alloy_sol_types::Panic::abi_decode(data)
478        .ok()
479        .and_then(|panic| u64::try_from(panic.code).ok())
480}
481
482/// Decode the string payload of a standard `Error(string)` revert.
483///
484/// Delegates to alloy's built-in decoder, which follows the ABI offset and
485/// validates the length rather than assuming a fixed in-memory layout — so it
486/// stays correct on non-standard or adversarial revert data.
487fn decode_solidity_error_string(data: &Bytes) -> Option<String> {
488    alloy_sol_types::Revert::abi_decode(data)
489        .ok()
490        .map(|revert| revert.reason)
491}
492
493/// A structured simulation revert with its decoded reason.
494#[derive(Debug, Clone)]
495pub struct SimulationError {
496    /// Gas consumed before the revert.
497    pub gas_used: u64,
498    /// Raw revert data returned by the EVM (the bytes that were decoded into
499    /// [`reason`](Self::reason)).
500    pub revert_data: Bytes,
501    /// The revert reason decoded from [`revert_data`](Self::revert_data).
502    pub reason: RevertReason,
503}
504
505impl SimulationError {
506    /// Create a simulation error from raw revert data, decoding with the
507    /// standard Solidity built-ins only.
508    pub fn from_revert(gas_used: u64, output: Bytes) -> Self {
509        let reason = decode_revert_reason(&output);
510        Self {
511            gas_used,
512            revert_data: output,
513            reason,
514        }
515    }
516
517    /// Create a simulation error from raw revert data, decoding custom errors
518    /// with the supplied [`RevertDecoder`].
519    pub fn from_revert_with(gas_used: u64, output: Bytes, decoder: &RevertDecoder) -> Self {
520        let reason = decoder.decode(&output);
521        Self {
522            gas_used,
523            revert_data: output,
524            reason,
525        }
526    }
527
528    /// The decoded revert reason. Equivalent to borrowing the public
529    /// [`reason`](Self::reason) field.
530    pub fn reason(&self) -> &RevertReason {
531        &self.reason
532    }
533
534    /// The `Error(string)` message, if this was a standard string revert.
535    pub fn revert_message(&self) -> Option<&str> {
536        match &self.reason {
537            RevertReason::Error(message) => Some(message.as_str()),
538            _ => None,
539        }
540    }
541
542    /// The panic code, if this was a standard `Panic(uint256)` revert.
543    pub fn panic_code(&self) -> Option<u64> {
544        match self.reason {
545            RevertReason::Panic(code) => Some(code),
546            _ => None,
547        }
548    }
549
550    /// The decoded custom error, if a registered custom error matched.
551    pub fn custom_error(&self) -> Option<&CustomRevert> {
552        match &self.reason {
553            RevertReason::Custom(custom) => Some(custom),
554            _ => None,
555        }
556    }
557
558    /// The 4-byte selector of the revert, if any (custom or unknown).
559    pub fn selector(&self) -> Option<FixedBytes<4>> {
560        match &self.reason {
561            RevertReason::Custom(custom) => Some(custom.selector),
562            RevertReason::Unknown { selector, .. } => Some(*selector),
563            _ => None,
564        }
565    }
566
567    /// `true` if the call reverted with no return data, i.e. the reason is
568    /// [`RevertReason::Empty`].
569    pub fn is_empty_revert(&self) -> bool {
570        matches!(self.reason, RevertReason::Empty)
571    }
572}
573
574impl fmt::Display for SimulationError {
575    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
576        write!(
577            f,
578            "SimulationError(gas_used={}, reason={})",
579            self.gas_used, self.reason
580        )
581    }
582}
583
584impl std::error::Error for SimulationError {}
585
586/// Result type returned by simulation entry points: `Ok(T)` on success, or a
587/// [`SimError`] distinguishing a transaction-level revert, an EVM halt, and a
588/// host-side failure.
589pub type SimulationResult<T> = Result<T, SimError>;
590
591/// Error returned by simulation entry points.
592///
593/// Distinguishes the three outcomes a caller must branch on: a transaction-level
594/// [`Revert`](SimError::Revert) (with a decoded reason), an EVM
595/// [`Halt`](SimError::Halt) (e.g. out of gas), and a host-side
596/// [`Other`](SimError::Other) failure (RPC, database, ABI encoding).
597///
598/// Note that when a revert decodes to [`RevertReason::Panic`], panic codes
599/// exceeding `u64::MAX` are dropped to `None` and so surface as
600/// [`RevertReason::Unknown`] rather than `Panic`. This is benign: real
601/// compiler-emitted panic codes are single-byte constants.
602#[derive(Debug, thiserror::Error)]
603pub enum SimError {
604    /// The transaction reverted; carries the decoded revert.
605    #[error("transaction reverted: {0}")]
606    Revert(#[source] Box<SimulationError>),
607    /// The EVM halted without returning revert data (e.g. out of gas, stack
608    /// overflow). `reason` is the debug rendering of revm's halt reason.
609    #[error("transaction halted: {reason} (gas used {gas_used})")]
610    Halt {
611        /// Debug rendering of the EVM halt reason.
612        reason: String,
613        /// Gas consumed before the halt.
614        gas_used: u64,
615    },
616    /// An unexpected host-side error (RPC, database, ABI encoding).
617    #[error("{0}")]
618    Other(anyhow::Error),
619}
620
621impl SimError {
622    /// `true` if this is a transaction-level revert, i.e. the
623    /// [`Revert`](SimError::Revert) variant.
624    pub fn is_revert(&self) -> bool {
625        matches!(self, SimError::Revert(_))
626    }
627
628    /// `true` if the EVM halted without returning revert data (e.g. out of
629    /// gas), i.e. the [`Halt`](SimError::Halt) variant.
630    pub fn is_halt(&self) -> bool {
631        matches!(self, SimError::Halt { .. })
632    }
633
634    /// The decoded [`SimulationError`] if this is a
635    /// [`Revert`](SimError::Revert), or `None` for a
636    /// [`Halt`](SimError::Halt) or [`Other`](SimError::Other) error.
637    pub fn as_revert(&self) -> Option<&SimulationError> {
638        match self {
639            SimError::Revert(e) => Some(e),
640            _ => None,
641        }
642    }
643}
644
645impl From<anyhow::Error> for SimError {
646    fn from(e: anyhow::Error) -> Self {
647        SimError::Other(e)
648    }
649}
650
651impl From<SimulationError> for SimError {
652    fn from(e: SimulationError) -> Self {
653        SimError::Revert(Box::new(e))
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660    use alloy_primitives::{Address, U256};
661    use alloy_sol_types::sol;
662
663    sol! {
664        #[derive(Debug)]
665        error Unauthorized(address caller);
666        #[derive(Debug)]
667        error Paused();
668        #[derive(Debug)]
669        error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
670    }
671
672    /// Build ABI-encoded revert data for a standard `Error(string)` revert.
673    /// Layout: selector(4) | offset(32) | length(32) | utf8 bytes (padded).
674    fn encode_solidity_error(message: &str) -> Bytes {
675        let bytes = message.as_bytes();
676        let mut out = Vec::new();
677        out.extend_from_slice(&ERROR_SELECTOR);
678
679        // Offset to the string data (always 0x20 from the start of args).
680        let mut offset = [0u8; 32];
681        offset[31] = 0x20;
682        out.extend_from_slice(&offset);
683
684        // String length (assumed < 256 for these test fixtures).
685        let mut length = [0u8; 32];
686        length[31] = bytes.len() as u8;
687        out.extend_from_slice(&length);
688
689        // String bytes, right-padded to a 32-byte boundary.
690        out.extend_from_slice(bytes);
691        let pad = (32 - (bytes.len() % 32)) % 32;
692        out.extend(std::iter::repeat_n(0u8, pad));
693
694        Bytes::from(out)
695    }
696
697    #[test]
698    fn decodes_solidity_error_string() {
699        let data = encode_solidity_error("transfer amount exceeds balance");
700        let reason = decode_revert_reason(&data);
701        assert_eq!(
702            reason,
703            RevertReason::Error("transfer amount exceeds balance".to_string())
704        );
705
706        let err = SimulationError::from_revert(21_000, data);
707        assert_eq!(
708            err.revert_message(),
709            Some("transfer amount exceeds balance")
710        );
711        assert!(err.panic_code().is_none());
712        assert!(err.custom_error().is_none());
713    }
714
715    #[test]
716    fn decodes_panic_uint256() {
717        // selector(4) | uint256 panic code (0x11 = arithmetic overflow).
718        let mut data = PANIC_SELECTOR.to_vec();
719        let mut code = [0u8; 32];
720        code[31] = 0x11;
721        data.extend_from_slice(&code);
722        let data = Bytes::from(data);
723
724        let reason = decode_revert_reason(&data);
725        assert_eq!(reason, RevertReason::Panic(0x11));
726
727        let err = SimulationError::from_revert(0, data);
728        assert_eq!(err.panic_code(), Some(0x11));
729        assert!(err.revert_message().is_none());
730    }
731
732    #[test]
733    fn standard_decoder_does_not_recognize_custom_errors() {
734        // A registered-only selector is Unknown to the standard decoder.
735        let data = Bytes::from(Paused::SELECTOR.to_vec());
736        match decode_revert_reason(&data) {
737            RevertReason::Unknown { selector, .. } => {
738                assert_eq!(selector.as_slice(), &Paused::SELECTOR);
739            }
740            other => panic!("expected Unknown, got {other}"),
741        }
742    }
743
744    #[test]
745    fn decodes_registered_custom_error_without_params() {
746        let decoder = RevertDecoder::new().with_error::<Paused>();
747        let data = Bytes::from(Paused::SELECTOR.to_vec());
748
749        match decoder.decode(&data) {
750            RevertReason::Custom(custom) => {
751                assert_eq!(custom.name, "Paused()");
752                assert_eq!(custom.selector.as_slice(), &Paused::SELECTOR);
753                assert_eq!(custom.params.as_deref(), Some("Paused"));
754            }
755            other => panic!("expected Custom, got {other}"),
756        }
757    }
758
759    #[test]
760    fn decodes_registered_custom_error_with_params() {
761        let decoder = RevertDecoder::new()
762            .with_error::<Unauthorized>()
763            .with_error::<ERC20InsufficientBalance>();
764
765        let caller = Address::repeat_byte(0xAB);
766        let data = Bytes::from(Unauthorized { caller }.abi_encode());
767        let custom = match decoder.decode(&data) {
768            RevertReason::Custom(custom) => custom,
769            other => panic!("expected Custom, got {other}"),
770        };
771        assert_eq!(custom.name, "Unauthorized(address)");
772        let params = custom.params.expect("params should decode");
773        // The Debug rendering of the decoded struct includes the address.
774        assert!(params.contains(&format!("{caller:?}")), "got {params}");
775
776        // The IERC6093 standard error decodes through the same mechanism.
777        let data = Bytes::from(
778            ERC20InsufficientBalance {
779                sender: caller,
780                balance: U256::from(1u64),
781                needed: U256::from(2u64),
782            }
783            .abi_encode(),
784        );
785        match decoder.decode(&data) {
786            RevertReason::Custom(custom) => {
787                assert_eq!(
788                    custom.name,
789                    "ERC20InsufficientBalance(address,uint256,uint256)"
790                );
791            }
792            other => panic!("expected Custom, got {other}"),
793        }
794    }
795
796    #[test]
797    fn register_raw_decodes_by_selector() {
798        let mut decoder = RevertDecoder::new();
799        decoder.register_raw([0xde, 0xad, 0xbe, 0xef], "MyError(uint256)", |data| {
800            Some(format!("raw {} bytes", data.len()))
801        });
802
803        let data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00]);
804        match decoder.decode(&data) {
805            RevertReason::Custom(custom) => {
806                assert_eq!(custom.name, "MyError(uint256)");
807                assert_eq!(custom.params.as_deref(), Some("raw 5 bytes"));
808            }
809            other => panic!("expected Custom, got {other}"),
810        }
811    }
812
813    #[test]
814    fn unknown_blob_is_classified_as_unknown() {
815        let data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00, 0x01, 0x02, 0x03]);
816        match decode_revert_reason(&data) {
817            RevertReason::Unknown {
818                selector,
819                data: blob,
820            } => {
821                assert_eq!(selector.as_slice(), &[0xde, 0xad, 0xbe, 0xef]);
822                assert_eq!(blob.len(), 8);
823            }
824            other => panic!("expected Unknown, got {other}"),
825        }
826
827        let err = SimulationError::from_revert(0, data);
828        assert_eq!(
829            err.selector().map(|s| s.to_vec()),
830            Some(vec![0xde, 0xad, 0xbe, 0xef])
831        );
832        assert!(!err.is_empty_revert());
833    }
834
835    #[test]
836    fn empty_revert_data_decodes_to_empty() {
837        let reason = decode_revert_reason(&Bytes::new());
838        assert_eq!(reason, RevertReason::Empty);
839
840        let err = SimulationError::from_revert(0, Bytes::new());
841        assert!(err.is_empty_revert());
842        assert!(err.selector().is_none());
843    }
844
845    #[test]
846    fn data_shorter_than_selector_is_unknown_with_padded_selector() {
847        let data = Bytes::from(vec![0x01, 0x02, 0x03]);
848        match decode_revert_reason(&data) {
849            RevertReason::Unknown { selector, .. } => {
850                assert_eq!(selector.as_slice(), &[0x01, 0x02, 0x03, 0x00]);
851            }
852            other => panic!("expected Unknown, got {other}"),
853        }
854    }
855}