anvil/evm/
celo_precompile.rs

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
14use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
15use alloy_primitives::{Address, U256, address};
16use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
17
18pub const CELO_TRANSFER_ADDRESS: Address = address!("0x00000000000000000000000000000000000000fd");
19
20/// Gas cost for Celo transfer precompile
21const CELO_TRANSFER_GAS_COST: u64 = 9000;
22
23/// Returns the celo native transfer
24pub 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.
31pub 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}