example_erc20_gas/
main.rs

1//! Example of a custom handler for ERC20 gas calculation.
2//!
3//! Gas is going to be deducted from ERC20 token.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6
7use alloy_provider::{network::Ethereum, DynProvider, Provider, ProviderBuilder};
8use alloy_sol_types::SolValue;
9use anyhow::Result;
10use exec::transact_erc20evm_commit;
11use revm::{
12    context::TxEnv,
13    database::{AlloyDB, BlockId, CacheDB},
14    database_interface::WrapDatabaseAsync,
15    primitives::{
16        address, hardfork::SpecId, keccak256, Address, StorageValue, TxKind, KECCAK_EMPTY, U256,
17    },
18    state::AccountInfo,
19    Context, Database, MainBuilder, MainContext,
20};
21
22/// Execution utilities for ERC20 gas payment transactions
23pub mod exec;
24/// Custom handler implementation for ERC20 gas payment
25pub mod handler;
26
27type AlloyCacheDB = CacheDB<WrapDatabaseAsync<AlloyDB<Ethereum, DynProvider>>>;
28
29// Constants
30/// USDC token address on Ethereum mainnet
31pub const TOKEN: Address = address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
32/// Treasury address that receives ERC20 gas payments
33pub const TREASURY: Address = address!("0000000000000000000000000000000000000001");
34
35#[tokio::main]
36async fn main() -> Result<()> {
37    // Initialize the Alloy provider and database
38    let rpc_url = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
39    let provider = ProviderBuilder::new().connect(rpc_url).await?.erased();
40
41    let alloy_db = WrapDatabaseAsync::new(AlloyDB::new(provider, BlockId::latest())).unwrap();
42    let mut cache_db = CacheDB::new(alloy_db);
43
44    // Random empty account: From
45    let account = address!("18B06aaF27d44B756FCF16Ca20C1f183EB49111f");
46    // Random empty account: To
47    let account_to = address!("21a4B6F62E51e59274b6Be1705c7c68781B87C77");
48
49    // USDC has 6 decimals
50    let hundred_tokens = U256::from(100_000_000_000_000_000u128);
51
52    let balance_slot = erc_address_storage(account);
53    println!("Balance slot: {balance_slot}");
54    cache_db
55        .insert_account_storage(TOKEN, balance_slot, hundred_tokens * StorageValue::from(2))
56        .unwrap();
57    cache_db.insert_account_info(
58        account,
59        AccountInfo {
60            nonce: 0,
61            balance: hundred_tokens * U256::from(2),
62            code_hash: KECCAK_EMPTY,
63            code: None,
64        },
65    );
66
67    let balance_before = balance_of(account, &mut cache_db).unwrap();
68    println!("Balance before: {balance_before}");
69
70    // Transfer 100 tokens from account to account_to
71    // Magic happens here with custom handlers
72    transfer(account, account_to, hundred_tokens, &mut cache_db)?;
73
74    let balance_after = balance_of(account, &mut cache_db)?;
75    println!("Balance after: {balance_after}");
76
77    Ok(())
78}
79
80fn balance_of(address: Address, alloy_db: &mut AlloyCacheDB) -> Result<StorageValue> {
81    let slot = erc_address_storage(address);
82    alloy_db.storage(TOKEN, slot).map_err(From::from)
83}
84
85fn transfer(from: Address, to: Address, amount: U256, cache_db: &mut AlloyCacheDB) -> Result<()> {
86    let mut ctx = Context::mainnet()
87        .with_db(cache_db)
88        .modify_cfg_chained(|cfg| {
89            cfg.spec = SpecId::CANCUN;
90        })
91        .with_tx(
92            TxEnv::builder()
93                .caller(from)
94                .kind(TxKind::Call(to))
95                .value(amount)
96                .gas_price(2)
97                .build()
98                .unwrap(),
99        )
100        .modify_block_chained(|b| {
101            b.basefee = 1;
102        })
103        .build_mainnet();
104
105    transact_erc20evm_commit(&mut ctx).unwrap();
106
107    Ok(())
108}
109
110/// Calculates the storage slot for an ERC20 balance mapping.
111/// This implements the standard Solidity mapping storage layout where
112/// slot = keccak256(abi.encode(address, slot_number))
113pub fn erc_address_storage(address: Address) -> U256 {
114    keccak256((address, U256::from(4)).abi_encode()).into()
115}