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