1use std::{
8 collections::HashMap,
9 sync::{Arc, LazyLock},
10};
11
12use crate::tip20::TIP20Error;
13use alloy::{
14 primitives::{Selector, U256},
15 sol_types::{Panic, PanicKind, SolError, SolInterface},
16};
17use revm::{
18 context::journaled_state::JournalLoadErasedError,
19 precompile::{PrecompileError, PrecompileOutput, PrecompileResult},
20};
21use tempo_contracts::precompiles::{
22 AccountKeychainError, FeeManagerError, NonceError, RolesAuthError, StablecoinDEXError,
23 TIP20FactoryError, TIP403RegistryError, TIPFeeAMMError, UnknownFunctionSelector,
24 ValidatorConfigError, ValidatorConfigV2Error,
25};
26
27#[derive(
29 Debug, Clone, PartialEq, Eq, thiserror::Error, derive_more::From, derive_more::TryInto,
30)]
31pub enum TempoPrecompileError {
32 #[error("Stablecoin DEX error: {0:?}")]
34 StablecoinDEX(StablecoinDEXError),
35
36 #[error("TIP20 token error: {0:?}")]
38 TIP20(TIP20Error),
39
40 #[error("TIP20 factory error: {0:?}")]
42 TIP20Factory(TIP20FactoryError),
43
44 #[error("Roles auth error: {0:?}")]
46 RolesAuthError(RolesAuthError),
47
48 #[error("TIP403 registry error: {0:?}")]
50 TIP403RegistryError(TIP403RegistryError),
51
52 #[error("TIP fee manager error: {0:?}")]
54 FeeManagerError(FeeManagerError),
55
56 #[error("TIP fee AMM error: {0:?}")]
58 TIPFeeAMMError(TIPFeeAMMError),
59
60 #[error("Tempo Transaction nonce error: {0:?}")]
62 NonceError(NonceError),
63
64 #[error("Panic({0:?})")]
66 Panic(PanicKind),
67
68 #[error("Validator config error: {0:?}")]
70 ValidatorConfigError(ValidatorConfigError),
71
72 #[error("Validator config v2 error: {0:?}")]
74 ValidatorConfigV2Error(ValidatorConfigV2Error),
75
76 #[error("Account keychain error: {0:?}")]
78 AccountKeychainError(AccountKeychainError),
79
80 #[error("Gas limit exceeded")]
82 OutOfGas,
83
84 #[error("Unknown function selector: {0:?}")]
86 UnknownFunctionSelector([u8; 4]),
87
88 #[error("Fatal precompile error: {0:?}")]
90 #[from(skip)]
91 Fatal(String),
92}
93
94impl From<JournalLoadErasedError> for TempoPrecompileError {
95 fn from(value: JournalLoadErasedError) -> Self {
96 match value {
97 JournalLoadErasedError::DBError(e) => Self::Fatal(e.to_string()),
98 JournalLoadErasedError::ColdLoadSkipped => Self::OutOfGas,
99 }
100 }
101}
102
103pub type Result<T> = std::result::Result<T, TempoPrecompileError>;
105
106impl TempoPrecompileError {
107 pub fn is_system_error(&self) -> bool {
110 match self {
111 Self::OutOfGas | Self::Fatal(_) | Self::Panic(_) => true,
112 Self::StablecoinDEX(_)
113 | Self::TIP20(_)
114 | Self::NonceError(_)
115 | Self::TIP20Factory(_)
116 | Self::RolesAuthError(_)
117 | Self::TIPFeeAMMError(_)
118 | Self::FeeManagerError(_)
119 | Self::TIP403RegistryError(_)
120 | Self::ValidatorConfigError(_)
121 | Self::ValidatorConfigV2Error(_)
122 | Self::AccountKeychainError(_)
123 | Self::UnknownFunctionSelector(_) => false,
124 }
125 }
126
127 pub fn under_overflow() -> Self {
129 Self::Panic(PanicKind::UnderOverflow)
130 }
131
132 pub fn array_oob() -> Self {
134 Self::Panic(PanicKind::ArrayOutOfBounds)
135 }
136
137 pub fn into_precompile_result(self, gas: u64) -> PrecompileResult {
143 let bytes = match self {
144 Self::StablecoinDEX(e) => e.abi_encode().into(),
145 Self::TIP20(e) => e.abi_encode().into(),
146 Self::TIP20Factory(e) => e.abi_encode().into(),
147 Self::RolesAuthError(e) => e.abi_encode().into(),
148 Self::TIP403RegistryError(e) => e.abi_encode().into(),
149 Self::FeeManagerError(e) => e.abi_encode().into(),
150 Self::TIPFeeAMMError(e) => e.abi_encode().into(),
151 Self::NonceError(e) => e.abi_encode().into(),
152 Self::Panic(kind) => {
153 let panic = Panic {
154 code: U256::from(kind as u32),
155 };
156
157 panic.abi_encode().into()
158 }
159 Self::ValidatorConfigError(e) => e.abi_encode().into(),
160 Self::ValidatorConfigV2Error(e) => e.abi_encode().into(),
161 Self::AccountKeychainError(e) => e.abi_encode().into(),
162 Self::OutOfGas => {
163 return Err(PrecompileError::OutOfGas);
164 }
165 Self::UnknownFunctionSelector(selector) => UnknownFunctionSelector {
166 selector: selector.into(),
167 }
168 .abi_encode()
169 .into(),
170 Self::Fatal(msg) => {
171 return Err(PrecompileError::Fatal(msg));
172 }
173 };
174 Ok(PrecompileOutput::new_reverted(gas, bytes))
175 }
176}
177
178pub fn add_errors_to_registry<T: SolInterface>(
180 registry: &mut TempoPrecompileErrorRegistry,
181 converter: impl Fn(T) -> TempoPrecompileError + 'static + Send + Sync,
182) {
183 let converter = Arc::new(converter);
184 for selector in T::selectors() {
185 let converter = Arc::clone(&converter);
186 registry.insert(
187 selector.into(),
188 Box::new(move |data: &[u8]| {
189 T::abi_decode(data)
190 .ok()
191 .map(|error| DecodedTempoPrecompileError {
192 error: converter(error),
193 revert_bytes: data,
194 })
195 }),
196 );
197 }
198}
199
200pub struct DecodedTempoPrecompileError<'a> {
202 pub error: TempoPrecompileError,
203 pub revert_bytes: &'a [u8],
204}
205
206pub type TempoPrecompileErrorRegistry = HashMap<
208 Selector,
209 Box<dyn for<'a> Fn(&'a [u8]) -> Option<DecodedTempoPrecompileError<'a>> + Send + Sync>,
210>;
211
212pub fn error_decoder_registry() -> TempoPrecompileErrorRegistry {
214 let mut registry: TempoPrecompileErrorRegistry = HashMap::new();
215
216 add_errors_to_registry(&mut registry, TempoPrecompileError::StablecoinDEX);
217 add_errors_to_registry(&mut registry, TempoPrecompileError::TIP20);
218 add_errors_to_registry(&mut registry, TempoPrecompileError::TIP20Factory);
219 add_errors_to_registry(&mut registry, TempoPrecompileError::RolesAuthError);
220 add_errors_to_registry(&mut registry, TempoPrecompileError::TIP403RegistryError);
221 add_errors_to_registry(&mut registry, TempoPrecompileError::FeeManagerError);
222 add_errors_to_registry(&mut registry, TempoPrecompileError::TIPFeeAMMError);
223 add_errors_to_registry(&mut registry, TempoPrecompileError::NonceError);
224 add_errors_to_registry(&mut registry, TempoPrecompileError::ValidatorConfigError);
225 add_errors_to_registry(&mut registry, TempoPrecompileError::ValidatorConfigV2Error);
226 add_errors_to_registry(&mut registry, TempoPrecompileError::AccountKeychainError);
227
228 registry
229}
230
231pub static ERROR_REGISTRY: LazyLock<TempoPrecompileErrorRegistry> =
233 LazyLock::new(error_decoder_registry);
234
235pub fn decode_error<'a>(data: &'a [u8]) -> Option<DecodedTempoPrecompileError<'a>> {
239 if data.len() < 4 {
240 return None;
241 }
242
243 let selector: [u8; 4] = data[0..4].try_into().ok()?;
244 ERROR_REGISTRY
245 .get(&selector)
246 .and_then(|decoder| decoder(data))
247}
248
249pub trait IntoPrecompileResult<T> {
251 fn into_precompile_result(
253 self,
254 gas: u64,
255 encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes,
256 ) -> PrecompileResult;
257}
258
259impl<T> IntoPrecompileResult<T> for Result<T> {
260 fn into_precompile_result(
261 self,
262 gas: u64,
263 encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes,
264 ) -> PrecompileResult {
265 match self {
266 Ok(res) => Ok(PrecompileOutput::new(gas, encode_ok(res))),
267 Err(err) => err.into_precompile_result(gas),
268 }
269 }
270}
271
272impl<T> IntoPrecompileResult<T> for TempoPrecompileError {
273 fn into_precompile_result(
274 self,
275 gas: u64,
276 _encode_ok: impl FnOnce(T) -> alloy::primitives::Bytes,
277 ) -> PrecompileResult {
278 Self::into_precompile_result(self, gas)
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use tempo_contracts::precompiles::StablecoinDEXError;
286
287 #[test]
288 fn test_add_errors_to_registry_populates_registry() {
289 let mut registry: TempoPrecompileErrorRegistry = HashMap::new();
290
291 assert!(registry.is_empty());
292
293 add_errors_to_registry(&mut registry, TempoPrecompileError::StablecoinDEX);
294
295 assert!(!registry.is_empty());
296
297 let order_not_found_selector = StablecoinDEXError::order_does_not_exist().selector();
298 assert!(
299 registry.contains_key(&order_not_found_selector),
300 "Registry should contain OrderDoesNotExist selector"
301 );
302 }
303
304 #[test]
305 fn test_error_decoder_registry_is_not_empty() {
306 let registry = error_decoder_registry();
307
308 assert!(
309 !registry.is_empty(),
310 "error_decoder_registry should return a populated registry"
311 );
312
313 let dex_selector = StablecoinDEXError::order_does_not_exist().selector();
314 assert!(registry.contains_key(&dex_selector));
315 }
316
317 #[test]
318 fn test_decode_error_returns_some_for_valid_error() {
319 let error = StablecoinDEXError::order_does_not_exist();
320 let encoded = error.abi_encode();
321
322 let result = decode_error(&encoded);
323 assert!(
324 result.is_some(),
325 "decode_error should return Some for valid error"
326 );
327
328 let decoded = result.unwrap();
329 assert!(matches!(
330 decoded.error,
331 TempoPrecompileError::StablecoinDEX(StablecoinDEXError::OrderDoesNotExist(_))
332 ));
333 }
334
335 #[test]
336 fn test_decode_error_data_length_boundary() {
337 let result = decode_error(&[]);
339 assert!(result.is_none(), "Empty data should return None");
340
341 let result = decode_error(&[0x01]);
343 assert!(result.is_none(), "1 byte should return None");
344
345 let result = decode_error(&[0x01, 0x02]);
347 assert!(result.is_none(), "2 bytes should return None");
348
349 let result = decode_error(&[0x01, 0x02, 0x03]);
351 assert!(result.is_none(), "3 bytes should return None");
352
353 let result = decode_error(&[0x00, 0x00, 0x00, 0x00]);
355 assert!(
356 result.is_none(),
357 "Unknown 4-byte selector should return None"
358 );
359
360 let error = StablecoinDEXError::order_does_not_exist();
362 let encoded = error.abi_encode();
363 let result = decode_error(&encoded);
364 assert!(
365 result.is_some(),
366 "Valid error at 4+ bytes should return Some"
367 );
368 }
369
370 #[test]
371 fn test_decode_error_with_tip20_error() {
372 let error = TIP20Error::insufficient_allowance();
374 let encoded = error.abi_encode();
375
376 let result = decode_error(&encoded);
377 assert!(result.is_some(), "Should decode TIP20 errors");
378
379 let decoded = result.unwrap();
380 match decoded.error {
382 TempoPrecompileError::TIP20(_) => {}
383 other => panic!("Expected TIP20 error, got {other:?}"),
384 }
385 }
386}