Skip to main content

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