From 460a95824236ca85fb55240d17b0c6bc1c10e9bc Mon Sep 17 00:00:00 2001 From: Karl Bartel Date: Mon, 21 Jul 2025 12:22:32 +0200 Subject: [PATCH 1/3] anvil: Add --celo flag --- crates/anvil/src/cmd.rs | 5 +++++ crates/anvil/src/config.rs | 15 +++++++++++++++ crates/anvil/src/eth/backend/env.rs | 11 +++++++++-- crates/anvil/src/eth/backend/executor.rs | 3 ++- crates/anvil/src/eth/backend/mem/mod.rs | 8 ++++++++ crates/anvil/src/evm.rs | 1 + 6 files changed, 40 insertions(+), 3 deletions(-) diff --git a/crates/anvil/src/cmd.rs b/crates/anvil/src/cmd.rs index cd3879b2ce496..0fd3489507932 100644 --- a/crates/anvil/src/cmd.rs +++ b/crates/anvil/src/cmd.rs @@ -279,6 +279,7 @@ impl NodeArgs { .with_max_persisted_states(self.max_persisted_states) .with_optimism(self.evm.optimism) .with_odyssey(self.evm.odyssey) + .with_celo(self.evm.celo) .with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer) .with_slots_in_an_epoch(self.slots_in_an_epoch) .with_memory_limit(self.evm.memory_limit) @@ -599,6 +600,10 @@ pub struct AnvilEvmArgs { /// Enable Odyssey features #[arg(long, alias = "alphanet")] pub odyssey: bool, + + /// Run a Celo chain + #[arg(long)] + pub celo: bool, } /// Resolves an alias passed as fork-url to the matching url defined in the rpc_endpoints section diff --git a/crates/anvil/src/config.rs b/crates/anvil/src/config.rs index aaaa715099f4c..e4a3faa537025 100644 --- a/crates/anvil/src/config.rs +++ b/crates/anvil/src/config.rs @@ -194,6 +194,8 @@ pub struct NodeConfig { pub precompile_factory: Option>, /// Enable Odyssey features. pub odyssey: bool, + /// Enable Celo features. + pub celo: bool, /// Do not print log messages. pub silent: bool, /// The path where states are cached. @@ -489,6 +491,7 @@ impl Default for NodeConfig { memory_limit: None, precompile_factory: None, odyssey: false, + celo: false, silent: false, cache_path: None, } @@ -1007,6 +1010,17 @@ impl NodeConfig { self } + /// Sets whether to enable Celo support + #[must_use] + pub fn with_celo(mut self, celo: bool) -> Self { + self.celo = celo; + if celo { + // Celo requires Optimism support + self.enable_optimism = true; + } + self + } + /// Makes the node silent to not emit anything on stdout #[must_use] pub fn silent(self) -> Self { @@ -1061,6 +1075,7 @@ impl NodeConfig { ..Default::default() }, self.enable_optimism, + self.celo, ); let fees = FeeManager::new( diff --git a/crates/anvil/src/eth/backend/env.rs b/crates/anvil/src/eth/backend/env.rs index d4b8de797023d..9f083f0f709bb 100644 --- a/crates/anvil/src/eth/backend/env.rs +++ b/crates/anvil/src/eth/backend/env.rs @@ -10,12 +10,19 @@ pub struct Env { pub evm_env: EvmEnv, pub tx: OpTransaction, pub is_optimism: bool, + pub is_celo: bool, } /// Helper container type for [`EvmEnv`] and [`OpTransaction`]. impl Env { - pub fn new(cfg: CfgEnv, block: BlockEnv, tx: OpTransaction, is_optimism: bool) -> Self { - Self { evm_env: EvmEnv { cfg_env: cfg, block_env: block }, tx, is_optimism } + pub fn new( + cfg: CfgEnv, + block: BlockEnv, + tx: OpTransaction, + is_optimism: bool, + is_celo: bool, + ) -> Self { + Self { evm_env: EvmEnv { cfg_env: cfg, block_env: block }, tx, is_optimism, is_celo } } } diff --git a/crates/anvil/src/eth/backend/executor.rs b/crates/anvil/src/eth/backend/executor.rs index a787a985672e8..55a892989dafa 100644 --- a/crates/anvil/src/eth/backend/executor.rs +++ b/crates/anvil/src/eth/backend/executor.rs @@ -120,6 +120,7 @@ pub struct TransactionExecutor<'a, Db: ?Sized, V: TransactionValidator> { pub enable_steps_tracing: bool, pub odyssey: bool, pub optimism: bool, + pub celo: bool, pub print_logs: bool, pub print_traces: bool, /// Recorder used for decoding traces, used together with print_traces @@ -264,7 +265,7 @@ impl TransactionExecutor<'_, DB, V> { tx_env.enveloped_tx = Some(alloy_rlp::encode(&tx.transaction.transaction).into()); } - Env::new(self.cfg_env.clone(), self.block_env.clone(), tx_env, self.optimism) + Env::new(self.cfg_env.clone(), self.block_env.clone(), tx_env, self.optimism, self.celo) } } diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index e9ee34c63f8ce..c22b9e589633d 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -835,6 +835,11 @@ impl Backend { self.env.read().is_optimism } + /// Returns true if Celo features are active + pub fn is_celo(&self) -> bool { + self.env.read().is_celo + } + /// Returns [`BlobParams`] corresponding to the current spec. pub fn blob_params(&self) -> BlobParams { let spec_id = self.env.read().evm_env.cfg_env.spec; @@ -1266,6 +1271,7 @@ impl Backend { precompile_factory: self.precompile_factory.clone(), odyssey: self.odyssey, optimism: self.is_optimism(), + celo: self.is_celo(), blob_params: self.blob_params(), }; @@ -1353,6 +1359,7 @@ impl Backend { odyssey: self.odyssey, precompile_factory: self.precompile_factory.clone(), optimism: self.is_optimism(), + celo: self.is_celo(), blob_params: self.blob_params(), }; let executed_tx = executor.execute(); @@ -2710,6 +2717,7 @@ impl Backend { precompile_factory: self.precompile_factory.clone(), odyssey: self.odyssey, optimism: self.is_optimism(), + celo: self.is_celo(), blob_params: self.blob_params(), }; diff --git a/crates/anvil/src/evm.rs b/crates/anvil/src/evm.rs index 603bcb11b6a48..b44e6f56a1976 100644 --- a/crates/anvil/src/evm.rs +++ b/crates/anvil/src/evm.rs @@ -149,6 +149,7 @@ mod tests { ..Default::default() }, is_optimism: true, + is_celo: false, }; let mut chain = L1BlockInfo::default(); From 08a0fda8fbb512dea0cfe7d3ec1e363eda54e389 Mon Sep 17 00:00:00 2001 From: Karl Bartel Date: Mon, 21 Jul 2025 12:22:32 +0200 Subject: [PATCH 2/3] Implement Celo transfer precompile --- crates/anvil/src/eth/backend/executor.rs | 5 ++ crates/anvil/src/evm.rs | 2 + crates/anvil/src/evm/celo_precompile.rs | 99 ++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 crates/anvil/src/evm/celo_precompile.rs diff --git a/crates/anvil/src/eth/backend/executor.rs b/crates/anvil/src/eth/backend/executor.rs index 55a892989dafa..a960ec78a7419 100644 --- a/crates/anvil/src/eth/backend/executor.rs +++ b/crates/anvil/src/eth/backend/executor.rs @@ -8,6 +8,7 @@ use crate::{ error::InvalidTransactionError, pool::transactions::PoolTransaction, }, + evm::celo_precompile::celo_precompile_lookup, inject_precompiles, mem::inspector::AnvilInspector, }; @@ -341,6 +342,10 @@ impl Iterator for &mut TransactionExec inject_precompiles(&mut evm, vec![(P256VERIFY, P256VERIFY_BASE_GAS_FEE)]); } + if self.celo { + evm.precompiles_mut().set_precompile_lookup(celo_precompile_lookup); + } + if let Some(factory) = &self.precompile_factory { inject_precompiles(&mut evm, factory.precompiles()); } diff --git a/crates/anvil/src/evm.rs b/crates/anvil/src/evm.rs index b44e6f56a1976..50246ddf63f1d 100644 --- a/crates/anvil/src/evm.rs +++ b/crates/anvil/src/evm.rs @@ -9,6 +9,8 @@ use foundry_evm_core::either_evm::EitherEvm; use op_revm::OpContext; use revm::{Inspector, precompile::PrecompileWithAddress}; +pub mod celo_precompile; + /// Object-safe trait that enables injecting extra precompiles when using /// `anvil` as a library. pub trait PrecompileFactory: Send + Sync + Unpin + Debug { diff --git a/crates/anvil/src/evm/celo_precompile.rs b/crates/anvil/src/evm/celo_precompile.rs new file mode 100644 index 0000000000000..fdb2eb5ff32d7 --- /dev/null +++ b/crates/anvil/src/evm/celo_precompile.rs @@ -0,0 +1,99 @@ +//! Celo precompile implementation for token transfers. +//! +//! This module implements the Celo transfer precompile that enables native token transfers from an +//! EVM contract. The precompile is part of Celo's token duality system, allowing transfer of +//! native tokens via ERC20. +//! +//! For more details, see: https://specs.celo.org/token_duality.html#the-transfer-precompile +//! +//! The transfer precompile is deployed at address 0xfd and accepts 96 bytes of input: +//! - from address (32 bytes, left-padded) +//! - to address (32 bytes, left-padded) +//! - value (32 bytes, big-endian U256) + +use alloy_evm::precompiles::{DynPrecompile, PrecompileInput}; +use alloy_primitives::{Address, U256, address}; +use revm::precompile::{PrecompileError, PrecompileOutput, PrecompileResult}; + +pub const CELO_TRANSFER_ADDRESS: Address = address!("0x00000000000000000000000000000000000000fd"); + +/// Gas cost for Celo transfer precompile +const CELO_TRANSFER_GAS_COST: u64 = 9000; + +/// Celo transfer precompile implementation. +/// +/// Uses load_account to modify balances directly, making it compatible with PrecompilesMap. +pub fn celo_transfer_precompile(input: PrecompileInput<'_>) -> PrecompileResult { + // Check minimum gas requirement + if input.gas < CELO_TRANSFER_GAS_COST { + return Err(PrecompileError::OutOfGas); + } + + // Validate input length (must be exactly 96 bytes: 32 + 32 + 32) + if input.data.len() != 96 { + return Err(PrecompileError::Other(format!( + "Invalid input length for Celo transfer precompile: expected 96 bytes, got {}", + input.data.len() + ))); + } + + // Parse input: from (bytes 12-32), to (bytes 44-64), value (bytes 64-96) + let from_bytes = &input.data[12..32]; + let to_bytes = &input.data[44..64]; + let value_bytes = &input.data[64..96]; + + let from_address = Address::from_slice(from_bytes); + let to_address = Address::from_slice(to_bytes); + let value = U256::from_be_slice(value_bytes); + + // Perform the transfer using load_account to modify balances directly + let mut internals = input.internals; + + // Load and check the from account balance first + { + let from_account = match internals.load_account(from_address) { + Ok(account) => account, + Err(e) => { + return Err(PrecompileError::Other(format!("Failed to load from account: {e:?}"))); + } + }; + + // Check if from account has sufficient balance + if from_account.data.info.balance < value { + return Err(PrecompileError::Other("Insufficient balance".into())); + } + + // Deduct balance from the from account + from_account.data.info.balance -= value; + } + + // Load and update the to account + { + let to_account = match internals.load_account(to_address) { + Ok(account) => account, + Err(e) => { + return Err(PrecompileError::Other(format!("Failed to load to account: {e:?}"))); + } + }; + + // Check for overflow in to account + if to_account.data.info.balance.checked_add(value).is_none() { + return Err(PrecompileError::Other("Balance overflow in to account".into())); + } + + // Add balance to the to account + to_account.data.info.balance += value; + } + + // No output data for successful transfer + Ok(PrecompileOutput::new(CELO_TRANSFER_GAS_COST, alloy_primitives::Bytes::new())) +} + +/// Can be used as PrecompilesMap lookup function +pub fn celo_precompile_lookup(address: &Address) -> Option { + if *address == CELO_TRANSFER_ADDRESS { + Some(DynPrecompile::new_stateful(celo_transfer_precompile)) + } else { + None + } +} From 3241e044e0003638ff85bcbed1d7df52ca1381e5 Mon Sep 17 00:00:00 2001 From: Karl Bartel Date: Mon, 11 Aug 2025 08:54:17 +0200 Subject: [PATCH 3/3] Fix link in docs --- crates/anvil/src/evm/celo_precompile.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/anvil/src/evm/celo_precompile.rs b/crates/anvil/src/evm/celo_precompile.rs index fdb2eb5ff32d7..1b93ff32efe32 100644 --- a/crates/anvil/src/evm/celo_precompile.rs +++ b/crates/anvil/src/evm/celo_precompile.rs @@ -4,7 +4,7 @@ //! EVM contract. The precompile is part of Celo's token duality system, allowing transfer of //! native tokens via ERC20. //! -//! For more details, see: https://specs.celo.org/token_duality.html#the-transfer-precompile +//! For more details, see: //! //! The transfer precompile is deployed at address 0xfd and accepts 96 bytes of input: //! - from address (32 bytes, left-padded)