1use crate::{TempoInvalidTransaction, 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, result::EVMError},
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},
21 tip403_registry::{AuthRole, TIP403Registry},
22};
23use tempo_primitives::{TempoAddressExt, TempoTxEnvelope};
24
25fn is_tip20_fee_inference_call(spec: TempoHardfork, input: &[u8]) -> bool {
29 input.first_chunk::<4>().is_some_and(|&s| {
30 matches!(
31 s,
32 ITIP20::transferCall::SELECTOR | ITIP20::transferWithMemoCall::SELECTOR
33 ) || (!spec.is_t7() && s == ITIP20::distributeRewardCall::SELECTOR)
34 })
35}
36
37#[auto_impl::auto_impl(&, Arc)]
39pub trait TempoTx {
40 fn fee_token(&self) -> Option<Address>;
42
43 fn is_aa(&self) -> bool;
45
46 fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)>;
48
49 fn caller(&self) -> Address;
51}
52
53impl TempoTx for TempoTxEnv {
54 fn fee_token(&self) -> Option<Address> {
55 self.fee_token
56 }
57
58 fn is_aa(&self) -> bool {
59 self.tempo_tx_env.is_some()
60 }
61
62 fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)> {
63 if let Some(aa) = self.tempo_tx_env.as_ref() {
64 Either::Left(aa.aa_calls.iter().map(|call| (call.to, &call.input)))
65 } else {
66 Either::Right(core::iter::once((self.inner.kind, &self.inner.data)))
67 }
68 }
69
70 fn caller(&self) -> Address {
71 self.inner.caller
72 }
73}
74
75impl TempoTx for Recovered<TempoTxEnvelope> {
76 fn fee_token(&self) -> Option<Address> {
77 self.inner().fee_token()
78 }
79
80 fn is_aa(&self) -> bool {
81 self.inner().is_aa()
82 }
83
84 fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)> {
85 self.inner().calls()
86 }
87
88 fn caller(&self) -> Address {
89 self.signer()
90 }
91}
92
93pub trait TempoStateAccess<M = ()> {
99 type Error: core::fmt::Display;
101
102 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error>;
104
105 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error>;
107
108 fn with_read_only_storage_ctx<R>(&mut self, spec: TempoHardfork, f: impl FnOnce() -> R) -> R
110 where
111 Self: Sized,
112 {
113 StorageCtx::enter(&mut ReadOnlyStorageProvider::new(self, spec), f)
114 }
115
116 fn get_fee_token(
118 &mut self,
119 tx: impl TempoTx,
120 fee_payer: Address,
121 spec: TempoHardfork,
122 ) -> TempoResult<Address>
123 where
124 Self: Sized,
125 {
126 if let Some(fee_token) = tx.fee_token() {
128 return Ok(fee_token);
129 }
130
131 if !tx.is_aa()
135 && fee_payer == tx.caller()
136 && let Some((kind, input)) = tx.calls().next()
137 && kind.to() == Some(&TIP_FEE_MANAGER_ADDRESS)
138 && let Ok(call) = IFeeManager::setUserTokenCall::abi_decode(input)
139 {
140 return Ok(call.token);
141 }
142
143 let user_token = self.with_read_only_storage_ctx(spec, || {
145 TipFeeManager::new().user_tokens[fee_payer].read()
147 })?;
148
149 if !user_token.is_zero() {
150 return Ok(user_token);
151 }
152
153 if let Some(to) = tx.calls().next().and_then(|(kind, _)| kind.to().copied()) {
155 let can_infer_tip20 =
156 if tx.is_aa() && fee_payer != tx.caller() {
158 false
159 }
160 else {
162 tx.calls().all(|(kind, input)| {
163 kind.to() == Some(&to) && is_tip20_fee_inference_call(spec, input)
164 })
165 }
166 ;
167
168 if can_infer_tip20 && self.is_valid_fee_token(spec, to)? {
169 return Ok(to);
170 }
171 }
172
173 let mut calls = tx.calls();
177 if let Some((kind, input)) = calls.next()
178 && kind.to() == Some(&STABLECOIN_DEX_ADDRESS)
179 && (!tx.is_aa() || calls.next().is_none())
180 {
181 if let Ok(call) = IStablecoinDEX::swapExactAmountInCall::abi_decode(input)
182 && self.is_valid_fee_token(spec, call.tokenIn)?
183 {
184 return Ok(call.tokenIn);
185 } else if let Ok(call) = IStablecoinDEX::swapExactAmountOutCall::abi_decode(input)
186 && self.is_valid_fee_token(spec, call.tokenIn)?
187 {
188 return Ok(call.tokenIn);
189 }
190 }
191
192 Ok(DEFAULT_FEE_TOKEN)
194 }
195
196 fn is_tip20_usd(&mut self, spec: TempoHardfork, fee_token: Address) -> TempoResult<bool>
200 where
201 Self: Sized,
202 {
203 self.with_read_only_storage_ctx(spec, || {
204 let token = TIP20Token::from_address_unchecked(fee_token);
206 Ok(token.currency.len()? == 3 && token.currency.read()?.as_str() == "USD")
207 })
208 }
209
210 fn ensure_tip20_usd(
214 &mut self,
215 spec: TempoHardfork,
216 fee_token: Address,
217 ) -> Result<(), EVMError<Self::Error, TempoInvalidTransaction>>
218 where
219 Self: Sized,
220 {
221 self.with_read_only_storage_ctx(spec, || {
222 let token = TIP20Token::from_address_unchecked(fee_token);
224 let len = token.currency.len()?;
225
226 let currency = if len > 31 {
227 format!("<{len} bytes>")
228 } else {
229 token.currency.read()?
230 };
231
232 if currency.as_str() != "USD" {
233 return Ok(Err(EVMError::Transaction(
234 TempoInvalidTransaction::FeeTokenNotUsdCurrency {
235 address: fee_token,
236 currency,
237 },
238 )));
239 }
240
241 Ok(Ok(()))
242 })
243 .map_err(|err: TempoPrecompileError| EVMError::Custom(err.to_string()))?
244 }
245
246 fn is_valid_fee_token(&mut self, spec: TempoHardfork, fee_token: Address) -> TempoResult<bool>
248 where
249 Self: Sized,
250 {
251 if !fee_token.is_tip20() {
253 return Ok(false);
254 }
255
256 self.is_tip20_usd(spec, fee_token)
258 }
259
260 fn is_fee_token_paused(&mut self, spec: TempoHardfork, fee_token: Address) -> TempoResult<bool>
262 where
263 Self: Sized,
264 {
265 self.with_read_only_storage_ctx(spec, || {
266 let token = TIP20Token::from_address(fee_token)?;
267 token.paused()
268 })
269 }
270
271 fn can_fee_payer_transfer(
273 &mut self,
274 fee_token: Address,
275 fee_payer: Address,
276 spec: TempoHardfork,
277 ) -> TempoResult<bool>
278 where
279 Self: Sized,
280 {
281 self.with_read_only_storage_ctx(spec, || {
282 let token = TIP20Token::from_address(fee_token)?;
283 if spec.is_t1c() {
284 token.is_transfer_authorized(fee_payer, TIP_FEE_MANAGER_ADDRESS)
286 } else {
287 let policy_id = token.transfer_policy_id.read()?;
288 TIP403Registry::new().is_authorized_as(policy_id, fee_payer, AuthRole::sender())
289 }
290 })
291 }
292
293 fn get_token_balance(
297 &mut self,
298 token: Address,
299 account: Address,
300 spec: TempoHardfork,
301 ) -> TempoResult<U256>
302 where
303 Self: Sized,
304 {
305 self.with_read_only_storage_ctx(spec, || {
306 TIP20Token::from_address(token)?.balances[account].read()
308 })
309 }
310}
311
312impl<DB: Database> TempoStateAccess<()> for DB {
313 type Error = DB::Error;
314
315 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error> {
316 self.basic(address).map(Option::unwrap_or_default)
317 }
318
319 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error> {
320 self.storage(address, key)
321 }
322}
323
324impl<T: JournalTr> TempoStateAccess<((), ())> for T {
325 type Error = <T::Database as Database>::Error;
326
327 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error> {
328 self.load_account(address).map(|s| s.data.info.clone())
329 }
330
331 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error> {
332 JournalTr::sload(self, address, key).map(|s| s.data)
333 }
334}
335
336#[cfg(feature = "reth")]
337impl<T: reth_storage_api::StateProvider> TempoStateAccess<((), (), ())> for T {
338 type Error = reth_evm::execute::ProviderError;
339
340 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error> {
341 self.basic_account(&address)
342 .map(Option::unwrap_or_default)
343 .map(Into::into)
344 }
345
346 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error> {
347 self.storage(address, key.into())
348 .map(Option::unwrap_or_default)
349 }
350}
351
352struct ReadOnlyStorageProvider<'a, S, M = ()> {
359 state: &'a mut S,
360 spec: TempoHardfork,
361 _marker: PhantomData<M>,
362}
363
364impl<'a, S, M> ReadOnlyStorageProvider<'a, S, M>
365where
366 S: TempoStateAccess<M>,
367{
368 fn new(state: &'a mut S, spec: TempoHardfork) -> Self {
370 Self {
371 state,
372 spec,
373 _marker: PhantomData,
374 }
375 }
376}
377
378impl<S, M> PrecompileStorageProvider for ReadOnlyStorageProvider<'_, S, M>
379where
380 S: TempoStateAccess<M>,
381{
382 fn spec(&self) -> TempoHardfork {
383 self.spec
384 }
385
386 fn amsterdam_eip8037_enabled(&self) -> bool {
387 false
390 }
391
392 fn gas_limit(&self) -> u64 {
393 0
394 }
395
396 fn is_static(&self) -> bool {
397 true
399 }
400
401 fn sload(&mut self, address: Address, key: U256) -> TempoResult<U256> {
402 let _ = self
403 .state
404 .basic(address)
405 .map_err(|e| TempoPrecompileError::Fatal(e.to_string()))?;
406 self.state
407 .sload(address, key)
408 .map_err(|e| TempoPrecompileError::Fatal(e.to_string()))
409 }
410
411 fn with_account_info(
412 &mut self,
413 address: Address,
414 f: &mut dyn FnMut(&AccountInfo),
415 ) -> TempoResult<()> {
416 let info = self
417 .state
418 .basic(address)
419 .map_err(|e| TempoPrecompileError::Fatal(e.to_string()))?;
420 f(&info);
421 Ok(())
422 }
423
424 fn chain_id(&self) -> u64 {
426 unreachable!("'chain_id' not implemented in read-only context yet")
427 }
428
429 fn timestamp(&self) -> U256 {
430 unreachable!("'timestamp' not implemented in read-only context yet")
431 }
432
433 fn beneficiary(&self) -> Address {
434 unreachable!("'beneficiary' not implemented in read-only context yet")
435 }
436
437 fn block_number(&self) -> u64 {
438 unreachable!("'block_number' not implemented in read-only context yet")
439 }
440
441 fn tload(&mut self, _: Address, _: U256) -> TempoResult<U256> {
442 unreachable!("'tload' not implemented in read-only context yet")
443 }
444
445 fn gas_used(&self) -> u64 {
446 unreachable!("'gas_used' not implemented in read-only context yet")
447 }
448
449 fn state_gas_used(&self) -> u64 {
450 unreachable!("'state_gas_used' not implemented in read-only context yet")
451 }
452
453 fn gas_refunded(&self) -> i64 {
454 unreachable!("'gas_refunded' not implemented in read-only context yet")
455 }
456
457 fn reservoir(&self) -> u64 {
458 unreachable!("'reservoir' not implemented in read-only context yet")
459 }
460
461 fn sstore(&mut self, _: Address, _: U256, _: U256) -> TempoResult<()> {
463 unreachable!("'sstore' not supported in read-only context")
464 }
465
466 fn set_code(&mut self, _: Address, _: Bytecode) -> TempoResult<()> {
467 unreachable!("'set_code' not supported in read-only context")
468 }
469
470 fn emit_event(&mut self, _: Address, _: LogData) -> TempoResult<()> {
471 unreachable!("'emit_event' not supported in read-only context")
472 }
473
474 fn tstore(&mut self, _: Address, _: U256, _: U256) -> TempoResult<()> {
475 unreachable!("'tstore' not supported in read-only context")
476 }
477
478 fn deduct_gas(&mut self, _: u64) -> TempoResult<()> {
479 unreachable!("'deduct_gas' not supported in read-only context")
480 }
481
482 fn refund_gas(&mut self, _: i64) {
483 unreachable!("'refund_gas' not supported in read-only context")
484 }
485
486 fn checkpoint(&mut self) -> revm::context::journaled_state::JournalCheckpoint {
487 unreachable!("'checkpoint' not supported in read-only context")
488 }
489
490 fn checkpoint_commit(&mut self, _: revm::context::journaled_state::JournalCheckpoint) {
491 unreachable!("'checkpoint_commit' not supported in read-only context")
492 }
493
494 fn checkpoint_revert(&mut self, _: revm::context::journaled_state::JournalCheckpoint) {
495 unreachable!("'checkpoint_revert' not supported in read-only context")
496 }
497
498 fn set_tip1060_storage_credits(&mut self, _enabled: bool) {
499 }
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use crate::{TempoBlockEnv, TempoEvm};
507 use alloy_primitives::{address, uint};
508 use reth_evm::EvmInternals;
509 use revm::{
510 Context, MainContext, context::TxEnv, database::EmptyDB,
511 interpreter::instructions::utility::IntoU256,
512 };
513 use tempo_precompiles::{
514 PATH_USD_ADDRESS,
515 storage::{StorageCtx, evm::EvmPrecompileStorageProvider},
516 test_util::TIP20Setup,
517 tip20::{IRolesAuth::*, ITIP20::*, TIP20Token, slots as tip20_slots},
518 tip403_registry::{ITIP403Registry, TIP403Registry},
519 };
520
521 #[test]
522 fn test_get_fee_token_fee_token_set() -> eyre::Result<()> {
523 let caller = Address::random();
524 let fee_token = Address::random();
525
526 let tx_env = TxEnv {
527 data: Bytes::new(),
528 caller,
529 ..Default::default()
530 };
531 let tx = TempoTxEnv {
532 inner: tx_env,
533 fee_token: Some(fee_token),
534 ..Default::default()
535 };
536
537 let mut db = EmptyDB::default();
538 let token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
539 assert_eq!(token, fee_token);
540 Ok(())
541 }
542
543 #[test]
544 fn test_get_fee_token_fee_manager() -> eyre::Result<()> {
545 let caller = Address::random();
546 let token = Address::random();
547
548 let call = IFeeManager::setUserTokenCall { token };
549 let tx_env = TxEnv {
550 data: call.abi_encode().into(),
551 kind: TxKind::Call(TIP_FEE_MANAGER_ADDRESS),
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, token);
563 Ok(())
564 }
565
566 #[test]
567 fn test_get_fee_token_user_token_set() -> eyre::Result<()> {
568 let caller = Address::random();
569 let user_token = Address::random();
570
571 let mut db = revm::database::CacheDB::new(EmptyDB::default());
573 let user_slot = TipFeeManager::new().user_tokens[caller].slot();
574 db.insert_account_storage(TIP_FEE_MANAGER_ADDRESS, user_slot, user_token.into_u256())
575 .unwrap();
576
577 let result_token =
578 db.get_fee_token(TempoTxEnv::default(), caller, TempoHardfork::Genesis)?;
579 assert_eq!(result_token, user_token);
580 Ok(())
581 }
582
583 #[test]
584 fn test_get_fee_token_tip20() -> eyre::Result<()> {
585 let caller = Address::random();
586 let tip20_token = Address::random();
587
588 let tx_env = TxEnv {
589 data: Bytes::from_static(b"transfer_data"),
590 kind: TxKind::Call(tip20_token),
591 caller,
592 ..Default::default()
593 };
594 let tx = TempoTxEnv {
595 inner: tx_env,
596 ..Default::default()
597 };
598
599 let mut db = EmptyDB::default();
600 let result_token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
601 assert_eq!(result_token, DEFAULT_FEE_TOKEN);
602 Ok(())
603 }
604
605 #[test]
606 fn test_get_fee_token_fallback() -> eyre::Result<()> {
607 let caller = Address::random();
608 let tx_env = TxEnv {
609 caller,
610 ..Default::default()
611 };
612 let tx = TempoTxEnv {
613 inner: tx_env,
614 ..Default::default()
615 };
616
617 let mut db = EmptyDB::default();
618 let result_token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
619 assert_eq!(result_token, DEFAULT_FEE_TOKEN);
621 Ok(())
622 }
623
624 #[test]
625 fn test_get_fee_token_stablecoin_dex() -> eyre::Result<()> {
626 let caller = Address::random();
627 let token_in = DEFAULT_FEE_TOKEN;
629 let token_out = address!("0x20C0000000000000000000000000000000000001");
630
631 let call = IStablecoinDEX::swapExactAmountInCall {
633 tokenIn: token_in,
634 tokenOut: token_out,
635 amountIn: 1000,
636 minAmountOut: 900,
637 };
638
639 let tx_env = TxEnv {
640 data: call.abi_encode().into(),
641 kind: TxKind::Call(STABLECOIN_DEX_ADDRESS),
642 caller,
643 ..Default::default()
644 };
645 let tx = TempoTxEnv {
646 inner: tx_env,
647 ..Default::default()
648 };
649
650 let mut db = EmptyDB::default();
651 let token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
652 assert_eq!(token, token_in);
653
654 let call = IStablecoinDEX::swapExactAmountOutCall {
656 tokenIn: token_in,
657 tokenOut: token_out,
658 amountOut: 900,
659 maxAmountIn: 1000,
660 };
661
662 let tx_env = TxEnv {
663 data: call.abi_encode().into(),
664 kind: TxKind::Call(STABLECOIN_DEX_ADDRESS),
665 caller,
666 ..Default::default()
667 };
668
669 let tx = TempoTxEnv {
670 inner: tx_env,
671 ..Default::default()
672 };
673
674 let token = db.get_fee_token(tx, caller, TempoHardfork::Genesis)?;
675 assert_eq!(token, token_in);
676
677 Ok(())
678 }
679
680 #[test]
681 fn test_read_token_balance_typed_storage() -> eyre::Result<()> {
682 let token_address = PATH_USD_ADDRESS;
683 let account = Address::random();
684 let expected_balance = U256::from(1000u64);
685
686 let mut db = revm::database::CacheDB::new(EmptyDB::default());
688 let balance_slot = TIP20Token::from_address(token_address)?.balances[account].slot();
689 db.insert_account_storage(token_address, balance_slot, expected_balance)?;
690
691 let balance = db.get_token_balance(token_address, account, TempoHardfork::Genesis)?;
693 assert_eq!(balance, expected_balance);
694
695 Ok(())
696 }
697
698 #[test]
699 fn test_is_tip20_fee_inference_call() {
700 for spec in [(TempoHardfork::T6), (TempoHardfork::T7)] {
701 assert!(is_tip20_fee_inference_call(spec, &transferCall::SELECTOR));
703 assert!(is_tip20_fee_inference_call(
704 spec,
705 &transferWithMemoCall::SELECTOR
706 ));
707 assert_eq!(
709 is_tip20_fee_inference_call(spec, &distributeRewardCall::SELECTOR),
710 !spec.is_t7()
711 );
712
713 assert!(!is_tip20_fee_inference_call(spec, &grantRoleCall::SELECTOR));
715 assert!(!is_tip20_fee_inference_call(spec, &mintCall::SELECTOR));
716 assert!(!is_tip20_fee_inference_call(spec, &approveCall::SELECTOR));
717
718 assert!(!is_tip20_fee_inference_call(spec, &[]));
720 assert!(!is_tip20_fee_inference_call(spec, &[0x00, 0x01, 0x02]));
721 }
722 }
723
724 #[test]
725 fn test_is_fee_token_paused() -> eyre::Result<()> {
726 let token_address = PATH_USD_ADDRESS;
727 let mut db = revm::database::CacheDB::new(EmptyDB::default());
728
729 assert!(!db.is_fee_token_paused(TempoHardfork::Genesis, token_address)?);
731
732 db.insert_account_storage(token_address, tip20_slots::PAUSED, U256::from(1))?;
734 assert!(db.is_fee_token_paused(TempoHardfork::Genesis, token_address)?);
735
736 Ok(())
737 }
738
739 #[test]
740 fn test_is_tip20_usd() -> eyre::Result<()> {
741 let fee_token = PATH_USD_ADDRESS;
742
743 let cases: &[(U256, bool, &str)] = &[
745 (
747 uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256),
748 true,
749 "USD",
750 ),
751 (
753 uint!(0x4555520000000000000000000000000000000000000000000000000000000006_U256),
754 false,
755 "EUR",
756 ),
757 (
759 uint!(0x5553000000000000000000000000000000000000000000000000000000000004_U256),
760 false,
761 "US",
762 ),
763 (U256::ZERO, false, "empty"),
765 ];
766
767 for (currency_value, expected, label) in cases {
768 let mut db = revm::database::CacheDB::new(EmptyDB::default());
769 db.insert_account_storage(fee_token, tip20_slots::CURRENCY, *currency_value)?;
770
771 let is_usd = db.is_tip20_usd(TempoHardfork::Genesis, fee_token)?;
772 assert_eq!(is_usd, *expected, "currency '{label}' failed");
773 }
774
775 Ok(())
776 }
777
778 #[test]
779 fn test_tip20_currency_for_error_does_not_read_long_currency() -> eyre::Result<()> {
780 let fee_token = PATH_USD_ADDRESS;
781 let mut db = revm::database::CacheDB::new(EmptyDB::default());
782 let len = 1024usize;
783
784 db.insert_account_storage(fee_token, tip20_slots::CURRENCY, U256::from(len * 2 + 1))?;
785
786 let err = db
787 .ensure_tip20_usd(TempoHardfork::Genesis, fee_token)
788 .expect_err("long non-USD currency returns an EVM error");
789 assert!(matches!(
790 err,
791 EVMError::Transaction(
792 TempoInvalidTransaction::FeeTokenNotUsdCurrency {
793 currency,
794 ..
795 }
796 ) if currency == "<1024 bytes>"
797 ));
798
799 Ok(())
800 }
801
802 #[test]
803 fn test_can_fee_payer_transfer_t1c() -> eyre::Result<()> {
804 let admin = Address::random();
805 let fee_payer = Address::random();
806 let db = revm::database::CacheDB::new(EmptyDB::new());
807 let mut evm = TempoEvm::new(
808 Context::mainnet()
809 .with_db(db)
810 .with_block(TempoBlockEnv::default())
811 .with_cfg(Default::default())
812 .with_tx(Default::default()),
813 (),
814 );
815
816 let policy_id = {
818 let ctx = &mut evm.ctx;
819 let internals =
820 EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
821 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
822 StorageCtx::enter(&mut provider, || -> eyre::Result<u64> {
823 TIP20Setup::path_usd(admin).apply()?;
824 let mut registry = TIP403Registry::new();
825 registry.initialize()?;
826
827 let policy_id = registry.create_policy(
828 admin,
829 ITIP403Registry::createPolicyCall {
830 admin,
831 policyType: ITIP403Registry::PolicyType::WHITELIST,
832 },
833 )?;
834 TIP20Token::from_address(PATH_USD_ADDRESS)?.change_transfer_policy_id(
835 admin,
836 ITIP20::changeTransferPolicyIdCall {
837 newPolicyId: policy_id,
838 },
839 )?;
840 registry.modify_policy_whitelist(
841 admin,
842 ITIP403Registry::modifyPolicyWhitelistCall {
843 policyId: policy_id,
844 account: fee_payer,
845 allowed: true,
846 },
847 )?;
848 Ok(policy_id)
849 })?
850 };
851
852 assert!(evm.ctx.journaled_state.can_fee_payer_transfer(
853 PATH_USD_ADDRESS,
854 fee_payer,
855 TempoHardfork::T1B
856 )?);
857
858 assert!(!evm.ctx.journaled_state.can_fee_payer_transfer(
860 PATH_USD_ADDRESS,
861 fee_payer,
862 TempoHardfork::T1C
863 )?);
864
865 {
867 let ctx = &mut evm.ctx;
868 let internals =
869 EvmInternals::new(&mut ctx.journaled_state, &ctx.block, &ctx.cfg, &ctx.tx);
870 let mut provider = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
871 StorageCtx::enter(&mut provider, || {
872 TIP403Registry::new().modify_policy_whitelist(
873 admin,
874 ITIP403Registry::modifyPolicyWhitelistCall {
875 policyId: policy_id,
876 account: TIP_FEE_MANAGER_ADDRESS,
877 allowed: true,
878 },
879 )
880 })?;
881 }
882
883 assert!(evm.ctx.journaled_state.can_fee_payer_transfer(
884 PATH_USD_ADDRESS,
885 fee_payer,
886 TempoHardfork::T1B
887 )?);
888
889 assert!(evm.ctx.journaled_state.can_fee_payer_transfer(
890 PATH_USD_ADDRESS,
891 fee_payer,
892 TempoHardfork::T1C
893 )?);
894
895 Ok(())
896 }
897}