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    context_interface::{
14        result::{InvalidHeader, InvalidTransaction},
15        ContextTr, JournalTr,
16    },
17    database::{AlloyDB, BlockId, CacheDB},
18    database_interface::WrapDatabaseAsync,
19    primitives::{
20        address, hardfork::SpecId, keccak256, Address, StorageValue, TxKind, KECCAK_EMPTY, U256,
21    },
22    state::AccountInfo,
23    Context, Database, MainBuilder, MainContext,
24};
25
26/// Execution utilities for ERC20 gas payment transactions
27pub mod exec;
28/// Custom handler implementation for ERC20 gas payment
29pub mod handler;
30
31type AlloyCacheDB = CacheDB<WrapDatabaseAsync<AlloyDB<Ethereum, DynProvider>>>;
32
33// Constants
34/// USDC token address on Ethereum mainnet
35pub const TOKEN: Address = address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
36/// Treasury address that receives ERC20 gas payments
37pub const TREASURY: Address = address!("0000000000000000000000000000000000000001");
38
39#[tokio::main]
40async fn main() -> Result<()> {
41    // Initialize the Alloy provider and database
42    let rpc_url = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
43    let provider = ProviderBuilder::new().connect(rpc_url).await?.erased();
44
45    let alloy_db = WrapDatabaseAsync::new(AlloyDB::new(provider, BlockId::latest())).unwrap();
46    let mut cache_db = CacheDB::new(alloy_db);
47
48    // Random empty account: From
49    let account = address!("18B06aaF27d44B756FCF16Ca20C1f183EB49111f");
50    // Random empty account: To
51    let account_to = address!("21a4B6F62E51e59274b6Be1705c7c68781B87C77");
52
53    // USDC has 6 decimals
54    let hundred_tokens = U256::from(100_000_000_000_000_000u128);
55
56    let balance_slot = erc_address_storage(account);
57    println!("Balance slot: {balance_slot}");
58    cache_db
59        .insert_account_storage(TOKEN, balance_slot, hundred_tokens * StorageValue::from(2))
60        .unwrap();
61    cache_db.insert_account_info(
62        account,
63        AccountInfo {
64            nonce: 0,
65            balance: hundred_tokens * U256::from(2),
66            code_hash: KECCAK_EMPTY,
67            code: None,
68        },
69    );
70
71    let balance_before = balance_of(account, &mut cache_db).unwrap();
72    println!("Balance before: {balance_before}");
73
74    // Transfer 100 tokens from account to account_to
75    // Magic happens here with custom handlers
76    transfer(account, account_to, hundred_tokens, &mut cache_db)?;
77
78    let balance_after = balance_of(account, &mut cache_db)?;
79    println!("Balance after: {balance_after}");
80
81    Ok(())
82}
83
84/// Helpers
85pub fn token_operation<CTX, ERROR>(
86    context: &mut CTX,
87    sender: Address,
88    recipient: Address,
89    amount: U256,
90) -> Result<(), ERROR>
91where
92    CTX: ContextTr,
93    ERROR: From<InvalidTransaction> + From<InvalidHeader> + From<<CTX::Db as Database>::Error>,
94{
95    let sender_balance_slot = erc_address_storage(sender);
96    let sender_balance = context
97        .journal_mut()
98        .sload(TOKEN, sender_balance_slot)?
99        .data;
100
101    if sender_balance < amount {
102        return Err(ERROR::from(
103            InvalidTransaction::MaxFeePerBlobGasNotSupported,
104        ));
105    }
106    // Subtract the amount from the sender's balance
107    let sender_new_balance = sender_balance.saturating_sub(amount);
108    context
109        .journal_mut()
110        .sstore(TOKEN, sender_balance_slot, sender_new_balance)?;
111
112    // Add the amount to the recipient's balance
113    let recipient_balance_slot = erc_address_storage(recipient);
114    let recipient_balance = context
115        .journal_mut()
116        .sload(TOKEN, recipient_balance_slot)?
117        .data;
118
119    let recipient_new_balance = recipient_balance.saturating_add(amount);
120    context
121        .journal_mut()
122        .sstore(TOKEN, recipient_balance_slot, recipient_new_balance)?;
123
124    Ok(())
125}
126
127fn balance_of(address: Address, alloy_db: &mut AlloyCacheDB) -> Result<StorageValue> {
128    let slot = erc_address_storage(address);
129    alloy_db.storage(TOKEN, slot).map_err(From::from)
130}
131
132fn transfer(from: Address, to: Address, amount: U256, cache_db: &mut AlloyCacheDB) -> Result<()> {
133    let mut ctx = Context::mainnet()
134        .with_db(cache_db)
135        .modify_cfg_chained(|cfg| {
136            cfg.spec = SpecId::CANCUN;
137        })
138        .with_tx(
139            TxEnv::builder()
140                .caller(from)
141                .kind(TxKind::Call(to))
142                .value(amount)
143                .gas_price(2)
144                .build()
145                .unwrap(),
146        )
147        .modify_block_chained(|b| {
148            b.basefee = 1;
149        })
150        .build_mainnet();
151
152    transact_erc20evm_commit(&mut ctx).unwrap();
153
154    Ok(())
155}
156
157/// Calculates the storage slot for an ERC20 balance mapping.
158/// This implements the standard Solidity mapping storage layout where
159/// slot = keccak256(abi.encode(address, slot_number))
160pub fn erc_address_storage(address: Address) -> U256 {
161    keccak256((address, U256::from(4)).abi_encode()).into()
162}