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