Skip to content

Commit 08a0fda

Browse files
committed
Implement Celo transfer precompile
1 parent 460a958 commit 08a0fda

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::{
88
error::InvalidTransactionError,
99
pool::transactions::PoolTransaction,
1010
},
11+
evm::celo_precompile::celo_precompile_lookup,
1112
inject_precompiles,
1213
mem::inspector::AnvilInspector,
1314
};
@@ -341,6 +342,10 @@ impl<DB: Db + ?Sized, V: TransactionValidator> Iterator for &mut TransactionExec
341342
inject_precompiles(&mut evm, vec![(P256VERIFY, P256VERIFY_BASE_GAS_FEE)]);
342343
}
343344

345+
if self.celo {
346+
evm.precompiles_mut().set_precompile_lookup(celo_precompile_lookup);
347+
}
348+
344349
if let Some(factory) = &self.precompile_factory {
345350
inject_precompiles(&mut evm, factory.precompiles());
346351
}

crates/anvil/src/evm.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use foundry_evm_core::either_evm::EitherEvm;
99
use op_revm::OpContext;
1010
use revm::{Inspector, precompile::PrecompileWithAddress};
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 {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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, 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+
/// Celo transfer precompile implementation.
24+
///
25+
/// Uses load_account to modify balances directly, making it compatible with PrecompilesMap.
26+
pub fn celo_transfer_precompile(input: PrecompileInput<'_>) -> PrecompileResult {
27+
// Check minimum gas requirement
28+
if input.gas < CELO_TRANSFER_GAS_COST {
29+
return Err(PrecompileError::OutOfGas);
30+
}
31+
32+
// Validate input length (must be exactly 96 bytes: 32 + 32 + 32)
33+
if input.data.len() != 96 {
34+
return Err(PrecompileError::Other(format!(
35+
"Invalid input length for Celo transfer precompile: expected 96 bytes, got {}",
36+
input.data.len()
37+
)));
38+
}
39+
40+
// Parse input: from (bytes 12-32), to (bytes 44-64), value (bytes 64-96)
41+
let from_bytes = &input.data[12..32];
42+
let to_bytes = &input.data[44..64];
43+
let value_bytes = &input.data[64..96];
44+
45+
let from_address = Address::from_slice(from_bytes);
46+
let to_address = Address::from_slice(to_bytes);
47+
let value = U256::from_be_slice(value_bytes);
48+
49+
// Perform the transfer using load_account to modify balances directly
50+
let mut internals = input.internals;
51+
52+
// Load and check the from account balance first
53+
{
54+
let from_account = match internals.load_account(from_address) {
55+
Ok(account) => account,
56+
Err(e) => {
57+
return Err(PrecompileError::Other(format!("Failed to load from account: {e:?}")));
58+
}
59+
};
60+
61+
// Check if from account has sufficient balance
62+
if from_account.data.info.balance < value {
63+
return Err(PrecompileError::Other("Insufficient balance".into()));
64+
}
65+
66+
// Deduct balance from the from account
67+
from_account.data.info.balance -= value;
68+
}
69+
70+
// Load and update the to account
71+
{
72+
let to_account = match internals.load_account(to_address) {
73+
Ok(account) => account,
74+
Err(e) => {
75+
return Err(PrecompileError::Other(format!("Failed to load to account: {e:?}")));
76+
}
77+
};
78+
79+
// Check for overflow in to account
80+
if to_account.data.info.balance.checked_add(value).is_none() {
81+
return Err(PrecompileError::Other("Balance overflow in to account".into()));
82+
}
83+
84+
// Add balance to the to account
85+
to_account.data.info.balance += value;
86+
}
87+
88+
// No output data for successful transfer
89+
Ok(PrecompileOutput::new(CELO_TRANSFER_GAS_COST, alloy_primitives::Bytes::new()))
90+
}
91+
92+
/// Can be used as PrecompilesMap lookup function
93+
pub fn celo_precompile_lookup(address: &Address) -> Option<DynPrecompile> {
94+
if *address == CELO_TRANSFER_ADDRESS {
95+
Some(DynPrecompile::new_stateful(celo_transfer_precompile))
96+
} else {
97+
None
98+
}
99+
}

0 commit comments

Comments
 (0)