Skip to main content

anvil/eth/backend/
tempo.rs

1//! Tempo precompile and fee token initialization for Anvil.
2//!
3//! When running in Tempo mode, Anvil needs to set up Tempo-specific precompiles
4//! and fee tokens (PathUSD, AlphaUSD, BetaUSD, ThetaUSD) to enable proper
5//! transaction validation.
6//!
7//! This module provides a storage provider adapter for Anvil's `Db` trait and
8//! uses the shared initialization logic from `foundry-evm-core`.
9
10use alloy_primitives::{Address, U256, address};
11use foundry_evm::core::tempo::{
12    ALPHA_USD_ADDRESS, BETA_USD_ADDRESS, PATH_USD_ADDRESS, THETA_USD_ADDRESS,
13    initialize_tempo_genesis_at_hardfork,
14};
15use revm::{
16    context::{BlockEnv, journaled_state::JournalCheckpoint},
17    state::{AccountInfo, Bytecode},
18};
19use std::collections::HashMap;
20use tempo_hardfork::TempoHardfork;
21use tempo_precompiles::{
22    TIP_FEE_MANAGER_ADDRESS,
23    account_keychain::{
24        AccountKeychain,
25        IAccountKeychain::{KeyRestrictions, SignatureType},
26    },
27    error::TempoPrecompileError,
28    storage::{PrecompileStorageProvider, StorageCtx},
29    tip_fee_manager::{IFeeManager, TipFeeManager},
30    tip20::{ITIP20, TIP20Token},
31};
32use tempo_primitives::TempoBlockEnv;
33
34use super::db::Db;
35
36/// Sender address used for genesis initialization.
37const SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
38/// Admin address used for genesis initialization.
39const ADMIN: Address = address!("0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f");
40
41/// Storage provider adapter for Anvil's Db to work with Tempo precompiles.
42pub struct AnvilStorageProvider<'a> {
43    db: &'a mut dyn Db,
44    chain_id: u64,
45    block_env: TempoBlockEnv,
46    gas_used: u64,
47    gas_refunded: i64,
48    reservoir: u64,
49    tip1060_storage_credits_enabled: bool,
50    transient: HashMap<(Address, U256), U256>,
51    hardfork: TempoHardfork,
52}
53
54impl<'a> AnvilStorageProvider<'a> {
55    pub fn new(
56        db: &'a mut dyn Db,
57        chain_id: u64,
58        timestamp: U256,
59        block_number: u64,
60        hardfork: TempoHardfork,
61    ) -> Self {
62        Self {
63            db,
64            chain_id,
65            block_env: TempoBlockEnv {
66                inner: BlockEnv {
67                    timestamp,
68                    number: U256::from(block_number),
69                    ..Default::default()
70                },
71                ..Default::default()
72            },
73            gas_used: 0,
74            gas_refunded: 0,
75            reservoir: 0,
76            tip1060_storage_credits_enabled: hardfork.is_t7(),
77            transient: HashMap::new(),
78            hardfork,
79        }
80    }
81}
82
83impl PrecompileStorageProvider for AnvilStorageProvider<'_> {
84    fn spec(&self) -> TempoHardfork {
85        self.hardfork
86    }
87
88    fn chain_id(&self) -> u64 {
89        self.chain_id
90    }
91
92    fn block_env(&self) -> &TempoBlockEnv {
93        &self.block_env
94    }
95
96    fn set_code(&mut self, address: Address, code: Bytecode) -> Result<(), TempoPrecompileError> {
97        self.db.insert_account(
98            address,
99            AccountInfo {
100                code_hash: code.hash_slow(),
101                code: Some(code),
102                nonce: 1,
103                ..Default::default()
104            },
105        );
106        Ok(())
107    }
108
109    fn with_account_info(
110        &mut self,
111        address: Address,
112        f: &mut dyn FnMut(&AccountInfo),
113    ) -> Result<(), TempoPrecompileError> {
114        use revm::DatabaseRef;
115        if let Some(info) =
116            self.db.basic_ref(address).map_err(|e| TempoPrecompileError::Fatal(e.to_string()))?
117        {
118            f(&info);
119            Ok(())
120        } else {
121            Err(TempoPrecompileError::Fatal(format!("account '{address}' not found")))
122        }
123    }
124
125    fn sstore(
126        &mut self,
127        address: Address,
128        key: U256,
129        value: U256,
130    ) -> Result<(), TempoPrecompileError> {
131        use alloy_primitives::B256;
132        self.db
133            .set_storage_at(address, B256::from(key), B256::from(value))
134            .map_err(|e| TempoPrecompileError::Fatal(e.to_string()))
135    }
136
137    fn sload(&mut self, address: Address, key: U256) -> Result<U256, TempoPrecompileError> {
138        revm::Database::storage(self.db, address, key)
139            .map_err(|e| TempoPrecompileError::Fatal(e.to_string()))
140    }
141
142    fn tstore(
143        &mut self,
144        address: Address,
145        key: U256,
146        value: U256,
147    ) -> Result<(), TempoPrecompileError> {
148        self.transient.insert((address, key), value);
149        Ok(())
150    }
151
152    fn tload(&mut self, address: Address, key: U256) -> Result<U256, TempoPrecompileError> {
153        Ok(self.transient.get(&(address, key)).copied().unwrap_or(U256::ZERO))
154    }
155
156    fn emit_event(
157        &mut self,
158        _address: Address,
159        _event: alloy_primitives::LogData,
160    ) -> Result<(), TempoPrecompileError> {
161        Ok(())
162    }
163
164    fn deduct_gas(&mut self, gas: u64) -> Result<(), TempoPrecompileError> {
165        self.gas_used = self.gas_used.saturating_add(gas);
166        Ok(())
167    }
168
169    fn gas_used(&self) -> u64 {
170        self.gas_used
171    }
172
173    fn state_gas_used(&self) -> u64 {
174        0
175    }
176
177    fn gas_limit(&self) -> u64 {
178        u64::MAX
179    }
180
181    fn gas_refunded(&self) -> i64 {
182        self.gas_refunded
183    }
184
185    fn reservoir(&self) -> u64 {
186        self.reservoir
187    }
188
189    fn refund_gas(&mut self, gas: i64) {
190        self.gas_refunded = self.gas_refunded.saturating_add(gas);
191    }
192
193    fn is_static(&self) -> bool {
194        false
195    }
196
197    fn checkpoint(&mut self) -> JournalCheckpoint {
198        JournalCheckpoint { log_i: 0, journal_i: 0, selfdestructed_i: 0 }
199    }
200
201    fn checkpoint_commit(&mut self, _checkpoint: JournalCheckpoint) {}
202
203    fn checkpoint_revert(&mut self, _checkpoint: JournalCheckpoint) {}
204
205    fn amsterdam_eip8037_enabled(&self) -> bool {
206        false
207    }
208
209    fn set_tip1060_storage_credits(&mut self, enabled: bool) {
210        self.tip1060_storage_credits_enabled = enabled && self.hardfork.is_t7();
211    }
212}
213
214/// Initialize Tempo precompiles and fee tokens for Anvil.
215///
216/// This sets up the same precompiles and tokens as Tempo's genesis, enabling
217/// proper fee token validation for transactions.
218///
219/// Additionally, mints fee tokens to the provided test accounts so they can
220/// send transactions in Tempo mode.
221pub fn initialize_tempo_precompiles(
222    db: &mut dyn Db,
223    chain_id: u64,
224    timestamp: u64,
225    test_accounts: &[Address],
226    hardfork: TempoHardfork,
227) -> Result<(), TempoPrecompileError> {
228    let timestamp = U256::from(timestamp);
229
230    let mut storage = AnvilStorageProvider::new(db, chain_id, timestamp, 0, hardfork);
231
232    // Initialize base Tempo genesis (precompiles and tokens)
233    initialize_tempo_genesis_at_hardfork(&mut storage, ADMIN, SENDER, hardfork)?;
234
235    // Mint fee tokens to test accounts
236    // u64::MAX per account - safe since u128::MAX can hold ~18 quintillion u64::MAX values
237    let mint_amount = U256::from(u64::MAX);
238    let tokens = [PATH_USD_ADDRESS, ALPHA_USD_ADDRESS, BETA_USD_ADDRESS, THETA_USD_ADDRESS];
239
240    StorageCtx::enter(&mut storage, || -> Result<(), TempoPrecompileError> {
241        // Mint fee tokens to test accounts
242        for &token_address in &tokens {
243            let mut token = TIP20Token::from_address(token_address)?;
244            for &account in test_accounts {
245                token.mint(ADMIN, ITIP20::mintCall { to: account, amount: mint_amount })?;
246            }
247        }
248
249        // Register secp256k1 keys for test accounts in the AccountKeychain
250        // This allows them to sign Tempo transactions using their private keys.
251        // The key ID is the account address itself (standard for secp256k1 keys).
252        let mut keychain = AccountKeychain::new();
253        for &account in test_accounts {
254            // Seed tx_origin so ensure_admin_caller passes on T2+ (requires
255            // tx_origin != zero && tx_origin == msg_sender).
256            keychain.set_tx_origin(account)?;
257            keychain.authorize_key(
258                account, // msg_sender (root account authorizes its own key)
259                account, // key ID = account address for secp256k1
260                SignatureType::Secp256k1,
261                KeyRestrictions {
262                    expiry: u64::MAX,     // never expires
263                    enforceLimits: false, // no spending limits
264                    limits: vec![],
265                    allowAnyCalls: true,
266                    allowedCalls: vec![],
267                },
268                None,
269            )?;
270        }
271
272        // Initialize TipFeeManager and set default fee tokens for test accounts
273        // Alice (0) -> AlphaUSD, Bob (1) -> BetaUSD, Charlie (2) -> ThetaUSD, others -> PathUSD
274        let mut fee_manager = TipFeeManager::new();
275        fee_manager.initialize()?;
276
277        for (i, &account) in test_accounts.iter().enumerate() {
278            let fee_token = match i {
279                0 => ALPHA_USD_ADDRESS, // Alice
280                1 => BETA_USD_ADDRESS,  // Bob
281                2 => THETA_USD_ADDRESS, // Charlie
282                _ => PATH_USD_ADDRESS,  // Everyone else
283            };
284            fee_manager
285                .set_user_token(account, IFeeManager::setUserTokenCall { token: fee_token })?;
286        }
287
288        // Mint fee tokens to the FeeManager contract for liquidity operations
289        for &token_address in &tokens {
290            let mut token = TIP20Token::from_address(token_address)?;
291            token.mint(
292                ADMIN,
293                ITIP20::mintCall { to: TIP_FEE_MANAGER_ADDRESS, amount: mint_amount },
294            )?;
295        }
296
297        // Mint pairwise FeeAMM liquidity for all fee token pairs (both directions)
298        // This enables EIP-1559/legacy transactions by allowing fee swaps between tokens
299        // Liquidity amount: 10^10 tokens (matching Tempo genesis)
300        let liquidity_amount = U256::from(10u64.pow(10));
301
302        // Create bidirectional liquidity pools between all fee tokens
303        // Pools are directional: user_token -> validator_token
304        for &user_token in &tokens {
305            for &validator_token in &tokens {
306                if user_token != validator_token {
307                    fee_manager.mint(
308                        ADMIN,
309                        user_token,
310                        validator_token,
311                        liquidity_amount,
312                        ADMIN,
313                    )?;
314                }
315            }
316        }
317
318        Ok(())
319    })?;
320
321    Ok(())
322}