diff --git a/Cargo.lock b/Cargo.lock index 8d1e5d5aca95..dfea9acbbe5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2868,6 +2868,25 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exex-evm" +version = "0.0.0" +dependencies = [ + "alloy-sol-types", + "eyre", + "futures", + "reth", + "reth-evm", + "reth-evm-ethereum", + "reth-exex", + "reth-node-api", + "reth-node-core", + "reth-node-ethereum", + "reth-primitives", + "reth-tracing", + "tokio", +] + [[package]] name = "exex-in-memory-state" version = "0.0.0" diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 12d6eedc57cb..f7bc4cb1272f 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -134,6 +134,12 @@ pub enum Transaction { Deposit(TxDeposit), } +impl AsRef for Transaction { + fn as_ref(&self) -> &Self { + self + } +} + // === impl Transaction === impl Transaction { diff --git a/examples/README.md b/examples/README.md index 6605fd2972b3..fb75740e83dc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,11 +24,12 @@ to make a PR! ## ExEx | Example | Description | -|-------------------------------------------|-----------------------------------------------------------------------------------| +| ----------------------------------------- | --------------------------------------------------------------------------------- | | [Minimal ExEx](./exex/minimal) | Illustrates how to build a simple ExEx | | [OP Bridge ExEx](./exex/op-bridge) | Illustrates an ExEx that decodes Optimism deposit and withdrawal receipts from L1 | | [Rollup](./exex/rollup) | Illustrates a rollup ExEx that derives the state from L1 | | [In Memory State](./exex/in-memory-state) | Illustrates an ExEx that tracks the plain state in memory | +| [EVM](./exex/evm) | Illustrates an ExEx that checks ERC20 balances | ## RPC @@ -57,11 +58,11 @@ to make a PR! ## P2P -| Example | Description | -| --------------------------- | ----------------------------------------------------------------- | -| [Manual P2P](./manual-p2p) | Illustrates how to connect and communicate with a peer | -| [Polygon P2P](./polygon-p2p) | Illustrates how to connect and communicate with a peer on Polygon | -| [BSC P2P](./bsc-p2p) | Illustrates how to connect and communicate with a peer on Binance Smart Chain | +| Example | Description | +| ---------------------------- | ----------------------------------------------------------------------------- | +| [Manual P2P](./manual-p2p) | Illustrates how to connect and communicate with a peer | +| [Polygon P2P](./polygon-p2p) | Illustrates how to connect and communicate with a peer on Polygon | +| [BSC P2P](./bsc-p2p) | Illustrates how to connect and communicate with a peer on Binance Smart Chain | ## Misc diff --git a/examples/exex/evm/Cargo.toml b/examples/exex/evm/Cargo.toml new file mode 100644 index 000000000000..89dd4e1323d7 --- /dev/null +++ b/examples/exex/evm/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "exex-evm" +version = "0.0.0" +publish = false +edition.workspace = true +license.workspace = true + +[dependencies] +reth.workspace = true +reth-evm.workspace = true +reth-evm-ethereum.workspace = true +reth-exex.workspace = true +reth-node-api.workspace = true +reth-node-core.workspace = true +reth-node-ethereum.workspace = true +reth-primitives.workspace = true +reth-tracing.workspace = true + +alloy-sol-types = { workspace = true, features = ["json"] } + +eyre.workspace = true +tokio.workspace = true +futures.workspace = true diff --git a/examples/exex/evm/erc20.json b/examples/exex/evm/erc20.json new file mode 100644 index 000000000000..a3de7b8f3898 --- /dev/null +++ b/examples/exex/evm/erc20.json @@ -0,0 +1 @@ +[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}] diff --git a/examples/exex/evm/src/main.rs b/examples/exex/evm/src/main.rs new file mode 100644 index 000000000000..431b5baafa03 --- /dev/null +++ b/examples/exex/evm/src/main.rs @@ -0,0 +1,128 @@ +use std::collections::{HashMap, HashSet}; + +use alloy_sol_types::{sol, SolEventInterface, SolInterface}; +use eyre::OptionExt; +use reth::{ + providers::{ + providers::BundleStateProvider, DatabaseProviderFactory, HistoricalStateProviderRef, + }, + revm::database::StateProviderDatabase, +}; +use reth_evm::ConfigureEvm; +use reth_evm_ethereum::EthEvmConfig; +use reth_exex::{ExExContext, ExExEvent}; +use reth_node_api::FullNodeComponents; +use reth_node_ethereum::EthereumNode; +use reth_primitives::{revm::env::fill_tx_env, Address, Transaction, TxKind, TxLegacy, U256}; +use reth_tracing::tracing::info; + +sol!(ERC20, "erc20.json"); +use crate::ERC20::{ERC20Calls, ERC20Events}; + +async fn exex(mut ctx: ExExContext) -> eyre::Result<()> { + let evm_config = EthEvmConfig::default(); + + while let Some(notification) = ctx.notifications.recv().await { + if let Some(chain) = notification.committed_chain() { + let database_provider = ctx.provider().database_provider_ro()?; + let provider = BundleStateProvider::new( + HistoricalStateProviderRef::new( + database_provider.tx_ref(), + chain.first().number.checked_sub(1).ok_or_eyre("block number underflow")?, + database_provider.static_file_provider().clone(), + ), + chain.execution_outcome(), + ); + let db = StateProviderDatabase::new(&provider); + + let mut evm = evm_config.evm(db); + + // Collect all ERC20 contract addresses and addresses that had balance changes + let erc20_contracts_and_addresses = chain + .block_receipts_iter() + .flatten() + .flatten() + .flat_map(|receipt| receipt.logs.iter()) + .fold(HashMap::>::new(), |mut acc, log| { + if let Ok(ERC20Events::Transfer(ERC20::Transfer { from, to, value: _ })) = + ERC20Events::decode_raw_log(log.topics(), &log.data.data, true) + { + acc.entry(log.address).or_default().extend([from, to]); + } + + acc + }); + + // Construct transactions to check the decimals of ERC20 contracts and balances of + //addresses that had balance changes + let txs = erc20_contracts_and_addresses.into_iter().map(|(contract, addresses)| { + ( + contract, + Transaction::Legacy(TxLegacy { + gas_limit: 50_000_000, + to: TxKind::Call(contract), + input: ERC20Calls::decimals(ERC20::decimalsCall {}).abi_encode().into(), + ..Default::default() + }), + addresses.into_iter().map(move |address| { + ( + address, + Transaction::Legacy(TxLegacy { + gas_limit: 50_000_000, + to: TxKind::Call(contract), + input: ERC20Calls::balanceOf(ERC20::balanceOfCall { + _owner: address, + }) + .abi_encode() + .into(), + ..Default::default() + }), + ) + }), + ) + }); + + for (contract, decimals_tx, balance_txs) in txs { + fill_tx_env(evm.tx_mut(), &decimals_tx, Address::ZERO); + let result = evm.transact()?.result; + let output = result.output().ok_or_eyre("no output for decimals tx")?; + let decimals = U256::try_from_be_slice(output); + + for (address, balance_tx) in balance_txs { + fill_tx_env(evm.tx_mut(), &balance_tx, Address::ZERO); + let result = evm.transact()?.result; + let output = result.output().ok_or_eyre("no output for balance tx")?; + let balance = U256::try_from_be_slice(output); + + if let Some((balance, decimals)) = balance.zip(decimals) { + let divisor = U256::from(10).pow(decimals); + let (balance, rem) = balance.div_rem(divisor); + let balance = f64::from(balance) + f64::from(rem) / f64::from(divisor); + info!(?contract, ?address, %balance, "Balance updated"); + } else { + info!( + ?contract, + ?address, + "Balance updated but too large to fit into U256" + ); + } + } + } + + ctx.events.send(ExExEvent::FinishedHeight(chain.tip().number))?; + } + } + Ok(()) +} + +fn main() -> eyre::Result<()> { + reth::cli::Cli::parse_args().run(|builder, _| async move { + let handle = builder + .node(EthereumNode::default()) + .install_exex("EVM", |ctx| async move { Ok(exex(ctx)) }) + .launch() + .await?; + + handle.wait_for_node_exit().await + }) +}