Skip to content

Add support for Celo's stateful transfer precompile #11209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/anvil/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Comment on lines +604 to +606
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, not convinced we want to add a new arg for every network that we can add custom precompiles for.

should we perhaps derive this from the chainid, although an additional celo flag doesnt really hurt

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we only rely on the chainid, new networks won't work with anvil until they have been explicitly supported. And getting PRs with config for short-lived and local networks sounds annoying to me. Adding a flag seems the lesser evil to me.

We can still automatically detect the flags for the most important networks based on the chainid. But if we need a flag anyway, that falls into the "nice to have" category.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also enable the transfer precompile by setting --hardfork cel2, but I'm not sure if adding chain-specific hardforks is better than adding a flag for the chain itself.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't be better to have a --precompiles arg and enable the precompiles based on the passed value? like --precompiles celo or --precompiles arbitrum

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think of it like the --optimism flag. Users want to run an Optimism-like chain because they plan to deploy on Optimism without explicitly thinking about every specific feature. Other features that might also be enabled by the --celo flag in the future could be a higher MaxCodeSize or Fee Abstraction. We didn't have requests to toggle such features separately for our blockchain client, so I would think the situation is similar for anvil. But I don't hold any strong opinions on this.

}

/// Resolves an alias passed as fork-url to the matching url defined in the rpc_endpoints section
Expand Down
15 changes: 15 additions & 0 deletions crates/anvil/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ pub struct NodeConfig {
pub precompile_factory: Option<Arc<dyn PrecompileFactory>>,
/// 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.
Expand Down Expand Up @@ -489,6 +491,7 @@ impl Default for NodeConfig {
memory_limit: None,
precompile_factory: None,
odyssey: false,
celo: false,
silent: false,
cache_path: None,
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1061,6 +1075,7 @@ impl NodeConfig {
..Default::default()
},
self.enable_optimism,
self.celo,
);

let fees = FeeManager::new(
Expand Down
11 changes: 9 additions & 2 deletions crates/anvil/src/eth/backend/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ pub struct Env {
pub evm_env: EvmEnv,
pub tx: OpTransaction<TxEnv>,
pub is_optimism: bool,
pub is_celo: bool,
}

/// Helper container type for [`EvmEnv`] and [`OpTransaction<TxEnv>`].
impl Env {
pub fn new(cfg: CfgEnv, block: BlockEnv, tx: OpTransaction<TxEnv>, 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<TxEnv>,
is_optimism: bool,
is_celo: bool,
) -> Self {
Self { evm_env: EvmEnv { cfg_env: cfg, block_env: block }, tx, is_optimism, is_celo }
}
}

Expand Down
8 changes: 7 additions & 1 deletion crates/anvil/src/eth/backend/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
error::InvalidTransactionError,
pool::transactions::PoolTransaction,
},
evm::celo_precompile::celo_precompile_lookup,
inject_precompiles,
mem::inspector::AnvilInspector,
};
Expand Down Expand Up @@ -120,6 +121,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
Expand Down Expand Up @@ -264,7 +266,7 @@ impl<DB: Db + ?Sized, V: TransactionValidator> 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)
}
}

Expand Down Expand Up @@ -340,6 +342,10 @@ impl<DB: Db + ?Sized, V: TransactionValidator> 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());
}
Expand Down
8 changes: 8 additions & 0 deletions crates/anvil/src/eth/backend/mem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
};

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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(),
};

Expand Down
3 changes: 3 additions & 0 deletions crates/anvil/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -149,6 +151,7 @@ mod tests {
..Default::default()
},
is_optimism: true,
is_celo: false,
};

let mut chain = L1BlockInfo::default();
Expand Down
99 changes: 99 additions & 0 deletions crates/anvil/src/evm/celo_precompile.rs
Original file line number Diff line number Diff line change
@@ -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<DynPrecompile> {
if *address == CELO_TRANSFER_ADDRESS {
Some(DynPrecompile::new_stateful(celo_transfer_precompile))
} else {
None
}
}
Loading