use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, OnceLock};
use alloy_primitives::{Bytes, FixedBytes};
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 {}
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(anyhow::Error),
}
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<anyhow::Error> for SimError {
fn from(e: anyhow::Error) -> Self {
SimError::Other(e)
}
}
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}"),
}
}
}