Skip to main content

tempo_precompiles/
error.rs

1//! Unified error handling for Tempo precompiles.
2//!
3//! Provides [`TempoPrecompileError`] — the top-level error enum — along with an
4//! ABI-selector-based decoder registry for mapping raw revert bytes back to
5//! typed error variants.
6
7use std::{
8    collections::HashMap,
9    sync::{Arc, LazyLock},
10};
11
12use crate::{tip20::TIP20Error, tip1060_storage_credits::StorageCreditsError};
13use alloy::{
14    primitives::{FixedBytes, Selector, U256},
15    sol_types::{Panic, PanicKind, SolError, SolInterface},
16};
17use alloy_evm::EvmInternalsError;
18use revm::{
19    context::journaled_state::JournalLoadError,
20    precompile::{PrecompileError, PrecompileHalt, PrecompileOutput, PrecompileResult},
21};
22use tempo_contracts::precompiles::{
23    AccountKeychainError, AddrRegistryError, FeeManagerError, NonceError, ReceivePolicyGuardError,
24    RolesAuthError, SignatureVerifierError, StablecoinDEXError, TIP20ChannelReserveError,
25    TIP20FactoryError, TIP403RegistryError, TIP1060StorageCreditsError, TIPFeeAMMError,
26    UnknownFunctionSelector, ValidatorConfigError, ValidatorConfigV2Error,
27};
28
29/// Top-level error type for all Tempo precompile operations
30#[derive(
31    Debug, Clone, PartialEq, Eq, thiserror::Error, derive_more::From, derive_more::TryInto,
32)]
33pub enum TempoPrecompileError {
34    /// Stablecoin DEX error
35    #[error("Stablecoin DEX error: {0:?}")]
36    StablecoinDEX(StablecoinDEXError),
37
38    /// Error from TIP20 token
39    #[error("TIP20 token error: {0:?}")]
40    TIP20(TIP20Error),
41
42    /// Error from TIP20 factory
43    #[error("TIP20 factory error: {0:?}")]
44    TIP20Factory(TIP20FactoryError),
45
46    /// Error from TIP-20 channel reserve
47    #[error("TIP20 channel reserve error: {0:?}")]
48    TIP20ChannelReserveError(TIP20ChannelReserveError),
49
50    /// Error from roles auth
51    #[error("Roles auth error: {0:?}")]
52    RolesAuthError(RolesAuthError),
53
54    /// Error from TIP20 registry (TIP-1022)
55    #[error("TIP20 registry error: {0:?}")]
56    AddrRegistryError(AddrRegistryError),
57
58    /// Error from 403 registry
59    #[error("TIP403 registry error: {0:?}")]
60    TIP403RegistryError(TIP403RegistryError),
61
62    /// Error from TIP fee manager
63    #[error("TIP fee manager error: {0:?}")]
64    FeeManagerError(FeeManagerError),
65
66    /// Error from TIP fee AMM
67    #[error("TIP fee AMM error: {0:?}")]
68    TIPFeeAMMError(TIPFeeAMMError),
69
70    /// Error from Tempo Transaction nonce manager
71    #[error("Tempo Transaction nonce error: {0:?}")]
72    NonceError(NonceError),
73
74    /// EVM panic (i.e. arithmetic under/overflow, out-of-bounds access).
75    #[error("Panic({0:?})")]
76    Panic(PanicKind),
77
78    /// Error from validator config
79    #[error("Validator config error: {0:?}")]
80    ValidatorConfigError(ValidatorConfigError),
81
82    /// Error from validator config v2
83    #[error("Validator config v2 error: {0:?}")]
84    ValidatorConfigV2Error(ValidatorConfigV2Error),
85
86    /// Error from account keychain precompile
87    #[error("Account keychain error: {0:?}")]
88    AccountKeychainError(AccountKeychainError),
89
90    /// Error from signature verifier precompile
91    #[error("Signature verifier error: {0:?}")]
92    SignatureVerifierError(SignatureVerifierError),
93
94    /// Error from TIP-1028 blocked transfers precompile
95    #[error("TIP1028 blocked transfers error: {0:?}")]
96    ReceivePolicyGuardError(ReceivePolicyGuardError),
97
98    /// Error from TIP-1060 storage credits precompile
99    #[error("TIP1060 storage credits error: {0:?}")]
100    TIP1060StorageCreditsError(TIP1060StorageCreditsError),
101
102    /// Gas limit exceeded during precompile execution.
103    #[error("Gas limit exceeded")]
104    OutOfGas,
105
106    /// The calldata's 4-byte selector does not match any known precompile function.
107    #[error("Unknown function selector: {0:?}")]
108    UnknownFunctionSelector([u8; 4]),
109
110    /// Unrecoverable internal error (e.g. database failure).
111    #[error("Fatal precompile error: {0:?}")]
112    #[from(skip)]
113    Fatal(String),
114}
115
116impl From<EvmInternalsError> for TempoPrecompileError {
117    fn from(value: EvmInternalsError) -> Self {
118        match value {
119            EvmInternalsError::Database(e) => Self::Fatal(e.to_string()),
120        }
121    }
122}
123
124impl From<JournalLoadError<EvmInternalsError>> for TempoPrecompileError {
125    fn from(value: JournalLoadError<EvmInternalsError>) -> Self {
126        match value {
127            JournalLoadError::DBError(e) => Self::from(e),
128            JournalLoadError::ColdLoadSkipped => Self::OutOfGas,
129        }
130    }
131}
132
133impl From<JournalLoadError<revm::context::ErasedError>> for TempoPrecompileError {
134    fn from(value: JournalLoadError<revm::context::ErasedError>) -> Self {
135        match value {
136            JournalLoadError::DBError(e) => Self::Fatal(e.to_string()),
137            JournalLoadError::ColdLoadSkipped => Self::OutOfGas,
138        }
139    }
140}
141
142/// Result type alias for Tempo precompile operations
143pub type Result<T> = std::result::Result<T, TempoPrecompileError>;
144
145impl TempoPrecompileError {
146    /// Returns this error's ABI selector. For those variants which can't be encoded as a selector, it returns `FixedBytes<4>::ZERO`.
147    pub fn selector(&self) -> FixedBytes<4> {
148        match self {
149            Self::StablecoinDEX(e) => e.selector(),
150            Self::TIP20(e) => e.selector(),
151            Self::TIP20ChannelReserveError(e) => e.selector(),
152            Self::NonceError(e) => e.selector(),
153            Self::TIP20Factory(e) => e.selector(),
154            Self::RolesAuthError(e) => e.selector(),
155            Self::AddrRegistryError(e) => e.selector(),
156            Self::TIPFeeAMMError(e) => e.selector(),
157            Self::FeeManagerError(e) => e.selector(),
158            Self::TIP403RegistryError(e) => e.selector(),
159            Self::ValidatorConfigError(e) => e.selector(),
160            Self::ValidatorConfigV2Error(e) => e.selector(),
161            Self::AccountKeychainError(e) => e.selector(),
162            Self::SignatureVerifierError(e) => e.selector(),
163            Self::ReceivePolicyGuardError(e) => e.selector(),
164            Self::TIP1060StorageCreditsError(e) => e.selector(),
165            Self::UnknownFunctionSelector(selector) => *selector,
166            Self::Panic(_) => Panic::SELECTOR,
167            Self::OutOfGas | Self::Fatal(_) => [0, 0, 0, 0],
168        }
169        .into()
170    }
171
172    /// Returns true if this error represents a system-level failure that must be propagated
173    /// rather than swallowed, because state may be inconsistent.
174    pub fn is_system_error(&self) -> bool {
175        match self {
176            Self::OutOfGas | Self::Fatal(_) | Self::Panic(_) => true,
177            Self::StablecoinDEX(_)
178            | Self::TIP20(_)
179            | Self::TIP20ChannelReserveError(_)
180            | Self::NonceError(_)
181            | Self::TIP20Factory(_)
182            | Self::RolesAuthError(_)
183            | Self::AddrRegistryError(_)
184            | Self::TIPFeeAMMError(_)
185            | Self::FeeManagerError(_)
186            | Self::TIP403RegistryError(_)
187            | Self::ValidatorConfigError(_)
188            | Self::ValidatorConfigV2Error(_)
189            | Self::AccountKeychainError(_)
190            | Self::SignatureVerifierError(_)
191            | Self::ReceivePolicyGuardError(_)
192            | Self::TIP1060StorageCreditsError(_)
193            | Self::UnknownFunctionSelector(_) => false,
194        }
195    }
196
197    /// Creates an arithmetic under/overflow panic error.
198    pub fn under_overflow() -> Self {
199        Self::Panic(PanicKind::UnderOverflow)
200    }
201
202    /// Creates an enum conversion error panic (Solidity Panic `0x21`).
203    pub fn enum_conversion_error() -> Self {
204        Self::Panic(PanicKind::EnumConversionError)
205    }
206
207    /// Creates an array out-of-bounds panic error.
208    pub fn array_oob() -> Self {
209        Self::Panic(PanicKind::ArrayOutOfBounds)
210    }
211
212    /// ABI-encodes this error and wraps it as a reverted [`PrecompileResult`].
213    ///
214    /// # Errors
215    /// - `PrecompileOutput::halt(PrecompileHalt::OutOfGas, ..)` — if the variant is [`OutOfGas`](Self::OutOfGas)
216    /// - `PrecompileError::Fatal` — if the variant is [`Fatal`](Self::Fatal)
217    pub fn into_precompile_result(self, gas: u64, reservoir: u64) -> PrecompileResult {
218        let bytes = match self {
219            Self::StablecoinDEX(e) => e.abi_encode().into(),
220            Self::TIP20(e) => e.abi_encode().into(),
221            Self::TIP20Factory(e) => e.abi_encode().into(),
222            Self::TIP20ChannelReserveError(e) => e.abi_encode().into(),
223            Self::RolesAuthError(e) => e.abi_encode().into(),
224            Self::AddrRegistryError(e) => e.abi_encode().into(),
225            Self::TIP403RegistryError(e) => e.abi_encode().into(),
226            Self::FeeManagerError(e) => e.abi_encode().into(),
227            Self::TIPFeeAMMError(e) => e.abi_encode().into(),
228            Self::NonceError(e) => e.abi_encode().into(),
229            Self::Panic(kind) => {
230                let panic = Panic {
231                    code: U256::from(kind as u32),
232                };
233
234                panic.abi_encode().into()
235            }
236            Self::ValidatorConfigError(e) => e.abi_encode().into(),
237            Self::ValidatorConfigV2Error(e) => e.abi_encode().into(),
238            Self::AccountKeychainError(e) => e.abi_encode().into(),
239            Self::SignatureVerifierError(e) => e.abi_encode().into(),
240            Self::ReceivePolicyGuardError(e) => e.abi_encode().into(),
241            Self::TIP1060StorageCreditsError(e) => e.abi_encode().into(),
242            Self::OutOfGas => {
243                return Ok(PrecompileOutput::halt(PrecompileHalt::OutOfGas, reservoir));
244            }
245            Self::UnknownFunctionSelector(selector) => UnknownFunctionSelector {
246                selector: selector.into(),
247            }
248            .abi_encode()
249            .into(),
250            Self::Fatal(msg) => {
251                return Err(PrecompileError::Fatal(msg));
252            }
253        };
254        Ok(PrecompileOutput::revert(gas, bytes, reservoir))
255    }
256}
257
258/// Registers all ABI error selectors for a [`SolInterface`] type into the decoder registry.
259pub fn add_errors_to_registry<T: SolInterface>(
260    registry: &mut TempoPrecompileErrorRegistry,
261    converter: impl Fn(T) -> TempoPrecompileError + 'static + Send + Sync,
262) {
263    let converter = Arc::new(converter);
264    for selector in T::selectors() {
265        let converter = Arc::clone(&converter);
266        registry.insert(
267            selector.into(),
268            Box::new(move |data: &[u8]| {
269                T::abi_decode(data)
270                    .ok()
271                    .map(|error| DecodedTempoPrecompileError {
272                        error: converter(error),
273                        revert_bytes: data,
274                    })
275            }),
276        );
277    }
278}
279
280/// A decoded precompile error together with the raw revert bytes.
281pub struct DecodedTempoPrecompileError<'a> {
282    pub error: TempoPrecompileError,
283    pub revert_bytes: &'a [u8],
284}
285
286/// Maps ABI error selectors to their decoder functions.
287pub type TempoPrecompileErrorRegistry = HashMap<
288    Selector,
289    Box<dyn for<'a> Fn(&'a [u8]) -> Option<DecodedTempoPrecompileError<'a>> + Send + Sync>,
290>;
291
292/// Builds a [`TempoPrecompileErrorRegistry`] mapping every known error selector to its decoder.
293pub fn error_decoder_registry() -> TempoPrecompileErrorRegistry {
294    let mut registry: TempoPrecompileErrorRegistry = HashMap::new();
295
296    add_errors_to_registry(&mut registry, TempoPrecompileError::StablecoinDEX);
297    add_errors_to_registry(&mut registry, TempoPrecompileError::TIP20);
298    add_errors_to_registry(&mut registry, TempoPrecompileError::TIP20Factory);
299    add_errors_to_registry(
300        &mut registry,
301        TempoPrecompileError::TIP20ChannelReserveError,
302    );
303    add_errors_to_registry(&mut registry, TempoPrecompileError::RolesAuthError);
304    add_errors_to_registry(&mut registry, TempoPrecompileError::AddrRegistryError);
305    add_errors_to_registry(&mut registry, TempoPrecompileError::TIP403RegistryError);
306    add_errors_to_registry(&mut registry, TempoPrecompileError::FeeManagerError);
307    add_errors_to_registry(&mut registry, TempoPrecompileError::TIPFeeAMMError);
308    add_errors_to_registry(&mut registry, TempoPrecompileError::NonceError);
309    add_errors_to_registry(&mut registry, TempoPrecompileError::ValidatorConfigError);
310    add_errors_to_registry(&mut registry, TempoPrecompileError::ValidatorConfigV2Error);
311    add_errors_to_registry(&mut registry, TempoPrecompileError::AccountKeychainError);
312    add_errors_to_registry(&mut registry, TempoPrecompileError::SignatureVerifierError);
313    add_errors_to_registry(&mut registry, TempoPrecompileError::ReceivePolicyGuardError);
314    add_errors_to_registry(
315        &mut registry,
316        TempoPrecompileError::TIP1060StorageCreditsError,
317    );
318
319    registry
320}
321
322/// Global lazily-initialized registry of all Tempo precompile error decoders.
323pub static ERROR_REGISTRY: LazyLock<TempoPrecompileErrorRegistry> =
324    LazyLock::new(error_decoder_registry);
325
326/// Decodes raw revert bytes into a typed [`DecodedTempoPrecompileError`] using the global
327/// [`ERROR_REGISTRY`], returning `None` if the data is shorter than 4 bytes or the selector
328/// is unrecognized.
329pub fn decode_error<'a>(data: &'a [u8]) -> Option<DecodedTempoPrecompileError<'a>> {
330    if data.len() < 4 {
331        return None;
332    }
333
334    let selector: [u8; 4] = data[0..4].try_into().ok()?;
335    ERROR_REGISTRY
336        .get(&selector)
337        .and_then(|decoder| decoder(data))
338}
339
340/// Extension trait to convert `Result<T, TempoPrecompileError>` into a [`PrecompileResult`].
341pub trait IntoPrecompileResult<T> {
342    /// Converts `self` into a [`PrecompileResult`], using `encode_ok` for the success path.
343    fn into_precompile_result(
344        self,
345        gas: u64,
346        reservoir: u64,
347        encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes,
348    ) -> PrecompileResult;
349}
350
351impl<T> IntoPrecompileResult<T> for Result<T> {
352    fn into_precompile_result(
353        self,
354        gas: u64,
355        reservoir: u64,
356        encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes,
357    ) -> PrecompileResult {
358        match self {
359            Ok(res) => Ok(PrecompileOutput::new(gas, encode_ok(res), reservoir)),
360            Err(err) => err.into_precompile_result(gas, reservoir),
361        }
362    }
363}
364
365impl StorageCreditsError for TempoPrecompileError {
366    fn out_of_gas() -> Self {
367        Self::OutOfGas
368    }
369
370    fn fatal_external() -> Self {
371        Self::Fatal("invalid storage credits state".to_string())
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378    use tempo_contracts::precompiles::StablecoinDEXError;
379
380    #[test]
381    fn test_add_errors_to_registry_populates_registry() {
382        let mut registry: TempoPrecompileErrorRegistry = HashMap::new();
383
384        assert!(registry.is_empty());
385
386        add_errors_to_registry(&mut registry, TempoPrecompileError::StablecoinDEX);
387
388        assert!(!registry.is_empty());
389
390        let order_not_found_selector = StablecoinDEXError::order_does_not_exist().selector();
391        assert!(
392            registry.contains_key(&order_not_found_selector),
393            "Registry should contain OrderDoesNotExist selector"
394        );
395    }
396
397    #[test]
398    fn test_error_decoder_registry_is_not_empty() {
399        let registry = error_decoder_registry();
400
401        assert!(
402            !registry.is_empty(),
403            "error_decoder_registry should return a populated registry"
404        );
405
406        let dex_selector = StablecoinDEXError::order_does_not_exist().selector();
407        assert!(registry.contains_key(&dex_selector));
408    }
409
410    #[test]
411    fn test_decode_error_returns_some_for_valid_error() {
412        let error = StablecoinDEXError::order_does_not_exist();
413        let encoded = error.abi_encode();
414
415        let result = decode_error(&encoded);
416        assert!(
417            result.is_some(),
418            "decode_error should return Some for valid error"
419        );
420
421        let decoded = result.unwrap();
422        assert!(matches!(
423            decoded.error,
424            TempoPrecompileError::StablecoinDEX(StablecoinDEXError::OrderDoesNotExist(_))
425        ));
426    }
427
428    #[test]
429    fn test_decode_error_data_length_boundary() {
430        // Empty data (len = 0) should return None (0 < 4)
431        let result = decode_error(&[]);
432        assert!(result.is_none(), "Empty data should return None");
433
434        // 1 byte (len = 1) should return None (1 < 4)
435        let result = decode_error(&[0x01]);
436        assert!(result.is_none(), "1 byte should return None");
437
438        // 2 bytes (len = 2) should return None (2 < 4)
439        let result = decode_error(&[0x01, 0x02]);
440        assert!(result.is_none(), "2 bytes should return None");
441
442        // 3 bytes (len = 3) should return None (3 < 4)
443        let result = decode_error(&[0x01, 0x02, 0x03]);
444        assert!(result.is_none(), "3 bytes should return None");
445
446        // 4 bytes with unknown selector returns None (selector not found)
447        let result = decode_error(&[0x00, 0x00, 0x00, 0x00]);
448        assert!(
449            result.is_none(),
450            "Unknown 4-byte selector should return None"
451        );
452
453        // 4 bytes with valid selector (exactly at boundary) should succeed
454        let error = StablecoinDEXError::order_does_not_exist();
455        let encoded = error.abi_encode();
456        let result = decode_error(&encoded);
457        assert!(
458            result.is_some(),
459            "Valid error at 4+ bytes should return Some"
460        );
461    }
462
463    #[test]
464    fn test_into_precompile_result_revert() {
465        let error = TempoPrecompileError::StablecoinDEX(StablecoinDEXError::order_does_not_exist());
466        let result = error.into_precompile_result(0, 0);
467
468        let output = result.expect("business-logic revert should be Ok");
469        assert!(output.status.is_revert());
470    }
471
472    #[test]
473    fn test_into_precompile_result_trait_success() {
474        let result: Result<u64> = Ok(42);
475        let precompile_result = result.into_precompile_result(0, 0, |val| {
476            alloy::primitives::Bytes::from(val.to_be_bytes().to_vec())
477        });
478
479        let output = precompile_result.expect("success should be Ok");
480        assert!(output.status.is_success());
481    }
482
483    #[test]
484    fn test_decode_error_with_tip20_error() {
485        // Use insufficient_allowance which has a unique selector (no collision with other errors)
486        let error = TIP20Error::insufficient_allowance();
487        let encoded = error.abi_encode();
488
489        let result = decode_error(&encoded);
490        assert!(result.is_some(), "Should decode TIP20 errors");
491
492        let decoded = result.unwrap();
493        // Verify it's a TIP20 error
494        match decoded.error {
495            TempoPrecompileError::TIP20(_) => {}
496            other => panic!("Expected TIP20 error, got {other:?}"),
497        }
498    }
499}