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 address_registry;
14pub mod nonce;
15pub mod receive_policy_guard;
16pub mod signature_verifier;
17pub mod stablecoin_dex;
18pub mod storage_credits;
19pub mod tip20;
20pub mod tip20_channel_reserve;
21pub mod tip20_factory;
22pub mod tip403_registry;
23pub mod tip_fee_manager;
24pub mod validator_config;
25pub mod validator_config_v2;
26
27#[cfg(any(test, feature = "test-utils"))]
28pub mod test_util;
29
30use crate::{
31 account_keychain::AccountKeychain,
32 address_registry::AddressRegistry,
33 nonce::NonceManager,
34 receive_policy_guard::ReceivePolicyGuard,
35 signature_verifier::SignatureVerifier,
36 stablecoin_dex::StablecoinDEX,
37 storage::{StorageCtx, actions::StorageActions},
38 storage_credits::StorageCredits,
39 tip_fee_manager::TipFeeManager,
40 tip20::TIP20Token,
41 tip20_channel_reserve::TIP20ChannelReserve,
42 tip20_factory::TIP20Factory,
43 tip403_registry::TIP403Registry,
44 validator_config::ValidatorConfig,
45 validator_config_v2::ValidatorConfigV2,
46};
47use tempo_chainspec::hardfork::TempoHardfork;
48use tempo_primitives::TempoAddressExt;
49
50#[cfg(test)]
51use alloy::sol_types::SolInterface;
52use alloy::{
53 primitives::{Address, Bytes},
54 sol,
55 sol_types::{SolCall, SolError},
56};
57use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap};
58use revm::{
59 context::CfgEnv,
60 handler::EthPrecompiles,
61 precompile::{PrecompileHalt, PrecompileId, PrecompileOutput, PrecompileResult},
62 primitives::hardfork::SpecId,
63};
64
65pub use tempo_contracts::precompiles::{
66 ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, DEFAULT_FEE_TOKEN,
67 NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS, RECEIVE_POLICY_GUARD_ADDRESS,
68 SIGNATURE_VERIFIER_ADDRESS, STABLECOIN_DEX_ADDRESS, STORAGE_CREDITS_ADDRESS,
69 TIP_FEE_MANAGER_ADDRESS, TIP20_CHANNEL_RESERVE_ADDRESS, TIP20_FACTORY_ADDRESS,
70 TIP403_REGISTRY_ADDRESS, VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
71};
72
73pub use account_keychain::AuthorizedKey;
75
76pub const SYSTEM_PRECOMPILES: &[(Address, TempoHardfork)] = &[
78 (TIP403_REGISTRY_ADDRESS, TempoHardfork::Genesis),
79 (TIP_FEE_MANAGER_ADDRESS, TempoHardfork::Genesis),
80 (STABLECOIN_DEX_ADDRESS, TempoHardfork::Genesis),
81 (NONCE_PRECOMPILE_ADDRESS, TempoHardfork::Genesis),
82 (ACCOUNT_KEYCHAIN_ADDRESS, TempoHardfork::Genesis),
83 (VALIDATOR_CONFIG_ADDRESS, TempoHardfork::Genesis),
84 (VALIDATOR_CONFIG_V2_ADDRESS, TempoHardfork::Genesis),
85 (TIP20_FACTORY_ADDRESS, TempoHardfork::Genesis),
86 (ADDRESS_REGISTRY_ADDRESS, TempoHardfork::T3),
87 (SIGNATURE_VERIFIER_ADDRESS, TempoHardfork::T3),
88 (TIP20_CHANNEL_RESERVE_ADDRESS, TempoHardfork::T5),
89 (RECEIVE_POLICY_GUARD_ADDRESS, TempoHardfork::T6),
90 (STORAGE_CREDITS_ADDRESS, TempoHardfork::T7),
91];
92
93pub fn is_precompile_address(addr: Address, spec: TempoHardfork) -> bool {
96 addr.is_tip20()
97 || SYSTEM_PRECOMPILES
98 .iter()
99 .any(|&(a, activated)| a == addr && spec >= activated)
100}
101
102pub const INPUT_PER_WORD_COST: u64 = 6;
106
107pub const ECRECOVER_GAS: u64 = 3_000;
109
110#[inline]
112pub fn input_cost(calldata_len: usize) -> u64 {
113 calldata_len
114 .div_ceil(32)
115 .saturating_mul(INPUT_PER_WORD_COST as usize) as u64
116}
117
118pub trait Precompile {
123 fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult;
133}
134
135#[cfg(test)]
140pub fn tempo_precompiles(cfg: &CfgEnv<TempoHardfork>) -> PrecompilesMap {
141 tempo_precompiles_with_actions(cfg, StorageActions::disabled())
142}
143
144pub fn tempo_precompiles_with_actions(
149 cfg: &CfgEnv<TempoHardfork>,
150 actions: StorageActions,
151) -> PrecompilesMap {
152 let spec = if cfg.spec.is_t1c() {
153 cfg.spec.into()
154 } else {
155 SpecId::PRAGUE
156 };
157 let mut precompiles = PrecompilesMap::from_static(EthPrecompiles::new(spec).precompiles);
158 extend_tempo_precompiles(&mut precompiles, cfg, actions);
159 precompiles
160}
161
162pub fn extend_tempo_precompiles(
168 precompiles: &mut PrecompilesMap,
169 cfg: &CfgEnv<TempoHardfork>,
170 actions: StorageActions,
171) {
172 let cfg = cfg.clone();
173
174 precompiles.set_precompile_lookup(move |address: &Address| {
175 if address.is_tip20() {
176 Some(TIP20Token::create_precompile(
177 *address,
178 &cfg,
179 actions.clone(),
180 ))
181 } else if *address == TIP20_FACTORY_ADDRESS {
182 Some(TIP20Factory::create_precompile(&cfg, actions.clone()))
183 } else if *address == TIP20_CHANNEL_RESERVE_ADDRESS && cfg.spec.is_t5() {
184 Some(TIP20ChannelReserve::create_precompile(
185 &cfg,
186 actions.clone(),
187 ))
188 } else if *address == ADDRESS_REGISTRY_ADDRESS && cfg.spec.is_t3() {
189 Some(AddressRegistry::create_precompile(&cfg, actions.clone()))
190 } else if *address == TIP403_REGISTRY_ADDRESS {
191 Some(TIP403Registry::create_precompile(&cfg, actions.clone()))
192 } else if *address == TIP_FEE_MANAGER_ADDRESS {
193 Some(TipFeeManager::create_precompile(&cfg, actions.clone()))
194 } else if *address == STABLECOIN_DEX_ADDRESS {
195 Some(StablecoinDEX::create_precompile(&cfg, actions.clone()))
196 } else if *address == NONCE_PRECOMPILE_ADDRESS {
197 Some(NonceManager::create_precompile(&cfg, actions.clone()))
198 } else if *address == VALIDATOR_CONFIG_ADDRESS {
199 Some(ValidatorConfig::create_precompile(&cfg, actions.clone()))
200 } else if *address == ACCOUNT_KEYCHAIN_ADDRESS {
201 Some(AccountKeychain::create_precompile(&cfg, actions.clone()))
202 } else if *address == VALIDATOR_CONFIG_V2_ADDRESS {
203 Some(ValidatorConfigV2::create_precompile(&cfg, actions.clone()))
204 } else if *address == SIGNATURE_VERIFIER_ADDRESS && cfg.spec.is_t3() {
205 Some(SignatureVerifier::create_precompile(&cfg, actions.clone()))
206 } else if *address == RECEIVE_POLICY_GUARD_ADDRESS && cfg.spec.is_t6() {
207 Some(ReceivePolicyGuard::create_precompile(&cfg, actions.clone()))
208 } else if *address == STORAGE_CREDITS_ADDRESS && cfg.spec.is_t7() {
209 Some(StorageCredits::create_precompile(&cfg, actions.clone()))
210 } else {
211 None
212 }
213 });
214}
215
216sol! {
217 error DelegateCallNotAllowed();
218 error StaticCallNotAllowed();
219}
220
221macro_rules! tempo_precompile {
222 ($id:expr, $cfg:expr, |$input:ident| $impl:expr) => {{
223 #[cfg(not(test))]
224 compile_error!("tempo_precompile! without actions is only available in tests");
225 #[cfg(test)]
226 tempo_precompile!($id, $cfg, StorageActions::disabled(), |$input| $impl)
227 }};
228 ($id:expr, $cfg:expr, $actions:expr, |$input:ident| $impl:expr) => {{
229 let spec = $cfg.spec;
230 let amsterdam_eip8037_enabled = $cfg.enable_amsterdam_eip8037;
231 let gas_params = $cfg.gas_params.clone();
232 let actions = $actions.clone();
233 DynPrecompile::new_stateful(PrecompileId::Custom($id.into()), move |$input| {
234 if !$input.is_direct_call() {
235 return Ok(PrecompileOutput::revert(
236 0,
237 DelegateCallNotAllowed {}.abi_encode().into(),
238 $input.reservoir,
239 ));
240 }
241 let mut storage = crate::storage::evm::EvmPrecompileStorageProvider::new(
242 $input.internals,
243 $input.gas,
244 $input.reservoir,
245 spec,
246 amsterdam_eip8037_enabled,
247 $input.is_static,
248 gas_params.clone(),
249 )
250 .with_actions(actions.clone());
251 crate::storage::StorageCtx::enter(&mut storage, || {
252 $impl.call($input.data, $input.caller)
253 })
254 })
255 }};
256}
257
258impl TipFeeManager {
259 pub fn create_precompile(
261 cfg: &CfgEnv<TempoHardfork>,
262 actions: StorageActions,
263 ) -> DynPrecompile {
264 tempo_precompile!("TipFeeManager", cfg, actions, |input| { Self::new() })
265 }
266}
267
268impl AddressRegistry {
269 pub fn create_precompile(
271 cfg: &CfgEnv<TempoHardfork>,
272 actions: StorageActions,
273 ) -> DynPrecompile {
274 tempo_precompile!("AddressRegistry", cfg, actions, |input| { Self::new() })
275 }
276}
277
278impl TIP403Registry {
279 pub fn create_precompile(
281 cfg: &CfgEnv<TempoHardfork>,
282 actions: StorageActions,
283 ) -> DynPrecompile {
284 tempo_precompile!("TIP403Registry", cfg, actions, |input| { Self::new() })
285 }
286}
287
288impl TIP20Factory {
289 pub fn create_precompile(
291 cfg: &CfgEnv<TempoHardfork>,
292 actions: StorageActions,
293 ) -> DynPrecompile {
294 tempo_precompile!("TIP20Factory", cfg, actions, |input| { Self::new() })
295 }
296}
297
298impl TIP20Token {
299 pub fn create_precompile(
301 address: Address,
302 cfg: &CfgEnv<TempoHardfork>,
303 actions: StorageActions,
304 ) -> DynPrecompile {
305 tempo_precompile!("TIP20Token", cfg, actions, |input| {
306 Self::from_address(address).expect("TIP20 prefix already verified")
307 })
308 }
309}
310
311impl StablecoinDEX {
312 pub fn create_precompile(
314 cfg: &CfgEnv<TempoHardfork>,
315 actions: StorageActions,
316 ) -> DynPrecompile {
317 tempo_precompile!("StablecoinDEX", cfg, actions, |input| { Self::new() })
318 }
319}
320
321impl NonceManager {
322 pub fn create_precompile(
324 cfg: &CfgEnv<TempoHardfork>,
325 actions: StorageActions,
326 ) -> DynPrecompile {
327 tempo_precompile!("NonceManager", cfg, actions, |input| { Self::new() })
328 }
329}
330
331impl AccountKeychain {
332 pub fn create_precompile(
334 cfg: &CfgEnv<TempoHardfork>,
335 actions: StorageActions,
336 ) -> DynPrecompile {
337 tempo_precompile!("AccountKeychain", cfg, actions, |input| { Self::new() })
338 }
339}
340
341impl ValidatorConfig {
342 pub fn create_precompile(
344 cfg: &CfgEnv<TempoHardfork>,
345 actions: StorageActions,
346 ) -> DynPrecompile {
347 tempo_precompile!("ValidatorConfig", cfg, actions, |input| { Self::new() })
348 }
349}
350
351impl ValidatorConfigV2 {
352 pub fn create_precompile(
354 cfg: &CfgEnv<TempoHardfork>,
355 actions: StorageActions,
356 ) -> DynPrecompile {
357 tempo_precompile!("ValidatorConfigV2", cfg, actions, |input| { Self::new() })
358 }
359}
360
361impl SignatureVerifier {
362 pub fn create_precompile(
364 cfg: &CfgEnv<TempoHardfork>,
365 actions: StorageActions,
366 ) -> DynPrecompile {
367 tempo_precompile!("SignatureVerifier", cfg, actions, |input| { Self::new() })
368 }
369}
370
371impl TIP20ChannelReserve {
372 pub fn create_precompile(
374 cfg: &CfgEnv<TempoHardfork>,
375 actions: StorageActions,
376 ) -> DynPrecompile {
377 tempo_precompile!("TIP20ChannelReserve", cfg, actions, |input| { Self::new() })
378 }
379}
380
381impl ReceivePolicyGuard {
382 pub fn create_precompile(
384 cfg: &CfgEnv<TempoHardfork>,
385 actions: StorageActions,
386 ) -> DynPrecompile {
387 tempo_precompile!("ReceivePolicyGuard", cfg, actions, |input| { Self::new() })
388 }
389}
390
391impl StorageCredits {
392 pub fn create_precompile(
394 cfg: &CfgEnv<TempoHardfork>,
395 actions: StorageActions,
396 ) -> DynPrecompile {
397 tempo_precompile!("TIP1060StorageCredits", cfg, actions, |input| {
398 Self::new()
399 })
400 }
401}
402
403#[inline]
405fn metadata<T: SolCall>(f: impl FnOnce() -> Result<T::Return>) -> PrecompileResult {
406 f().into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into())
407}
408
409#[inline]
411fn view<T: SolCall>(call: T, f: impl FnOnce(T) -> Result<T::Return>) -> PrecompileResult {
412 f(call).into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into())
413}
414
415#[inline]
419fn mutate<T: SolCall>(
420 call: T,
421 sender: Address,
422 f: impl FnOnce(Address, T) -> Result<T::Return>,
423) -> PrecompileResult {
424 if StorageCtx.is_static() {
425 return Ok(PrecompileOutput::revert(
426 0,
427 StaticCallNotAllowed {}.abi_encode().into(),
428 StorageCtx.reservoir(),
429 ));
430 }
431 f(sender, call).into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into())
432}
433
434#[inline]
438fn mutate_void<T: SolCall>(
439 call: T,
440 sender: Address,
441 f: impl FnOnce(Address, T) -> Result<()>,
442) -> PrecompileResult {
443 if StorageCtx.is_static() {
444 return Ok(PrecompileOutput::revert(
445 0,
446 StaticCallNotAllowed {}.abi_encode().into(),
447 StorageCtx.reservoir(),
448 ));
449 }
450 f(sender, call).into_precompile_result(0, 0, |()| Bytes::new())
451}
452
453#[inline]
455pub(crate) fn charge_input_cost(
456 storage: &mut StorageCtx,
457 calldata: &[u8],
458) -> Option<PrecompileResult> {
459 if storage.deduct_gas(input_cost(calldata.len())).is_err() {
460 return Some(Ok(storage.halt_output(PrecompileHalt::OutOfGas)));
461 }
462 None
463}
464
465#[inline]
476fn fill_state_gas(output: &mut PrecompileOutput, storage: &StorageCtx) {
477 if storage.spec().is_t4() && output.is_success() {
478 output.gas_refunded = storage.gas_refunded();
479 }
480
481 if storage.amsterdam_eip8037_enabled() {
482 if output.is_success() {
483 output.reservoir = storage.reservoir();
485 output.state_gas_used = storage.state_gas_used();
486 } else {
487 output.reservoir = storage.state_gas_used() + storage.reservoir();
490 output.state_gas_used = 0;
491 }
492 }
493}
494
495#[derive(Clone, Copy, Debug, Default)]
500pub(crate) struct SelectorSchedule<'a> {
501 hardfork: TempoHardfork,
502 added: &'a [[u8; 4]],
503 dropped: &'a [[u8; 4]],
504}
505
506impl<'a> SelectorSchedule<'a> {
507 pub(crate) const fn new(hardfork: TempoHardfork) -> Self {
509 Self {
510 hardfork,
511 added: &[],
512 dropped: &[],
513 }
514 }
515
516 pub(crate) const fn with_added(mut self, selectors: &'a [[u8; 4]]) -> Self {
520 self.added = selectors;
521 self
522 }
523
524 pub(crate) const fn with_dropped(mut self, selectors: &'a [[u8; 4]]) -> Self {
528 self.dropped = selectors;
529 self
530 }
531
532 #[inline]
534 fn rejects(self, selector: [u8; 4], active: TempoHardfork) -> bool {
535 if self.hardfork <= active {
536 self.dropped
537 } else {
538 self.added
539 }
540 .contains(&selector)
541 }
542}
543
544#[inline]
550pub(crate) fn dispatch_call<T>(
551 calldata: &[u8],
552 hardforks: &[SelectorSchedule<'_>],
553 decode: impl FnOnce(&[u8]) -> core::result::Result<T, alloy::sol_types::Error>,
554 f: impl FnOnce(T) -> PrecompileResult,
555) -> PrecompileResult {
556 let storage = StorageCtx::default();
557
558 if calldata.len() < 4 {
559 if storage.spec().is_t1() {
560 return Ok(storage.revert_output(Bytes::new()));
561 } else {
562 return Ok(storage.halt_output(PrecompileHalt::Other(
563 "Invalid input: missing function selector".into(),
564 )));
565 }
566 }
567
568 let selector: [u8; 4] = calldata[..4].try_into().expect("calldata len >= 4");
569 if hardforks
570 .iter()
571 .any(|schedule| schedule.rejects(selector, storage.spec()))
572 {
573 return storage.error_result(error::TempoPrecompileError::UnknownFunctionSelector(
574 selector,
575 ));
576 }
577
578 let result = decode(calldata);
579
580 match result {
581 Ok(call) => f(call).map(|mut res| {
582 res.gas_used = storage.gas_used();
584 fill_state_gas(&mut res, &storage);
585 res
586 }),
587 Err(alloy::sol_types::Error::UnknownSelector { selector, .. }) => storage.error_result(
588 error::TempoPrecompileError::UnknownFunctionSelector(*selector),
589 ),
590 Err(_) => Ok(storage.revert_output(Bytes::new())),
591 }
592}
593
594#[cfg(test)]
596pub fn expect_precompile_revert<E>(result: &PrecompileResult, expected_error: E)
597where
598 E: SolInterface + PartialEq + std::fmt::Debug,
599{
600 match result {
601 Ok(result) => {
602 assert!(result.is_revert());
603 let decoded = E::abi_decode(&result.bytes).unwrap();
604 assert_eq!(decoded, expected_error);
605 }
606 Err(other) => {
607 panic!("expected reverted output, got: {other:?}");
608 }
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::*;
615 use crate::{
616 storage::{StorageCtx, hashmap::HashMapStorageProvider},
617 tip20::TIP20Token,
618 };
619 use alloy::primitives::{Address, Bytes, U256, bytes};
620 use alloy_evm::{
621 EthEvmFactory, EvmEnv, EvmFactory, EvmInternals,
622 precompiles::{Precompile as AlloyEvmPrecompile, PrecompileInput},
623 };
624 use revm::{
625 context::{ContextTr, TxEnv},
626 database::{CacheDB, EmptyDB},
627 state::{AccountInfo, Bytecode},
628 };
629 use tempo_contracts::precompiles::{ITIP20, UnknownFunctionSelector};
630
631 #[test]
632 fn test_precompile_delegatecall() {
633 let cfg = CfgEnv::<TempoHardfork>::default();
634 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
635 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
636 });
637
638 let db = CacheDB::new(EmptyDB::new());
639 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
640 let block = evm.block.clone();
641 let tx = TxEnv::default();
642 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
643
644 let target_address = Address::random();
645 let bytecode_address = Address::random();
646 let input = PrecompileInput {
647 data: &Bytes::new(),
648 caller: Address::ZERO,
649 internals: evm_internals,
650 gas: 0,
651 value: U256::ZERO,
652 is_static: false,
653 target_address,
654 bytecode_address,
655 reservoir: 0,
656 };
657
658 let result = AlloyEvmPrecompile::call(&precompile, input);
659
660 match result {
661 Ok(output) => {
662 assert!(output.is_revert());
663 let decoded = DelegateCallNotAllowed::abi_decode(&output.bytes).unwrap();
664 assert!(matches!(decoded, DelegateCallNotAllowed {}));
665 }
666 Err(_) => panic!("expected reverted output"),
667 }
668 }
669
670 #[test]
671 fn test_precompile_static_call() {
672 let cfg = CfgEnv::<TempoHardfork>::default();
673 let tx = TxEnv::default();
674 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
675 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
676 });
677
678 let token_address = PATH_USD_ADDRESS;
679
680 let call_static = |calldata: Bytes| {
681 let mut db = CacheDB::new(EmptyDB::new());
682 db.insert_account_info(
683 token_address,
684 AccountInfo {
685 code: Some(Bytecode::new_raw(bytes!("0xEF"))),
686 ..Default::default()
687 },
688 );
689 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
690 let block = evm.block.clone();
691 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
692
693 let input = PrecompileInput {
694 data: &calldata,
695 caller: Address::ZERO,
696 internals: evm_internals,
697 gas: 1_000_000,
698 is_static: true,
699 value: U256::ZERO,
700 target_address: token_address,
701 bytecode_address: token_address,
702 reservoir: 0,
703 };
704
705 AlloyEvmPrecompile::call(&precompile, input)
706 };
707
708 let result = call_static(Bytes::from(
710 ITIP20::transferCall {
711 to: Address::random(),
712 amount: U256::from(100),
713 }
714 .abi_encode(),
715 ));
716 let output = result.expect("expected Ok");
717 assert!(output.is_revert());
718 assert!(StaticCallNotAllowed::abi_decode(&output.bytes).is_ok());
719
720 let result = call_static(Bytes::from(
722 ITIP20::approveCall {
723 spender: Address::random(),
724 amount: U256::from(100),
725 }
726 .abi_encode(),
727 ));
728 let output = result.expect("expected Ok");
729 assert!(output.is_revert());
730 assert!(StaticCallNotAllowed::abi_decode(&output.bytes).is_ok());
731
732 let result = call_static(Bytes::from(
734 ITIP20::balanceOfCall {
735 account: Address::random(),
736 }
737 .abi_encode(),
738 ));
739 let output = result.expect("expected Ok");
740 assert!(
741 !output.is_revert(),
742 "view function should not revert in static context"
743 );
744 }
745
746 #[test]
751 fn test_early_return_revert_reports_gas_used() {
752 let mut cfg = CfgEnv::<TempoHardfork>::default();
753 cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T1);
754 let tx = TxEnv::default();
755 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
756 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
757 });
758
759 let token_address = PATH_USD_ADDRESS;
760
761 let db = CacheDB::new(EmptyDB::new());
763 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
764 let block = evm.block.clone();
765 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
766
767 let calldata = Bytes::from(
768 ITIP20::transferCall {
769 to: Address::random(),
770 amount: U256::from(100),
771 }
772 .abi_encode(),
773 );
774
775 let input = PrecompileInput {
776 data: &calldata,
777 caller: Address::ZERO,
778 internals: evm_internals,
779 gas: 1_000_000,
780 is_static: false,
781 value: U256::ZERO,
782 target_address: token_address,
783 bytecode_address: token_address,
784 reservoir: 0,
785 };
786
787 let result = AlloyEvmPrecompile::call(&precompile, input);
788 let output = result.expect("expected Ok");
789 assert!(
790 output.status.is_revert(),
791 "uninitialized token should revert"
792 );
793 assert!(
795 output.gas_used > 0,
796 "early-return revert should report non-zero gas_used, got {}",
797 output.gas_used
798 );
799 }
800
801 #[test]
802 fn test_invalid_calldata_hardfork_behavior() {
803 let call_with_spec = |calldata: Bytes, spec: TempoHardfork| {
804 let mut cfg = CfgEnv::<TempoHardfork>::default();
805 cfg.set_spec_and_mainnet_gas_params(spec);
806 let tx = TxEnv::default();
807 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
808 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
809 });
810
811 let mut db = CacheDB::new(EmptyDB::new());
812 db.insert_account_info(
813 PATH_USD_ADDRESS,
814 AccountInfo {
815 code: Some(Bytecode::new_raw(bytes!("0xEF"))),
816 ..Default::default()
817 },
818 );
819 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
820 let block = evm.block.clone();
821 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
822
823 let input = PrecompileInput {
824 data: &calldata,
825 caller: Address::ZERO,
826 internals: evm_internals,
827 gas: 1_000_000,
828 is_static: false,
829 value: U256::ZERO,
830 target_address: PATH_USD_ADDRESS,
831 bytecode_address: PATH_USD_ADDRESS,
832 reservoir: 0,
833 };
834
835 AlloyEvmPrecompile::call(&precompile, input)
836 };
837
838 let empty = call_with_spec(Bytes::new(), TempoHardfork::T1)
840 .expect("T1: expected Ok with reverted output");
841 assert!(empty.is_revert(), "T1: expected reverted output");
842 assert!(empty.bytes.is_empty());
843 assert!(empty.gas_used > 0);
845
846 let unknown = call_with_spec(Bytes::from([0xAA; 4]), TempoHardfork::T1)
848 .expect("T1: expected Ok with reverted output");
849 assert!(unknown.is_revert(), "T1: expected reverted output");
850
851 let decoded =
853 tempo_contracts::precompiles::UnknownFunctionSelector::abi_decode(&unknown.bytes)
854 .expect("T1: expected UnknownFunctionSelector error");
855 assert_eq!(decoded.selector.as_slice(), &[0xAA, 0xAA, 0xAA, 0xAA]);
856
857 assert!(unknown.gas_used >= empty.gas_used);
859
860 let result = call_with_spec(Bytes::new(), TempoHardfork::T0);
862 let output = result.expect("T0: expected Ok(halt) for invalid calldata");
863 assert!(
864 output.is_halt(),
865 "T0: expected halted output for invalid calldata"
866 );
867 }
868
869 #[test]
873 fn test_precompile_state_gas_zero_pre_t4() {
874 let call_with_spec = |calldata: Bytes, spec: TempoHardfork| {
875 let mut cfg = CfgEnv::<TempoHardfork>::default();
876 cfg.set_spec_and_mainnet_gas_params(spec);
877 let tx = TxEnv::default();
878 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
879 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
880 });
881
882 let mut db = CacheDB::new(EmptyDB::new());
883 db.insert_account_info(
884 PATH_USD_ADDRESS,
885 AccountInfo {
886 code: Some(Bytecode::new_raw(bytes!("0xEF"))),
887 ..Default::default()
888 },
889 );
890 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
891 let block = evm.block.clone();
892 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
893
894 let input = PrecompileInput {
895 data: &calldata,
896 caller: Address::ZERO,
897 internals: evm_internals,
898 gas: 1_000_000,
899 is_static: false,
900 value: U256::ZERO,
901 target_address: PATH_USD_ADDRESS,
902 bytecode_address: PATH_USD_ADDRESS,
903 reservoir: 0,
904 };
905
906 AlloyEvmPrecompile::call(&precompile, input)
907 };
908
909 let result = call_with_spec(
911 ITIP20::balanceOfCall::new((Address::ZERO,))
912 .abi_encode()
913 .into(),
914 TempoHardfork::T2,
915 )
916 .expect("T2 balanceOf should succeed");
917 assert!(result.gas_used > 0, "precompile should consume gas");
918 assert_eq!(
919 result.state_gas_used, 0,
920 "pre-T4 precompile must not report state_gas_used, got {}",
921 result.state_gas_used
922 );
923
924 let reverted =
926 call_with_spec(Bytes::new(), TempoHardfork::T1).expect("T1 empty should revert");
927 assert!(reverted.status.is_revert());
928 assert_eq!(
929 reverted.state_gas_used, 0,
930 "pre-T4 reverted precompile must not report state_gas_used"
931 );
932 }
933
934 #[test]
938 fn test_t4_state_gas_only_includes_state_creating_ops() {
939 let mut cfg = CfgEnv::<TempoHardfork>::default();
940 cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T4);
941
942 let sender = Address::repeat_byte(0x01);
943 let recipient = Address::repeat_byte(0x02);
944
945 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
946 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
947 });
948
949 let db = CacheDB::new(EmptyDB::new());
950 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
951
952 {
954 let block = evm.block.clone();
955 let tx = TxEnv::default();
956 let internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
957 let mut provider =
958 crate::storage::evm::EvmPrecompileStorageProvider::new_max_gas(internals, &cfg);
959 crate::storage::StorageCtx::enter(&mut provider, || {
960 crate::test_util::TIP20Setup::path_usd(sender)
961 .with_issuer(sender)
962 .with_mint(sender, U256::from(1000))
963 .apply()
964 })
965 .expect("TIP20 setup should succeed");
966 }
967
968 let calldata: Bytes = ITIP20::balanceOfCall { account: sender }
970 .abi_encode()
971 .into();
972 let block = evm.block.clone();
973 let tx = TxEnv::default();
974 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
975 let input = PrecompileInput {
976 data: &calldata,
977 caller: sender,
978 internals: evm_internals,
979 gas: 1_000_000,
980 is_static: false,
981 value: U256::ZERO,
982 target_address: PATH_USD_ADDRESS,
983 bytecode_address: PATH_USD_ADDRESS,
984 reservoir: 0,
985 };
986 let output =
987 AlloyEvmPrecompile::call(&precompile, input).expect("balanceOf should succeed");
988 assert!(output.is_success());
989 assert!(output.gas_used > 0, "balanceOf should consume gas");
990 assert_eq!(
991 output.state_gas_used, 0,
992 "read-only balanceOf must have state_gas_used == 0, got {}",
993 output.state_gas_used
994 );
995
996 {
999 let block = evm.block.clone();
1001 let tx = TxEnv::default();
1002 let internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
1003 let mut provider =
1004 crate::storage::evm::EvmPrecompileStorageProvider::new_max_gas(internals, &cfg);
1005 crate::storage::StorageCtx::enter(&mut provider, || {
1006 crate::test_util::TIP20Setup::path_usd(sender)
1007 .with_mint(recipient, U256::from(1))
1008 .apply()
1009 })
1010 .expect("TIP20 setup should succeed");
1011 }
1012 let calldata: Bytes = ITIP20::transferCall {
1013 to: recipient,
1014 amount: U256::from(100),
1015 }
1016 .abi_encode()
1017 .into();
1018 let block = evm.block.clone();
1019 let tx = TxEnv::default();
1020 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
1021 let input = PrecompileInput {
1022 data: &calldata,
1023 caller: sender,
1024 internals: evm_internals,
1025 gas: 1_000_000,
1026 is_static: false,
1027 value: U256::ZERO,
1028 target_address: PATH_USD_ADDRESS,
1029 bytecode_address: PATH_USD_ADDRESS,
1030 reservoir: 0,
1031 };
1032 let output = AlloyEvmPrecompile::call(&precompile, input).expect("transfer should succeed");
1033 assert!(output.is_success());
1034 assert!(output.gas_used > 0, "transfer should consume gas");
1035 assert_eq!(
1036 output.state_gas_used, 0,
1037 "transfer to existing account (nonzero->nonzero SSTORE) must have state_gas_used == 0, got {}",
1038 output.state_gas_used
1039 );
1040 }
1041
1042 #[test]
1048 fn test_precompile_gas_refund_in_reservoir_t4() {
1049 let mut cfg = CfgEnv::<TempoHardfork>::default();
1050 cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T4);
1051 cfg.enable_amsterdam_eip8037 = true;
1053
1054 let sender = Address::repeat_byte(0x01);
1055 let recipient = Address::repeat_byte(0x02);
1056
1057 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
1058 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
1059 });
1060
1061 let db = CacheDB::new(EmptyDB::new());
1062 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
1063
1064 {
1066 let block = evm.block.clone();
1067 let tx = TxEnv::default();
1068 let internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
1069 let mut provider =
1070 crate::storage::evm::EvmPrecompileStorageProvider::new_max_gas(internals, &cfg);
1071 crate::storage::StorageCtx::enter(&mut provider, || {
1072 crate::test_util::TIP20Setup::path_usd(sender)
1073 .with_issuer(sender)
1074 .with_mint(sender, U256::from(1000))
1075 .apply()
1076 })
1077 .expect("TIP20 setup should succeed");
1078 }
1079
1080 let calldata: Bytes = ITIP20::transferCall {
1083 to: recipient,
1084 amount: U256::from(1000),
1085 }
1086 .abi_encode()
1087 .into();
1088
1089 let block = evm.block.clone();
1090 let tx = TxEnv::default();
1091 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
1092
1093 let input = PrecompileInput {
1094 data: &calldata,
1095 caller: sender,
1096 internals: evm_internals,
1097 gas: 1_000_000,
1098 is_static: false,
1099 value: U256::ZERO,
1100 target_address: PATH_USD_ADDRESS,
1101 bytecode_address: PATH_USD_ADDRESS,
1102 reservoir: 0,
1103 };
1104
1105 let output = AlloyEvmPrecompile::call(&precompile, input).expect("transfer should succeed");
1106 assert!(output.is_success(), "transfer should be successful");
1107
1108 assert!(
1110 output.gas_refunded != 0,
1111 "T4+ successful precompile with SSTORE refund must encode refund in gas_refunded, got 0"
1112 );
1113 }
1114
1115 #[test]
1116 fn test_dispatch_call_applies_hardfork_selector_gates() -> eyre::Result<()> {
1117 alloy::sol! {
1118 interface ISelectorGatedTest {
1119 function stable() external;
1120 function t2Added(uint256 value) external;
1121 function t3Removed() external;
1122 }
1123 }
1124
1125 const SELECTOR_SCHEDULE: &[SelectorSchedule<'static>] = &[
1126 SelectorSchedule::new(TempoHardfork::T2)
1127 .with_added(&[ISelectorGatedTest::t2AddedCall::SELECTOR]),
1128 SelectorSchedule::new(TempoHardfork::T3)
1129 .with_dropped(&[ISelectorGatedTest::t3RemovedCall::SELECTOR]),
1130 ];
1131
1132 let call_with_spec = |spec: TempoHardfork, calldata: &[u8]| {
1133 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
1134 StorageCtx::enter(&mut storage, || {
1135 dispatch_call(
1136 calldata,
1137 SELECTOR_SCHEDULE,
1138 ISelectorGatedTest::ISelectorGatedTestCalls::abi_decode,
1139 |call| match call {
1140 ISelectorGatedTest::ISelectorGatedTestCalls::stable(_) => {
1141 Ok(PrecompileOutput::new(0, Bytes::from_static(b"stable"), 0))
1142 }
1143 ISelectorGatedTest::ISelectorGatedTestCalls::t2Added(_) => {
1144 Ok(PrecompileOutput::new(0, Bytes::from_static(b"added"), 0))
1145 }
1146 ISelectorGatedTest::ISelectorGatedTestCalls::t3Removed(_) => {
1147 Ok(PrecompileOutput::new(0, Bytes::from_static(b"removed"), 0))
1148 }
1149 },
1150 )
1151 })
1152 };
1153
1154 let t2_added_calldata = ISelectorGatedTest::t2AddedCall { value: U256::ZERO }.abi_encode();
1155 let t3_removed_calldata = ISelectorGatedTest::t3RemovedCall {}.abi_encode();
1156
1157 let pre_t2_added = call_with_spec(TempoHardfork::T1, &t2_added_calldata)?;
1159 assert!(pre_t2_added.is_revert());
1160 let decoded = UnknownFunctionSelector::abi_decode(&pre_t2_added.bytes)?;
1161 assert_eq!(
1162 decoded.selector.as_slice(),
1163 &ISelectorGatedTest::t2AddedCall::SELECTOR
1164 );
1165
1166 let post_t2_added = call_with_spec(TempoHardfork::T2, &t2_added_calldata)?;
1168 assert!(!post_t2_added.is_revert());
1169 assert_eq!(post_t2_added.bytes.as_ref(), b"added");
1170
1171 let pre_t3_removed = call_with_spec(TempoHardfork::T2, &t3_removed_calldata)?;
1173 assert!(!pre_t3_removed.is_revert());
1174 assert_eq!(pre_t3_removed.bytes.as_ref(), b"removed");
1175
1176 let post_t3_removed = call_with_spec(TempoHardfork::T3, &t3_removed_calldata)?;
1178 assert!(post_t3_removed.is_revert());
1179 let decoded = UnknownFunctionSelector::abi_decode(&post_t3_removed.bytes)?;
1180 assert_eq!(
1181 decoded.selector.as_slice(),
1182 &ISelectorGatedTest::t3RemovedCall::SELECTOR
1183 );
1184
1185 let malformed_added = call_with_spec(
1187 TempoHardfork::T1,
1188 &ISelectorGatedTest::t2AddedCall::SELECTOR,
1189 )?;
1190 assert!(malformed_added.is_revert());
1191 let decoded = UnknownFunctionSelector::abi_decode(&malformed_added.bytes)?;
1192 assert_eq!(
1193 decoded.selector.as_slice(),
1194 &ISelectorGatedTest::t2AddedCall::SELECTOR
1195 );
1196
1197 Ok(())
1198 }
1199
1200 #[test]
1201 fn test_input_cost_returns_non_zero_for_input() {
1202 assert_eq!(input_cost(0), 0);
1204
1205 assert_eq!(input_cost(1), INPUT_PER_WORD_COST);
1207
1208 assert_eq!(input_cost(32), INPUT_PER_WORD_COST);
1210
1211 assert_eq!(input_cost(33), INPUT_PER_WORD_COST * 2);
1213 }
1214
1215 #[test]
1216 fn test_extend_tempo_precompiles_registers_precompiles() {
1217 let mut cfg = CfgEnv::<TempoHardfork>::default();
1218 cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T3);
1219 let precompiles = tempo_precompiles(&cfg);
1220
1221 let factory_precompile = precompiles.get(&TIP20_FACTORY_ADDRESS);
1223 assert!(
1224 factory_precompile.is_some(),
1225 "TIP20Factory should be registered"
1226 );
1227
1228 let registry_precompile = precompiles.get(&TIP403_REGISTRY_ADDRESS);
1230 assert!(
1231 registry_precompile.is_some(),
1232 "TIP403Registry should be registered"
1233 );
1234
1235 let fee_manager_precompile = precompiles.get(&TIP_FEE_MANAGER_ADDRESS);
1237 assert!(
1238 fee_manager_precompile.is_some(),
1239 "TipFeeManager should be registered"
1240 );
1241
1242 let dex_precompile = precompiles.get(&STABLECOIN_DEX_ADDRESS);
1244 assert!(
1245 dex_precompile.is_some(),
1246 "StablecoinDEX should be registered"
1247 );
1248
1249 let nonce_precompile = precompiles.get(&NONCE_PRECOMPILE_ADDRESS);
1251 assert!(
1252 nonce_precompile.is_some(),
1253 "NonceManager should be registered"
1254 );
1255
1256 let validator_precompile = precompiles.get(&VALIDATOR_CONFIG_ADDRESS);
1258 assert!(
1259 validator_precompile.is_some(),
1260 "ValidatorConfig should be registered"
1261 );
1262
1263 let validator_v2_precompile = precompiles.get(&VALIDATOR_CONFIG_V2_ADDRESS);
1265 assert!(
1266 validator_v2_precompile.is_some(),
1267 "ValidatorConfigV2 should be registered"
1268 );
1269
1270 let keychain_precompile = precompiles.get(&ACCOUNT_KEYCHAIN_ADDRESS);
1272 assert!(
1273 keychain_precompile.is_some(),
1274 "AccountKeychain should be registered"
1275 );
1276
1277 let sig_verifier_precompile = precompiles.get(&SIGNATURE_VERIFIER_ADDRESS);
1279 assert!(
1280 sig_verifier_precompile.is_some(),
1281 "SignatureVerifier should be registered at T3"
1282 );
1283
1284 let channel_reserve_precompile = precompiles.get(&TIP20_CHANNEL_RESERVE_ADDRESS);
1286 assert!(
1287 channel_reserve_precompile.is_none(),
1288 "TIP20 channel reserve should not be registered before T5"
1289 );
1290
1291 let tip20_precompile = precompiles.get(&PATH_USD_ADDRESS);
1293 assert!(
1294 tip20_precompile.is_some(),
1295 "TIP20 tokens should be registered"
1296 );
1297
1298 let random_address = Address::random();
1300 let random_precompile = precompiles.get(&random_address);
1301 assert!(
1302 random_precompile.is_none(),
1303 "Random address should not be a precompile"
1304 );
1305 }
1306
1307 #[test]
1308 fn test_signature_verifier_not_registered_pre_t3() {
1309 let cfg = CfgEnv::<TempoHardfork>::default();
1310 let precompiles = tempo_precompiles(&cfg);
1311
1312 assert!(
1313 precompiles.get(&SIGNATURE_VERIFIER_ADDRESS).is_none(),
1314 "SignatureVerifier should NOT be registered before T3"
1315 );
1316 }
1317
1318 #[test]
1319 fn test_channel_reserve_registered_at_t5_only() {
1320 let pre_t5 = CfgEnv::<TempoHardfork>::default();
1321 assert!(
1322 tempo_precompiles(&pre_t5)
1323 .get(&TIP20_CHANNEL_RESERVE_ADDRESS)
1324 .is_none(),
1325 "TIP20 channel reserve should NOT be registered before T5"
1326 );
1327
1328 let mut t5 = CfgEnv::<TempoHardfork>::default();
1329 t5.set_spec_and_mainnet_gas_params(TempoHardfork::T5);
1330 assert!(
1331 tempo_precompiles(&t5)
1332 .get(&TIP20_CHANNEL_RESERVE_ADDRESS)
1333 .is_some(),
1334 "TIP20 channel reserve should be registered at T5"
1335 );
1336 }
1337
1338 #[test]
1339 fn test_is_precompile_address() {
1340 for &(address, activated) in SYSTEM_PRECOMPILES {
1341 assert!(is_precompile_address(address, activated));
1342 assert!(is_precompile_address(address, TempoHardfork::T7));
1343
1344 if activated != TempoHardfork::Genesis {
1345 assert!(!is_precompile_address(address, TempoHardfork::Genesis));
1346 }
1347 }
1348
1349 assert!(PATH_USD_ADDRESS.is_tip20());
1351 assert!(is_precompile_address(
1352 PATH_USD_ADDRESS,
1353 TempoHardfork::Genesis
1354 ));
1355 }
1356
1357 #[test]
1358 fn test_p256verify_availability_across_t1c_boundary() {
1359 let has_p256 = |spec: TempoHardfork| -> bool {
1360 let p256_addr = Address::from_word(U256::from(256).into());
1362
1363 let mut cfg = CfgEnv::<TempoHardfork>::default();
1364 cfg.set_spec_and_mainnet_gas_params(spec);
1365 tempo_precompiles(&cfg).get(&p256_addr).is_some()
1366 };
1367
1368 for spec in [
1370 TempoHardfork::Genesis,
1371 TempoHardfork::T0,
1372 TempoHardfork::T1,
1373 TempoHardfork::T1A,
1374 TempoHardfork::T1B,
1375 ] {
1376 assert!(
1377 !has_p256(spec),
1378 "P256VERIFY should NOT be available at {spec:?} (pre-T1C)"
1379 );
1380 }
1381
1382 for spec in [TempoHardfork::T1C, TempoHardfork::T2] {
1384 assert!(
1385 has_p256(spec),
1386 "P256VERIFY should be available at {spec:?} (T1C+)"
1387 );
1388 }
1389 }
1390}