Skip to content

Commit 81b65df

Browse files
mattssekarlb
andauthored
feat(anvil): support celo transfer precompile (#11491)
* anvil: Add --celo flag * Implement Celo transfer precompile * Fix link in docs * install celo precompile for call evn --------- Co-authored-by: Karl Bartel <[email protected]>
1 parent 0c7e1b8 commit 81b65df

File tree

7 files changed

+153
-3
lines changed

7 files changed

+153
-3
lines changed

crates/anvil/src/cmd.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ impl NodeArgs {
279279
.with_max_persisted_states(self.max_persisted_states)
280280
.with_optimism(self.evm.optimism)
281281
.with_odyssey(self.evm.odyssey)
282+
.with_celo(self.evm.celo)
282283
.with_disable_default_create2_deployer(self.evm.disable_default_create2_deployer)
283284
.with_disable_pool_balance_checks(self.evm.disable_pool_balance_checks)
284285
.with_slots_in_an_epoch(self.slots_in_an_epoch)
@@ -604,6 +605,10 @@ pub struct AnvilEvmArgs {
604605
/// Enable Odyssey features
605606
#[arg(long, alias = "alphanet")]
606607
pub odyssey: bool,
608+
609+
/// Run a Celo chain
610+
#[arg(long)]
611+
pub celo: bool,
607612
}
608613

609614
/// Resolves an alias passed as fork-url to the matching url defined in the rpc_endpoints section

crates/anvil/src/config.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ pub struct NodeConfig {
196196
pub precompile_factory: Option<Arc<dyn PrecompileFactory>>,
197197
/// Enable Odyssey features.
198198
pub odyssey: bool,
199+
/// Enable Celo features.
200+
pub celo: bool,
199201
/// Do not print log messages.
200202
pub silent: bool,
201203
/// The path where states are cached.
@@ -492,6 +494,7 @@ impl Default for NodeConfig {
492494
memory_limit: None,
493495
precompile_factory: None,
494496
odyssey: false,
497+
celo: false,
495498
silent: false,
496499
cache_path: None,
497500
}
@@ -1017,6 +1020,17 @@ impl NodeConfig {
10171020
self
10181021
}
10191022

1023+
/// Sets whether to enable Celo support
1024+
#[must_use]
1025+
pub fn with_celo(mut self, celo: bool) -> Self {
1026+
self.celo = celo;
1027+
if celo {
1028+
// Celo requires Optimism support
1029+
self.enable_optimism = true;
1030+
}
1031+
self
1032+
}
1033+
10201034
/// Makes the node silent to not emit anything on stdout
10211035
#[must_use]
10221036
pub fn silent(self) -> Self {
@@ -1071,6 +1085,7 @@ impl NodeConfig {
10711085
..Default::default()
10721086
},
10731087
self.enable_optimism,
1088+
self.celo,
10741089
);
10751090

10761091
let fees = FeeManager::new(

crates/anvil/src/eth/backend/env.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,19 @@ pub struct Env {
1010
pub evm_env: EvmEnv,
1111
pub tx: OpTransaction<TxEnv>,
1212
pub is_optimism: bool,
13+
pub is_celo: bool,
1314
}
1415

1516
/// Helper container type for [`EvmEnv`] and [`OpTransaction<TxEnv>`].
1617
impl Env {
17-
pub fn new(cfg: CfgEnv, block: BlockEnv, tx: OpTransaction<TxEnv>, is_optimism: bool) -> Self {
18-
Self { evm_env: EvmEnv { cfg_env: cfg, block_env: block }, tx, is_optimism }
18+
pub fn new(
19+
cfg: CfgEnv,
20+
block: BlockEnv,
21+
tx: OpTransaction<TxEnv>,
22+
is_optimism: bool,
23+
is_celo: bool,
24+
) -> Self {
25+
Self { evm_env: EvmEnv { cfg_env: cfg, block_env: block }, tx, is_optimism, is_celo }
1926
}
2027
}
2128

crates/anvil/src/eth/backend/executor.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::{
1111
error::InvalidTransactionError,
1212
pool::transactions::PoolTransaction,
1313
},
14+
evm::celo_precompile,
1415
inject_precompiles,
1516
mem::inspector::AnvilInspector,
1617
};
@@ -127,6 +128,7 @@ pub struct TransactionExecutor<'a, Db: ?Sized, V: TransactionValidator> {
127128
pub enable_steps_tracing: bool,
128129
pub odyssey: bool,
129130
pub optimism: bool,
131+
pub celo: bool,
130132
pub print_logs: bool,
131133
pub print_traces: bool,
132134
/// Recorder used for decoding traces, used together with print_traces
@@ -272,7 +274,7 @@ impl<DB: Db + ?Sized, V: TransactionValidator> TransactionExecutor<'_, DB, V> {
272274
tx_env.enveloped_tx = Some(alloy_rlp::encode(&tx.transaction.transaction).into());
273275
}
274276

275-
Env::new(self.cfg_env.clone(), self.block_env.clone(), tx_env, self.optimism)
277+
Env::new(self.cfg_env.clone(), self.block_env.clone(), tx_env, self.optimism, self.celo)
276278
}
277279
}
278280

@@ -348,6 +350,13 @@ impl<DB: Db + ?Sized, V: TransactionValidator> Iterator for &mut TransactionExec
348350
inject_precompiles(&mut evm, vec![(P256VERIFY, P256VERIFY_BASE_GAS_FEE)]);
349351
}
350352

353+
if self.celo {
354+
evm.precompiles_mut()
355+
.apply_precompile(&celo_precompile::CELO_TRANSFER_ADDRESS, move |_| {
356+
Some(celo_precompile::precompile())
357+
});
358+
}
359+
351360
if let Some(factory) = &self.precompile_factory {
352361
inject_precompiles(&mut evm, factory.precompiles());
353362
}

crates/anvil/src/eth/backend/mem/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use crate::{
2727
pool::transactions::PoolTransaction,
2828
sign::build_typed_transaction,
2929
},
30+
evm::celo_precompile,
3031
inject_precompiles,
3132
mem::{
3233
inspector::AnvilInspector,
@@ -837,6 +838,11 @@ impl Backend {
837838
self.env.read().is_optimism
838839
}
839840

841+
/// Returns true if Celo features are active
842+
pub fn is_celo(&self) -> bool {
843+
self.env.read().is_celo
844+
}
845+
840846
/// Returns [`BlobParams`] corresponding to the current spec.
841847
pub fn blob_params(&self) -> BlobParams {
842848
let spec_id = self.env.read().evm_env.cfg_env.spec;
@@ -1177,6 +1183,13 @@ impl Backend {
11771183
inject_precompiles(&mut evm, vec![(P256VERIFY, P256VERIFY_BASE_GAS_FEE)]);
11781184
}
11791185

1186+
if self.is_celo() {
1187+
evm.precompiles_mut()
1188+
.apply_precompile(&celo_precompile::CELO_TRANSFER_ADDRESS, move |_| {
1189+
Some(celo_precompile::precompile())
1190+
});
1191+
}
1192+
11801193
if let Some(factory) = &self.precompile_factory {
11811194
inject_precompiles(&mut evm, factory.precompiles());
11821195
}
@@ -1279,6 +1292,7 @@ impl Backend {
12791292
precompile_factory: self.precompile_factory.clone(),
12801293
odyssey: self.odyssey,
12811294
optimism: self.is_optimism(),
1295+
celo: self.is_celo(),
12821296
blob_params: self.blob_params(),
12831297
cheats: self.cheats().clone(),
12841298
};
@@ -1369,6 +1383,7 @@ impl Backend {
13691383
odyssey: self.odyssey,
13701384
precompile_factory: self.precompile_factory.clone(),
13711385
optimism: self.is_optimism(),
1386+
celo: self.is_celo(),
13721387
blob_params: self.blob_params(),
13731388
cheats: self.cheats().clone(),
13741389
};
@@ -2723,6 +2738,7 @@ impl Backend {
27232738
precompile_factory: self.precompile_factory.clone(),
27242739
odyssey: self.odyssey,
27252740
optimism: self.is_optimism(),
2741+
celo: self.is_celo(),
27262742
blob_params: self.blob_params(),
27272743
cheats: self.cheats().clone(),
27282744
};

crates/anvil/src/evm.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use op_revm::OpContext;
99
use revm::{Inspector, precompile::Precompile};
1010
use std::fmt::Debug;
1111

12+
pub mod celo_precompile;
13+
1214
/// Object-safe trait that enables injecting extra precompiles when using
1315
/// `anvil` as a library.
1416
pub trait PrecompileFactory: Send + Sync + Unpin + Debug {
@@ -152,6 +154,7 @@ mod tests {
152154
..Default::default()
153155
},
154156
is_optimism: true,
157+
is_celo: false,
155158
};
156159

157160
let mut chain = L1BlockInfo::default();
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! Celo precompile implementation for token transfers.
2+
//!
3+
//! This module implements the Celo transfer precompile that enables native token transfers from an
4+
//! EVM contract. The precompile is part of Celo's token duality system, allowing transfer of
5+
//! native tokens via ERC20.
6+
//!
7+
//! For more details, see: <https://specs.celo.org/token_duality.html#the-transfer-precompile>
8+
//!
9+
//! The transfer precompile is deployed at address 0xfd and accepts 96 bytes of input:
10+
//! - from address (32 bytes, left-padded)
11+
//! - to address (32 bytes, left-padded)
12+
//! - value (32 bytes, big-endian U256)
13+
14+
use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
15+
use alloy_primitives::{Address, U256, address};
16+
use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
17+
18+
pub const CELO_TRANSFER_ADDRESS: Address = address!("0x00000000000000000000000000000000000000fd");
19+
20+
/// Gas cost for Celo transfer precompile
21+
const CELO_TRANSFER_GAS_COST: u64 = 9000;
22+
23+
/// Returns the celo native transfer
24+
pub fn precompile() -> DynPrecompile {
25+
DynPrecompile::new_stateful(PrecompileId::custom("celo transfer"), celo_transfer_precompile)
26+
}
27+
28+
/// Celo transfer precompile implementation.
29+
///
30+
/// Uses load_account to modify balances directly, making it compatible with PrecompilesMap.
31+
pub fn celo_transfer_precompile(input: PrecompileInput<'_>) -> PrecompileResult {
32+
// Check minimum gas requirement
33+
if input.gas < CELO_TRANSFER_GAS_COST {
34+
return Err(PrecompileError::OutOfGas);
35+
}
36+
37+
// Validate input length (must be exactly 96 bytes: 32 + 32 + 32)
38+
if input.data.len() != 96 {
39+
return Err(PrecompileError::Other(format!(
40+
"Invalid input length for Celo transfer precompile: expected 96 bytes, got {}",
41+
input.data.len()
42+
)));
43+
}
44+
45+
// Parse input: from (bytes 12-32), to (bytes 44-64), value (bytes 64-96)
46+
let from_bytes = &input.data[12..32];
47+
let to_bytes = &input.data[44..64];
48+
let value_bytes = &input.data[64..96];
49+
50+
let from_address = Address::from_slice(from_bytes);
51+
let to_address = Address::from_slice(to_bytes);
52+
let value = U256::from_be_slice(value_bytes);
53+
54+
// Perform the transfer using load_account to modify balances directly
55+
let mut internals = input.internals;
56+
57+
// Load and check the from account balance first
58+
{
59+
let from_account = match internals.load_account(from_address) {
60+
Ok(account) => account,
61+
Err(e) => {
62+
return Err(PrecompileError::Other(format!("Failed to load from account: {e:?}")));
63+
}
64+
};
65+
66+
// Check if from account has sufficient balance
67+
if from_account.data.info.balance < value {
68+
return Err(PrecompileError::Other("Insufficient balance".into()));
69+
}
70+
71+
// Deduct balance from the from account
72+
from_account.data.info.balance -= value;
73+
}
74+
75+
// Load and update the to account
76+
{
77+
let to_account = match internals.load_account(to_address) {
78+
Ok(account) => account,
79+
Err(e) => {
80+
return Err(PrecompileError::Other(format!("Failed to load to account: {e:?}")));
81+
}
82+
};
83+
84+
// Check for overflow in to account
85+
if to_account.data.info.balance.checked_add(value).is_none() {
86+
return Err(PrecompileError::Other("Balance overflow in to account".into()));
87+
}
88+
89+
// Add balance to the to account
90+
to_account.data.info.balance += value;
91+
}
92+
93+
// No output data for successful transfer
94+
Ok(PrecompileOutput::new(CELO_TRANSFER_GAS_COST, alloy_primitives::Bytes::new()))
95+
}

0 commit comments

Comments
 (0)