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