use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::sync::{Arc, OnceLock};
use std::{borrow::Cow, io};
use alloy_primitives::{Address, B256, Bytes, FixedBytes, U256};
use alloy_sol_types::SolError;
use tracing::warn;
pub const ERROR_SELECTOR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0];
pub const PANIC_SELECTOR: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomRevert {
pub name: Cow<'static, str>,
pub selector: FixedBytes<4>,
pub params: Option<String>,
pub data: Bytes,
}
impl fmt::Display for CustomRevert {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.params {
Some(params) => write!(f, "{params}"),
None => write!(f, "{}", self.name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RevertReason {
Empty,
Error(String),
Panic(u64),
Custom(CustomRevert),
Unknown {
selector: FixedBytes<4>,
data: Bytes,
},
}
impl fmt::Display for RevertReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RevertReason::Empty => write!(f, "<empty revert>"),
RevertReason::Error(msg) => write!(f, "Error({msg:?})"),
RevertReason::Panic(code) => write!(f, "Panic({code:#x})"),
RevertReason::Custom(custom) => write!(f, "{custom}"),
RevertReason::Unknown { selector, data } => {
write!(f, "Unknown(selector={selector}, data_len={})", data.len())
}
}
}
}
type DecodeFn = Arc<dyn Fn(&Bytes) -> Option<String> + Send + Sync>;
#[derive(Clone)]
struct CustomErrorDecoder {
name: Cow<'static, str>,
decode: DecodeFn,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DuplicateSelectorError {
pub selector: FixedBytes<4>,
pub existing: Cow<'static, str>,
pub attempted: Cow<'static, str>,
}
impl fmt::Display for DuplicateSelectorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"duplicate custom error selector {}: keeping {}, ignoring {}",
self.selector, self.existing, self.attempted
)
}
}
impl std::error::Error for DuplicateSelectorError {}
#[derive(Clone, Default)]
pub struct RevertDecoder {
custom: HashMap<[u8; 4], CustomErrorDecoder>,
}
impl fmt::Debug for RevertDecoder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut names: Vec<&str> = self.custom.values().map(|d| d.name.as_ref()).collect();
names.sort_unstable();
f.debug_struct("RevertDecoder")
.field("custom_errors", &names)
.finish()
}
}
impl RevertDecoder {
pub fn new() -> Self {
Self::default()
}
pub fn with_error<E>(mut self) -> Self
where
E: SolError + fmt::Debug + 'static,
{
self.register::<E>();
self
}
pub fn register<E>(&mut self) -> &mut Self
where
E: SolError + fmt::Debug + 'static,
{
if let Err(err) = self.try_register::<E>() {
warn_duplicate_selector(&err);
}
self
}
pub fn try_register<E>(&mut self) -> Result<&mut Self, DuplicateSelectorError>
where
E: SolError + fmt::Debug + 'static,
{
let decode: DecodeFn =
Arc::new(|data: &Bytes| E::abi_decode(data).ok().map(|err| format!("{err:?}")));
self.insert_custom_error(
E::SELECTOR,
CustomErrorDecoder {
name: Cow::Borrowed(E::SIGNATURE),
decode,
},
)
}
pub fn register_raw(
&mut self,
selector: [u8; 4],
name: impl Into<Cow<'static, str>>,
decode: impl Fn(&Bytes) -> Option<String> + Send + Sync + 'static,
) -> &mut Self {
if let Err(err) = self.try_register_raw(selector, name, decode) {
warn_duplicate_selector(&err);
}
self
}
pub fn try_register_raw(
&mut self,
selector: [u8; 4],
name: impl Into<Cow<'static, str>>,
decode: impl Fn(&Bytes) -> Option<String> + Send + Sync + 'static,
) -> Result<&mut Self, DuplicateSelectorError> {
self.insert_custom_error(
selector,
CustomErrorDecoder {
name: name.into(),
decode: Arc::new(decode),
},
)
}
pub fn len(&self) -> usize {
self.custom.len()
}
pub fn is_empty(&self) -> bool {
self.custom.is_empty()
}
pub fn decode(&self, data: &Bytes) -> RevertReason {
if data.is_empty() {
return RevertReason::Empty;
}
if data.len() < 4 {
let mut selector = [0u8; 4];
selector[..data.len()].copy_from_slice(&data[..]);
return RevertReason::Unknown {
selector: FixedBytes::from(selector),
data: data.clone(),
};
}
let selector: [u8; 4] = data[..4].try_into().expect("length checked >= 4");
if selector == ERROR_SELECTOR
&& let Some(message) = decode_solidity_error_string(data)
{
return RevertReason::Error(message);
}
if selector == PANIC_SELECTOR
&& let Some(code) = decode_solidity_panic(data)
{
return RevertReason::Panic(code);
}
if let Some(entry) = self.custom.get(&selector) {
return RevertReason::Custom(CustomRevert {
name: entry.name.clone(),
selector: FixedBytes::from(selector),
params: (entry.decode)(data),
data: data.clone(),
});
}
RevertReason::Unknown {
selector: FixedBytes::from(selector),
data: data.clone(),
}
}
fn insert_custom_error(
&mut self,
selector: [u8; 4],
decoder: CustomErrorDecoder,
) -> Result<&mut Self, DuplicateSelectorError> {
if let Some(existing) = self.custom.get(&selector) {
return Err(DuplicateSelectorError {
selector: FixedBytes::from(selector),
existing: existing.name.clone(),
attempted: decoder.name,
});
}
self.custom.insert(selector, decoder);
Ok(self)
}
}
fn warn_duplicate_selector(err: &DuplicateSelectorError) {
warn!(
selector = %err.selector,
existing = err.existing.as_ref(),
attempted = err.attempted.as_ref(),
"duplicate custom error selector registration ignored; keeping first registration"
);
}
pub fn decode_revert_reason(data: &Bytes) -> RevertReason {
static STANDARD: OnceLock<RevertDecoder> = OnceLock::new();
STANDARD.get_or_init(RevertDecoder::new).decode(data)
}
fn decode_solidity_panic(data: &Bytes) -> Option<u64> {
alloy_sol_types::Panic::abi_decode(data)
.ok()
.and_then(|panic| u64::try_from(panic.code).ok())
}
fn decode_solidity_error_string(data: &Bytes) -> Option<String> {
alloy_sol_types::Revert::abi_decode(data)
.ok()
.map(|revert| revert.reason)
}
#[derive(Debug, Clone)]
pub struct SimulationError {
pub gas_used: u64,
pub revert_data: Bytes,
pub reason: RevertReason,
}
impl SimulationError {
pub fn from_revert(gas_used: u64, output: Bytes) -> Self {
let reason = decode_revert_reason(&output);
Self {
gas_used,
revert_data: output,
reason,
}
}
pub fn from_revert_with(gas_used: u64, output: Bytes, decoder: &RevertDecoder) -> Self {
let reason = decoder.decode(&output);
Self {
gas_used,
revert_data: output,
reason,
}
}
pub fn reason(&self) -> &RevertReason {
&self.reason
}
pub fn revert_message(&self) -> Option<&str> {
match &self.reason {
RevertReason::Error(message) => Some(message.as_str()),
_ => None,
}
}
pub fn panic_code(&self) -> Option<u64> {
match self.reason {
RevertReason::Panic(code) => Some(code),
_ => None,
}
}
pub fn custom_error(&self) -> Option<&CustomRevert> {
match &self.reason {
RevertReason::Custom(custom) => Some(custom),
_ => None,
}
}
pub fn selector(&self) -> Option<FixedBytes<4>> {
match &self.reason {
RevertReason::Custom(custom) => Some(custom.selector),
RevertReason::Unknown { selector, .. } => Some(*selector),
_ => None,
}
}
pub fn is_empty_revert(&self) -> bool {
matches!(self.reason, RevertReason::Empty)
}
}
impl fmt::Display for SimulationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"SimulationError(gas_used={}, reason={})",
self.gas_used, self.reason
)
}
}
impl std::error::Error for SimulationError {}
#[derive(Debug, thiserror::Error)]
pub enum BlockContextError {
#[error("block-context header fetch failed: {0}")]
FetchFailed(String),
#[error("required block-context field missing: {field}")]
MissingField {
field: &'static str,
},
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum RuntimeError {
#[error(
"evm-fork-cache RPC operations require a multi-thread tokio runtime; \
found a current-thread runtime (block_in_place is not supported there). \
Build the runtime with `tokio::runtime::Builder::new_multi_thread()` \
or annotate with `#[tokio::main(flavor = \"multi_thread\")]`"
)]
CurrentThreadRuntime,
#[error(
"evm-fork-cache RPC operations require a running multi-thread tokio runtime: {details}"
)]
MissingRuntime {
details: String,
},
}
#[derive(Debug, thiserror::Error)]
pub enum RpcError {
#[error(transparent)]
Runtime(#[from] RuntimeError),
#[error("RPC provider call {operation} failed: {details}")]
Provider {
operation: &'static str,
details: String,
},
#[error("custom RPC callback failed: {message}")]
Custom {
message: String,
},
}
impl RpcError {
pub fn provider(operation: &'static str, source: impl fmt::Display) -> Self {
Self::Provider {
operation,
details: source.to_string(),
}
}
pub fn custom(message: impl Into<String>) -> Self {
Self::Custom {
message: message.into(),
}
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum StorageFetchError {
#[error(transparent)]
Runtime(#[from] RuntimeError),
#[error("storage/provider request {operation} failed: {details}")]
Provider {
operation: &'static str,
details: String,
},
#[error("failed to serialize storage batch request: {details}")]
Serialization {
details: String,
},
#[error("failed to send storage batch request: {details}")]
BatchSend {
details: String,
},
#[error("custom storage/proof fetcher failed: {message}")]
Custom {
message: String,
},
}
impl StorageFetchError {
pub fn provider(operation: &'static str, source: impl fmt::Display) -> Self {
Self::Provider {
operation,
details: source.to_string(),
}
}
pub fn batch_send(source: impl fmt::Display) -> Self {
Self::BatchSend {
details: source.to_string(),
}
}
pub fn serialization(source: impl fmt::Display) -> Self {
Self::Serialization {
details: source.to_string(),
}
}
pub fn custom(message: impl Into<String>) -> Self {
Self::Custom {
message: message.into(),
}
}
}
pub type StorageFetchResult<T> = Result<T, StorageFetchError>;
#[derive(Debug, thiserror::Error)]
pub enum PersistenceError {
#[error("failed to serialize {label}: {source}")]
Serialize {
label: &'static str,
#[source]
source: bincode::Error,
},
#[error("failed to create directory {path:?}: {source}")]
CreateDir {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to write {path:?}: {source}")]
Write {
path: PathBuf,
#[source]
source: io::Error,
},
}
impl PersistenceError {
pub(crate) fn serialize(label: &'static str, source: bincode::Error) -> Self {
Self::Serialize { label, source }
}
pub(crate) fn create_dir(path: impl Into<PathBuf>, source: io::Error) -> Self {
Self::CreateDir {
path: path.into(),
source,
}
}
pub(crate) fn write(path: impl Into<PathBuf>, source: io::Error) -> Self {
Self::Write {
path: path.into(),
source,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CacheError {
#[error(transparent)]
Runtime(#[from] RuntimeError),
#[error(transparent)]
Rpc(#[from] RpcError),
#[error(transparent)]
Persistence(#[from] PersistenceError),
#[error("storage verification requires a storage batch fetcher")]
MissingStorageBatchFetcher,
#[error("code-seed verification requires an account fields fetcher")]
MissingAccountFieldsFetcher,
#[error(
"reconcile could not fetch any of the {requested} requested slot(s) \
(no usable storage fetcher / provider unreachable)"
)]
ReconcileFetchFailed {
requested: usize,
},
#[error("failed to fetch account {address}: {details}")]
AccountFetch {
address: Address,
details: String,
},
#[error(
"code seed for {address} conflicts with chain-fetched code \
(cached hash {cached}, seeded hash {seeded})"
)]
CodeSeedConflict {
address: Address,
cached: B256,
seeded: B256,
},
#[error("cannot seed/etch empty code at {address}")]
CodeSeedEmpty {
address: Address,
},
#[error("storage read failed for {address} slot {slot}: {details}")]
StorageRead {
address: Address,
slot: U256,
details: String,
},
#[error("storage insert failed for {address} slot {slot}: {details}")]
StorageInsert {
address: Address,
slot: U256,
details: String,
},
#[error("failed to build transaction environment: {details}")]
TxEnv {
details: String,
},
#[error("failed to transact: {details}")]
Transact {
details: String,
},
#[error("EVM call did not succeed: {result}")]
CallNotSuccessful {
result: String,
},
#[error("failed to parse block state trace: {details}")]
TraceParse {
details: String,
},
#[error("failed to decode {what}: {details}")]
Decode {
what: &'static str,
details: String,
},
#[error("Solidity call {signature} from {from:?} to {to:?} did not succeed: {result}")]
SolCallFailed {
signature: &'static str,
from: Address,
to: Address,
result: String,
},
#[error(
"failed to decode Solidity call {signature} return data from {from:?} to {to:?}: \
output_len={output_len}, error: {details}"
)]
SolCallDecode {
signature: &'static str,
from: Address,
to: Address,
output_len: usize,
details: String,
},
#[error("contract deployment succeeded but no address returned")]
DeploymentMissingAddress,
#[error("contract deployment reverted: 0x{output_hex}")]
DeploymentReverted {
output_hex: String,
},
#[error("contract deployment halted: {reason}")]
DeploymentHalted {
reason: String,
},
#[error("no bytecode found at source address {source_address}")]
MissingSourceBytecode {
source_address: Address,
},
#[error("{role} account {address} has no runtime bytecode")]
MissingRuntimeCode {
role: &'static str,
address: Address,
},
#[error(
"target account {target} not found; use override_or_create_account_code for synthetic targets"
)]
MissingTargetAccount {
target: Address,
},
#[error("failed to fetch target account {target}: {details}")]
TargetAccountFetch {
target: Address,
details: String,
},
}
impl CacheError {
pub fn tx_env(source: impl fmt::Debug) -> Self {
Self::TxEnv {
details: format!("{source:?}"),
}
}
pub fn transact(source: impl fmt::Debug) -> Self {
Self::Transact {
details: format!("{source:?}"),
}
}
}
pub type CacheResult<T, E = CacheError> = Result<T, E>;
#[derive(Debug, thiserror::Error)]
pub enum OverlayError {
#[error("failed to build transaction environment: {details}")]
TxEnv {
details: String,
},
#[error("failed to transact: {details}")]
Transact {
details: String,
},
#[error("Solidity call {signature} from {from:?} to {to:?} did not succeed: {result}")]
SolCallFailed {
signature: &'static str,
from: Address,
to: Address,
result: String,
},
#[error(
"failed to decode return of {signature} from {from:?} to {to:?} ({output_len} bytes): {details}"
)]
SolCallDecode {
signature: &'static str,
from: Address,
to: Address,
output_len: usize,
details: String,
},
}
impl OverlayError {
pub fn tx_env(source: impl fmt::Debug) -> Self {
Self::TxEnv {
details: format!("{source:?}"),
}
}
pub fn transact(source: impl fmt::Debug) -> Self {
Self::Transact {
details: format!("{source:?}"),
}
}
}
pub type OverlayResult<T, E = OverlayError> = Result<T, E>;
#[derive(Debug, thiserror::Error)]
pub enum SimHostError {
#[error("failed to build transaction environment: {details}")]
TxEnv {
details: String,
},
#[error("failed to transact: {details}")]
Transact {
details: String,
},
#[error("database operation failed: {details}")]
Database {
details: String,
},
#[error(transparent)]
Cache(#[from] CacheError),
#[error(transparent)]
Overlay(#[from] OverlayError),
}
impl SimHostError {
pub fn tx_env(source: impl fmt::Debug) -> Self {
Self::TxEnv {
details: format!("{source:?}"),
}
}
pub fn transact(source: impl fmt::Debug) -> Self {
Self::Transact {
details: format!("{source:?}"),
}
}
pub fn database(source: impl fmt::Debug) -> Self {
Self::Database {
details: format!("{source:?}"),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum MulticallError {
#[error(transparent)]
Cache(#[from] CacheError),
#[error("Multicall3 aggregate call failed: {result}")]
AggregateFailed {
result: String,
},
#[error("multicall result indicates the call failed")]
CallFailed,
#[error("failed to decode multicall result: {details}")]
Decode {
details: String,
},
}
pub type MulticallResult<T> = Result<T, MulticallError>;
#[derive(Debug, thiserror::Error)]
pub enum DeployError {
#[error("failed to read Foundry artifact at {path}: {source}")]
ReadArtifact {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("failed to parse Foundry artifact JSON at {path}: {source}")]
ParseArtifact {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("artifact {path} has no `bytecode` field")]
MissingBytecodeField {
path: PathBuf,
},
#[error("artifact {path} has no `bytecode.object` string")]
MissingBytecodeObject {
path: PathBuf,
},
#[error("empty bytecode")]
EmptyBytecode,
#[error("bytecode contains unresolved library placeholders")]
UnresolvedLibraryPlaceholders,
#[error("invalid hex bytecode: {details}")]
InvalidHex {
details: String,
},
#[error(transparent)]
Cache(#[from] CacheError),
#[error("deploying Foundry artifact {path} failed: {source}")]
ArtifactDeploy {
path: PathBuf,
#[source]
source: CacheError,
},
#[error("validating target contract {target} failed: {source}")]
TargetValidation {
target: Address,
#[source]
source: CacheError,
},
#[error("etching runtime bytecode at {target} failed: {source}")]
EtchRuntime {
target: Address,
#[source]
source: CacheError,
},
}
pub type DeployResult<T> = Result<T, DeployError>;
#[derive(Debug, thiserror::Error)]
pub enum AccessListError {
#[error("failed to query {operation}: {details}")]
Query {
operation: &'static str,
details: String,
},
}
impl AccessListError {
pub fn query(operation: &'static str, source: impl fmt::Display) -> Self {
Self::Query {
operation,
details: source.to_string(),
}
}
}
pub type AccessListResult<T> = Result<T, AccessListError>;
#[derive(Debug, thiserror::Error)]
pub enum FreshnessError {
#[error("validation handle already consumed")]
ValidationHandleConsumed,
#[error("validation task failed: {source}")]
ValidationTaskFailed {
#[source]
source: tokio::task::JoinError,
},
#[error(transparent)]
Overlay(#[from] OverlayError),
}
pub type FreshnessResult<T> = Result<T, FreshnessError>;
pub type SimulationResult<T> = Result<T, SimError>;
#[derive(Debug, thiserror::Error)]
pub enum SimError {
#[error("transaction reverted: {0}")]
Revert(#[source] Box<SimulationError>),
#[error("transaction halted: {reason} (gas used {gas_used})")]
Halt {
reason: String,
gas_used: u64,
},
#[error("{0}")]
Other(#[source] SimHostError),
}
impl SimError {
pub fn is_revert(&self) -> bool {
matches!(self, SimError::Revert(_))
}
pub fn is_halt(&self) -> bool {
matches!(self, SimError::Halt { .. })
}
pub fn as_revert(&self) -> Option<&SimulationError> {
match self {
SimError::Revert(e) => Some(e),
_ => None,
}
}
}
impl From<SimHostError> for SimError {
fn from(e: SimHostError) -> Self {
SimError::Other(e)
}
}
impl From<CacheError> for SimError {
fn from(e: CacheError) -> Self {
SimError::Other(e.into())
}
}
impl From<OverlayError> for SimError {
fn from(e: OverlayError) -> Self {
SimError::Other(e.into())
}
}
impl From<SimulationError> for SimError {
fn from(e: SimulationError) -> Self {
SimError::Revert(Box::new(e))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
sol! {
#[derive(Debug)]
error Unauthorized(address caller);
#[derive(Debug)]
error Paused();
#[derive(Debug)]
error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
}
fn encode_solidity_error(message: &str) -> Bytes {
let bytes = message.as_bytes();
let mut out = Vec::new();
out.extend_from_slice(&ERROR_SELECTOR);
let mut offset = [0u8; 32];
offset[31] = 0x20;
out.extend_from_slice(&offset);
let mut length = [0u8; 32];
length[31] = bytes.len() as u8;
out.extend_from_slice(&length);
out.extend_from_slice(bytes);
let pad = (32 - (bytes.len() % 32)) % 32;
out.extend(std::iter::repeat_n(0u8, pad));
Bytes::from(out)
}
#[test]
fn decodes_solidity_error_string() {
let data = encode_solidity_error("transfer amount exceeds balance");
let reason = decode_revert_reason(&data);
assert_eq!(
reason,
RevertReason::Error("transfer amount exceeds balance".to_string())
);
let err = SimulationError::from_revert(21_000, data);
assert_eq!(
err.revert_message(),
Some("transfer amount exceeds balance")
);
assert!(err.panic_code().is_none());
assert!(err.custom_error().is_none());
}
#[test]
fn decodes_panic_uint256() {
let mut data = PANIC_SELECTOR.to_vec();
let mut code = [0u8; 32];
code[31] = 0x11;
data.extend_from_slice(&code);
let data = Bytes::from(data);
let reason = decode_revert_reason(&data);
assert_eq!(reason, RevertReason::Panic(0x11));
let err = SimulationError::from_revert(0, data);
assert_eq!(err.panic_code(), Some(0x11));
assert!(err.revert_message().is_none());
}
#[test]
fn standard_decoder_does_not_recognize_custom_errors() {
let data = Bytes::from(Paused::SELECTOR.to_vec());
match decode_revert_reason(&data) {
RevertReason::Unknown { selector, .. } => {
assert_eq!(selector.as_slice(), &Paused::SELECTOR);
}
other => panic!("expected Unknown, got {other}"),
}
}
#[test]
fn decodes_registered_custom_error_without_params() {
let decoder = RevertDecoder::new().with_error::<Paused>();
let data = Bytes::from(Paused::SELECTOR.to_vec());
match decoder.decode(&data) {
RevertReason::Custom(custom) => {
assert_eq!(custom.name, "Paused()");
assert_eq!(custom.selector.as_slice(), &Paused::SELECTOR);
assert_eq!(custom.params.as_deref(), Some("Paused"));
}
other => panic!("expected Custom, got {other}"),
}
}
#[test]
fn decodes_registered_custom_error_with_params() {
let decoder = RevertDecoder::new()
.with_error::<Unauthorized>()
.with_error::<ERC20InsufficientBalance>();
let caller = Address::repeat_byte(0xAB);
let data = Bytes::from(Unauthorized { caller }.abi_encode());
let custom = match decoder.decode(&data) {
RevertReason::Custom(custom) => custom,
other => panic!("expected Custom, got {other}"),
};
assert_eq!(custom.name, "Unauthorized(address)");
let params = custom.params.expect("params should decode");
assert!(params.contains(&format!("{caller:?}")), "got {params}");
let data = Bytes::from(
ERC20InsufficientBalance {
sender: caller,
balance: U256::from(1u64),
needed: U256::from(2u64),
}
.abi_encode(),
);
match decoder.decode(&data) {
RevertReason::Custom(custom) => {
assert_eq!(
custom.name,
"ERC20InsufficientBalance(address,uint256,uint256)"
);
}
other => panic!("expected Custom, got {other}"),
}
}
#[test]
fn register_raw_decodes_by_selector() {
let mut decoder = RevertDecoder::new();
decoder.register_raw([0xde, 0xad, 0xbe, 0xef], "MyError(uint256)", |data| {
Some(format!("raw {} bytes", data.len()))
});
let data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00]);
match decoder.decode(&data) {
RevertReason::Custom(custom) => {
assert_eq!(custom.name, "MyError(uint256)");
assert_eq!(custom.params.as_deref(), Some("raw 5 bytes"));
}
other => panic!("expected Custom, got {other}"),
}
}
#[test]
fn unknown_blob_is_classified_as_unknown() {
let data = Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0x00, 0x01, 0x02, 0x03]);
match decode_revert_reason(&data) {
RevertReason::Unknown {
selector,
data: blob,
} => {
assert_eq!(selector.as_slice(), &[0xde, 0xad, 0xbe, 0xef]);
assert_eq!(blob.len(), 8);
}
other => panic!("expected Unknown, got {other}"),
}
let err = SimulationError::from_revert(0, data);
assert_eq!(
err.selector().map(|s| s.to_vec()),
Some(vec![0xde, 0xad, 0xbe, 0xef])
);
assert!(!err.is_empty_revert());
}
#[test]
fn empty_revert_data_decodes_to_empty() {
let reason = decode_revert_reason(&Bytes::new());
assert_eq!(reason, RevertReason::Empty);
let err = SimulationError::from_revert(0, Bytes::new());
assert!(err.is_empty_revert());
assert!(err.selector().is_none());
}
#[test]
fn data_shorter_than_selector_is_unknown_with_padded_selector() {
let data = Bytes::from(vec![0x01, 0x02, 0x03]);
match decode_revert_reason(&data) {
RevertReason::Unknown { selector, .. } => {
assert_eq!(selector.as_slice(), &[0x01, 0x02, 0x03, 0x00]);
}
other => panic!("expected Unknown, got {other}"),
}
}
}