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 signature_verifier;
16pub mod stablecoin_dex;
17pub mod tip20;
18pub mod tip20_factory;
19pub mod tip403_registry;
20pub mod tip_fee_manager;
21pub mod validator_config;
22pub mod validator_config_v2;
23
24#[cfg(any(test, feature = "test-utils"))]
25pub mod test_util;
26
27use crate::{
28 account_keychain::AccountKeychain, address_registry::AddressRegistry, nonce::NonceManager,
29 signature_verifier::SignatureVerifier, stablecoin_dex::StablecoinDEX, storage::StorageCtx,
30 tip_fee_manager::TipFeeManager, tip20::TIP20Token, tip20_factory::TIP20Factory,
31 tip403_registry::TIP403Registry, validator_config::ValidatorConfig,
32 validator_config_v2::ValidatorConfigV2,
33};
34use tempo_chainspec::hardfork::TempoHardfork;
35use tempo_primitives::TempoAddressExt;
36
37#[cfg(test)]
38use alloy::sol_types::SolInterface;
39use alloy::{
40 primitives::{Address, Bytes},
41 sol,
42 sol_types::{SolCall, SolError},
43};
44use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap};
45use revm::{
46 context::CfgEnv,
47 handler::EthPrecompiles,
48 precompile::{PrecompileHalt, PrecompileId, PrecompileOutput, PrecompileResult},
49 primitives::hardfork::SpecId,
50};
51
52pub use tempo_contracts::precompiles::{
53 ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, DEFAULT_FEE_TOKEN,
54 NONCE_PRECOMPILE_ADDRESS, PATH_USD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS, STABLECOIN_DEX_ADDRESS,
55 TIP_FEE_MANAGER_ADDRESS, TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS,
56 VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
57};
58
59pub use account_keychain::AuthorizedKey;
61
62pub const INPUT_PER_WORD_COST: u64 = 6;
66
67pub const ECRECOVER_GAS: u64 = 3_000;
69
70#[inline]
72pub fn input_cost(calldata_len: usize) -> u64 {
73 calldata_len
74 .div_ceil(32)
75 .saturating_mul(INPUT_PER_WORD_COST as usize) as u64
76}
77
78pub trait Precompile {
83 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 address.is_tip20() {
120 Some(TIP20Token::create_precompile(*address, &cfg))
121 } else if *address == TIP20_FACTORY_ADDRESS {
122 Some(TIP20Factory::create_precompile(&cfg))
123 } else if *address == ADDRESS_REGISTRY_ADDRESS && cfg.spec.is_t3() {
124 Some(AddressRegistry::create_precompile(&cfg))
125 } else if *address == TIP403_REGISTRY_ADDRESS {
126 Some(TIP403Registry::create_precompile(&cfg))
127 } else if *address == TIP_FEE_MANAGER_ADDRESS {
128 Some(TipFeeManager::create_precompile(&cfg))
129 } else if *address == STABLECOIN_DEX_ADDRESS {
130 Some(StablecoinDEX::create_precompile(&cfg))
131 } else if *address == NONCE_PRECOMPILE_ADDRESS {
132 Some(NonceManager::create_precompile(&cfg))
133 } else if *address == VALIDATOR_CONFIG_ADDRESS {
134 Some(ValidatorConfig::create_precompile(&cfg))
135 } else if *address == ACCOUNT_KEYCHAIN_ADDRESS {
136 Some(AccountKeychain::create_precompile(&cfg))
137 } else if *address == VALIDATOR_CONFIG_V2_ADDRESS {
138 Some(ValidatorConfigV2::create_precompile(&cfg))
139 } else if *address == SIGNATURE_VERIFIER_ADDRESS && cfg.spec.is_t3() {
140 Some(SignatureVerifier::create_precompile(&cfg))
141 } else {
142 None
143 }
144 });
145}
146
147sol! {
148 error DelegateCallNotAllowed();
149 error StaticCallNotAllowed();
150}
151
152macro_rules! tempo_precompile {
153 ($id:expr, $cfg:expr, |$input:ident| $impl:expr) => {{
154 let spec = $cfg.spec;
155 let amsterdam_eip8037_enabled = $cfg.enable_amsterdam_eip8037;
156 let gas_params = $cfg.gas_params.clone();
157 DynPrecompile::new_stateful(PrecompileId::Custom($id.into()), move |$input| {
158 if !$input.is_direct_call() {
159 return Ok(PrecompileOutput::revert(
160 0,
161 DelegateCallNotAllowed {}.abi_encode().into(),
162 $input.reservoir,
163 ));
164 }
165 let mut storage = crate::storage::evm::EvmPrecompileStorageProvider::new(
166 $input.internals,
167 $input.gas,
168 $input.reservoir,
169 spec,
170 amsterdam_eip8037_enabled,
171 $input.is_static,
172 gas_params.clone(),
173 );
174 crate::storage::StorageCtx::enter(&mut storage, || {
175 $impl.call($input.data, $input.caller)
176 })
177 })
178 }};
179}
180
181impl TipFeeManager {
182 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
184 tempo_precompile!("TipFeeManager", cfg, |input| { Self::new() })
185 }
186}
187
188impl AddressRegistry {
189 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
191 tempo_precompile!("AddressRegistry", cfg, |input| { Self::new() })
192 }
193}
194
195impl TIP403Registry {
196 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
198 tempo_precompile!("TIP403Registry", cfg, |input| { Self::new() })
199 }
200}
201
202impl TIP20Factory {
203 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
205 tempo_precompile!("TIP20Factory", cfg, |input| { Self::new() })
206 }
207}
208
209impl TIP20Token {
210 pub fn create_precompile(address: Address, cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
212 tempo_precompile!("TIP20Token", cfg, |input| {
213 Self::from_address(address).expect("TIP20 prefix already verified")
214 })
215 }
216}
217
218impl StablecoinDEX {
219 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
221 tempo_precompile!("StablecoinDEX", cfg, |input| { Self::new() })
222 }
223}
224
225impl NonceManager {
226 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
228 tempo_precompile!("NonceManager", cfg, |input| { Self::new() })
229 }
230}
231
232impl AccountKeychain {
233 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
235 tempo_precompile!("AccountKeychain", cfg, |input| { Self::new() })
236 }
237}
238
239impl ValidatorConfig {
240 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
242 tempo_precompile!("ValidatorConfig", cfg, |input| { Self::new() })
243 }
244}
245
246impl ValidatorConfigV2 {
247 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
249 tempo_precompile!("ValidatorConfigV2", cfg, |input| { Self::new() })
250 }
251}
252
253impl SignatureVerifier {
254 pub fn create_precompile(cfg: &CfgEnv<TempoHardfork>) -> DynPrecompile {
256 tempo_precompile!("SignatureVerifier", cfg, |input| { Self::new() })
257 }
258}
259
260#[inline]
262fn metadata<T: SolCall>(f: impl FnOnce() -> Result<T::Return>) -> PrecompileResult {
263 f().into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into())
264}
265
266#[inline]
268fn view<T: SolCall>(call: T, f: impl FnOnce(T) -> Result<T::Return>) -> PrecompileResult {
269 f(call).into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into())
270}
271
272#[inline]
276fn mutate<T: SolCall>(
277 call: T,
278 sender: Address,
279 f: impl FnOnce(Address, T) -> Result<T::Return>,
280) -> PrecompileResult {
281 if StorageCtx.is_static() {
282 return Ok(PrecompileOutput::revert(
283 0,
284 StaticCallNotAllowed {}.abi_encode().into(),
285 StorageCtx.reservoir(),
286 ));
287 }
288 f(sender, call).into_precompile_result(0, 0, |ret| T::abi_encode_returns(&ret).into())
289}
290
291#[inline]
295fn mutate_void<T: SolCall>(
296 call: T,
297 sender: Address,
298 f: impl FnOnce(Address, T) -> Result<()>,
299) -> PrecompileResult {
300 if StorageCtx.is_static() {
301 return Ok(PrecompileOutput::revert(
302 0,
303 StaticCallNotAllowed {}.abi_encode().into(),
304 StorageCtx.reservoir(),
305 ));
306 }
307 f(sender, call).into_precompile_result(0, 0, |()| Bytes::new())
308}
309
310#[inline]
312pub(crate) fn charge_input_cost(
313 storage: &mut StorageCtx,
314 calldata: &[u8],
315) -> Option<PrecompileResult> {
316 if storage.deduct_gas(input_cost(calldata.len())).is_err() {
317 return Some(Ok(storage.halt_output(PrecompileHalt::OutOfGas)));
318 }
319 None
320}
321
322#[inline]
333fn fill_state_gas(output: &mut PrecompileOutput, storage: &StorageCtx) {
334 if storage.spec().is_t4() && output.is_success() {
335 output.gas_refunded = storage.gas_refunded();
336 }
337
338 if storage.amsterdam_eip8037_enabled() {
339 if output.is_success() {
340 output.reservoir = storage.reservoir();
342 output.state_gas_used = storage.state_gas_used();
343 } else {
344 output.reservoir = storage.state_gas_used() + storage.reservoir();
347 output.state_gas_used = 0;
348 }
349 }
350}
351
352#[derive(Clone, Copy, Debug, Default)]
357pub(crate) struct SelectorSchedule<'a> {
358 hardfork: TempoHardfork,
359 added: &'a [[u8; 4]],
360 dropped: &'a [[u8; 4]],
361}
362
363impl<'a> SelectorSchedule<'a> {
364 pub(crate) const fn new(hardfork: TempoHardfork) -> Self {
366 Self {
367 hardfork,
368 added: &[],
369 dropped: &[],
370 }
371 }
372
373 pub(crate) const fn with_added(mut self, selectors: &'a [[u8; 4]]) -> Self {
377 self.added = selectors;
378 self
379 }
380
381 pub(crate) const fn with_dropped(mut self, selectors: &'a [[u8; 4]]) -> Self {
385 self.dropped = selectors;
386 self
387 }
388
389 #[inline]
391 fn rejects(self, selector: [u8; 4], active: TempoHardfork) -> bool {
392 if self.hardfork <= active {
393 self.dropped
394 } else {
395 self.added
396 }
397 .contains(&selector)
398 }
399}
400
401#[inline]
407pub(crate) fn dispatch_call<T>(
408 calldata: &[u8],
409 hardforks: &[SelectorSchedule<'_>],
410 decode: impl FnOnce(&[u8]) -> core::result::Result<T, alloy::sol_types::Error>,
411 f: impl FnOnce(T) -> PrecompileResult,
412) -> PrecompileResult {
413 let storage = StorageCtx::default();
414
415 if calldata.len() < 4 {
416 if storage.spec().is_t1() {
417 return Ok(storage.revert_output(Bytes::new()));
418 } else {
419 return Ok(storage.halt_output(PrecompileHalt::Other(
420 "Invalid input: missing function selector".into(),
421 )));
422 }
423 }
424
425 let selector: [u8; 4] = calldata[..4].try_into().expect("calldata len >= 4");
426 if hardforks
427 .iter()
428 .any(|schedule| schedule.rejects(selector, storage.spec()))
429 {
430 return storage.error_result(error::TempoPrecompileError::UnknownFunctionSelector(
431 selector,
432 ));
433 }
434
435 let result = decode(calldata);
436
437 match result {
438 Ok(call) => f(call).map(|mut res| {
439 res.gas_used = storage.gas_used();
441 fill_state_gas(&mut res, &storage);
442 res
443 }),
444 Err(alloy::sol_types::Error::UnknownSelector { selector, .. }) => storage.error_result(
445 error::TempoPrecompileError::UnknownFunctionSelector(*selector),
446 ),
447 Err(_) => Ok(storage.revert_output(Bytes::new())),
448 }
449}
450
451#[cfg(test)]
453pub fn expect_precompile_revert<E>(result: &PrecompileResult, expected_error: E)
454where
455 E: SolInterface + PartialEq + std::fmt::Debug,
456{
457 match result {
458 Ok(result) => {
459 assert!(result.is_revert());
460 let decoded = E::abi_decode(&result.bytes).unwrap();
461 assert_eq!(decoded, expected_error);
462 }
463 Err(other) => {
464 panic!("expected reverted output, got: {other:?}");
465 }
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::{
473 storage::{StorageCtx, hashmap::HashMapStorageProvider},
474 tip20::TIP20Token,
475 };
476 use alloy::primitives::{Address, Bytes, U256, bytes};
477 use alloy_evm::{
478 EthEvmFactory, EvmEnv, EvmFactory, EvmInternals,
479 precompiles::{Precompile as AlloyEvmPrecompile, PrecompileInput},
480 };
481 use revm::{
482 context::{ContextTr, TxEnv},
483 database::{CacheDB, EmptyDB},
484 state::{AccountInfo, Bytecode},
485 };
486 use tempo_contracts::precompiles::{ITIP20, UnknownFunctionSelector};
487
488 #[test]
489 fn test_precompile_delegatecall() {
490 let cfg = CfgEnv::<TempoHardfork>::default();
491 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
492 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
493 });
494
495 let db = CacheDB::new(EmptyDB::new());
496 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
497 let block = evm.block.clone();
498 let tx = TxEnv::default();
499 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
500
501 let target_address = Address::random();
502 let bytecode_address = Address::random();
503 let input = PrecompileInput {
504 data: &Bytes::new(),
505 caller: Address::ZERO,
506 internals: evm_internals,
507 gas: 0,
508 value: U256::ZERO,
509 is_static: false,
510 target_address,
511 bytecode_address,
512 reservoir: 0,
513 };
514
515 let result = AlloyEvmPrecompile::call(&precompile, input);
516
517 match result {
518 Ok(output) => {
519 assert!(output.is_revert());
520 let decoded = DelegateCallNotAllowed::abi_decode(&output.bytes).unwrap();
521 assert!(matches!(decoded, DelegateCallNotAllowed {}));
522 }
523 Err(_) => panic!("expected reverted output"),
524 }
525 }
526
527 #[test]
528 fn test_precompile_static_call() {
529 let cfg = CfgEnv::<TempoHardfork>::default();
530 let tx = TxEnv::default();
531 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
532 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
533 });
534
535 let token_address = PATH_USD_ADDRESS;
536
537 let call_static = |calldata: Bytes| {
538 let mut db = CacheDB::new(EmptyDB::new());
539 db.insert_account_info(
540 token_address,
541 AccountInfo {
542 code: Some(Bytecode::new_raw(bytes!("0xEF"))),
543 ..Default::default()
544 },
545 );
546 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
547 let block = evm.block.clone();
548 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
549
550 let input = PrecompileInput {
551 data: &calldata,
552 caller: Address::ZERO,
553 internals: evm_internals,
554 gas: 1_000_000,
555 is_static: true,
556 value: U256::ZERO,
557 target_address: token_address,
558 bytecode_address: token_address,
559 reservoir: 0,
560 };
561
562 AlloyEvmPrecompile::call(&precompile, input)
563 };
564
565 let result = call_static(Bytes::from(
567 ITIP20::transferCall {
568 to: Address::random(),
569 amount: U256::from(100),
570 }
571 .abi_encode(),
572 ));
573 let output = result.expect("expected Ok");
574 assert!(output.is_revert());
575 assert!(StaticCallNotAllowed::abi_decode(&output.bytes).is_ok());
576
577 let result = call_static(Bytes::from(
579 ITIP20::approveCall {
580 spender: Address::random(),
581 amount: U256::from(100),
582 }
583 .abi_encode(),
584 ));
585 let output = result.expect("expected Ok");
586 assert!(output.is_revert());
587 assert!(StaticCallNotAllowed::abi_decode(&output.bytes).is_ok());
588
589 let result = call_static(Bytes::from(
591 ITIP20::balanceOfCall {
592 account: Address::random(),
593 }
594 .abi_encode(),
595 ));
596 let output = result.expect("expected Ok");
597 assert!(
598 !output.is_revert(),
599 "view function should not revert in static context"
600 );
601 }
602
603 #[test]
608 fn test_early_return_revert_reports_gas_used() {
609 let mut cfg = CfgEnv::<TempoHardfork>::default();
610 cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T1);
611 let tx = TxEnv::default();
612 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
613 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
614 });
615
616 let token_address = PATH_USD_ADDRESS;
617
618 let db = CacheDB::new(EmptyDB::new());
620 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
621 let block = evm.block.clone();
622 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
623
624 let calldata = Bytes::from(
625 ITIP20::transferCall {
626 to: Address::random(),
627 amount: U256::from(100),
628 }
629 .abi_encode(),
630 );
631
632 let input = PrecompileInput {
633 data: &calldata,
634 caller: Address::ZERO,
635 internals: evm_internals,
636 gas: 1_000_000,
637 is_static: false,
638 value: U256::ZERO,
639 target_address: token_address,
640 bytecode_address: token_address,
641 reservoir: 0,
642 };
643
644 let result = AlloyEvmPrecompile::call(&precompile, input);
645 let output = result.expect("expected Ok");
646 assert!(
647 output.status.is_revert(),
648 "uninitialized token should revert"
649 );
650 assert!(
652 output.gas_used > 0,
653 "early-return revert should report non-zero gas_used, got {}",
654 output.gas_used
655 );
656 }
657
658 #[test]
659 fn test_invalid_calldata_hardfork_behavior() {
660 let call_with_spec = |calldata: Bytes, spec: TempoHardfork| {
661 let mut cfg = CfgEnv::<TempoHardfork>::default();
662 cfg.set_spec_and_mainnet_gas_params(spec);
663 let tx = TxEnv::default();
664 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
665 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
666 });
667
668 let mut db = CacheDB::new(EmptyDB::new());
669 db.insert_account_info(
670 PATH_USD_ADDRESS,
671 AccountInfo {
672 code: Some(Bytecode::new_raw(bytes!("0xEF"))),
673 ..Default::default()
674 },
675 );
676 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
677 let block = evm.block.clone();
678 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
679
680 let input = PrecompileInput {
681 data: &calldata,
682 caller: Address::ZERO,
683 internals: evm_internals,
684 gas: 1_000_000,
685 is_static: false,
686 value: U256::ZERO,
687 target_address: PATH_USD_ADDRESS,
688 bytecode_address: PATH_USD_ADDRESS,
689 reservoir: 0,
690 };
691
692 AlloyEvmPrecompile::call(&precompile, input)
693 };
694
695 let empty = call_with_spec(Bytes::new(), TempoHardfork::T1)
697 .expect("T1: expected Ok with reverted output");
698 assert!(empty.is_revert(), "T1: expected reverted output");
699 assert!(empty.bytes.is_empty());
700 assert!(empty.gas_used > 0);
702
703 let unknown = call_with_spec(Bytes::from([0xAA; 4]), TempoHardfork::T1)
705 .expect("T1: expected Ok with reverted output");
706 assert!(unknown.is_revert(), "T1: expected reverted output");
707
708 let decoded =
710 tempo_contracts::precompiles::UnknownFunctionSelector::abi_decode(&unknown.bytes)
711 .expect("T1: expected UnknownFunctionSelector error");
712 assert_eq!(decoded.selector.as_slice(), &[0xAA, 0xAA, 0xAA, 0xAA]);
713
714 assert!(unknown.gas_used >= empty.gas_used);
716
717 let result = call_with_spec(Bytes::new(), TempoHardfork::T0);
719 let output = result.expect("T0: expected Ok(halt) for invalid calldata");
720 assert!(
721 output.is_halt(),
722 "T0: expected halted output for invalid calldata"
723 );
724 }
725
726 #[test]
730 fn test_precompile_state_gas_zero_pre_t4() {
731 let call_with_spec = |calldata: Bytes, spec: TempoHardfork| {
732 let mut cfg = CfgEnv::<TempoHardfork>::default();
733 cfg.set_spec_and_mainnet_gas_params(spec);
734 let tx = TxEnv::default();
735 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
736 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
737 });
738
739 let mut db = CacheDB::new(EmptyDB::new());
740 db.insert_account_info(
741 PATH_USD_ADDRESS,
742 AccountInfo {
743 code: Some(Bytecode::new_raw(bytes!("0xEF"))),
744 ..Default::default()
745 },
746 );
747 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
748 let block = evm.block.clone();
749 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
750
751 let input = PrecompileInput {
752 data: &calldata,
753 caller: Address::ZERO,
754 internals: evm_internals,
755 gas: 1_000_000,
756 is_static: false,
757 value: U256::ZERO,
758 target_address: PATH_USD_ADDRESS,
759 bytecode_address: PATH_USD_ADDRESS,
760 reservoir: 0,
761 };
762
763 AlloyEvmPrecompile::call(&precompile, input)
764 };
765
766 let result = call_with_spec(
768 ITIP20::balanceOfCall::new((Address::ZERO,))
769 .abi_encode()
770 .into(),
771 TempoHardfork::T2,
772 )
773 .expect("T2 balanceOf should succeed");
774 assert!(result.gas_used > 0, "precompile should consume gas");
775 assert_eq!(
776 result.state_gas_used, 0,
777 "pre-T4 precompile must not report state_gas_used, got {}",
778 result.state_gas_used
779 );
780
781 let reverted =
783 call_with_spec(Bytes::new(), TempoHardfork::T1).expect("T1 empty should revert");
784 assert!(reverted.status.is_revert());
785 assert_eq!(
786 reverted.state_gas_used, 0,
787 "pre-T4 reverted precompile must not report state_gas_used"
788 );
789 }
790
791 #[test]
795 fn test_t4_state_gas_only_includes_state_creating_ops() {
796 let mut cfg = CfgEnv::<TempoHardfork>::default();
797 cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T4);
798
799 let sender = Address::repeat_byte(0x01);
800 let recipient = Address::repeat_byte(0x02);
801
802 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
803 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
804 });
805
806 let db = CacheDB::new(EmptyDB::new());
807 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
808
809 {
811 let block = evm.block.clone();
812 let tx = TxEnv::default();
813 let internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
814 let mut provider =
815 crate::storage::evm::EvmPrecompileStorageProvider::new_max_gas(internals, &cfg);
816 crate::storage::StorageCtx::enter(&mut provider, || {
817 crate::test_util::TIP20Setup::path_usd(sender)
818 .with_issuer(sender)
819 .with_mint(sender, U256::from(1000))
820 .apply()
821 })
822 .expect("TIP20 setup should succeed");
823 }
824
825 let calldata: Bytes = ITIP20::balanceOfCall { account: sender }
827 .abi_encode()
828 .into();
829 let block = evm.block.clone();
830 let tx = TxEnv::default();
831 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
832 let input = PrecompileInput {
833 data: &calldata,
834 caller: sender,
835 internals: evm_internals,
836 gas: 1_000_000,
837 is_static: false,
838 value: U256::ZERO,
839 target_address: PATH_USD_ADDRESS,
840 bytecode_address: PATH_USD_ADDRESS,
841 reservoir: 0,
842 };
843 let output =
844 AlloyEvmPrecompile::call(&precompile, input).expect("balanceOf should succeed");
845 assert!(output.is_success());
846 assert!(output.gas_used > 0, "balanceOf should consume gas");
847 assert_eq!(
848 output.state_gas_used, 0,
849 "read-only balanceOf must have state_gas_used == 0, got {}",
850 output.state_gas_used
851 );
852
853 {
856 let block = evm.block.clone();
858 let tx = TxEnv::default();
859 let internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
860 let mut provider =
861 crate::storage::evm::EvmPrecompileStorageProvider::new_max_gas(internals, &cfg);
862 crate::storage::StorageCtx::enter(&mut provider, || {
863 crate::test_util::TIP20Setup::path_usd(sender)
864 .with_mint(recipient, U256::from(1))
865 .apply()
866 })
867 .expect("TIP20 setup should succeed");
868 }
869 let calldata: Bytes = ITIP20::transferCall {
870 to: recipient,
871 amount: U256::from(100),
872 }
873 .abi_encode()
874 .into();
875 let block = evm.block.clone();
876 let tx = TxEnv::default();
877 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
878 let input = PrecompileInput {
879 data: &calldata,
880 caller: sender,
881 internals: evm_internals,
882 gas: 1_000_000,
883 is_static: false,
884 value: U256::ZERO,
885 target_address: PATH_USD_ADDRESS,
886 bytecode_address: PATH_USD_ADDRESS,
887 reservoir: 0,
888 };
889 let output = AlloyEvmPrecompile::call(&precompile, input).expect("transfer should succeed");
890 assert!(output.is_success());
891 assert!(output.gas_used > 0, "transfer should consume gas");
892 assert_eq!(
893 output.state_gas_used, 0,
894 "transfer to existing account (nonzero->nonzero SSTORE) must have state_gas_used == 0, got {}",
895 output.state_gas_used
896 );
897 }
898
899 #[test]
905 fn test_precompile_gas_refund_in_reservoir_t4() {
906 let mut cfg = CfgEnv::<TempoHardfork>::default();
907 cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T4);
908 cfg.enable_amsterdam_eip8037 = true;
910
911 let sender = Address::repeat_byte(0x01);
912 let recipient = Address::repeat_byte(0x02);
913
914 let precompile = tempo_precompile!("TIP20Token", &cfg, |input| {
915 TIP20Token::from_address(PATH_USD_ADDRESS).expect("PATH_USD_ADDRESS is valid")
916 });
917
918 let db = CacheDB::new(EmptyDB::new());
919 let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());
920
921 {
923 let block = evm.block.clone();
924 let tx = TxEnv::default();
925 let internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
926 let mut provider =
927 crate::storage::evm::EvmPrecompileStorageProvider::new_max_gas(internals, &cfg);
928 crate::storage::StorageCtx::enter(&mut provider, || {
929 crate::test_util::TIP20Setup::path_usd(sender)
930 .with_issuer(sender)
931 .with_mint(sender, U256::from(1000))
932 .apply()
933 })
934 .expect("TIP20 setup should succeed");
935 }
936
937 let calldata: Bytes = ITIP20::transferCall {
940 to: recipient,
941 amount: U256::from(1000),
942 }
943 .abi_encode()
944 .into();
945
946 let block = evm.block.clone();
947 let tx = TxEnv::default();
948 let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx);
949
950 let input = PrecompileInput {
951 data: &calldata,
952 caller: sender,
953 internals: evm_internals,
954 gas: 1_000_000,
955 is_static: false,
956 value: U256::ZERO,
957 target_address: PATH_USD_ADDRESS,
958 bytecode_address: PATH_USD_ADDRESS,
959 reservoir: 0,
960 };
961
962 let output = AlloyEvmPrecompile::call(&precompile, input).expect("transfer should succeed");
963 assert!(output.is_success(), "transfer should be successful");
964
965 assert!(
967 output.gas_refunded != 0,
968 "T4+ successful precompile with SSTORE refund must encode refund in gas_refunded, got 0"
969 );
970 }
971
972 #[test]
973 fn test_dispatch_call_applies_hardfork_selector_gates() -> eyre::Result<()> {
974 alloy::sol! {
975 interface ISelectorGatedTest {
976 function stable() external;
977 function t2Added(uint256 value) external;
978 function t3Removed() external;
979 }
980 }
981
982 const SELECTOR_SCHEDULE: &[SelectorSchedule<'static>] = &[
983 SelectorSchedule::new(TempoHardfork::T2)
984 .with_added(&[ISelectorGatedTest::t2AddedCall::SELECTOR]),
985 SelectorSchedule::new(TempoHardfork::T3)
986 .with_dropped(&[ISelectorGatedTest::t3RemovedCall::SELECTOR]),
987 ];
988
989 let call_with_spec = |spec: TempoHardfork, calldata: &[u8]| {
990 let mut storage = HashMapStorageProvider::new_with_spec(1, spec);
991 StorageCtx::enter(&mut storage, || {
992 dispatch_call(
993 calldata,
994 SELECTOR_SCHEDULE,
995 ISelectorGatedTest::ISelectorGatedTestCalls::abi_decode,
996 |call| match call {
997 ISelectorGatedTest::ISelectorGatedTestCalls::stable(_) => {
998 Ok(PrecompileOutput::new(0, Bytes::from_static(b"stable"), 0))
999 }
1000 ISelectorGatedTest::ISelectorGatedTestCalls::t2Added(_) => {
1001 Ok(PrecompileOutput::new(0, Bytes::from_static(b"added"), 0))
1002 }
1003 ISelectorGatedTest::ISelectorGatedTestCalls::t3Removed(_) => {
1004 Ok(PrecompileOutput::new(0, Bytes::from_static(b"removed"), 0))
1005 }
1006 },
1007 )
1008 })
1009 };
1010
1011 let t2_added_calldata = ISelectorGatedTest::t2AddedCall { value: U256::ZERO }.abi_encode();
1012 let t3_removed_calldata = ISelectorGatedTest::t3RemovedCall {}.abi_encode();
1013
1014 let pre_t2_added = call_with_spec(TempoHardfork::T1, &t2_added_calldata)?;
1016 assert!(pre_t2_added.is_revert());
1017 let decoded = UnknownFunctionSelector::abi_decode(&pre_t2_added.bytes)?;
1018 assert_eq!(
1019 decoded.selector.as_slice(),
1020 &ISelectorGatedTest::t2AddedCall::SELECTOR
1021 );
1022
1023 let post_t2_added = call_with_spec(TempoHardfork::T2, &t2_added_calldata)?;
1025 assert!(!post_t2_added.is_revert());
1026 assert_eq!(post_t2_added.bytes.as_ref(), b"added");
1027
1028 let pre_t3_removed = call_with_spec(TempoHardfork::T2, &t3_removed_calldata)?;
1030 assert!(!pre_t3_removed.is_revert());
1031 assert_eq!(pre_t3_removed.bytes.as_ref(), b"removed");
1032
1033 let post_t3_removed = call_with_spec(TempoHardfork::T3, &t3_removed_calldata)?;
1035 assert!(post_t3_removed.is_revert());
1036 let decoded = UnknownFunctionSelector::abi_decode(&post_t3_removed.bytes)?;
1037 assert_eq!(
1038 decoded.selector.as_slice(),
1039 &ISelectorGatedTest::t3RemovedCall::SELECTOR
1040 );
1041
1042 let malformed_added = call_with_spec(
1044 TempoHardfork::T1,
1045 &ISelectorGatedTest::t2AddedCall::SELECTOR,
1046 )?;
1047 assert!(malformed_added.is_revert());
1048 let decoded = UnknownFunctionSelector::abi_decode(&malformed_added.bytes)?;
1049 assert_eq!(
1050 decoded.selector.as_slice(),
1051 &ISelectorGatedTest::t2AddedCall::SELECTOR
1052 );
1053
1054 Ok(())
1055 }
1056
1057 #[test]
1058 fn test_input_cost_returns_non_zero_for_input() {
1059 assert_eq!(input_cost(0), 0);
1061
1062 assert_eq!(input_cost(1), INPUT_PER_WORD_COST);
1064
1065 assert_eq!(input_cost(32), INPUT_PER_WORD_COST);
1067
1068 assert_eq!(input_cost(33), INPUT_PER_WORD_COST * 2);
1070 }
1071
1072 #[test]
1073 fn test_extend_tempo_precompiles_registers_precompiles() {
1074 let mut cfg = CfgEnv::<TempoHardfork>::default();
1075 cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T3);
1076 let precompiles = tempo_precompiles(&cfg);
1077
1078 let factory_precompile = precompiles.get(&TIP20_FACTORY_ADDRESS);
1080 assert!(
1081 factory_precompile.is_some(),
1082 "TIP20Factory should be registered"
1083 );
1084
1085 let registry_precompile = precompiles.get(&TIP403_REGISTRY_ADDRESS);
1087 assert!(
1088 registry_precompile.is_some(),
1089 "TIP403Registry should be registered"
1090 );
1091
1092 let fee_manager_precompile = precompiles.get(&TIP_FEE_MANAGER_ADDRESS);
1094 assert!(
1095 fee_manager_precompile.is_some(),
1096 "TipFeeManager should be registered"
1097 );
1098
1099 let dex_precompile = precompiles.get(&STABLECOIN_DEX_ADDRESS);
1101 assert!(
1102 dex_precompile.is_some(),
1103 "StablecoinDEX should be registered"
1104 );
1105
1106 let nonce_precompile = precompiles.get(&NONCE_PRECOMPILE_ADDRESS);
1108 assert!(
1109 nonce_precompile.is_some(),
1110 "NonceManager should be registered"
1111 );
1112
1113 let validator_precompile = precompiles.get(&VALIDATOR_CONFIG_ADDRESS);
1115 assert!(
1116 validator_precompile.is_some(),
1117 "ValidatorConfig should be registered"
1118 );
1119
1120 let validator_v2_precompile = precompiles.get(&VALIDATOR_CONFIG_V2_ADDRESS);
1122 assert!(
1123 validator_v2_precompile.is_some(),
1124 "ValidatorConfigV2 should be registered"
1125 );
1126
1127 let keychain_precompile = precompiles.get(&ACCOUNT_KEYCHAIN_ADDRESS);
1129 assert!(
1130 keychain_precompile.is_some(),
1131 "AccountKeychain should be registered"
1132 );
1133
1134 let sig_verifier_precompile = precompiles.get(&SIGNATURE_VERIFIER_ADDRESS);
1136 assert!(
1137 sig_verifier_precompile.is_some(),
1138 "SignatureVerifier should be registered at T3"
1139 );
1140
1141 let tip20_precompile = precompiles.get(&PATH_USD_ADDRESS);
1143 assert!(
1144 tip20_precompile.is_some(),
1145 "TIP20 tokens should be registered"
1146 );
1147
1148 let random_address = Address::random();
1150 let random_precompile = precompiles.get(&random_address);
1151 assert!(
1152 random_precompile.is_none(),
1153 "Random address should not be a precompile"
1154 );
1155 }
1156
1157 #[test]
1158 fn test_signature_verifier_not_registered_pre_t3() {
1159 let cfg = CfgEnv::<TempoHardfork>::default();
1160 let precompiles = tempo_precompiles(&cfg);
1161
1162 assert!(
1163 precompiles.get(&SIGNATURE_VERIFIER_ADDRESS).is_none(),
1164 "SignatureVerifier should NOT be registered before T3"
1165 );
1166 }
1167
1168 #[test]
1169 fn test_p256verify_availability_across_t1c_boundary() {
1170 let has_p256 = |spec: TempoHardfork| -> bool {
1171 let p256_addr = Address::from_word(U256::from(256).into());
1173
1174 let mut cfg = CfgEnv::<TempoHardfork>::default();
1175 cfg.set_spec_and_mainnet_gas_params(spec);
1176 tempo_precompiles(&cfg).get(&p256_addr).is_some()
1177 };
1178
1179 for spec in [
1181 TempoHardfork::Genesis,
1182 TempoHardfork::T0,
1183 TempoHardfork::T1,
1184 TempoHardfork::T1A,
1185 TempoHardfork::T1B,
1186 ] {
1187 assert!(
1188 !has_p256(spec),
1189 "P256VERIFY should NOT be available at {spec:?} (pre-T1C)"
1190 );
1191 }
1192
1193 for spec in [TempoHardfork::T1C, TempoHardfork::T2] {
1195 assert!(
1196 has_p256(spec),
1197 "P256VERIFY should be available at {spec:?} (T1C+)"
1198 );
1199 }
1200 }
1201}