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}
1031
1032impl OverlayError {
1033 /// Convert a transaction-builder error into [`OverlayError::TxEnv`].
1034 pub fn tx_env(source: impl fmt::Debug) -> Self {
1035 Self::TxEnv {
1036 details: format!("{source:?}"),
1037 }
1038 }
1039
1040 /// Convert a revm host/database transaction error into
1041 /// [`OverlayError::Transact`].
1042 pub fn transact(source: impl fmt::Debug) -> Self {
1043 Self::Transact {
1044 details: format!("{source:?}"),
1045 }
1046 }
1047}
1048
1049/// Result type returned by overlay APIs that return raw [`ExecutionResult`]
1050/// values instead of classifying reverts.
1051///
1052/// [`ExecutionResult`]: revm::context::result::ExecutionResult
1053pub type OverlayResult<T, E = OverlayError> = Result<T, E>;
1054
1055/// Host-side failure for simulation entry points.
1056#[derive(Debug, thiserror::Error)]
1057pub enum SimHostError {
1058 /// Transaction environment construction failed.
1059 #[error("failed to build transaction environment: {details}")]
1060 TxEnv {
1061 /// Builder error text.
1062 details: String,
1063 },
1064 /// revm returned a host/database transaction error.
1065 #[error("failed to transact: {details}")]
1066 Transact {
1067 /// revm/database error text.
1068 details: String,
1069 },
1070 /// Database read failed outside a transaction execution.
1071 #[error("database operation failed: {details}")]
1072 Database {
1073 /// Database error text.
1074 details: String,
1075 },
1076 /// Cache helper failed.
1077 #[error(transparent)]
1078 Cache(#[from] CacheError),
1079 /// Overlay helper failed.
1080 #[error(transparent)]
1081 Overlay(#[from] OverlayError),
1082}
1083
1084impl SimHostError {
1085 /// Convert a transaction-builder error into [`SimHostError::TxEnv`].
1086 pub fn tx_env(source: impl fmt::Debug) -> Self {
1087 Self::TxEnv {
1088 details: format!("{source:?}"),
1089 }
1090 }
1091
1092 /// Convert a revm host/database transaction error into
1093 /// [`SimHostError::Transact`].
1094 pub fn transact(source: impl fmt::Debug) -> Self {
1095 Self::Transact {
1096 details: format!("{source:?}"),
1097 }
1098 }
1099
1100 /// Convert a database error into [`SimHostError::Database`].
1101 pub fn database(source: impl fmt::Debug) -> Self {
1102 Self::Database {
1103 details: format!("{source:?}"),
1104 }
1105 }
1106}
1107
1108/// Multicall3 helper failure.
1109#[derive(Debug, thiserror::Error)]
1110pub enum MulticallError {
1111 /// Underlying EVM cache call failed.
1112 #[error(transparent)]
1113 Cache(#[from] CacheError),
1114 /// The aggregate3 call reverted or halted.
1115 #[error("Multicall3 aggregate call failed: {result}")]
1116 AggregateFailed {
1117 /// Debug rendering of the execution result.
1118 result: String,
1119 },
1120 /// A per-call result marked `success = false`.
1121 #[error("multicall result indicates the call failed")]
1122 CallFailed,
1123 /// ABI decode failure.
1124 #[error("failed to decode multicall result: {details}")]
1125 Decode {
1126 /// Decoder error text.
1127 details: String,
1128 },
1129}
1130
1131/// Result type returned by Multicall3 helpers.
1132pub type MulticallResult<T> = Result<T, MulticallError>;
1133
1134/// Deployment and Foundry-artifact loading failure.
1135#[derive(Debug, thiserror::Error)]
1136pub enum DeployError {
1137 /// Artifact file could not be read.
1138 #[error("failed to read Foundry artifact at {path}: {source}")]
1139 ReadArtifact {
1140 /// Artifact path.
1141 path: PathBuf,
1142 /// Filesystem error.
1143 #[source]
1144 source: io::Error,
1145 },
1146 /// Artifact JSON was invalid.
1147 #[error("failed to parse Foundry artifact JSON at {path}: {source}")]
1148 ParseArtifact {
1149 /// Artifact path.
1150 path: PathBuf,
1151 /// JSON parse error.
1152 #[source]
1153 source: serde_json::Error,
1154 },
1155 /// Artifact had no bytecode field.
1156 #[error("artifact {path} has no `bytecode` field")]
1157 MissingBytecodeField {
1158 /// Artifact path.
1159 path: PathBuf,
1160 },
1161 /// Artifact bytecode was not a supported string shape.
1162 #[error("artifact {path} has no `bytecode.object` string")]
1163 MissingBytecodeObject {
1164 /// Artifact path.
1165 path: PathBuf,
1166 },
1167 /// Bytecode was empty.
1168 #[error("empty bytecode")]
1169 EmptyBytecode,
1170 /// Bytecode still contains unresolved Foundry library placeholders.
1171 #[error("bytecode contains unresolved library placeholders")]
1172 UnresolvedLibraryPlaceholders,
1173 /// Hex bytecode could not be decoded.
1174 #[error("invalid hex bytecode: {details}")]
1175 InvalidHex {
1176 /// Hex decoder error text.
1177 details: String,
1178 },
1179 /// Underlying cache operation failed.
1180 #[error(transparent)]
1181 Cache(#[from] CacheError),
1182 /// Artifact deployment failed, with path context.
1183 #[error("deploying Foundry artifact {path} failed: {source}")]
1184 ArtifactDeploy {
1185 /// Artifact path.
1186 path: PathBuf,
1187 /// Cache operation failure.
1188 #[source]
1189 source: CacheError,
1190 },
1191 /// Target validation failed before etching.
1192 #[error("validating target contract {target} failed: {source}")]
1193 TargetValidation {
1194 /// Target address.
1195 target: Address,
1196 /// Cache operation failure.
1197 #[source]
1198 source: CacheError,
1199 },
1200 /// Runtime bytecode etch failed.
1201 #[error("etching runtime bytecode at {target} failed: {source}")]
1202 EtchRuntime {
1203 /// Target address.
1204 target: Address,
1205 /// Cache operation failure.
1206 #[source]
1207 source: CacheError,
1208 },
1209}
1210
1211/// Result type returned by deployment helpers.
1212pub type DeployResult<T> = Result<T, DeployError>;
1213
1214/// Access-list pricing query failure.
1215#[derive(Debug, thiserror::Error)]
1216pub enum AccessListError {
1217 /// Provider/oracle query failed.
1218 #[error("failed to query {operation}: {details}")]
1219 Query {
1220 /// Operation being queried.
1221 operation: &'static str,
1222 /// Provider/transport error text.
1223 details: String,
1224 },
1225}
1226
1227impl AccessListError {
1228 /// Build a query error from any displayable provider error.
1229 pub fn query(operation: &'static str, source: impl fmt::Display) -> Self {
1230 Self::Query {
1231 operation,
1232 details: source.to_string(),
1233 }
1234 }
1235}
1236
1237/// Result type returned by access-list pricing helpers.
1238pub type AccessListResult<T> = Result<T, AccessListError>;
1239
1240/// Error returned by speculative freshness orchestration.
1241#[derive(Debug, thiserror::Error)]
1242pub enum FreshnessError {
1243 /// The validation handle was already consumed.
1244 #[error("validation handle already consumed")]
1245 ValidationHandleConsumed,
1246 /// The background validation task failed to join.
1247 #[error("validation task failed: {source}")]
1248 ValidationTaskFailed {
1249 /// Tokio join failure.
1250 #[source]
1251 source: tokio::task::JoinError,
1252 },
1253 /// An optimistic simulation failed before validation was spawned.
1254 #[error(transparent)]
1255 Overlay(#[from] OverlayError),
1256}
1257
1258/// Result type returned by freshness APIs.
1259pub type FreshnessResult<T> = Result<T, FreshnessError>;
1260
1261/// Result type returned by simulation entry points: `Ok(T)` on success, or a
1262/// [`SimError`] distinguishing a transaction-level revert, an EVM halt, and a
1263/// host-side failure.
1264pub type SimulationResult<T> = Result<T, SimError>;
1265
1266/// Error returned by simulation entry points.
1267///
1268/// Distinguishes the three outcomes a caller must branch on: a transaction-level
1269/// [`Revert`](SimError::Revert) (with a decoded reason), an EVM
1270/// [`Halt`](SimError::Halt) (e.g. out of gas), and a host-side
1271/// [`Other`](SimError::Other) failure (RPC, database, ABI encoding).
1272///
1273/// Note that when a revert decodes to [`RevertReason::Panic`], panic codes
1274/// exceeding `u64::MAX` are dropped to `None` and so surface as
1275/// [`RevertReason::Unknown`] rather than `Panic`. This is benign: real
1276/// compiler-emitted panic codes are single-byte constants.
1277#[derive(Debug, thiserror::Error)]
1278pub enum SimError {
1279 /// The transaction reverted; carries the decoded revert.
1280 #[error("transaction reverted: {0}")]
1281 Revert(#[source] Box<SimulationError>),
1282 /// The EVM halted without returning revert data (e.g. out of gas, stack
1283 /// overflow). `reason` is the debug rendering of revm's halt reason.
1284 #[error("transaction halted: {reason} (gas used {gas_used})")]
1285 Halt {
1286 /// Debug rendering of the EVM halt reason.
1287 reason: String,
1288 /// Gas consumed before the halt.
1289 gas_used: u64,
1290 },
1291 /// An unexpected host-side error (RPC, database, ABI encoding).
1292 #[error("{0}")]
1293 Other(#[source] SimHostError),
1294}
1295
1296impl SimError {
1297 /// `true` if this is a transaction-level revert, i.e. the
1298 /// [`Revert`](SimError::Revert) variant.
1299 pub fn is_revert(&self) -> bool {
1300 matches!(self, SimError::Revert(_))
1301 }
1302
1303 /// `true` if the EVM halted without returning revert data (e.g. out of
1304 /// gas), i.e. the [`Halt`](SimError::Halt) variant.
1305 pub fn is_halt(&self) -> bool {
1306 matches!(self, SimError::Halt { .. })
1307 }
1308
1309 /// The decoded [`SimulationError`] if this is a
1310 /// [`Revert`](SimError::Revert), or `None` for a
1311 /// [`Halt`](SimError::Halt) or [`Other`](SimError::Other) error.
1312 pub fn as_revert(&self) -> Option<&SimulationError> {
1313 match self {
1314 SimError::Revert(e) => Some(e),
1315 _ => None,
1316 }
1317 }
1318}
1319
1320impl From<SimHostError> for SimError {
1321 fn from(e: SimHostError) -> Self {
1322 SimError::Other(e)
1323 }
1324}
1325
1326impl From<CacheError> for SimError {
1327 fn from(e: CacheError) -> Self {
1328 SimError::Other(e.into())
1329 }
1330}
1331
1332impl From<OverlayError> for SimError {
1333 fn from(e: OverlayError) -> Self {
1334 SimError::Other(e.into())
1335 }
1336}
1337
1338impl From<SimulationError> for SimError {
1339 fn from(e: SimulationError) -> Self {
1340 SimError::Revert(Box::new(e))
1341 }
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346 use super::*;
1347 use alloy_primitives::{Address, U256};
1348 use alloy_sol_types::sol;
1349
1350 sol! {
1351 #[derive(Debug)]
1352 error Unauthorized(address caller);
1353 #[derive(Debug)]
1354 error Paused();
1355 #[derive(Debug)]
1356 error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
1357 }
1358
1359 /// Build ABI-encoded revert data for a standard `Error(string)` revert.
1360 /// Layout: selector(4) | offset(32) | length(32) | utf8 bytes (padded).
1361 fn encode_solidity_error(message: &str) -> Bytes {
1362 let bytes = message.as_bytes();
1363 let mut out = Vec::new();
1364 out.extend_from_slice(&ERROR_SELECTOR);
1365
1366 // Offset to the string data (always 0x20 from the start of args).
1367 let mut offset = [0u8; 32];
1368 offset[31] = 0x20;
1369 out.extend_from_slice(&offset);
1370
1371 // String length (assumed < 256 for these test fixtures).
1372 let mut length = [0u8; 32];
1373 length[31] = bytes.len() as u8;
1374 out.extend_from_slice(&length);
1375
1376 // String bytes, right-padded to a 32-byte boundary.
1377 out.extend_from_slice(bytes);
1378 let pad = (32 - (bytes.len() % 32)) % 32;
1379 out.extend(std::iter::repeat_n(0u8, pad));
1380
1381 Bytes::from(out)
1382 }
1383
1384 #[test]
1385 fn decodes_solidity_error_string() {
1386 let data = encode_solidity_error("transfer amount exceeds balance");
1387 let reason = decode_revert_reason(&data);
1388 assert_eq!(
1389 reason,
1390 RevertReason::Error("transfer amount exceeds balance".to_string())
1391 );
1392
1393 let err = SimulationError::from_revert(21_000, data);
1394 assert_eq!(
1395 err.revert_message(),
1396 Some("transfer amount exceeds balance")
1397 );
1398 assert!(err.panic_code().is_none());
1399 assert!(err.custom_error().is_none());
1400 }
1401
1402 #[test]
1403 fn decodes_panic_uint256() {
1404 // selector(4) | uint256 panic code (0x11 = arithmetic overflow).
1405 let mut data = PANIC_SELECTOR.to_vec();
1406 let mut code = [0u8; 32];
1407 code[31] = 0x11;
1408 data.extend_from_slice(&code);
1409 let data = Bytes::from(data);
1410
1411 let reason = decode_revert_reason(&data);
1412 assert_eq!(reason, RevertReason::Panic(0x11));
1413
1414 let err = SimulationError::from_revert(0, data);
1415 assert_eq!(err.panic_code(), Some(0x11));
1416 assert!(err.revert_message().is_none());
1417 }
1418
1419 #[test]
1420 fn standard_decoder_does_not_recognize_custom_errors() {
1421 // A registered-only selector is Unknown to the standard decoder.
1422 let data = Bytes::from(Paused::SELECTOR.to_vec());
1423 match decode_revert_reason(&data) {
1424 RevertReason::Unknown { selector, .. } => {
1425 assert_eq!(selector.as_slice(), &Paused::SELECTOR);
1426 }
1427 other => panic!("expected Unknown, got {other}"),
1428 }
1429 }
1430
1431 #[test]
1432 fn decodes_registered_custom_error_without_params() {
1433 let decoder = RevertDecoder::new().with_error::<Paused>();
1434 let data = Bytes::from(Paused::SELECTOR.to_vec());
1435
1436 match decoder.decode(&data) {
1437 RevertReason::Custom(custom) => {
1438 assert_eq!(custom.name, "Paused()");
1439 assert_eq!(custom.selector.as_slice(), &Paused::SELECTOR);
1440 assert_eq!(custom.params.as_deref(), Some("Paused"));
1441 }
1442 other => panic!("expected Custom, got {other}"),
1443 }
1444 }
1445
1446 #[test]
1447 fn decodes_registered_custom_error_with_params() {
1448 let decoder = RevertDecoder::new()
1449 .with_error::<Unauthorized>()
1450 .with_error::<ERC20InsufficientBalance>();
1451
1452 let caller = Address::repeat_byte(0xAB);
1453 let data = Bytes::from(Unauthorized { caller }.abi_encode());
1454 let custom = match decoder.decode(&data) {
1455 RevertReason::Custom(custom) => custom,
1456 other => panic!("expected Custom, got {other}"),
1457 };
1458 assert_eq!(custom.name, "Unauthorized(address)");
1459 let params = custom.params.expect("params should decode");
1460 // The Debug rendering of the decoded struct includes the address.
1461 assert!(params.contains(&format!("{caller:?}")), "got {params}");
1462
1463 // The IERC6093 standard error decodes through the same mechanism.
1464 let data = Bytes::from(
1465 ERC20InsufficientBalance {
1466 sender: caller,
1467 balance: U256::from(1u64),
1468 needed: U256::from(2u64),
1469 }
1470 .abi_encode(),
1471 );
1472 match decoder.decode(&data) {
1473 RevertReason::Custom(custom) => {
1474 assert_eq!(
1475 custom.name,
1476 "ERC20InsufficientBalance(address,uint256,uint256)"
1477 );
1478 }
1479 other => panic!("expected Custom, got {other}"),
1480 }
1481 }
1482
1483 #[test]
1484 fn register_raw_decodes_by_selector() {
1485 let mut decoder = RevertDecoder::new();
1486 decoder.register_raw([0xde, 0xad, 0xbe, 0xef], "MyError(uint256)", |data| {
1487 Some(format!("raw {} bytes", data.len()))
1488 });
1489
1490 let data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00]);
1491 match decoder.decode(&data) {
1492 RevertReason::Custom(custom) => {
1493 assert_eq!(custom.name, "MyError(uint256)");
1494 assert_eq!(custom.params.as_deref(), Some("raw 5 bytes"));
1495 }
1496 other => panic!("expected Custom, got {other}"),
1497 }
1498 }
1499
1500 #[test]
1501 fn unknown_blob_is_classified_as_unknown() {
1502 let data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00, 0x01, 0x02, 0x03]);
1503 match decode_revert_reason(&data) {
1504 RevertReason::Unknown {
1505 selector,
1506 data: blob,
1507 } => {
1508 assert_eq!(selector.as_slice(), &[0xde, 0xad, 0xbe, 0xef]);
1509 assert_eq!(blob.len(), 8);
1510 }
1511 other => panic!("expected Unknown, got {other}"),
1512 }
1513
1514 let err = SimulationError::from_revert(0, data);
1515 assert_eq!(
1516 err.selector().map(|s| s.to_vec()),
1517 Some(vec![0xde, 0xad, 0xbe, 0xef])
1518 );
1519 assert!(!err.is_empty_revert());
1520 }
1521
1522 #[test]
1523 fn empty_revert_data_decodes_to_empty() {
1524 let reason = decode_revert_reason(&Bytes::new());
1525 assert_eq!(reason, RevertReason::Empty);
1526
1527 let err = SimulationError::from_revert(0, Bytes::new());
1528 assert!(err.is_empty_revert());
1529 assert!(err.selector().is_none());
1530 }
1531
1532 #[test]
1533 fn data_shorter_than_selector_is_unknown_with_padded_selector() {
1534 let data = Bytes::from(vec![0x01, 0x02, 0x03]);
1535 match decode_revert_reason(&data) {
1536 RevertReason::Unknown { selector, .. } => {
1537 assert_eq!(selector.as_slice(), &[0x01, 0x02, 0x03, 0x00]);
1538 }
1539 other => panic!("expected Unknown, got {other}"),
1540 }
1541 }
1542}