1use crate::TempoTxEnv;
2use alloy_consensus::transaction::{Either, Recovered};
3use alloy_primitives::{Address, Bytes, LogData, TxKind, U256};
4use alloy_sol_types::SolCall;
5use core::marker::PhantomData;
6use revm::{
7 Database,
8 context::JournalTr,
9 state::{AccountInfo, Bytecode},
10};
11use tempo_chainspec::hardfork::TempoHardfork;
12use tempo_contracts::precompiles::{
13 DEFAULT_FEE_TOKEN, IFeeManager, IStablecoinDEX, STABLECOIN_DEX_ADDRESS,
14};
15use tempo_precompiles::{
16 TIP_FEE_MANAGER_ADDRESS,
17 error::{Result as TempoResult, TempoPrecompileError},
18 storage::{Handler, PrecompileStorageProvider, StorageCtx},
19 tip_fee_manager::TipFeeManager,
20 tip20::{ITIP20, TIP20Token, is_tip20_prefix},
21 tip403_registry::{AuthRole, TIP403Registry},
22};
23use tempo_primitives::TempoTxEnvelope;
24
25fn is_tip20_fee_inference_call(input: &[u8]) -> bool {
28 input.first_chunk::<4>().is_some_and(|&s| {
29 matches!(
30 s,
31 ITIP20::transferCall::SELECTOR
32 | ITIP20::transferWithMemoCall::SELECTOR
33 | ITIP20::distributeRewardCall::SELECTOR
34 )
35 })
36}
37
38#[auto_impl::auto_impl(&, Arc)]
40pub trait TempoTx {
41 fn fee_token(&self) -> Option<Address>;
43
44 fn is_aa(&self) -> bool;
46
47 fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)>;
49
50 fn caller(&self) -> Address;
52}
53
54impl TempoTx for TempoTxEnv {
55 fn fee_token(&self) -> Option<Address> {
56 self.fee_token
57 }
58
59 fn is_aa(&self) -> bool {
60 self.tempo_tx_env.is_some()
61 }
62
63 fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)> {
64 if let Some(aa) = self.tempo_tx_env.as_ref() {
65 Either::Left(aa.aa_calls.iter().map(|call| (call.to, &call.input)))
66 } else {
67 Either::Right(core::iter::once((self.inner.kind, &self.inner.data)))
68 }
69 }
70
71 fn caller(&self) -> Address {
72 self.inner.caller
73 }
74}
75
76impl TempoTx for Recovered<TempoTxEnvelope> {
77 fn fee_token(&self) -> Option<Address> {
78 self.inner().fee_token()
79 }
80
81 fn is_aa(&self) -> bool {
82 self.inner().is_aa()
83 }
84
85 fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)> {
86 self.inner().calls()
87 }
88
89 fn caller(&self) -> Address {
90 self.signer()
91 }
92}
93
94pub trait TempoStateAccess<M = ()> {
100 type Error: core::fmt::Display;
102
103 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error>;
105
106 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error>;
108
109 fn with_read_only_storage_ctx<R>(&mut self, spec: TempoHardfork, f: impl FnOnce() -> R) -> R
111 where
112 Self: Sized,
113 {
114 StorageCtx::enter(&mut ReadOnlyStorageProvider::new(self, spec), f)
115 }
116
117 fn get_fee_token(
119 &mut self,
120 tx: impl TempoTx,
121 fee_payer: Address,
122 spec: TempoHardfork,
123 ) -> TempoResult<Address>
124 where
125 Self: Sized,
126 {
127 if let Some(fee_token) = tx.fee_token() {
129 return Ok(fee_token);
130 }
131
132 if !tx.is_aa()
136 && fee_payer == tx.caller()
137 && let Some((kind, input)) = tx.calls().next()
138 && kind.to() == Some(&TIP_FEE_MANAGER_ADDRESS)
139 && let Ok(call) = IFeeManager::setUserTokenCall::abi_decode(input)
140 {
141 return Ok(call.token);
142 }
143
144 let user_token = self.with_read_only_storage_ctx(spec, || {
146 TipFeeManager::new().user_tokens[fee_payer].read()
148 })?;
149
150 if !user_token.is_zero() {
151 return Ok(user_token);
152 }
153
154 if let Some(to) = tx.calls().next().and_then(|(kind, _)| kind.to().copied()) {
156 let can_infer_tip20 =
157 if tx.is_aa() && fee_payer != tx.caller() {
159 false
160 }
161 else {
163 tx.calls().all(|(kind, input)| {
164 kind.to() == Some(&to) && is_tip20_fee_inference_call(input)
165 })
166 }
167 ;
168
169 if can_infer_tip20 && self.is_valid_fee_token(spec, to)? {
170 return Ok(to);
171 }
172 }
173
174 let mut calls = tx.calls();
178 if let Some((kind, input)) = calls.next()
179 && kind.to() == Some(&STABLECOIN_DEX_ADDRESS)
180 && (!tx.is_aa() || calls.next().is_none())
181 {
182 if let Ok(call) = IStablecoinDEX::swapExactAmountInCall::abi_decode(input)
183 && self.is_valid_fee_token(spec, call.tokenIn)?
184 {
185 return Ok(call.tokenIn);
186 } else if let Ok(call) = IStablecoinDEX::swapExactAmountOutCall::abi_decode(input)
187 && self.is_valid_fee_token(spec, call.tokenIn)?
188 {
189 return Ok(call.tokenIn);
190 }
191 }
192
193 Ok(DEFAULT_FEE_TOKEN)
195 }
196
197 fn is_tip20_usd(&mut self, spec: TempoHardfork, fee_token: Address) -> TempoResult<bool>
201 where
202 Self: Sized,
203 {
204 self.with_read_only_storage_ctx(spec, || {
205 let token = TIP20Token::from_address_unchecked(fee_token);
207 Ok(token.currency.len()? == 3 && token.currency.read()?.as_str() == "USD")
208 })
209 }
210
211 fn is_valid_fee_token(&mut self, spec: TempoHardfork, fee_token: Address) -> TempoResult<bool>
213 where
214 Self: Sized,
215 {
216 if !is_tip20_prefix(fee_token) {
218 return Ok(false);
219 }
220
221 self.is_tip20_usd(spec, fee_token)
223 }
224
225 fn is_fee_token_paused(&mut self, spec: TempoHardfork, fee_token: Address) -> TempoResult<bool>
227 where
228 Self: Sized,
229 {
230 self.with_read_only_storage_ctx(spec, || {
231 let token = TIP20Token::from_address(fee_token)?;
232 token.paused()
233 })
234 }
235
236 fn can_fee_payer_transfer(
238 &mut self,
239 fee_token: Address,
240 fee_payer: Address,
241 spec: TempoHardfork,
242 ) -> TempoResult<bool>
243 where
244 Self: Sized,
245 {
246 self.with_read_only_storage_ctx(spec, || {
247 let token = TIP20Token::from_address(fee_token)?;
248 if spec.is_t1c() {
249 token.is_transfer_authorized(fee_payer, TIP_FEE_MANAGER_ADDRESS)
251 } else {
252 let policy_id = token.transfer_policy_id.read()?;
253 TIP403Registry::new().is_authorized_as(policy_id, fee_payer, AuthRole::sender())
254 }
255 })
256 }
257
258 fn get_token_balance(
262 &mut self,
263 token: Address,
264 account: Address,
265 spec: TempoHardfork,
266 ) -> TempoResult<U256>
267 where
268 Self: Sized,
269 {
270 self.with_read_only_storage_ctx(spec, || {
271 TIP20Token::from_address(token)?.balances[account].read()
273 })
274 }
275}
276
277impl<DB: Database> TempoStateAccess<()> for DB {
278 type Error = DB::Error;
279
280 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error> {
281 self.basic(address).map(Option::unwrap_or_default)
282 }
283
284 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error> {
285 self.storage(address, key)
286 }
287}
288
289impl<T: JournalTr> TempoStateAccess<((), ())> for T {
290 type Error = <T::Database as Database>::Error;
291
292 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error> {
293 self.load_account(address).map(|s| s.data.info.clone())
294 }
295
296 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error> {
297 JournalTr::sload(self, address, key).map(|s| s.data)
298 }
299}
300
301#[cfg(feature = "reth")]
302impl<T: reth_storage_api::StateProvider> TempoStateAccess<((), (), ())> for T {
303 type Error = reth_evm::execute::ProviderError;
304
305 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error> {
306 self.basic_account(&address)
307 .map(Option::unwrap_or_default)
308 .map(Into::into)
309 }
310
311 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error> {
312 self.storage(address, key.into())
313 .map(Option::unwrap_or_default)
314 }
315}
316
317struct ReadOnlyStorageProvider<'a, S, M = ()> {
324 state: &'a mut S,
325 spec: TempoHardfork,
326 _marker: PhantomData<M>,
327}
328
329impl<'a, S, M> ReadOnlyStorageProvider<'a, S, M>
330where
331 S: TempoStateAccess<M>,
332{
333 fn new(state: &'a mut S, spec: TempoHardfork) -> Self {
335 Self {
336 state,
337 spec,
338 _marker: PhantomData,
339 }
340 }
341}
342
343impl<S, M> PrecompileStorageProvider for ReadOnlyStorageProvider<'_, S, M>
344where
345 S: TempoStateAccess<M>,
346{
347 fn spec(&self) -> TempoHardfork {
348 self.spec
349 }
350
351 fn is_static(&self) -> bool {
352 true
354 }
355
356 fn sload(&mut self, address: Address, key: U256) -> TempoResult<U256> {
357 let _ = self
358 .state
359 .basic(address)
360 .map_err(|e| TempoPrecompileError::Fatal(e.to_string()))?;
361 self.state
362 .sload(address, key)
363 .map_err(|e| TempoPrecompileError::Fatal(e.to_string()))
364 }
365
366 fn with_account_info(
367 &mut self,
368 address: Address,
369 f: &mut dyn FnMut(&AccountInfo),
370 ) -> TempoResult<()> {
371 let info = self
372 .state
373 .basic(address)
374 .map_err(|e| TempoPrecompileError::Fatal(e.to_string()))?;
375 f(&info);
376 Ok(())
377 }
378
379 fn chain_id(&self) -> u64 {
381 unreachable!("'chain_id' not implemented in read-only context yet")
382 }
383
384 fn timestamp(&self) -> U256 {
385 unreachable!("'timestamp' not implemented in read-only context yet")
386 }
387
388 fn beneficiary(&self) -> Address {
389 unreachable!("'beneficiary' not implemented in read-only context yet")
390 }
391
392 fn block_number(&self) -> u64 {
393 unreachable!("'block_number' not implemented in read-only context yet")
394 }
395
396 fn tload(&mut self, _: Address, _: U256) -> TempoResult<U256> {
397 unreachable!("'tload' not implemented in read-only context yet")
398 }
399
400 fn gas_used(&self) -> u64 {
401 unreachable!("'gas_used' not implemented in read-only context yet")
402 }
403
404 fn gas_refunded(&self) -> i64 {
405 unreachable!("'gas_refunded' not implemented in read-only context yet")
406 }
407
408 fn sstore(&mut self, _: Address, _: U256, _: U256) -> TempoResult<()> {
410 unreachable!("'sstore' not supported in read-only context")
411 }
412
413 fn set_code(&mut self, _: Address, _: Bytecode) -> TempoResult<()> {
414 unreachable!("'set_code' not supported in read-only context")
415 }
416
417 fn emit_event(&mut self, _: Address, _: LogData) -> TempoResult<()> {
418 unreachable!("'emit_event' not supported in read-only context")
419 }
420
421 fn tstore(&mut self, _: Address, _: U256, _: U256) -> TempoResult<()> {
422 unreachable!("'tstore' not supported in read-only context")
423 }
424
425 fn deduct_gas(&mut self, _: u64) -> TempoResult<()> {
426 unreachable!("'deduct_gas' not supported in read-only context")
427 }
428
429 fn refund_gas(&mut self, _: i64) {
430 unreachable!("'refund_gas' not supported in read-only context")
431 }
432
433 fn checkpoint(&mut self) -> revm::context::journaled_state::JournalCheckpoint {
434 unreachable!("'checkpoint' not supported in read-only context")
435 }
436
437 fn checkpoint_commit(&mut self, _: revm::context::journaled_state::JournalCheckpoint) {
438 unreachable!("'checkpoint_commit' not supported in read-only context")
439 }
440
441 fn checkpoint_revert(&mut self, _: revm::context::journaled_state::JournalCheckpoint) {
442 unreachable!("'checkpoint_revert' not supported in read-only context")
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use crate::{TempoBlockEnv, TempoEvm};
450 use alloy_primitives::{address, uint};
451 use reth_evm::EvmInternals;
452 use revm::{
453 Context, MainContext, context::TxEnv, database::EmptyDB,
454 interpreter::instructions::utility::IntoU256,
455 };
456 use tempo_precompiles::{
457 PATH_USD_ADDRESS,
458 storage::{StorageCtx, evm::EvmPrecompileStorageProvider},
459 test_util::TIP20Setup,
460 tip20::{IRolesAuth::*, ITIP20::*, TIP20Token, slots as tip20_slots},
461 tip403_registry::{ITIP403Registry, TIP403Registry},
462 };
463
464 #[test]
465 fn test_get_fee_token_fee_token_set() -> eyre::Result<()> {
466 let caller = Address::random();
467 let fee_token = Address::random();
468
469 let tx_env = TxEnv {
470 data: Bytes::new(),
471 caller,
472 ..Default::default()
473 };
474 let tx = TempoTxEnv {
475 inner: tx_env,
476 fee_token: Some(fee_token),
477 ..Default::default()
478 };
479
480 let mut db = EmptyDB::default();
481 let token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
482 assert_eq!(token, fee_token);
483 Ok(())
484 }
485
486 #[test]
487 fn test_get_fee_token_fee_manager() -> eyre::Result<()> {
488 let caller = Address::random();
489 let token = Address::random();
490
491 let call = IFeeManager::setUserTokenCall { token };
492 let tx_env = TxEnv {
493 data: call.abi_encode().into(),
494 kind: TxKind::Call(TIP_FEE_MANAGER_ADDRESS),
495 caller,
496 ..Default::default()
497 };
498 let tx = TempoTxEnv {
499 inner: tx_env,
500 ..Default::default()
501 };
502
503 let mut db = EmptyDB::default();
504 let result_token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
505 assert_eq!(result_token, token);
506 Ok(())
507 }
508
509 #[test]
510 fn test_get_fee_token_user_token_set() -> eyre::Result<()> {
511 let caller = Address::random();
512 let user_token = Address::random();
513
514 let mut db = revm::database::CacheDB::new(EmptyDB::default());
516 let user_slot = TipFeeManager::new().user_tokens[caller].slot();
517 db.insert_account_storage(TIP_FEE_MANAGER_ADDRESS, user_slot, user_token.into_u256())
518 .unwrap();
519
520 let result_token =
521 db.get_fee_token(TempoTxEnv::default(), caller, TempoHardfork::Genesis)?;
522 assert_eq!(result_token, user_token);
523 Ok(())
524 }
525
526 #[test]
527 fn test_get_fee_token_tip20() -> eyre::Result<()> {
528 let caller = Address::random();
529 let tip20_token = Address::random();
530
531 let tx_env = TxEnv {
532 data: Bytes::from_static(b"transfer_data"),
533 kind: TxKind::Call(tip20_token),
534 caller,
535 ..Default::default()
536 };
537 let tx = TempoTxEnv {
538 inner: tx_env,
539 ..Default::default()
540 };
541
542 let mut db = EmptyDB::default();
543 let result_token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
544 assert_eq!(result_token, DEFAULT_FEE_TOKEN);
545 Ok(())
546 }
547
548 #[test]
549 fn test_get_fee_token_fallback() -> eyre::Result<()> {
550 let caller = Address::random();
551 let tx_env = TxEnv {
552 caller,
553 ..Default::default()
554 };
555 let tx = TempoTxEnv {
556 inner: tx_env,
557 ..Default::default()
558 };
559
560 let mut db = EmptyDB::default();
561 let result_token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
562 assert_eq!(result_token, DEFAULT_FEE_TOKEN);
564 Ok(())
565 }
566
567 #[test]
568 fn test_get_fee_token_stablecoin_dex() -> eyre::Result<()> {
569 let caller = Address::random();
570 let token_in = DEFAULT_FEE_TOKEN;
572 let token_out = address!("0x20C0000000000000000000000000000000000001");
573
574 let call = IStablecoinDEX::swapExactAmountInCall {
576 tokenIn: token_in,
577 tokenOut: token_out,
578 amountIn: 1000,
579 minAmountOut: 900,
580 };
581
582 let tx_env = TxEnv {
583 data: call.abi_encode().into(),
584 kind: TxKind::Call(STABLECOIN_DEX_ADDRESS),
585 caller,
586 ..Default::default()
587 };
588 let tx = TempoTxEnv {
589 inner: tx_env,
590 ..Default::default()
591 };
592
593 let mut db = EmptyDB::default();
594 let token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
595 assert_eq!(token, token_in);
596
597 let call = IStablecoinDEX::swapExactAmountOutCall {
599 tokenIn: token_in,
600 tokenOut: token_out,
601 amountOut: 900,
602 maxAmountIn: 1000,
603 };
604
605 let tx_env = TxEnv {
606 data: call.abi_encode().into(),
607 kind: TxKind::Call(STABLECOIN_DEX_ADDRESS),
608 caller,
609 ..Default::default()
610 };
611
612 let tx = TempoTxEnv {
613 inner: tx_env,
614 ..Default::default()
615 };
616
617 let token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
618 assert_eq!(token, token_in);
619
620 Ok(())
621 }
622
623 #[test]
624 fn test_read_token_balance_typed_storage() -> eyre::Result<()> {
625 let token_address = PATH_USD_ADDRESS;
626 let account = Address::random();
627 let expected_balance = U256::from(1000u64);
628
629 let mut db = revm::database::CacheDB::new(EmptyDB::default());
631 let balance_slot = TIP20Token::from_address(token_address)?.balances[account].slot();
632 db.insert_account_storage(token_address, balance_slot, expected_balance)?;
633
634 let balance = db.get_token_balance(token_address, account, TempoHardfork::Genesis)?;
636 assert_eq!(balance, expected_balance);
637
638 Ok(())
639 }
640
641 #[test]
642 fn test_is_tip20_fee_inference_call() {
643 assert!(is_tip20_fee_inference_call(&transferCall::SELECTOR));
645 assert!(is_tip20_fee_inference_call(&transferWithMemoCall::SELECTOR));
646 assert!(is_tip20_fee_inference_call(&distributeRewardCall::SELECTOR));
647
648 assert!(!is_tip20_fee_inference_call(&grantRoleCall::SELECTOR));
650 assert!(!is_tip20_fee_inference_call(&mintCall::SELECTOR));
651 assert!(!is_tip20_fee_inference_call(&approveCall::SELECTOR));
652
653 assert!(!is_tip20_fee_inference_call(&[]));
655 assert!(!is_tip20_fee_inference_call(&[0x00, 0x01, 0x02]));
656 }
657
658 #[test]
659 fn test_is_fee_token_paused() -> eyre::Result<()> {
660 let token_address = PATH_USD_ADDRESS;
661 let mut db = revm::database::CacheDB::new(EmptyDB::default());
662
663 assert!(!db.is_fee_token_paused(TempoHardfork::Genesis, token_address)?);
665
666 db.insert_account_storage(token_address, tip20_slots::PAUSED, U256::from(1))?;
668 assert!(db.is_fee_token_paused(TempoHardfork::Genesis, token_address)?);
669
670 Ok(())
671 }
672
673 #[test]
674 fn test_is_tip20_usd() -> eyre::Result<()> {
675 let fee_token = PATH_USD_ADDRESS;
676
677 let cases: &[(U256, bool, &str)] = &[
679 (
681 uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256),
682 true,
683 "USD",
684 ),
685 (
687 uint!(0x4555520000000000000000000000000000000000000000000000000000000006_U256),
688 false,
689 "EUR",
690 ),
691 (
693 uint!(0x5553000000000000000000000000000000000000000000000000000000000004_U256),
694 false,
695 "US",
696 ),
697 (U256::ZERO, false, "empty"),
699 ];
700
701 for (currency_value, expected, label) in cases {
702 let mut db = revm::database::CacheDB::new(EmptyDB::default());
703 db.insert_account_storage(fee_token, tip20_slots::CURRENCY, *currency_value)?;
704
705 let is_usd = db.is_tip20_usd(TempoHardfork::Genesis, fee_token)?;
706 assert_eq!(is_usd, *expected, "currency '{label}' failed");
707 }
708
709 Ok(())
710 }
711
712 #[test]
713 fn test_can_fee_payer_transfer_t1c() -> eyre::Result<()> {
714 let admin = Address::random();
715 let fee_payer = Address::random();
716 let db = revm::database::CacheDB::new(EmptyDB::new());
717 let mut evm = TempoEvm::new(
718 Context::mainnet()
719 .with_db(db)
720 .with_block(TempoBlockEnv::default())
721 .with_cfg(Default::default())
722 .with_tx(Default::default()),
723 (),
724 );
725
726 let policy_id = {
728 let ctx = &mut evm.ctx;
729 let internals =
730 EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
731 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
732 StorageCtx::enter(&mut provider, || -> eyre::Result<u64> {
733 TIP20Setup::path_usd(admin).apply()?;
734 let mut registry = TIP403Registry::new();
735 registry.initialize()?;
736
737 let policy_id = registry.create_policy(
738 admin,
739 ITIP403Registry::createPolicyCall {
740 admin,
741 policyType: ITIP403Registry::PolicyType::WHITELIST,
742 },
743 )?;
744 TIP20Token::from_address(PATH_USD_ADDRESS)?.change_transfer_policy_id(
745 admin,
746 ITIP20::changeTransferPolicyIdCall {
747 newPolicyId: policy_id,
748 },
749 )?;
750 registry.modify_policy_whitelist(
751 admin,
752 ITIP403Registry::modifyPolicyWhitelistCall {
753 policyId: policy_id,
754 account: fee_payer,
755 allowed: true,
756 },
757 )?;
758 Ok(policy_id)
759 })?
760 };
761
762 assert!(evm.ctx.journaled_state.can_fee_payer_transfer(
763 PATH_USD_ADDRESS,
764 fee_payer,
765 TempoHardfork::T1B
766 )?);
767
768 assert!(!evm.ctx.journaled_state.can_fee_payer_transfer(
770 PATH_USD_ADDRESS,
771 fee_payer,
772 TempoHardfork::T1C
773 )?);
774
775 {
777 let ctx = &mut evm.ctx;
778 let internals =
779 EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
780 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
781 StorageCtx::enter(&mut provider, || {
782 TIP403Registry::new().modify_policy_whitelist(
783 admin,
784 ITIP403Registry::modifyPolicyWhitelistCall {
785 policyId: policy_id,
786 account: TIP_FEE_MANAGER_ADDRESS,
787 allowed: true,
788 },
789 )
790 })?;
791 }
792
793 assert!(evm.ctx.journaled_state.can_fee_payer_transfer(
794 PATH_USD_ADDRESS,
795 fee_payer,
796 TempoHardfork::T1B
797 )?);
798
799 assert!(evm.ctx.journaled_state.can_fee_payer_transfer(
800 PATH_USD_ADDRESS,
801 fee_payer,
802 TempoHardfork::T1C
803 )?);
804
805 Ok(())
806 }
807}