1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5pub mod error;
6pub use error::{IntoPrecompileResult, Result};
7
8pub mod storage;
9
10pub(crate) mod ip_validation;
11
12pub mod account_keychain;
13pub mod nonce;
14pub mod stablecoin_dex;
15pub mod tip20;
16pub mod tip20_factory;
17pub mod tip403_registry;
18pub mod tip_fee_manager;
19pub mod validator_config;
20pub mod validator_config_v2;
21
22#[cfg(any(test, feature = "test-utils"))]
23pub mod test_util;
24
25use crate::{
26 account_keychain::AccountKeychain,
27 nonce::NonceManager,
28 stablecoin_dex::StablecoinDEX,
29 storage::StorageCtx,
30 tip_fee_manager::TipFeeManager,
31 tip20::{TIP20Token, is_tip20_prefix},
32 tip20_factory::TIP20Factory,
33 tip403_registry::TIP403Registry,
34 validator_config::ValidatorConfig,
35 validator_config_v2::ValidatorConfigV2,
36};
37use tempo_chainspec::hardfork::TempoHardfork;
38
39#[cfg(test)]
40use alloy::sol_types::SolInterface;
41use alloy::{
42 primitives::{Address, Bytes},
43 sol,
44 sol_types::{SolCall, SolError},
45};
46use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap};
47use revm::{
48 context::CfgEnv,
49 handler::EthPrecompiles,
50 precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult},
51 primitives::hardfork::SpecId,
52};
53
54pub use tempo_contracts::precompiles::{
55 ACCOUNT_KEYCHAIN_ADDRESS, DEFAULT_FEE_TOKEN, NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS,
56 STABLECOIN_DEX_ADDRESS, TIP_FEE_MANAGER_ADDRESS, TIP20_FACTORY_ADDRESS,
57 TIP403_REGISTRY_ADDRESS, VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
58};
59
60pub use account_keychain::AuthorizedKey;
62
63pub const INPUT_PER_WORD_COST: u64 = 6;
67
68pub const ECRECOVER_GAS: u64 = 3_000;
70
71#[inline]
73pub fn input_cost(calldata_len: usize) -> u64 {
74 calldata_len
75 .div_ceil(32)
76 .saturating_mul(INPUT_PER_WORD_COST as usize) as u64
77}
78
79pub trait Precompile {
84 fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult;
93}
94
95pub fn tempo_precompiles(cfg: &CfgEnv<TempoHardfork>) -> PrecompilesMap {
100 let spec = if cfg.spec.is_t1c() {
101 cfg.spec.into()
102 } else {
103 SpecId::PRAGUE
104 };
105 let mut precompiles = PrecompilesMap::from_static(EthPrecompiles::new(spec).precompiles);
106 extend_tempo_precompiles(&mut precompiles, cfg);
107 precompiles
108}
109
110pub fn extend_tempo_precompiles(precompiles: &mut PrecompilesMap, cfg: &CfgEnv<TempoHardfork>) {
116 let cfg = cfg.clone();
117
118 precompiles.set_precompile_lookup(move |address: &Address| {
119 if is_tip20_prefix(*address) {
120 Some(TIP20Token::create_precompile(*address, &cfg))
121 } else if *address == TIP20_FACTORY_ADDRESS {
122 Some(TIP20Factory::create_precompile(&cfg))
123 } else if *address == TIP403_REGISTRY_ADDRESS {
124 Some(TIP403Registry::create_precompile(&cfg))
125 } else if *address == TIP_FEE_MANAGER_ADDRESS {
126 Some(TipFeeManager::create_precompile(&cfg))
127 } else if *address == STABLECOIN_DEX_ADDRESS {
128 Some(StablecoinDEX::create_precompile(&cfg))
129 } else if *address == NONCE_PRECOMPILE_ADDRESS {
130 Some(NonceManager::create_precompile(&cfg))
131 } else if *address == VALIDATOR_CONFIG_ADDRESS {
132 Some(ValidatorConfig::create_precompile(&cfg))
133 } else if *address == ACCOUNT_KEYCHAIN_ADDRESS {
134 Some(AccountKeychain::create_precompile(&cfg))
135 } else if *address == VALIDATOR_CONFIG_V2_ADDRESS {
136 Some(ValidatorConfigV2::create_precompile(&cfg))
137 } else {
138 None
139 }
140 });
141}
142
143sol! {
144 error DelegateCallNotAllowed();
145 error StaticCallNotAllowed();
146}
147
148macro_rules! tempo_precompile {
149 ($id:expr, $cfg:expr, |$input:ident| $impl:expr) => {{
150 let spec = $cfg.spec;
151 let gas_params = $cfg.gas_params.clone();
152 DynPrecompile::new_stateful(PrecompileId::Custom($id.into()), move |$input| {
153 if !$input.is_direct_call() {
154 return Ok(PrecompileOutput::new_reverted(
155 0,
156 DelegateCallNotAllowed {}.abi_encode().into(),
157 ));
158 }
159 let mut storage = crate::storage::evm::EvmPrecompileStorageProvider::new(
160 $input.internals,
161 $input.gas,
162 spec,
163 $input.is_static,
164 gas_params.clone(),
165 );
166 crate::storage::StorageCtx::enter(&mut storage, || {
167 $impl.call($input.data, $input.caller)
168 })
169 })
170 }};
171}
172
173impl TipFeeManager {
174 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
176 tempo_precompile!("TipFeeManager", cfg, |input| { Self::new() })
177 }
178}
179
180impl TIP403Registry {
181 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
183 tempo_precompile!("TIP403Registry", cfg, |input| { Self::new() })
184 }
185}
186
187impl TIP20Factory {
188 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
190 tempo_precompile!("TIP20Factory", cfg, |input| { Self::new() })
191 }
192}
193
194impl TIP20Token {
195 pub fn create_precompile(address: Address, cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
197 tempo_precompile!("TIP20Token", cfg, |input| {
198 Self::from_address(address).expect("TIP20 prefix already verified")
199 })
200 }
201}
202
203impl StablecoinDEX {
204 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
206 tempo_precompile!("StablecoinDEX", cfg, |input| { Self::new() })
207 }
208}
209
210impl NonceManager {
211 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
213 tempo_precompile!("NonceManager", cfg, |input| { Self::new() })
214 }
215}
216
217impl AccountKeychain {
218 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
220 tempo_precompile!("AccountKeychain", cfg, |input| { Self::new() })
221 }
222}
223
224impl ValidatorConfig {
225 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
227 tempo_precompile!("ValidatorConfig", cfg, |input| { Self::new() })
228 }
229}
230
231impl ValidatorConfigV2 {
232 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
234 tempo_precompile!("ValidatorConfigV2", cfg, |input| { Self::new() })
235 }
236}
237
238#[inline]
240fn metadata<T: SolCall>(f: impl FnOnce() -> Result<T::Return>) -> PrecompileResult {
241 f().into_precompile_result(0, |ret| T::abi_encode_returns(&ret).into())
242}
243
244#[inline]
246fn view<T: SolCall>(call: T, f: impl FnOnce(T) -> Result<T::Return>) -> PrecompileResult {
247 f(call).into_precompile_result(0, |ret| T::abi_encode_returns(&ret).into())
248}
249
250#[inline]
254fn mutate<T: SolCall>(
255 call: T,
256 sender: Address,
257 f: impl FnOnce(Address, T) -> Result<T::Return>,
258) -> PrecompileResult {
259 if StorageCtx.is_static() {
260 return Ok(PrecompileOutput::new_reverted(
261 0,
262 StaticCallNotAllowed {}.abi_encode().into(),
263 ));
264 }
265 f(sender, call).into_precompile_result(0, |ret| T::abi_encode_returns(&ret).into())
266}
267
268#[inline]
272fn mutate_void<T: SolCall>(
273 call: T,
274 sender: Address,
275 f: impl FnOnce(Address, T) -> Result<()>,
276) -> PrecompileResult {
277 if StorageCtx.is_static() {
278 return Ok(PrecompileOutput::new_reverted(
279 0,
280 StaticCallNotAllowed {}.abi_encode().into(),
281 ));
282 }
283 f(sender, call).into_precompile_result(0, |()| Bytes::new())
284}
285
286#[inline]
288fn fill_precompile_output(mut output: PrecompileOutput, storage: &StorageCtx) -> PrecompileOutput {
289 output.gas_used = storage.gas_used();
290
291 if !output.reverted {
293 output.gas_refunded = storage.gas_refunded();
294 }
295 output
296}
297
298#[inline]
300pub fn unknown_selector(selector: [u8; 4], gas: u64) -> PrecompileResult {
301 error::TempoPrecompileError::UnknownFunctionSelector(selector).into_precompile_result(gas)
302}
303
304#[inline]
311fn dispatch_call<T>(
312 calldata: &[u8],
313 decode: impl FnOnce(&[u8]) -> core::result::Result<T, alloy::sol_types::Error>,
314 f: impl FnOnce(T) -> PrecompileResult,
315) -> PrecompileResult {
316 let storage = StorageCtx::default();
317
318 if calldata.len() < 4 {
319 if storage.spec().is_t1() {
320 return Ok(fill_precompile_output(
321 PrecompileOutput::new_reverted(0, Bytes::new()),
322 &storage,
323 ));
324 } else {
325 return Err(PrecompileError::Other(
326 "Invalid input: missing function selector".into(),
327 ));
328 }
329 }
330 let result = decode(calldata);
331
332 match result {
333 Ok(call) => f(call).map(|res| fill_precompile_output(res, &storage)),
334 Err(alloy::sol_types::Error::UnknownSelector { selector, .. }) => {
335 unknown_selector(*selector, storage.gas_used())
336 .map(|res| fill_precompile_output(res, &storage))
337 }
338 Err(_) => Ok(fill_precompile_output(
339 PrecompileOutput::new_reverted(0, Bytes::new()),
340 &storage,
341 )),
342 }
343}
344
345#[cfg(test)]
347pub fn expect_precompile_revert<E>(result: &PrecompileResult, expected_error: E)
348where
349 E: SolInterface + PartialEq + std::fmt::Debug,
350{
351 match result {
352 Ok(result) => {
353 assert!(result.reverted);
354 let decoded = E::abi_decode(&result.bytes).unwrap();
355 assert_eq!(decoded, expected_error);
356 }
357 Err(other) => {
358 panic!("expected reverted output, got: {other:?}");
359 }
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::tip20::TIP20Token;
367 use alloy::primitives::{Address, Bytes, U256, bytes};
368 use alloy_evm::{
369 EthEvmFactory, EvmEnv, EvmFactory, EvmInternals,
370 precompiles::{Precompile as AlloyEvmPrecompile, PrecompileInput},
371 };
372 use revm::{
373 context::{ContextTr, TxEnv},
374 database::{CacheDB, EmptyDB},
375 state::{AccountInfo, Bytecode},
376 };
377 use tempo_contracts::precompiles::ITIP20;
378
379 #[test]
380 fn test_precompile_delegatecall() {
381 let cfg = CfgEnv::<TempoHardfork>::default();
382 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
383 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
384 });
385
386 let db = CacheDB::new(EmptyDB::new());
387 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
388 let block = evm.block.clone();
389 let tx = TxEnv::default();
390 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
391
392 let target_address = Address::random();
393 let bytecode_address = Address::random();
394 let input = PrecompileInput {
395 data: &Bytes::new(),
396 caller: Address::ZERO,
397 internals: evm_internals,
398 gas: 0,
399 value: U256::ZERO,
400 is_static: false,
401 target_address,
402 bytecode_address,
403 };
404
405 let result = AlloyEvmPrecompile::call(&precompile, input);
406
407 match result {
408 Ok(output) => {
409 assert!(output.reverted);
410 let decoded = DelegateCallNotAllowed::abi_decode(&output.bytes).unwrap();
411 assert!(matches!(decoded, DelegateCallNotAllowed {}));
412 }
413 Err(_) => panic!("expected reverted output"),
414 }
415 }
416
417 #[test]
418 fn test_precompile_static_call() {
419 let cfg = CfgEnv::<TempoHardfork>::default();
420 let tx = TxEnv::default();
421 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
422 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
423 });
424
425 let token_address = PATH_USD_ADDRESS;
426
427 let call_static = |calldata: Bytes| {
428 let mut db = CacheDB::new(EmptyDB::new());
429 db.insert_account_info(
430 token_address,
431 AccountInfo {
432 code: Some(Bytecode::new_raw(bytes!("0xEF"))),
433 ..Default::default()
434 },
435 );
436 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
437 let block = evm.block.clone();
438 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
439
440 let input = PrecompileInput {
441 data: &calldata,
442 caller: Address::ZERO,
443 internals: evm_internals,
444 gas: 1_000_000,
445 is_static: true,
446 value: U256::ZERO,
447 target_address: token_address,
448 bytecode_address: token_address,
449 };
450
451 AlloyEvmPrecompile::call(&precompile, input)
452 };
453
454 let result = call_static(Bytes::from(
456 ITIP20::transferCall {
457 to: Address::random(),
458 amount: U256::from(100),
459 }
460 .abi_encode(),
461 ));
462 let output = result.expect("expected Ok");
463 assert!(output.reverted);
464 assert!(StaticCallNotAllowed::abi_decode(&output.bytes).is_ok());
465
466 let result = call_static(Bytes::from(
468 ITIP20::approveCall {
469 spender: Address::random(),
470 amount: U256::from(100),
471 }
472 .abi_encode(),
473 ));
474 let output = result.expect("expected Ok");
475 assert!(output.reverted);
476 assert!(StaticCallNotAllowed::abi_decode(&output.bytes).is_ok());
477
478 let result = call_static(Bytes::from(
480 ITIP20::balanceOfCall {
481 account: Address::random(),
482 }
483 .abi_encode(),
484 ));
485 let output = result.expect("expected Ok");
486 assert!(
487 !output.reverted,
488 "view function should not revert in static context"
489 );
490 }
491
492 #[test]
493 fn test_invalid_calldata_hardfork_behavior() {
494 let call_with_spec = |calldata: Bytes, spec: TempoHardfork| {
495 let mut cfg = CfgEnv::<TempoHardfork>::default();
496 cfg.set_spec(spec);
497 let tx = TxEnv::default();
498 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
499 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
500 });
501
502 let mut db = CacheDB::new(EmptyDB::new());
503 db.insert_account_info(
504 PATH_USD_ADDRESS,
505 AccountInfo {
506 code: Some(Bytecode::new_raw(bytes!("0xEF"))),
507 ..Default::default()
508 },
509 );
510 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
511 let block = evm.block.clone();
512 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
513
514 let input = PrecompileInput {
515 data: &calldata,
516 caller: Address::ZERO,
517 internals: evm_internals,
518 gas: 1_000_000,
519 is_static: false,
520 value: U256::ZERO,
521 target_address: PATH_USD_ADDRESS,
522 bytecode_address: PATH_USD_ADDRESS,
523 };
524
525 AlloyEvmPrecompile::call(&precompile, input)
526 };
527
528 let empty = call_with_spec(Bytes::new(), TempoHardfork::T1)
530 .expect("T1: expected Ok with reverted output");
531 assert!(empty.reverted, "T1: expected reverted output");
532 assert!(empty.bytes.is_empty());
533 assert!(empty.gas_used != 0);
534 assert_eq!(empty.gas_refunded, 0);
535
536 let unknown = call_with_spec(Bytes::from([0xAA; 4]), TempoHardfork::T1)
538 .expect("T1: expected Ok with reverted output");
539 assert!(unknown.reverted, "T1: expected reverted output");
540
541 let decoded =
543 tempo_contracts::precompiles::UnknownFunctionSelector::abi_decode(&unknown.bytes)
544 .expect("T1: expected UnknownFunctionSelector error");
545 assert_eq!(decoded.selector.as_slice(), &[0xAA, 0xAA, 0xAA, 0xAA]);
546
547 assert!(unknown.gas_used >= empty.gas_used);
549 assert_eq!(unknown.gas_refunded, empty.gas_refunded);
550
551 let result = call_with_spec(Bytes::new(), TempoHardfork::T0);
553 assert!(
554 matches!(
555 &result,
556 Err(PrecompileError::Other(msg)) if msg.contains("missing function selector")
557 ),
558 "T0: expected PrecompileError for invalid calldata, got {result:?}"
559 );
560 }
561
562 #[test]
563 fn test_input_cost_returns_non_zero_for_input() {
564 assert_eq!(input_cost(0), 0);
566
567 assert_eq!(input_cost(1), INPUT_PER_WORD_COST);
569
570 assert_eq!(input_cost(32), INPUT_PER_WORD_COST);
572
573 assert_eq!(input_cost(33), INPUT_PER_WORD_COST * 2);
575 }
576
577 #[test]
578 fn test_extend_tempo_precompiles_registers_precompiles() {
579 let cfg = CfgEnv::<TempoHardfork>::default();
580 let precompiles = tempo_precompiles(&cfg);
581
582 let factory_precompile = precompiles.get(&TIP20_FACTORY_ADDRESS);
584 assert!(
585 factory_precompile.is_some(),
586 "TIP20Factory should be registered"
587 );
588
589 let registry_precompile = precompiles.get(&TIP403_REGISTRY_ADDRESS);
591 assert!(
592 registry_precompile.is_some(),
593 "TIP403Registry should be registered"
594 );
595
596 let fee_manager_precompile = precompiles.get(&TIP_FEE_MANAGER_ADDRESS);
598 assert!(
599 fee_manager_precompile.is_some(),
600 "TipFeeManager should be registered"
601 );
602
603 let dex_precompile = precompiles.get(&STABLECOIN_DEX_ADDRESS);
605 assert!(
606 dex_precompile.is_some(),
607 "StablecoinDEX should be registered"
608 );
609
610 let nonce_precompile = precompiles.get(&NONCE_PRECOMPILE_ADDRESS);
612 assert!(
613 nonce_precompile.is_some(),
614 "NonceManager should be registered"
615 );
616
617 let validator_precompile = precompiles.get(&VALIDATOR_CONFIG_ADDRESS);
619 assert!(
620 validator_precompile.is_some(),
621 "ValidatorConfig should be registered"
622 );
623
624 let validator_v2_precompile = precompiles.get(&VALIDATOR_CONFIG_V2_ADDRESS);
626 assert!(
627 validator_v2_precompile.is_some(),
628 "ValidatorConfigV2 should be registered"
629 );
630
631 let keychain_precompile = precompiles.get(&ACCOUNT_KEYCHAIN_ADDRESS);
633 assert!(
634 keychain_precompile.is_some(),
635 "AccountKeychain should be registered"
636 );
637
638 let tip20_precompile = precompiles.get(&PATH_USD_ADDRESS);
640 assert!(
641 tip20_precompile.is_some(),
642 "TIP20 tokens should be registered"
643 );
644
645 let random_address = Address::random();
647 let random_precompile = precompiles.get(&random_address);
648 assert!(
649 random_precompile.is_none(),
650 "Random address should not be a precompile"
651 );
652 }
653
654 #[test]
655 fn test_p256verify_availability_across_t1c_boundary() {
656 let has_p256 = |spec: TempoHardfork| -> bool {
657 let p256_addr = Address::from_word(U256::from(256).into());
659
660 let mut cfg = CfgEnv::<TempoHardfork>::default();
661 cfg.set_spec(spec);
662 tempo_precompiles(&cfg).get(&p256_addr).is_some()
663 };
664
665 for spec in [
667 TempoHardfork::Genesis,
668 TempoHardfork::T0,
669 TempoHardfork::T1,
670 TempoHardfork::T1A,
671 TempoHardfork::T1B,
672 ] {
673 assert!(
674 !has_p256(spec),
675 "P256VERIFY should NOT be available at {spec:?} (pre-T1C)"
676 );
677 }
678
679 for spec in [TempoHardfork::T1C, TempoHardfork::T2] {
681 assert!(
682 has_p256(spec),
683 "P256VERIFY should be available at {spec:?} (T1C+)"
684 );
685 }
686 }
687}