1use alloy_evm::{
2 Database, Evm, EvmEnv, EvmFactory, IntoTxEnv,
3 precompiles::PrecompilesMap,
4 revm::{
5 Context, ExecuteEvm, InspectEvm, Inspector, SystemCallEvm,
6 context::{
7 DBErrorMarker,
8 result::{EVMError, ResultAndState, ResultGas},
9 },
10 inspector::NoOpInspector,
11 },
12};
13use alloy_primitives::{Address, Bytes, TxKind};
14use reth_revm::{
15 InspectSystemCallEvm, MainContext,
16 context::{CfgEnv, result::ExecutionResult},
17};
18use std::ops::{Deref, DerefMut};
19use tempo_chainspec::hardfork::TempoHardfork;
20use tempo_precompiles::storage::{StorageAction, StorageActions};
21use tempo_revm::{
22 TempoHaltReason, TempoInvalidTransaction, TempoTxEnv, ValidationContext, evm::TempoContext,
23 handler::TempoEvmHandler,
24};
25
26use crate::TempoBlockEnv;
27
28#[derive(Debug, Default, Clone, Copy)]
29#[non_exhaustive]
30pub struct TempoEvmFactory;
31
32impl EvmFactory for TempoEvmFactory {
33 type Evm<DB: Database, I: Inspector<Self::Context<DB>>> = TempoEvm<DB, I>;
34 type Context<DB: Database> = TempoContext<DB>;
35 type Tx = TempoTxEnv;
36 type Error<DBError: DBErrorMarker> = EVMError<DBError, TempoInvalidTransaction>;
37 type HaltReason = TempoHaltReason;
38 type Spec = TempoHardfork;
39 type BlockEnv = TempoBlockEnv;
40 type Precompiles = PrecompilesMap;
41
42 fn create_evm<DB: Database>(
43 &self,
44 db: DB,
45 input: EvmEnv<Self::Spec, Self::BlockEnv>,
46 ) -> Self::Evm<DB, NoOpInspector> {
47 TempoEvm::new(db, input)
48 }
49
50 fn create_evm_with_inspector<DB: Database, I: Inspector<Self::Context<DB>>>(
51 &self,
52 db: DB,
53 input: EvmEnv<Self::Spec, Self::BlockEnv>,
54 inspector: I,
55 ) -> Self::Evm<DB, I> {
56 TempoEvm::new(db, input).with_inspector(inspector)
57 }
58}
59
60#[expect(missing_debug_implementations)]
66pub struct TempoEvm<DB: Database, I = NoOpInspector> {
67 inner: tempo_revm::TempoEvm<DB, I>,
68 actions: StorageActions,
70 inspect: bool,
71}
72
73impl<DB: Database> TempoEvm<DB> {
74 pub fn new(db: DB, input: EvmEnv<TempoHardfork, TempoBlockEnv>) -> Self {
76 let ctx = Context::mainnet()
80 .with_db(db)
81 .with_block(input.block_env)
82 .with_cfg(input.cfg_env)
83 .with_tx(Default::default());
84
85 let actions = StorageActions::disabled();
86 Self {
87 inner: tempo_revm::TempoEvm::new_with_actions(ctx, NoOpInspector {}, actions.clone()),
88 actions,
89 inspect: false,
90 }
91 }
92}
93
94impl<DB: Database, I> TempoEvm<DB, I> {
95 pub fn into_inner(self) -> tempo_revm::TempoEvm<DB, I> {
97 self.inner
98 }
99
100 pub const fn ctx(&self) -> &TempoContext<DB> {
102 &self.inner.inner.ctx
103 }
104
105 pub fn into_ctx(self) -> TempoContext<DB> {
107 self.inner.inner.ctx
108 }
109
110 pub fn evm_env(&self) -> EvmEnv<TempoHardfork, TempoBlockEnv> {
112 EvmEnv {
113 cfg_env: self.ctx().cfg.clone(),
114 block_env: self.ctx().block.clone(),
115 }
116 }
117
118 pub fn ctx_mut(&mut self) -> &mut TempoContext<DB> {
120 &mut self.inner.inner.ctx
121 }
122
123 pub fn inner_mut(&mut self) -> &mut tempo_revm::TempoEvm<DB, I> {
125 &mut self.inner
126 }
127
128 pub fn validator_fee(&self) -> alloy_primitives::U256 {
131 self.inner.validator_fee
132 }
133
134 pub fn with_inspector<OINSP>(self, inspector: OINSP) -> TempoEvm<DB, OINSP> {
136 TempoEvm {
137 inner: self.inner.with_inspector(inspector),
138 actions: self.actions,
139 inspect: true,
140 }
141 }
142
143 pub fn validate_transaction(
147 &mut self,
148 tx: impl IntoTxEnv<TempoTxEnv>,
149 ) -> Result<ValidationContext, EVMError<DB::Error, TempoInvalidTransaction>> {
150 self.inner.inner.ctx.tx = tx.into_tx_env();
151 let mut handler = TempoEvmHandler::new();
152 handler.validate_transaction(&mut self.inner)
153 }
154
155 pub fn with_actions(self) -> Self {
157 self.actions.enable();
158 self
159 }
160
161 pub fn take_actions(&mut self) -> Option<Vec<StorageAction>> {
163 self.actions.take()
164 }
165
166 pub fn replace_actions(&mut self, actions: Vec<StorageAction>) -> Option<Vec<StorageAction>> {
168 self.actions.replace(actions)
169 }
170}
171
172impl<DB: Database, I> Deref for TempoEvm<DB, I>
173where
174 DB: Database,
175 I: Inspector<TempoContext<DB>>,
176{
177 type Target = TempoContext<DB>;
178
179 #[inline]
180 fn deref(&self) -> &Self::Target {
181 self.ctx()
182 }
183}
184
185impl<DB: Database, I> DerefMut for TempoEvm<DB, I>
186where
187 DB: Database,
188 I: Inspector<TempoContext<DB>>,
189{
190 #[inline]
191 fn deref_mut(&mut self) -> &mut Self::Target {
192 self.ctx_mut()
193 }
194}
195
196impl<DB, I> Evm for TempoEvm<DB, I>
197where
198 DB: Database,
199 I: Inspector<TempoContext<DB>>,
200{
201 type DB = DB;
202 type Tx = TempoTxEnv;
203 type Error = EVMError<DB::Error, TempoInvalidTransaction>;
204 type HaltReason = TempoHaltReason;
205 type Spec = TempoHardfork;
206 type BlockEnv = TempoBlockEnv;
207 type Precompiles = PrecompilesMap;
208 type Inspector = I;
209
210 fn block(&self) -> &Self::BlockEnv {
211 &self.block
212 }
213
214 fn cfg_env(&self) -> &CfgEnv<Self::Spec> {
215 &self.cfg
216 }
217
218 fn chain_id(&self) -> u64 {
219 self.cfg.chain_id
220 }
221
222 fn transact_raw(
223 &mut self,
224 tx: Self::Tx,
225 ) -> Result<ResultAndState<Self::HaltReason>, Self::Error> {
226 if tx.is_system_tx {
227 let TxKind::Call(to) = tx.inner.kind else {
228 return Err(TempoInvalidTransaction::SystemTransactionMustBeCall.into());
229 };
230
231 let mut result = if self.inspect {
232 self.inner
233 .inspect_system_call_with_caller(tx.inner.caller, to, tx.inner.data)?
234 } else {
235 self.inner
236 .system_call_with_caller(tx.inner.caller, to, tx.inner.data)?
237 };
238
239 let ExecutionResult::Success { gas, .. } = &mut result.result else {
241 return Err(
242 TempoInvalidTransaction::SystemTransactionFailed(result.result.into()).into(),
243 );
244 };
245
246 *gas = ResultGas::default();
247
248 Ok(result)
249 } else if self.inspect {
250 self.inner.inspect_tx(tx)
251 } else {
252 self.inner.transact(tx)
253 }
254 }
255
256 fn transact_system_call(
257 &mut self,
258 caller: Address,
259 contract: Address,
260 data: Bytes,
261 ) -> Result<ResultAndState<Self::HaltReason>, Self::Error> {
262 self.inner.system_call_with_caller(caller, contract, data)
263 }
264
265 fn finish(self) -> (Self::DB, EvmEnv<Self::Spec, Self::BlockEnv>) {
266 let Context {
267 block: block_env,
268 cfg: cfg_env,
269 journaled_state,
270 ..
271 } = self.inner.inner.ctx;
272
273 (journaled_state.database, EvmEnv { block_env, cfg_env })
274 }
275
276 fn set_inspector_enabled(&mut self, enabled: bool) {
277 self.inspect = enabled;
278 }
279
280 fn components(&self) -> (&Self::DB, &Self::Inspector, &Self::Precompiles) {
281 (
282 &self.inner.inner.ctx.journaled_state.database,
283 &self.inner.inner.inspector,
284 &self.inner.inner.precompiles,
285 )
286 }
287
288 fn components_mut(&mut self) -> (&mut Self::DB, &mut Self::Inspector, &mut Self::Precompiles) {
289 (
290 &mut self.inner.inner.ctx.journaled_state.database,
291 &mut self.inner.inner.inspector,
292 &mut self.inner.inner.precompiles,
293 )
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use crate::test_utils::{test_evm, test_evm_with_basefee};
300 use alloy_primitives::U256;
301 use alloy_sol_types::SolCall;
302 use revm::{
303 DatabaseCommit,
304 context::{BlockEnv, CfgEnv, JournalTr, TxEnv},
305 database::{EmptyDB, in_memory_db::CacheDB},
306 state::EvmState,
307 };
308 use std::collections::BTreeMap;
309 use tempo_chainspec::hardfork::TempoHardfork;
310 use tempo_precompiles::{
311 PATH_USD_ADDRESS, STORAGE_CREDITS_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
312 TIP403_REGISTRY_ADDRESS,
313 storage::{StorageAction, StorageCtx, StorageKey},
314 storage_credits::StorageCredits,
315 test_util::TIP20Setup,
316 tip_fee_manager::slots as fee_manager_slots,
317 tip20::{
318 ITIP20, USD_CURRENCY, rewards::__packing_user_reward_info as user_reward_info_slots,
319 slots as tip20_slots,
320 },
321 tip403_registry::slots as tip403_registry_slots,
322 };
323 use tempo_primitives::transaction::calc_gas_balance_spending;
324 use tempo_revm::gas_params::tempo_gas_params_with_amsterdam;
325
326 use super::*;
327
328 #[test]
329 fn can_execute_system_tx() {
330 let mut evm = test_evm(EmptyDB::default());
331 let result = evm
332 .transact(TempoTxEnv {
333 inner: TxEnv {
334 caller: Address::ZERO,
335 gas_price: 0,
336 gas_limit: 21000,
337 ..Default::default()
338 },
339 is_system_tx: true,
340 ..Default::default()
341 })
342 .unwrap();
343
344 assert!(result.result.is_success());
345 }
346
347 #[test]
348 fn test_transact_raw() {
349 let mut evm = test_evm_with_basefee(EmptyDB::default(), 0);
350
351 let tx = TempoTxEnv {
352 inner: TxEnv {
353 caller: Address::repeat_byte(0x01),
354 gas_price: 0,
355 gas_limit: 21000,
356 kind: TxKind::Call(Address::repeat_byte(0x02)),
357 ..Default::default()
358 },
359 is_system_tx: false,
360 fee_token: None,
361 ..Default::default()
362 };
363
364 let result = evm.transact_raw(tx);
365 assert!(result.is_ok());
366
367 let result = result.unwrap();
368 assert!(result.result.is_success());
369 assert_eq!(result.result.tx_gas_used(), 21000);
370 }
371
372 #[test]
373 fn test_transact_raw_system_tx() {
374 let mut evm = test_evm(EmptyDB::default());
375
376 let tx = TempoTxEnv {
378 inner: TxEnv {
379 caller: Address::ZERO,
380 gas_price: 0,
381 gas_limit: 21000,
382 kind: TxKind::Call(Address::repeat_byte(0x01)),
383 ..Default::default()
384 },
385 is_system_tx: true,
386 ..Default::default()
387 };
388
389 let result = evm.transact_raw(tx);
390 assert!(result.is_ok());
391
392 let result = result.unwrap();
393 assert!(result.result.is_success());
394 assert_eq!(result.result.tx_gas_used(), 0);
396 }
397
398 #[test]
399 fn test_transact_raw_system_tx_must_be_call() {
400 let mut evm = test_evm(EmptyDB::default());
401
402 let tx = TempoTxEnv {
404 inner: TxEnv {
405 caller: Address::ZERO,
406 gas_price: 0,
407 gas_limit: 21000,
408 kind: TxKind::Create,
409 ..Default::default()
410 },
411 is_system_tx: true,
412 ..Default::default()
413 };
414
415 let result = evm.transact_raw(tx);
416 assert!(result.is_err());
417
418 let err = result.unwrap_err();
419 assert!(matches!(
420 err,
421 EVMError::Transaction(TempoInvalidTransaction::SystemTransactionMustBeCall)
422 ));
423 }
424
425 #[test]
426 fn test_transact_raw_system_tx_failed() {
427 let mut cache_db = CacheDB::new(EmptyDB::default());
428 let revert_code = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd]);
430 let contract_addr = Address::repeat_byte(0xaa);
431
432 cache_db.insert_account_info(
433 contract_addr,
434 revm::state::AccountInfo {
435 code_hash: alloy_primitives::keccak256(&revert_code),
436 code: Some(revm::bytecode::Bytecode::new_raw(revert_code)),
437 ..Default::default()
438 },
439 );
440
441 let mut evm = test_evm(cache_db);
442
443 let tx = TempoTxEnv {
445 inner: TxEnv {
446 caller: Address::ZERO,
447 gas_price: 0,
448 gas_limit: 1_000_000,
449 kind: TxKind::Call(contract_addr),
450 ..Default::default()
451 },
452 is_system_tx: true,
453 ..Default::default()
454 };
455
456 let result = evm.transact_raw(tx);
457 assert!(result.is_err());
458
459 let err = result.unwrap_err();
460 assert!(matches!(
461 err,
462 EVMError::Transaction(TempoInvalidTransaction::SystemTransactionFailed(_))
463 ));
464 }
465
466 #[test]
467 fn test_transact_system_call() {
468 let mut evm = test_evm(EmptyDB::default());
469
470 let caller = Address::repeat_byte(0x01);
471 let contract = Address::repeat_byte(0x02);
472 let data = Bytes::from_static(&[0x01, 0x02, 0x03]);
473
474 let result = evm.transact_system_call(caller, contract, data);
475 assert!(result.is_ok());
476
477 let result = result.unwrap();
478 assert!(result.result.is_success());
479 }
480
481 fn assert_storage_actions_reconstruct_evm_state(
482 actions: &[StorageAction],
483 state: &EvmState,
484 hardfork: TempoHardfork,
485 ) {
486 let mut original_values = BTreeMap::<(Address, U256), U256>::new();
487 for (address, account) in state {
488 for (slot, storage_slot) in &account.storage {
489 original_values.insert((*address, *slot), storage_slot.original_value());
490 }
491 }
492
493 let mut first_loads = BTreeMap::<(Address, U256), U256>::new();
494 let mut reconstructed = BTreeMap::<(Address, U256), U256>::new();
495
496 for action in actions {
497 match *action {
498 StorageAction::Sload(address, slot, value) => {
499 let key = (address, slot);
500 match reconstructed.get(&key) {
501 Some(previous) => assert_eq!(
502 *previous, value,
503 "SLOAD must match reconstructed current value for {address:?}:{slot:?} on {hardfork:?}",
504 ),
505 None => {
506 first_loads.insert(key, value);
507 reconstructed.insert(key, value);
508 }
509 }
510 }
511 StorageAction::Sstore(address, slot, value) => {
512 let key = (address, slot);
513 assert!(
514 reconstructed.contains_key(&key),
515 "SSTORE without prior SLOAD for {address:?}:{slot:?} on {hardfork:?}",
516 );
517 reconstructed.insert(key, value);
518 }
519 StorageAction::Sinc(address, slot, delta) => {
520 let key = (address, slot);
521 let current = match reconstructed.get(&key) {
522 Some(current) => *current,
523 None => {
524 let original = *original_values.get(&key).unwrap_or_else(|| {
525 panic!(
526 "SINC without prior SLOAD for unknown EVM output storage cell {address:?}:{slot:?} on {hardfork:?}",
527 )
528 });
529 first_loads.insert(key, original);
530 reconstructed.insert(key, original);
531 original
532 }
533 };
534 let value = current.checked_add(delta).unwrap_or_else(|| {
535 panic!("SINC overflow for {address:?}:{slot:?} on {hardfork:?}")
536 });
537 reconstructed.insert(key, value);
538 }
539 StorageAction::Sdec(address, slot, delta) => {
540 let key = (address, slot);
541 let current = match reconstructed.get(&key) {
542 Some(current) => *current,
543 None => {
544 let original = *original_values.get(&key).unwrap_or_else(|| {
545 panic!(
546 "SDEC without prior SLOAD for unknown EVM output storage cell {address:?}:{slot:?} on {hardfork:?}",
547 )
548 });
549 first_loads.insert(key, original);
550 reconstructed.insert(key, original);
551 original
552 }
553 };
554 let value = current.checked_sub(delta).unwrap_or_else(|| {
555 panic!("SDEC underflow for {address:?}:{slot:?} on {hardfork:?}")
556 });
557 reconstructed.insert(key, value);
558 }
559 }
560 }
561
562 for (address, account) in state {
563 for (slot, storage_slot) in &account.storage {
564 let key = (*address, *slot);
565 let original_value = first_loads.get(&key).unwrap_or_else(|| {
566 panic!(
567 "EVM output storage cell {address:?}:{slot:?} was not loaded in StorageActions on {hardfork:?}",
568 )
569 });
570 assert_eq!(
571 *original_value,
572 storage_slot.original_value(),
573 "reconstructed original value mismatch for {address:?}:{slot:?} on {hardfork:?}",
574 );
575
576 let reconstructed_value = reconstructed.get(&key).unwrap_or_else(|| {
577 panic!(
578 "EVM output storage cell {address:?}:{slot:?} was not reconstructed from StorageActions on {hardfork:?}",
579 )
580 });
581 assert_eq!(
582 *reconstructed_value,
583 storage_slot.present_value(),
584 "reconstructed present value mismatch for {address:?}:{slot:?} on {hardfork:?}",
585 );
586 }
587 }
588 }
589
590 fn short_string_word(bytes: &[u8]) -> U256 {
591 assert!(bytes.len() <= 31);
592
593 let mut word = [0u8; 32];
594 word[..bytes.len()].copy_from_slice(bytes);
595 word[31] = (bytes.len() * 2) as u8;
596 U256::from_be_bytes(word)
597 }
598
599 fn hardforks_for_storage_action_recording() -> Vec<TempoHardfork> {
600 let current = current_mainnet_hardfork();
601 let latest = latest_available_hardfork();
602
603 if current == latest {
604 vec![current]
605 } else {
606 vec![current, latest]
607 }
608 }
609
610 fn current_mainnet_hardfork() -> TempoHardfork {
611 #[allow(clippy::disallowed_methods)]
612 let timestamp = std::time::SystemTime::now()
613 .duration_since(std::time::UNIX_EPOCH)
614 .expect("system clock should be after unix epoch")
615 .as_secs();
616
617 TempoHardfork::VARIANTS
618 .iter()
619 .rev()
620 .copied()
621 .find(|fork| {
622 fork.mainnet_activation_timestamp()
623 .is_some_and(|activation| timestamp >= activation)
624 })
625 .unwrap_or(TempoHardfork::Genesis)
626 }
627
628 fn latest_available_hardfork() -> TempoHardfork {
629 *TempoHardfork::VARIANTS
630 .last()
631 .expect("TempoHardfork must have at least one variant")
632 }
633
634 #[test]
635 fn test_tip20_full_evm_records_storage_actions_with_fees() {
636 for hardfork in hardforks_for_storage_action_recording() {
637 let sender = Address::repeat_byte(0x01);
638 let recipient = Address::repeat_byte(0x02);
639 let beneficiary = Address::repeat_byte(0x03);
640 let starting_balance = U256::from(1_000_000);
641 let transfer_amount = U256::from(100);
642 let gas_limit = 1_000_000;
643 let gas_price = 1_000_000_000u64;
644
645 let mut cfg = CfgEnv::<TempoHardfork>::default();
646 cfg.set_spec_and_mainnet_gas_params(hardfork);
647
648 let mut evm = TempoEvm::new(
649 CacheDB::new(EmptyDB::default()),
650 EvmEnv {
651 cfg_env: cfg,
652 block_env: TempoBlockEnv {
653 inner: BlockEnv {
654 beneficiary,
655 basefee: gas_price,
656 gas_limit: 30_000_000,
657 ..Default::default()
658 },
659 ..Default::default()
660 },
661 },
662 );
663
664 StorageCtx::enter_ctx(evm.ctx_mut(), StorageActions::disabled(), || {
665 TIP20Setup::path_usd(sender)
666 .with_issuer(sender)
667 .with_mint(sender, starting_balance)
668 .apply()
669 })
670 .expect("TIP20 setup should succeed");
671 let setup_state = evm.ctx_mut().journaled_state.finalize();
672 evm.db_mut().commit(setup_state);
673
674 let mut evm = evm.with_actions();
675 assert_eq!(evm.take_actions(), Some(vec![]));
676
677 let calldata: Bytes = ITIP20::transferCall {
678 to: recipient,
679 amount: transfer_amount,
680 }
681 .abi_encode()
682 .into();
683 let tx = TempoTxEnv {
684 inner: TxEnv {
685 caller: sender,
686 gas_price: u128::from(gas_price),
687 gas_limit,
688 kind: TxKind::Call(PATH_USD_ADDRESS),
689 data: calldata.clone(),
690 ..Default::default()
691 },
692 fee_token: Some(PATH_USD_ADDRESS),
693 ..Default::default()
694 };
695 let result = evm.transact_raw(tx).expect("transfer should execute");
696 assert!(result.result.is_success(), "hardfork: {hardfork:?}");
697
698 let max_fee_spending = calc_gas_balance_spending(gas_limit, u128::from(gas_price));
699 let actual_spending =
700 calc_gas_balance_spending(result.result.tx_gas_used(), u128::from(gas_price));
701 assert!(
702 !actual_spending.is_zero(),
703 "test must exercise post-tx fee settlement"
704 );
705 assert!(
706 max_fee_spending > actual_spending,
707 "test must exercise post-tx fee refund"
708 );
709
710 let refund_amount = max_fee_spending - actual_spending;
711 let sender_balance_slot = sender.mapping_slot(tip20_slots::BALANCES);
712 let fee_manager_balance_slot =
713 TIP_FEE_MANAGER_ADDRESS.mapping_slot(tip20_slots::BALANCES);
714 let recipient_balance_slot = recipient.mapping_slot(tip20_slots::BALANCES);
715 let sender_reward_info_slot = sender.mapping_slot(tip20_slots::USER_REWARD_INFO);
716 let recipient_reward_info_slot = recipient.mapping_slot(tip20_slots::USER_REWARD_INFO);
717 let reward_recipient_offset = user_reward_info_slots::REWARD_RECIPIENT;
718 let reward_per_token_offset = user_reward_info_slots::REWARD_PER_TOKEN;
719 let reward_balance_offset = user_reward_info_slots::REWARD_BALANCE;
720 let transfer_policy_id_word =
721 U256::from(1) << (tip20_slots::TRANSFER_POLICY_ID_OFFSET * 8);
722 let receive_policy_config_slot =
723 recipient.mapping_slot(tip403_registry_slots::RECEIVE_POLICIES);
724 let validator_token_slot =
725 beneficiary.mapping_slot(fee_manager_slots::VALIDATOR_TOKENS);
726 let collected_fees_slot = PATH_USD_ADDRESS
727 .mapping_slot(beneficiary.mapping_slot(fee_manager_slots::COLLECTED_FEES));
728 let path_usd_storage_credit_slot = StorageCredits::slot(PATH_USD_ADDRESS);
729 let currency_word = short_string_word(USD_CURRENCY.as_bytes());
730
731 let sender_after_fee = starting_balance - max_fee_spending;
732 let sender_after_transfer = sender_after_fee - transfer_amount;
733
734 let actions = evm
735 .take_actions()
736 .expect("storage action recording should be enabled");
737
738 let expected = if hardfork == latest_available_hardfork() {
739 vec![
740 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::CURRENCY, currency_word),
742 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::CURRENCY, currency_word),
744 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, validator_token_slot, U256::ZERO),
746 StorageAction::Sload(
748 PATH_USD_ADDRESS,
749 tip20_slots::TRANSFER_POLICY_ID,
750 transfer_policy_id_word,
751 ),
752 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::PAUSED, U256::ZERO),
754 StorageAction::Sdec(PATH_USD_ADDRESS, sender_balance_slot, max_fee_spending),
756 StorageAction::Sload(PATH_USD_ADDRESS, fee_manager_balance_slot, U256::ZERO),
758 StorageAction::Sstore(
760 PATH_USD_ADDRESS,
761 fee_manager_balance_slot,
762 max_fee_spending,
763 ),
764 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::PAUSED, U256::ZERO),
766 StorageAction::Sload(
768 PATH_USD_ADDRESS,
769 tip20_slots::TRANSFER_POLICY_ID,
770 transfer_policy_id_word,
771 ),
772 StorageAction::Sload(
774 TIP403_REGISTRY_ADDRESS,
775 receive_policy_config_slot,
776 U256::ZERO,
777 ),
778 StorageAction::Sdec(PATH_USD_ADDRESS, sender_balance_slot, transfer_amount),
780 StorageAction::Sload(PATH_USD_ADDRESS, recipient_balance_slot, U256::ZERO),
782 StorageAction::Sstore(
784 PATH_USD_ADDRESS,
785 recipient_balance_slot,
786 transfer_amount,
787 ),
788 StorageAction::Sload(
790 STORAGE_CREDITS_ADDRESS,
791 path_usd_storage_credit_slot,
792 U256::ZERO,
793 ),
794 StorageAction::Sdec(PATH_USD_ADDRESS, fee_manager_balance_slot, refund_amount),
796 StorageAction::Sinc(PATH_USD_ADDRESS, sender_balance_slot, refund_amount),
798 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, validator_token_slot, U256::ZERO),
800 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, collected_fees_slot, U256::ZERO),
802 StorageAction::Sstore(
804 TIP_FEE_MANAGER_ADDRESS,
805 collected_fees_slot,
806 actual_spending,
807 ),
808 ]
809 } else {
810 vec![
811 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::CURRENCY, currency_word),
813 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::CURRENCY, currency_word),
815 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, validator_token_slot, U256::ZERO),
817 StorageAction::Sload(
819 PATH_USD_ADDRESS,
820 tip20_slots::TRANSFER_POLICY_ID,
821 transfer_policy_id_word,
822 ),
823 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::PAUSED, U256::ZERO),
825 StorageAction::Sload(
827 PATH_USD_ADDRESS,
828 sender_reward_info_slot + reward_recipient_offset,
829 U256::ZERO,
830 ),
831 StorageAction::Sload(
833 PATH_USD_ADDRESS,
834 sender_reward_info_slot + reward_per_token_offset,
835 U256::ZERO,
836 ),
837 StorageAction::Sload(
839 PATH_USD_ADDRESS,
840 sender_reward_info_slot + reward_balance_offset,
841 U256::ZERO,
842 ),
843 StorageAction::Sload(
845 PATH_USD_ADDRESS,
846 tip20_slots::GLOBAL_REWARD_PER_TOKEN,
847 U256::ZERO,
848 ),
849 StorageAction::Sdec(PATH_USD_ADDRESS, sender_balance_slot, max_fee_spending),
851 StorageAction::Sload(PATH_USD_ADDRESS, fee_manager_balance_slot, U256::ZERO),
853 StorageAction::Sstore(
855 PATH_USD_ADDRESS,
856 fee_manager_balance_slot,
857 max_fee_spending,
858 ),
859 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::PAUSED, U256::ZERO),
861 StorageAction::Sload(
863 PATH_USD_ADDRESS,
864 tip20_slots::TRANSFER_POLICY_ID,
865 transfer_policy_id_word,
866 ),
867 StorageAction::Sload(PATH_USD_ADDRESS, sender_balance_slot, sender_after_fee),
869 StorageAction::Sload(
871 PATH_USD_ADDRESS,
872 sender_reward_info_slot + reward_recipient_offset,
873 U256::ZERO,
874 ),
875 StorageAction::Sload(
877 PATH_USD_ADDRESS,
878 sender_reward_info_slot + reward_per_token_offset,
879 U256::ZERO,
880 ),
881 StorageAction::Sload(
883 PATH_USD_ADDRESS,
884 sender_reward_info_slot + reward_balance_offset,
885 U256::ZERO,
886 ),
887 StorageAction::Sload(
889 PATH_USD_ADDRESS,
890 tip20_slots::GLOBAL_REWARD_PER_TOKEN,
891 U256::ZERO,
892 ),
893 StorageAction::Sload(
895 PATH_USD_ADDRESS,
896 recipient_reward_info_slot + reward_recipient_offset,
897 U256::ZERO,
898 ),
899 StorageAction::Sload(
901 PATH_USD_ADDRESS,
902 recipient_reward_info_slot + reward_per_token_offset,
903 U256::ZERO,
904 ),
905 StorageAction::Sload(
907 PATH_USD_ADDRESS,
908 recipient_reward_info_slot + reward_balance_offset,
909 U256::ZERO,
910 ),
911 StorageAction::Sload(
913 PATH_USD_ADDRESS,
914 tip20_slots::GLOBAL_REWARD_PER_TOKEN,
915 U256::ZERO,
916 ),
917 StorageAction::Sstore(
919 PATH_USD_ADDRESS,
920 sender_balance_slot,
921 sender_after_transfer,
922 ),
923 StorageAction::Sload(PATH_USD_ADDRESS, recipient_balance_slot, U256::ZERO),
925 StorageAction::Sstore(
927 PATH_USD_ADDRESS,
928 recipient_balance_slot,
929 transfer_amount,
930 ),
931 StorageAction::Sload(
933 PATH_USD_ADDRESS,
934 sender_reward_info_slot + reward_recipient_offset,
935 U256::ZERO,
936 ),
937 StorageAction::Sload(
939 PATH_USD_ADDRESS,
940 sender_reward_info_slot + reward_per_token_offset,
941 U256::ZERO,
942 ),
943 StorageAction::Sload(
945 PATH_USD_ADDRESS,
946 sender_reward_info_slot + reward_balance_offset,
947 U256::ZERO,
948 ),
949 StorageAction::Sload(
951 PATH_USD_ADDRESS,
952 tip20_slots::GLOBAL_REWARD_PER_TOKEN,
953 U256::ZERO,
954 ),
955 StorageAction::Sdec(PATH_USD_ADDRESS, fee_manager_balance_slot, refund_amount),
957 StorageAction::Sinc(PATH_USD_ADDRESS, sender_balance_slot, refund_amount),
959 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, validator_token_slot, U256::ZERO),
961 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, collected_fees_slot, U256::ZERO),
963 StorageAction::Sstore(
965 TIP_FEE_MANAGER_ADDRESS,
966 collected_fees_slot,
967 actual_spending,
968 ),
969 ]
970 };
971
972 assert_eq!(actions, expected, "hardfork: {hardfork:?}");
973 assert_storage_actions_reconstruct_evm_state(&actions, &result.state, hardfork);
974 evm.db_mut().commit(result.state);
975
976 let tx = TempoTxEnv {
977 inner: TxEnv {
978 caller: sender,
979 gas_price: u128::from(gas_price),
980 gas_limit,
981 kind: TxKind::Call(PATH_USD_ADDRESS),
982 data: calldata,
983 nonce: 1,
984 ..Default::default()
985 },
986 fee_token: Some(PATH_USD_ADDRESS),
987 ..Default::default()
988 };
989 let result = evm.transact_raw(tx).expect("transfer should execute");
990 assert!(result.result.is_success(), "hardfork: {hardfork:?}");
991 let second_actual_spending =
992 calc_gas_balance_spending(result.result.tx_gas_used(), u128::from(gas_price));
993 let second_refund_amount = max_fee_spending - second_actual_spending;
994 let sender_after_first_tx = sender_after_transfer + refund_amount;
995 let sender_after_second_fee = sender_after_first_tx - max_fee_spending;
996 let sender_after_second_transfer = sender_after_second_fee - transfer_amount;
997
998 let actions = evm
999 .take_actions()
1000 .expect("storage action recording should be enabled");
1001
1002 let expected = if hardfork == latest_available_hardfork() {
1005 vec![
1006 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::CURRENCY, currency_word),
1008 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::CURRENCY, currency_word),
1010 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, validator_token_slot, U256::ZERO),
1012 StorageAction::Sload(
1014 PATH_USD_ADDRESS,
1015 tip20_slots::TRANSFER_POLICY_ID,
1016 transfer_policy_id_word,
1017 ),
1018 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::PAUSED, U256::ZERO),
1020 StorageAction::Sdec(PATH_USD_ADDRESS, sender_balance_slot, max_fee_spending),
1022 StorageAction::Sinc(
1024 PATH_USD_ADDRESS,
1025 fee_manager_balance_slot,
1026 max_fee_spending,
1027 ),
1028 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::PAUSED, U256::ZERO),
1030 StorageAction::Sload(
1032 PATH_USD_ADDRESS,
1033 tip20_slots::TRANSFER_POLICY_ID,
1034 transfer_policy_id_word,
1035 ),
1036 StorageAction::Sload(
1038 TIP403_REGISTRY_ADDRESS,
1039 receive_policy_config_slot,
1040 U256::ZERO,
1041 ),
1042 StorageAction::Sdec(PATH_USD_ADDRESS, sender_balance_slot, transfer_amount),
1044 StorageAction::Sinc(PATH_USD_ADDRESS, recipient_balance_slot, transfer_amount),
1046 StorageAction::Sdec(
1048 PATH_USD_ADDRESS,
1049 fee_manager_balance_slot,
1050 second_refund_amount,
1051 ),
1052 StorageAction::Sinc(
1054 PATH_USD_ADDRESS,
1055 sender_balance_slot,
1056 second_refund_amount,
1057 ),
1058 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, validator_token_slot, U256::ZERO),
1060 StorageAction::Sinc(
1062 TIP_FEE_MANAGER_ADDRESS,
1063 collected_fees_slot,
1064 second_actual_spending,
1065 ),
1066 ]
1067 } else {
1068 vec![
1069 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::CURRENCY, currency_word),
1071 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::CURRENCY, currency_word),
1073 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, validator_token_slot, U256::ZERO),
1075 StorageAction::Sload(
1077 PATH_USD_ADDRESS,
1078 tip20_slots::TRANSFER_POLICY_ID,
1079 transfer_policy_id_word,
1080 ),
1081 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::PAUSED, U256::ZERO),
1083 StorageAction::Sload(
1085 PATH_USD_ADDRESS,
1086 sender_reward_info_slot + reward_recipient_offset,
1087 U256::ZERO,
1088 ),
1089 StorageAction::Sload(
1091 PATH_USD_ADDRESS,
1092 sender_reward_info_slot + reward_per_token_offset,
1093 U256::ZERO,
1094 ),
1095 StorageAction::Sload(
1097 PATH_USD_ADDRESS,
1098 sender_reward_info_slot + reward_balance_offset,
1099 U256::ZERO,
1100 ),
1101 StorageAction::Sload(
1103 PATH_USD_ADDRESS,
1104 tip20_slots::GLOBAL_REWARD_PER_TOKEN,
1105 U256::ZERO,
1106 ),
1107 StorageAction::Sdec(PATH_USD_ADDRESS, sender_balance_slot, max_fee_spending),
1109 StorageAction::Sinc(
1111 PATH_USD_ADDRESS,
1112 fee_manager_balance_slot,
1113 max_fee_spending,
1114 ),
1115 StorageAction::Sload(PATH_USD_ADDRESS, tip20_slots::PAUSED, U256::ZERO),
1117 StorageAction::Sload(
1119 PATH_USD_ADDRESS,
1120 tip20_slots::TRANSFER_POLICY_ID,
1121 transfer_policy_id_word,
1122 ),
1123 StorageAction::Sload(
1125 PATH_USD_ADDRESS,
1126 sender_balance_slot,
1127 sender_after_second_fee,
1128 ),
1129 StorageAction::Sload(
1131 PATH_USD_ADDRESS,
1132 sender_reward_info_slot + reward_recipient_offset,
1133 U256::ZERO,
1134 ),
1135 StorageAction::Sload(
1137 PATH_USD_ADDRESS,
1138 sender_reward_info_slot + reward_per_token_offset,
1139 U256::ZERO,
1140 ),
1141 StorageAction::Sload(
1143 PATH_USD_ADDRESS,
1144 sender_reward_info_slot + reward_balance_offset,
1145 U256::ZERO,
1146 ),
1147 StorageAction::Sload(
1149 PATH_USD_ADDRESS,
1150 tip20_slots::GLOBAL_REWARD_PER_TOKEN,
1151 U256::ZERO,
1152 ),
1153 StorageAction::Sload(
1155 PATH_USD_ADDRESS,
1156 recipient_reward_info_slot + reward_recipient_offset,
1157 U256::ZERO,
1158 ),
1159 StorageAction::Sload(
1161 PATH_USD_ADDRESS,
1162 recipient_reward_info_slot + reward_per_token_offset,
1163 U256::ZERO,
1164 ),
1165 StorageAction::Sload(
1167 PATH_USD_ADDRESS,
1168 recipient_reward_info_slot + reward_balance_offset,
1169 U256::ZERO,
1170 ),
1171 StorageAction::Sload(
1173 PATH_USD_ADDRESS,
1174 tip20_slots::GLOBAL_REWARD_PER_TOKEN,
1175 U256::ZERO,
1176 ),
1177 StorageAction::Sstore(
1179 PATH_USD_ADDRESS,
1180 sender_balance_slot,
1181 sender_after_second_transfer,
1182 ),
1183 StorageAction::Sinc(PATH_USD_ADDRESS, recipient_balance_slot, transfer_amount),
1185 StorageAction::Sload(
1187 PATH_USD_ADDRESS,
1188 sender_reward_info_slot + reward_recipient_offset,
1189 U256::ZERO,
1190 ),
1191 StorageAction::Sload(
1193 PATH_USD_ADDRESS,
1194 sender_reward_info_slot + reward_per_token_offset,
1195 U256::ZERO,
1196 ),
1197 StorageAction::Sload(
1199 PATH_USD_ADDRESS,
1200 sender_reward_info_slot + reward_balance_offset,
1201 U256::ZERO,
1202 ),
1203 StorageAction::Sload(
1205 PATH_USD_ADDRESS,
1206 tip20_slots::GLOBAL_REWARD_PER_TOKEN,
1207 U256::ZERO,
1208 ),
1209 StorageAction::Sdec(
1211 PATH_USD_ADDRESS,
1212 fee_manager_balance_slot,
1213 second_refund_amount,
1214 ),
1215 StorageAction::Sinc(
1217 PATH_USD_ADDRESS,
1218 sender_balance_slot,
1219 second_refund_amount,
1220 ),
1221 StorageAction::Sload(TIP_FEE_MANAGER_ADDRESS, validator_token_slot, U256::ZERO),
1223 StorageAction::Sinc(
1225 TIP_FEE_MANAGER_ADDRESS,
1226 collected_fees_slot,
1227 second_actual_spending,
1228 ),
1229 ]
1230 };
1231
1232 assert_eq!(actions, expected, "hardfork: {hardfork:?}");
1233 assert_storage_actions_reconstruct_evm_state(&actions, &result.state, hardfork);
1234 }
1235 }
1236
1237 fn evm_env_with_spec(
1241 spec: tempo_chainspec::hardfork::TempoHardfork,
1242 ) -> EvmEnv<tempo_chainspec::hardfork::TempoHardfork, TempoBlockEnv> {
1243 EvmEnv::<tempo_chainspec::hardfork::TempoHardfork, TempoBlockEnv>::new(
1244 CfgEnv::new_with_spec_and_gas_params(
1245 spec,
1246 tempo_gas_params_with_amsterdam(spec, false),
1247 ),
1248 TempoBlockEnv::default(),
1249 )
1250 }
1251
1252 #[test]
1257 fn test_tempo_evm_applies_gas_params() {
1258 let evm = TempoEvm::new(EmptyDB::default(), evm_env_with_spec(TempoHardfork::T1));
1260
1261 let gas_params = &evm.ctx().cfg.gas_params;
1264 assert_eq!(
1265 gas_params.tx_eip7702_per_empty_account_cost(),
1266 12_500,
1267 "T1 should have EIP-7702 per empty account cost of 12,500"
1268 );
1269 }
1270
1271 #[test]
1277 fn test_tempo_evm_respects_gas_cap() {
1278 let mut env = evm_env_with_spec(TempoHardfork::T1);
1279 env.cfg_env.tx_gas_limit_cap = TempoHardfork::T1.tx_gas_limit_cap();
1280
1281 let evm = TempoEvm::new(EmptyDB::default(), env);
1282
1283 assert_eq!(
1285 evm.ctx().cfg.tx_gas_limit_cap,
1286 TempoHardfork::T1.tx_gas_limit_cap(),
1287 "TempoEvm should preserve the gas limit cap from input"
1288 );
1289 }
1290
1291 #[test]
1293 fn test_tempo_evm_gas_params_differ_t0_vs_t1() {
1294 let t0_evm = TempoEvm::new(EmptyDB::default(), evm_env_with_spec(TempoHardfork::T0));
1296 let t1_evm = TempoEvm::new(EmptyDB::default(), evm_env_with_spec(TempoHardfork::T1));
1297
1298 let t0_eip7702_cost = t0_evm
1301 .ctx()
1302 .cfg
1303 .gas_params
1304 .tx_eip7702_per_empty_account_cost();
1305 let t1_eip7702_cost = t1_evm
1306 .ctx()
1307 .cfg
1308 .gas_params
1309 .tx_eip7702_per_empty_account_cost();
1310
1311 assert_eq!(t0_eip7702_cost, 25_000, "T0 should have default 25,000");
1312 assert_eq!(t1_eip7702_cost, 12_500, "T1 should have reduced 12,500");
1313 assert_ne!(
1314 t0_eip7702_cost, t1_eip7702_cost,
1315 "Gas params should differ between T0 and T1"
1316 );
1317 }
1318
1319 #[test]
1321 fn test_tempo_evm_t1_state_creation_costs() {
1322 use revm::context_interface::cfg::GasId;
1323
1324 let evm = TempoEvm::new(EmptyDB::default(), evm_env_with_spec(TempoHardfork::T1));
1325 let gas_params = &evm.ctx().cfg.gas_params;
1326
1327 assert_eq!(
1329 gas_params.get(GasId::sstore_set_without_load_cost()),
1330 250_000,
1331 "T1 SSTORE set cost should be 250,000"
1332 );
1333 assert_eq!(
1334 gas_params.get(GasId::tx_create_cost()),
1335 500_000,
1336 "T1 TX create cost should be 500,000"
1337 );
1338 assert_eq!(
1339 gas_params.get(GasId::create()),
1340 500_000,
1341 "T1 CREATE opcode cost should be 500,000"
1342 );
1343 assert_eq!(
1344 gas_params.get(GasId::new_account_cost()),
1345 250_000,
1346 "T1 new account cost should be 250,000"
1347 );
1348 assert_eq!(
1349 gas_params.get(GasId::code_deposit_cost()),
1350 1_000,
1351 "T1 code deposit cost should be 1,000 per byte"
1352 );
1353 }
1354}