1use crate::TempoTxEnv;
2use alloy_consensus::transaction::{Either, Recovered};
3use alloy_primitives::{Address, Bytes, TxKind, U256, uint};
4use alloy_sol_types::SolCall;
5use revm::{
6 Database, context::JournalTr, interpreter::instructions::utility::IntoAddress,
7 state::AccountInfo,
8};
9use tempo_chainspec::hardfork::TempoHardfork;
10use tempo_contracts::precompiles::{
11 DEFAULT_FEE_TOKEN_POST_ALLEGRETTO, DEFAULT_FEE_TOKEN_PRE_ALLEGRETTO, IFeeManager,
12 IStablecoinExchange, ITIP403Registry, PATH_USD_ADDRESS, STABLECOIN_EXCHANGE_ADDRESS,
13};
14use tempo_precompiles::{
15 TIP_FEE_MANAGER_ADDRESS, TIP403_REGISTRY_ADDRESS,
16 storage::{self, Storable, StorableType, double_mapping_slot, slots::mapping_slot},
17 tip_fee_manager,
18 tip20::{self, is_tip20_prefix},
19 tip403_registry,
20};
21use tempo_primitives::TempoTxEnvelope;
22
23const USD_CURRENCY_SLOT_VALUE: U256 =
25 uint!(0x5553440000000000000000000000000000000000000000000000000000000006_U256);
26
27#[auto_impl::auto_impl(&)]
29pub trait TempoTx {
30 fn fee_token(&self) -> Option<Address>;
32
33 fn is_aa(&self) -> bool;
35
36 fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)>;
38
39 fn caller(&self) -> Address;
41}
42
43impl TempoTx for TempoTxEnv {
44 fn fee_token(&self) -> Option<Address> {
45 self.fee_token
46 }
47
48 fn is_aa(&self) -> bool {
49 self.tempo_tx_env.is_some()
50 }
51
52 fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)> {
53 if let Some(aa) = self.tempo_tx_env.as_ref() {
54 Either::Left(aa.aa_calls.iter().map(|call| (call.to, &call.input)))
55 } else {
56 Either::Right(core::iter::once((self.inner.kind, &self.inner.data)))
57 }
58 }
59
60 fn caller(&self) -> Address {
61 self.inner.caller
62 }
63}
64
65impl TempoTx for Recovered<TempoTxEnvelope> {
66 fn fee_token(&self) -> Option<Address> {
67 self.inner().fee_token()
68 }
69
70 fn is_aa(&self) -> bool {
71 self.inner().is_aa()
72 }
73
74 fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)> {
75 self.inner().calls()
76 }
77
78 fn caller(&self) -> Address {
79 self.signer()
80 }
81}
82
83pub trait TempoStateAccess<T> {
89 type Error;
90
91 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error>;
93
94 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error>;
96
97 fn get_fee_token(
99 &mut self,
100 tx: impl TempoTx,
101 validator: Address,
102 fee_payer: Address,
103 spec: TempoHardfork,
104 ) -> Result<Address, Self::Error> {
105 if let Some(fee_token) = tx.fee_token() {
107 return Ok(fee_token);
108 }
109
110 if !tx.is_aa()
114 && fee_payer == tx.caller()
115 && let Some((kind, input)) = tx.calls().next()
116 && kind.to() == Some(&TIP_FEE_MANAGER_ADDRESS)
117 && let Ok(call) = IFeeManager::setUserTokenCall::abi_decode(input)
118 {
119 return Ok(call.token);
120 }
121
122 let user_slot = mapping_slot(fee_payer, tip_fee_manager::slots::USER_TOKENS);
123 self.basic(TIP_FEE_MANAGER_ADDRESS)?;
125 let stored_user_token = self
126 .sload(TIP_FEE_MANAGER_ADDRESS, user_slot)?
127 .into_address();
128
129 if !stored_user_token.is_zero() {
130 return Ok(stored_user_token);
131 }
132
133 if let Some(to) = tx.calls().next().and_then(|(kind, _)| kind.to().copied())
135 && tx.calls().all(|(kind, _)| kind.to() == Some(&to))
136 && self.is_valid_fee_token(to, spec)?
137 {
138 return Ok(to);
139 }
140
141 if spec.is_allegretto() {
145 let mut calls = tx.calls();
146 if let Some((kind, input)) = calls.next()
147 && kind.to() == Some(&STABLECOIN_EXCHANGE_ADDRESS)
148 && (!tx.is_aa() || calls.next().is_none())
149 {
150 if let Ok(call) = IStablecoinExchange::swapExactAmountInCall::abi_decode(input)
151 && self.is_valid_fee_token(call.tokenIn, spec)?
152 {
153 return Ok(call.tokenIn);
154 } else if let Ok(call) =
155 IStablecoinExchange::swapExactAmountOutCall::abi_decode(input)
156 && self.is_valid_fee_token(call.tokenIn, spec)?
157 {
158 return Ok(call.tokenIn);
159 }
160 }
161 }
162
163 if spec.is_allegretto() {
165 Ok(DEFAULT_FEE_TOKEN_POST_ALLEGRETTO)
166 } else {
167 let validator_slot = mapping_slot(validator, tip_fee_manager::slots::VALIDATOR_TOKENS);
170 let validator_fee_token = self
171 .sload(TIP_FEE_MANAGER_ADDRESS, validator_slot)?
172 .into_address();
173
174 if validator_fee_token.is_zero() {
175 Ok(DEFAULT_FEE_TOKEN_PRE_ALLEGRETTO)
176 } else {
177 Ok(validator_fee_token)
178 }
179 }
180 }
181
182 fn is_valid_fee_token(
184 &mut self,
185 fee_token: Address,
186 spec: TempoHardfork,
187 ) -> Result<bool, Self::Error> {
188 if !is_tip20_prefix(fee_token) {
190 return Ok(false);
191 }
192
193 if !spec.is_allegretto() && fee_token == PATH_USD_ADDRESS {
195 return Ok(false);
196 }
197
198 self.basic(fee_token)?;
201 Ok(self.sload(fee_token, tip20::slots::CURRENCY)? == USD_CURRENCY_SLOT_VALUE)
202 }
203
204 fn can_fee_payer_transfer(
206 &mut self,
207 fee_token: Address,
208 fee_payer: Address,
209 ) -> Result<bool, Self::Error> {
210 if !is_tip20_prefix(fee_token) {
212 return Ok(false);
213 }
214
215 let Ok(transfer_policy_id) = storage::packing::extract_packed_value::<1, u64>(
217 self.sload(fee_token, tip20::slots::TRANSFER_POLICY_ID)?,
218 tip20::slots::TRANSFER_POLICY_ID_OFFSET,
219 <u64 as StorableType>::BYTES,
220 ) else {
221 tracing::warn!(%fee_token, "failed to extract transfer_policy_id from packed value");
223 return Ok(false);
224 };
225
226 let auth = {
228 if transfer_policy_id < 2 {
230 return Ok(transfer_policy_id == 1);
233 }
234
235 let policy_data_word = self.sload(
236 TIP403_REGISTRY_ADDRESS,
237 mapping_slot(
238 transfer_policy_id.to_be_bytes(),
239 tip403_registry::slots::POLICY_DATA,
240 ),
241 )?;
242 let Ok(data) = tip403_registry::PolicyData::from_evm_words([policy_data_word]) else {
243 tracing::warn!(
244 transfer_policy_id,
245 "failed to parse PolicyData from storage"
246 );
247 return Ok(false);
248 };
249 let Ok(policy_type) = data.policy_type.try_into() else {
250 tracing::warn!(transfer_policy_id, policy_type = ?data.policy_type, "invalid policy type");
251 return Ok(false);
252 };
253
254 let is_in_set = self
255 .sload(
256 TIP403_REGISTRY_ADDRESS,
257 double_mapping_slot(
258 transfer_policy_id.to_be_bytes(),
259 fee_payer,
260 tip403_registry::slots::POLICY_SET,
261 ),
262 )?
263 .to::<bool>();
264
265 match policy_type {
266 ITIP403Registry::PolicyType::WHITELIST => is_in_set,
267 ITIP403Registry::PolicyType::BLACKLIST => !is_in_set,
268 ITIP403Registry::PolicyType::__Invalid => false,
269 }
270 };
271
272 Ok(auth)
273 }
274
275 fn get_token_balance(&mut self, token: Address, account: Address) -> Result<U256, Self::Error> {
277 let balance_slot = mapping_slot(account, tip20::slots::BALANCES);
279 self.basic(token)?;
281 self.sload(token, balance_slot)
282 }
283}
284
285impl<DB: Database> TempoStateAccess<()> for DB {
286 type Error = DB::Error;
287
288 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error> {
289 self.basic(address).map(Option::unwrap_or_default)
290 }
291
292 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error> {
293 self.storage(address, key)
294 }
295}
296
297impl<T: JournalTr> TempoStateAccess<((), ())> for T {
298 type Error = <T::Database as Database>::Error;
299
300 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error> {
301 self.load_account(address).map(|s| s.data.info.clone())
302 }
303
304 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error> {
305 JournalTr::sload(self, address, key).map(|s| s.data)
306 }
307}
308
309#[cfg(feature = "reth")]
310impl<T: reth_storage_api::StateProvider> TempoStateAccess<((), (), ())> for T {
311 type Error = reth_evm::execute::ProviderError;
312
313 fn basic(&mut self, address: Address) -> Result<AccountInfo, Self::Error> {
314 self.basic_account(&address)
315 .map(Option::unwrap_or_default)
316 .map(Into::into)
317 }
318
319 fn sload(&mut self, address: Address, key: U256) -> Result<U256, Self::Error> {
320 self.storage(address, key.into())
321 .map(Option::unwrap_or_default)
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use revm::{context::TxEnv, database::EmptyDB, interpreter::instructions::utility::IntoU256};
329
330 #[test]
331 fn test_get_fee_token_fee_token_set() -> eyre::Result<()> {
332 let caller = Address::random();
333 let fee_token = Address::random();
334
335 let tx_env = TxEnv {
336 data: Bytes::new(),
337 caller,
338 ..Default::default()
339 };
340 let tx = TempoTxEnv {
341 inner: tx_env,
342 fee_token: Some(fee_token),
343 ..Default::default()
344 };
345
346 let mut db = EmptyDB::default();
347 let token = db.get_fee_token(tx, Address::ZERO, caller, TempoHardfork::default())?;
348 assert_eq!(token, fee_token);
349 Ok(())
350 }
351
352 #[test]
353 fn test_get_fee_token_fee_manager() -> eyre::Result<()> {
354 let caller = Address::random();
355 let token = Address::random();
356
357 let call = IFeeManager::setUserTokenCall { token };
358 let tx_env = TxEnv {
359 data: call.abi_encode().into(),
360 kind: TxKind::Call(TIP_FEE_MANAGER_ADDRESS),
361 caller,
362 ..Default::default()
363 };
364 let tx = TempoTxEnv {
365 inner: tx_env,
366 ..Default::default()
367 };
368
369 let mut db = EmptyDB::default();
370 let result_token =
371 db.get_fee_token(tx, Address::ZERO, caller, TempoHardfork::Allegretto)?;
372 assert_eq!(result_token, token);
373 Ok(())
374 }
375
376 #[test]
377 fn test_get_fee_token_user_token_set() -> eyre::Result<()> {
378 let caller = Address::random();
379 let user_token = Address::random();
380
381 let mut db = revm::database::CacheDB::new(EmptyDB::default());
383 let user_slot = mapping_slot(caller, tip_fee_manager::slots::USER_TOKENS);
384 db.insert_account_storage(TIP_FEE_MANAGER_ADDRESS, user_slot, user_token.into_u256())
385 .unwrap();
386
387 let result_token = db.get_fee_token(
388 TempoTxEnv::default(),
389 Address::ZERO,
390 caller,
391 TempoHardfork::default(),
392 )?;
393 assert_eq!(result_token, user_token);
394 Ok(())
395 }
396
397 #[test]
398 fn test_get_fee_token_tip20() -> eyre::Result<()> {
399 let caller = Address::random();
400 let tip20_token = Address::random();
401
402 let tx_env = TxEnv {
403 data: Bytes::from_static(b"transfer_data"),
404 kind: TxKind::Call(tip20_token),
405 caller,
406 ..Default::default()
407 };
408 let tx = TempoTxEnv {
409 inner: tx_env,
410 ..Default::default()
411 };
412
413 let mut db = EmptyDB::default();
414 let result_token =
415 db.get_fee_token(tx, Address::ZERO, caller, TempoHardfork::Allegretto)?;
416 assert_eq!(result_token, DEFAULT_FEE_TOKEN_POST_ALLEGRETTO);
417 Ok(())
418 }
419
420 #[test]
421 fn test_get_fee_token_fallback_pre_allegretto() -> eyre::Result<()> {
422 let caller = Address::random();
423 let validator = Address::random();
424 let validator_token = Address::random();
425
426 let tx_env = TxEnv {
427 caller,
428 ..Default::default()
429 };
430 let tx = TempoTxEnv {
431 inner: tx_env,
432 ..Default::default()
433 };
434
435 let mut db = revm::database::CacheDB::new(EmptyDB::default());
437 let validator_slot = mapping_slot(validator, tip_fee_manager::slots::VALIDATOR_TOKENS);
438 db.insert_account_storage(
439 TIP_FEE_MANAGER_ADDRESS,
440 validator_slot,
441 validator_token.into_u256(),
442 )
443 .unwrap();
444
445 let result_token =
446 db.get_fee_token(tx.clone(), validator, caller, TempoHardfork::Adagio)?;
447 assert_eq!(result_token, validator_token);
448
449 let mut db2 = EmptyDB::default();
451 let result_token2 = db2.get_fee_token(tx, Address::ZERO, caller, TempoHardfork::Adagio)?;
452 assert_eq!(result_token2, DEFAULT_FEE_TOKEN_PRE_ALLEGRETTO);
453
454 Ok(())
455 }
456
457 #[test]
458 fn test_get_fee_token_fallback_post_allegretto() -> eyre::Result<()> {
459 let caller = Address::random();
460 let tx_env = TxEnv {
461 caller,
462 ..Default::default()
463 };
464 let tx = TempoTxEnv {
465 inner: tx_env,
466 ..Default::default()
467 };
468
469 let mut db = EmptyDB::default();
470 let result_token =
471 db.get_fee_token(tx, Address::ZERO, caller, TempoHardfork::Allegretto)?;
472 assert_eq!(result_token, DEFAULT_FEE_TOKEN_POST_ALLEGRETTO);
474 Ok(())
475 }
476
477 #[test]
478 fn test_get_fee_token_stablecoin_exchange_post_allegretto() -> eyre::Result<()> {
479 let caller = Address::random();
480 let token_in = DEFAULT_FEE_TOKEN_POST_ALLEGRETTO;
482 let token_out = DEFAULT_FEE_TOKEN_PRE_ALLEGRETTO;
483
484 let call = IStablecoinExchange::swapExactAmountInCall {
486 tokenIn: token_in,
487 tokenOut: token_out,
488 amountIn: 1000,
489 minAmountOut: 900,
490 };
491
492 let tx_env = TxEnv {
493 data: call.abi_encode().into(),
494 kind: TxKind::Call(STABLECOIN_EXCHANGE_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 token = db.get_fee_token(tx, Address::ZERO, caller, TempoHardfork::Allegretto)?;
506 assert_eq!(token, token_in);
507
508 let call = IStablecoinExchange::swapExactAmountOutCall {
510 tokenIn: token_in,
511 tokenOut: token_out,
512 amountOut: 900,
513 maxAmountIn: 1000,
514 };
515
516 let tx_env = TxEnv {
517 data: call.abi_encode().into(),
518 kind: TxKind::Call(STABLECOIN_EXCHANGE_ADDRESS),
519 caller,
520 ..Default::default()
521 };
522
523 let tx = TempoTxEnv {
524 inner: tx_env,
525 ..Default::default()
526 };
527
528 let token = db.get_fee_token(tx, Address::ZERO, caller, TempoHardfork::Allegretto)?;
529 assert_eq!(token, token_in);
530
531 Ok(())
532 }
533}