foundry_evm_networks/celo/
transfer.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 std::borrow::Cow;
15
16use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
17use alloy_primitives::{Address, U256, address};
18use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
19
20/// Label of the Celo transfer precompile to display in traces.
21pub const CELO_TRANSFER_LABEL: &str = "CELO_TRANSFER_PRECOMPILE";
22
23/// Address of the Celo transfer precompile.
24pub const CELO_TRANSFER_ADDRESS: Address = address!("0x00000000000000000000000000000000000000fd");
25
26/// ID for the [Celo transfer precompile](CELO_TRANSFER_ADDRESS).
27pub static PRECOMPILE_ID_CELO_TRANSFER: PrecompileId =
28    PrecompileId::Custom(Cow::Borrowed("celo transfer"));
29
30/// Gas cost for Celo transfer precompile.
31const CELO_TRANSFER_GAS_COST: u64 = 9000;
32
33/// Returns the Celo native transfer.
34pub fn precompile() -> DynPrecompile {
35    DynPrecompile::new_stateful(PRECOMPILE_ID_CELO_TRANSFER.clone(), celo_transfer_precompile)
36}
37
38/// Celo transfer precompile implementation.
39///
40/// Uses load_account to modify balances directly, making it compatible with PrecompilesMap.
41pub fn celo_transfer_precompile(input: PrecompileInput<'_>) -> PrecompileResult {
42    // Check minimum gas requirement
43    if input.gas < CELO_TRANSFER_GAS_COST {
44        return Err(PrecompileError::OutOfGas);
45    }
46
47    // Validate input length (must be exactly 96 bytes: 32 + 32 + 32)
48    if input.data.len() != 96 {
49        return Err(PrecompileError::Other(format!(
50            "Invalid input length for Celo transfer precompile: expected 96 bytes, got {}",
51            input.data.len()
52        )));
53    }
54
55    // Parse input: from (bytes 12-32), to (bytes 44-64), value (bytes 64-96)
56    let from_bytes = &input.data[12..32];
57    let to_bytes = &input.data[44..64];
58    let value_bytes = &input.data[64..96];
59
60    let from_address = Address::from_slice(from_bytes);
61    let to_address = Address::from_slice(to_bytes);
62    let value = U256::from_be_slice(value_bytes);
63
64    // Perform the transfer using load_account to modify balances directly
65    let mut internals = input.internals;
66
67    // Load and check the from account balance first
68    {
69        let from_account = match internals.load_account(from_address) {
70            Ok(account) => account,
71            Err(e) => {
72                return Err(PrecompileError::Other(format!("Failed to load from account: {e:?}")));
73            }
74        };
75
76        // Check if from account has sufficient balance
77        if from_account.data.info.balance < value {
78            return Err(PrecompileError::Other("Insufficient balance".into()));
79        }
80
81        // Deduct balance from the from account
82        from_account.data.info.balance -= value;
83    }
84
85    // Load and update the to account
86    {
87        let to_account = match internals.load_account(to_address) {
88            Ok(account) => account,
89            Err(e) => {
90                return Err(PrecompileError::Other(format!("Failed to load to account: {e:?}")));
91            }
92        };
93
94        // Check for overflow in to account
95        if to_account.data.info.balance.checked_add(value).is_none() {
96            return Err(PrecompileError::Other("Balance overflow in to account".into()));
97        }
98
99        // Add balance to the to account
100        to_account.data.info.balance += value;
101    }
102
103    // No output data for successful transfer
104    Ok(PrecompileOutput::new(CELO_TRANSFER_GAS_COST, alloy_primitives::Bytes::new()))
105}