Skip to content

Commit 39678e7

Browse files
committed
Implement Celo transfer precompile
1 parent 0e7eca2 commit 39678e7

File tree

6 files changed

+188
-17
lines changed

6 files changed

+188
-17
lines changed

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
PrecompileFactory,
2+
HybridPrecompileProvider, PrecompileFactory,
33
eth::{
44
backend::{
55
db::Db, env::Env, mem::op_haltreason_to_instruction_result,
@@ -434,7 +434,7 @@ pub fn new_evm_with_inspector<DB, I>(
434434
db: DB,
435435
env: &Env,
436436
inspector: I,
437-
) -> EitherEvm<DB, I, PrecompilesMap>
437+
) -> EitherEvm<DB, I, HybridPrecompileProvider>
438438
where
439439
DB: Database<Error = DatabaseError> + Debug,
440440
I: Inspector<EthEvmContext<DB>> + Inspector<OpContext<DB>>,
@@ -461,7 +461,7 @@ where
461461
op_context,
462462
inspector,
463463
EthInstructions::default(),
464-
PrecompilesMap::from_static(op_precompiles),
464+
HybridPrecompileProvider::standard_only(PrecompilesMap::from_static(op_precompiles)),
465465
));
466466

467467
let op = OpEvm::new(op_evm, true);
@@ -492,7 +492,9 @@ where
492492
eth_context,
493493
inspector,
494494
EthInstructions::default(),
495-
PrecompilesMap::from_static(eth_precompiles),
495+
// TODO: Only use standard precompiles here and pull out version with stateful
496+
// precompiles into a Celo-specific version of the Optimism branch.
497+
HybridPrecompileProvider::new(PrecompilesMap::from_static(eth_precompiles)),
496498
);
497499

498500
let eth = EthEvm::new(eth_evm, true);
@@ -506,7 +508,7 @@ pub fn new_evm_with_inspector_ref<'db, DB, I>(
506508
db: &'db DB,
507509
env: &Env,
508510
inspector: &'db mut I,
509-
) -> EitherEvm<WrapDatabaseRef<&'db DB>, &'db mut I, PrecompilesMap>
511+
) -> EitherEvm<WrapDatabaseRef<&'db DB>, &'db mut I, HybridPrecompileProvider>
510512
where
511513
DB: DatabaseRef<Error = DatabaseError> + Debug + 'db + ?Sized,
512514
I: Inspector<EthEvmContext<WrapDatabaseRef<&'db DB>>>

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ use alloy_evm::{
4545
Database, Evm,
4646
eth::EthEvmContext,
4747
overrides::{OverrideBlockHashes, apply_state_overrides},
48-
precompiles::PrecompilesMap,
4948
};
5049
use alloy_network::{
5150
AnyHeader, AnyRpcBlock, AnyRpcHeader, AnyRpcTransaction, AnyTxEnvelope, AnyTxType,
@@ -1162,7 +1161,7 @@ impl Backend {
11621161
db: &'db DB,
11631162
env: &Env,
11641163
inspector: &'db mut I,
1165-
) -> EitherEvm<WrapDatabaseRef<&'db DB>, &'db mut I, PrecompilesMap>
1164+
) -> EitherEvm<WrapDatabaseRef<&'db DB>, &'db mut I, crate::HybridPrecompileProvider>
11661165
where
11671166
DB: DatabaseRef + ?Sized,
11681167
I: Inspector<EthEvmContext<WrapDatabaseRef<&'db DB>>>

crates/anvil/src/evm.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ use std::fmt::Debug;
33
use alloy_evm::{
44
Database, Evm,
55
eth::EthEvmContext,
6-
precompiles::{DynPrecompile, PrecompileInput, PrecompilesMap},
6+
precompiles::{DynPrecompile, PrecompileInput},
77
};
88
use foundry_evm_core::either_evm::EitherEvm;
99
use op_revm::OpContext;
1010
use revm::{Inspector, precompile::PrecompileWithAddress};
1111

12+
mod celo_precompile;
13+
mod stateful_precompile;
14+
pub use stateful_precompile::HybridPrecompileProvider;
15+
1216
/// Object-safe trait that enables injecting extra precompiles when using
1317
/// `anvil` as a library.
1418
pub trait PrecompileFactory: Send + Sync + Unpin + Debug {
@@ -18,14 +22,14 @@ pub trait PrecompileFactory: Send + Sync + Unpin + Debug {
1822

1923
/// Inject precompiles into the EVM dynamically.
2024
pub fn inject_precompiles<DB, I>(
21-
evm: &mut EitherEvm<DB, I, PrecompilesMap>,
25+
evm: &mut EitherEvm<DB, I, HybridPrecompileProvider>,
2226
precompiles: Vec<(PrecompileWithAddress, u64)>,
2327
) where
2428
DB: Database,
2529
I: Inspector<EthEvmContext<DB>> + Inspector<OpContext<DB>>,
2630
{
2731
for (PrecompileWithAddress(addr, func), gas) in precompiles {
28-
evm.precompiles_mut().apply_precompile(&addr, move |_| {
32+
evm.precompiles_mut().standard_precompiles.apply_precompile(&addr, move |_| {
2933
Some(DynPrecompile::from(move |input: PrecompileInput<'_>| func(input.data, gas)))
3034
});
3135
}
@@ -55,7 +59,7 @@ mod tests {
5559
primitives::hardfork::SpecId,
5660
};
5761

58-
use crate::{PrecompileFactory, inject_precompiles};
62+
use crate::{HybridPrecompileProvider, PrecompileFactory, inject_precompiles};
5963

6064
// A precompile activated in the `Prague` spec.
6165
const ETH_PRAGUE_PRECOMPILE: Address = address!("0x0000000000000000000000000000000000000011");
@@ -91,8 +95,10 @@ mod tests {
9195
/// Creates a new EVM instance with the custom precompile factory.
9296
fn create_eth_evm(
9397
spec: SpecId,
94-
) -> (foundry_evm::Env, EitherEvm<EmptyDBTyped<Infallible>, NoOpInspector, PrecompilesMap>)
95-
{
98+
) -> (
99+
foundry_evm::Env,
100+
EitherEvm<EmptyDBTyped<Infallible>, NoOpInspector, HybridPrecompileProvider>,
101+
) {
96102
let eth_env = foundry_evm::Env {
97103
evm_env: EvmEnv { block_env: Default::default(), cfg_env: CfgEnv::new_with_spec(spec) },
98104
tx: TxEnv {
@@ -122,7 +128,9 @@ mod tests {
122128
eth_evm_context,
123129
NoOpInspector,
124130
EthInstructions::<EthInterpreter, EthEvmContext<EmptyDB>>::default(),
125-
PrecompilesMap::from_static(eth_precompiles),
131+
HybridPrecompileProvider::standard_only(PrecompilesMap::from_static(
132+
eth_precompiles,
133+
)),
126134
),
127135
true,
128136
));
@@ -136,7 +144,7 @@ mod tests {
136144
op_spec: OpSpecId,
137145
) -> (
138146
crate::eth::backend::env::Env,
139-
EitherEvm<EmptyDBTyped<Infallible>, NoOpInspector, PrecompilesMap>,
147+
EitherEvm<EmptyDBTyped<Infallible>, NoOpInspector, HybridPrecompileProvider>,
140148
) {
141149
let op_env = crate::eth::backend::env::Env {
142150
evm_env: EvmEnv { block_env: Default::default(), cfg_env: CfgEnv::new_with_spec(spec) },
@@ -180,7 +188,9 @@ mod tests {
180188
op_evm_context,
181189
NoOpInspector,
182190
EthInstructions::<EthInterpreter, OpContext<EmptyDB>>::default(),
183-
PrecompilesMap::from_static(op_precompiles),
191+
HybridPrecompileProvider::standard_only(PrecompilesMap::from_static(
192+
op_precompiles,
193+
)),
184194
)),
185195
true,
186196
));
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use alloy_primitives::{Address, U256};
2+
use revm::context::{ContextTr, JournalTr};
3+
4+
/// Celo transfer precompile implementation
5+
/// Address: 0xfd (253)
6+
/// Input: from (32 bytes) || to (32 bytes) || value (32 bytes) = 96 bytes total
7+
/// Gas cost: 9000
8+
pub fn celo_transfer_precompile<CTX: ContextTr>(
9+
mut context: CTX,
10+
inputs: &revm::interpreter::InputsImpl,
11+
gas_limit: u64,
12+
) -> Result<Option<revm::interpreter::InterpreterResult>, String> {
13+
use revm::interpreter::{Gas, InstructionResult, InterpreterResult};
14+
15+
// Gas cost for Celo transfer precompile
16+
const CELO_TRANSFER_GAS_COST: u64 = 9000;
17+
18+
// Check minimum gas requirement
19+
if gas_limit < CELO_TRANSFER_GAS_COST {
20+
return Err("Insufficient gas for Celo transfer precompile".to_string());
21+
}
22+
23+
let mut result = InterpreterResult {
24+
result: InstructionResult::Return,
25+
gas: Gas::new(gas_limit),
26+
output: Default::default(),
27+
};
28+
29+
// Record gas cost
30+
if !result.gas.record_cost(CELO_TRANSFER_GAS_COST) {
31+
return Err("Out of gas in Celo transfer precompile".to_string());
32+
}
33+
34+
// Validate input length (must be exactly 96 bytes: 32 + 32 + 32)
35+
let input_bytes = inputs.input.bytes(&mut context);
36+
if input_bytes.len() != 96 {
37+
return Err(format!(
38+
"Invalid input length for Celo transfer precompile: expected 96 bytes, got {}",
39+
input_bytes.len()
40+
));
41+
}
42+
43+
// Parse input: from (bytes 12-32), to (bytes 44-64), value (bytes 64-96)
44+
let from_bytes = &input_bytes[12..32];
45+
let to_bytes = &input_bytes[44..64];
46+
let value_bytes = &input_bytes[64..96];
47+
48+
let from_address = Address::from_slice(from_bytes);
49+
let to_address = Address::from_slice(to_bytes);
50+
let value = U256::from_be_slice(value_bytes);
51+
52+
eprintln!("\t[Celo Transfer] from: {from_address:?}, to: {to_address:?}, value: {value}");
53+
54+
// Perform the transfer using JournalTr.transfer
55+
if let Err(e) = context.journal_mut().transfer(from_address, to_address, value) {
56+
eprintln!("[Celo Transfer] Transfer failed: {e:?}");
57+
result.result = InstructionResult::PrecompileError;
58+
return Ok(Some(result));
59+
}
60+
61+
eprintln!("\t[Celo Transfer] Transfer successful");
62+
63+
// No output data for successful transfer
64+
result.output = alloy_primitives::Bytes::new();
65+
result.result = InstructionResult::Return;
66+
67+
Ok(Some(result))
68+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use super::celo_precompile::celo_transfer_precompile;
2+
use alloy_evm::precompiles::PrecompilesMap;
3+
use alloy_primitives::{Address, address};
4+
use revm::{
5+
context::{Cfg, ContextTr},
6+
handler::PrecompileProvider,
7+
};
8+
use std::fmt::Debug;
9+
10+
const CELO_TRANSFER_ADDRESS: Address = address!("0x00000000000000000000000000000000000000fd");
11+
12+
/// Hybrid precompile provider that combines standard precompiles with stateful precompiles
13+
#[derive(Debug)]
14+
pub struct HybridPrecompileProvider {
15+
/// Standard precompiles map
16+
pub standard_precompiles: PrecompilesMap,
17+
// This should be mapping, but we can't store generic function pointers in a HashMap and
18+
// ContextTr is not dyn-compatible.
19+
// stateful_precompiles: HashMap<
20+
// Address,
21+
// fn(
22+
// &mut CTX,
23+
// &revm::interpreter::InputsImpl,
24+
// u64,
25+
// ) -> Result<Option<revm::interpreter::InterpreterResult>, String>,
26+
// >,
27+
/// Stateful precompile addresses
28+
stateful_precompile_addresses: Vec<Address>,
29+
}
30+
31+
impl HybridPrecompileProvider {
32+
/// Create a new hybrid provider with standard precompiles and stateful precompiles
33+
pub fn new(standard_precompiles: PrecompilesMap) -> Self {
34+
Self { standard_precompiles, stateful_precompile_addresses: vec![CELO_TRANSFER_ADDRESS] }
35+
}
36+
37+
/// Create a new hybrid provider with only standard precompiles
38+
pub fn standard_only(standard_precompiles: PrecompilesMap) -> Self {
39+
Self { standard_precompiles, stateful_precompile_addresses: Vec::new() }
40+
}
41+
42+
/// Get all addresses (both standard and stateful precompiles)
43+
pub fn addresses(&self) -> Box<impl Iterator<Item = Address>> {
44+
let standard_addresses = self.standard_precompiles.addresses().copied();
45+
Box::new(self.stateful_precompile_addresses.iter().copied().chain(standard_addresses))
46+
}
47+
}
48+
49+
impl<CTX> PrecompileProvider<CTX> for HybridPrecompileProvider
50+
where
51+
CTX: ContextTr,
52+
PrecompilesMap: PrecompileProvider<CTX, Output = revm::interpreter::InterpreterResult>,
53+
{
54+
type Output = revm::interpreter::InterpreterResult;
55+
56+
fn set_spec(&mut self, spec: <<CTX as ContextTr>::Cfg as Cfg>::Spec) -> bool {
57+
<PrecompilesMap as PrecompileProvider<CTX>>::set_spec(&mut self.standard_precompiles, spec)
58+
}
59+
60+
fn run(
61+
&mut self,
62+
context: &mut CTX,
63+
address: &Address,
64+
inputs: &revm::interpreter::InputsImpl,
65+
is_static: bool,
66+
gas_limit: u64,
67+
) -> Result<Option<Self::Output>, String> {
68+
// Check if this is the Celo transfer precompile
69+
if address == &CELO_TRANSFER_ADDRESS {
70+
return celo_transfer_precompile(context, inputs, gas_limit);
71+
}
72+
73+
// Fall back to standard precompiles
74+
self.standard_precompiles.run(context, address, inputs, is_static, gas_limit)
75+
}
76+
77+
fn warm_addresses(&self) -> Box<impl Iterator<Item = Address>> {
78+
let standard_addresses =
79+
<PrecompilesMap as PrecompileProvider<CTX>>::warm_addresses(&self.standard_precompiles);
80+
Box::new(self.stateful_precompile_addresses.iter().copied().chain(standard_addresses))
81+
}
82+
83+
fn contains(&self, address: &Address) -> bool {
84+
// Check stateful precompiles first
85+
if self.stateful_precompile_addresses.contains(address) {
86+
return true;
87+
}
88+
89+
// Check standard precompiles
90+
<PrecompilesMap as PrecompileProvider<CTX>>::contains(&self.standard_precompiles, address)
91+
}
92+
}

crates/anvil/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub use alloy_hardforks::EthereumHardfork;
5454
pub mod eth;
5555
/// Evm related abstractions
5656
mod evm;
57-
pub use evm::{PrecompileFactory, inject_precompiles};
57+
pub use evm::{HybridPrecompileProvider, PrecompileFactory, inject_precompiles};
5858

5959
/// support for polling filters
6060
pub mod filter;

0 commit comments

Comments
 (0)