revm_trace/utils/
erc20_utils.rs

1//! ERC20 token utilities for querying token information and balances
2//!
3//! Provides functions to interact with ERC20 tokens including balance queries,
4//! token metadata retrieval, and transfer event parsing.
5
6use crate::{
7    errors::{EvmError, TokenError},
8    evm::TraceEvm,
9    types::{TokenInfo, ERC20_TRANSFER_EVENT_SIGNATURE},
10};
11use alloy::{
12    primitives::{Address, Bytes, FixedBytes, TxKind, U256},
13    sol,
14    sol_types::SolCall,
15};
16use anyhow::Result;
17use revm::{
18    context::TxEnv,
19    context_interface::result::{ExecutionResult, Output},
20    database::Database,
21    ExecuteEvm,
22};
23
24// ERC20 interface for common token functions
25//
26// Generates Rust bindings for:
27// - name(): Returns token name
28// - symbol(): Returns token symbol
29// - decimals(): Returns token decimal places
30// - balanceOf(address): Returns token balance for an address
31// - totalSupply(): Returns total token supply
32sol! {
33    function name() public returns (string);
34    function symbol() public returns (string);
35    function decimals() public returns (uint8);
36    function balanceOf(address owner) public returns (uint256);
37    function totalSupply() public returns (uint256);
38}
39
40/// Query ERC20 token balance for a specific address
41///
42/// Executes the `balanceOf(address)` function on the specified token contract.
43///
44/// # Arguments
45/// - `evm`: EVM instance for contract execution
46/// - `token_address`: Address of the ERC20 token contract
47/// - `owner`: Address to query balance for
48///
49/// # Returns
50/// - `Ok(U256)`: Token balance in the token's smallest unit
51/// - `Err(...)`: If the contract call fails or returns invalid data
52pub fn query_erc20_balance<DB, INSP>(
53    evm: &mut TraceEvm<DB, INSP>,
54    token_address: Address,
55    owner: Address,
56) -> Result<U256>
57where
58    DB: Database,
59{
60    let data: Bytes = balanceOfCall { owner }.abi_encode().into();
61
62    // Use zero address as caller for read-only calls (no nonce needed)
63    let tx = TxEnv::builder()
64        .caller(Address::ZERO)
65        .kind(TxKind::Call(token_address))
66        .chain_id(Some(evm.cfg.chain_id))
67        .data(data)
68        .nonce(0) // Read-only call, nonce doesn't matter
69        .build_fill();
70    let ref_tx = evm
71        .transact(tx)
72        .map_err(|e| anyhow::anyhow!("Failed to query ERC20 balance: {}", e))?;
73    let value = match ref_tx.result {
74        ExecutionResult::Success {
75            output: Output::Call(value),
76            ..
77        } => value,
78        _ => return Err(anyhow::anyhow!("Failed to execute balanceOf call")),
79    };
80    let balance = balanceOfCall::abi_decode_returns(&value)?;
81
82    Ok(balance)
83}
84
85/// Internal helper to query all token information with pre-encoded call data
86///
87/// Executes name(), symbol(), decimals(), and totalSupply() calls for a token.
88///
89/// # Arguments
90/// - `evm`: EVM instance for contract execution
91/// - `token_address`: Token contract address
92/// - `name_encoded`: Pre-encoded name() call data
93/// - `symbol_encoded`: Pre-encoded symbol() call data  
94/// - `decimals_encoded`: Pre-encoded decimals() call data
95/// - `total_supply_encoded`: Pre-encoded totalSupply() call data
96///
97/// # Returns
98/// - `Ok(TokenInfo)`: Complete token information
99/// - `Err(TokenError)`: If any call fails or returns invalid data
100fn query_token_info<DB, INSP>(
101    evm: &mut TraceEvm<DB, INSP>,
102    token_address: Address,
103    name_encoded: Bytes,
104    symbol_encoded: Bytes,
105    decimals_encoded: Bytes,
106    total_supply_encoded: Bytes,
107) -> Result<TokenInfo, TokenError>
108where
109    DB: Database,
110{
111    let tx_name = TxEnv {
112        caller: Address::ZERO,
113        kind: TxKind::Call(token_address),
114        data: name_encoded,
115        chain_id: Some(evm.cfg.chain_id),
116        nonce: 0,
117        ..Default::default()
118    };
119    let ref_tx = evm
120        .transact(tx_name)
121        .map_err(|e| TokenError::AnyhowError(format!("Failed to query token name: {e}")))?;
122    let name = match ref_tx.result {
123        ExecutionResult::Success {
124            output: Output::Call(value),
125            ..
126        } => nameCall::abi_decode_returns(&value).map_err(|_| TokenError::NameDecode {
127            address: token_address.to_string(),
128            reason: "Failed to decode name".to_string(),
129        })?,
130        _ => {
131            return Err(TokenError::CallReverted {
132                address: token_address.to_string(),
133            })
134        }
135    };
136
137    let tx_symbol = TxEnv {
138        caller: Address::ZERO,
139        kind: TxKind::Call(token_address),
140        chain_id: Some(evm.cfg.chain_id),
141        data: symbol_encoded,
142        ..Default::default()
143    };
144    let ref_tx = evm
145        .transact(tx_symbol)
146        .map_err(|e| TokenError::AnyhowError(format!("Failed to query token symbol: {e}")))?;
147    let symbol = match ref_tx.result {
148        ExecutionResult::Success {
149            output: Output::Call(value),
150            ..
151        } => symbolCall::abi_decode_returns(&value).map_err(|_| TokenError::SymbolDecode {
152            address: token_address.to_string(),
153            reason: "Failed to decode symbol".to_string(),
154        })?,
155        _ => {
156            return Err(TokenError::CallReverted {
157                address: token_address.to_string(),
158            })
159        }
160    };
161
162    let tx_decimals = TxEnv {
163        kind: TxKind::Call(token_address),
164        data: decimals_encoded,
165        chain_id: Some(evm.cfg.chain_id),
166        ..Default::default()
167    };
168    let ref_tx = evm
169        .transact(tx_decimals)
170        .map_err(|e| TokenError::AnyhowError(format!("Failed to query token decimals: {e}")))?;
171    let decimals = match ref_tx.result {
172        ExecutionResult::Success {
173            output: Output::Call(value),
174            ..
175        } => decimalsCall::abi_decode_returns(&value).map_err(|_| TokenError::DecimalsDecode {
176            address: token_address.to_string(),
177            reason: "Failed to decode decimals".to_string(),
178        })?,
179        _ => {
180            return Err(TokenError::CallReverted {
181                address: token_address.to_string(),
182            })
183        }
184    };
185    let tx_total_supply = TxEnv {
186        kind: TxKind::Call(token_address),
187        data: total_supply_encoded,
188        chain_id: Some(evm.cfg.chain_id),
189        ..Default::default()
190    };
191    let ref_tx = evm
192        .transact(tx_total_supply)
193        .map_err(|e| TokenError::AnyhowError(format!("Failed to query token total supply: {e}")))?;
194    let total_supply = match ref_tx.result {
195        ExecutionResult::Success {
196            output: Output::Call(value),
197            ..
198        } => totalSupplyCall::abi_decode_returns(&value).map_err(|_| {
199            TokenError::TotalSupplyDecode {
200                address: token_address.to_string(),
201                reason: "Failed to decode total supply".to_string(),
202            }
203        })?,
204        _ => {
205            return Err(TokenError::CallReverted {
206                address: token_address.to_string(),
207            })
208        }
209    };
210
211    Ok(TokenInfo {
212        name,
213        symbol,
214        decimals,
215        total_supply,
216    })
217}
218
219/// Query token information for multiple ERC20 tokens in batch
220///
221/// Efficiently retrieves name, symbol, decimals, and total supply for multiple tokens.
222///
223/// # Arguments
224/// - `evm`: EVM instance for contract execution
225/// - `tokens`: Array of token contract addresses
226///
227/// # Returns
228/// - `Ok(Vec<TokenInfo>)`: Array of token information in the same order as input
229/// - `Err(EvmError)`: If any contract call fails
230pub fn get_token_infos<DB, INSP>(
231    evm: &mut TraceEvm<DB, INSP>,
232    tokens: &[Address],
233) -> Result<Vec<TokenInfo>, EvmError>
234where
235    DB: Database,
236{
237    let name_encoded: Bytes = nameCall {}.abi_encode().into();
238    let symbol_encoded: Bytes = symbolCall {}.abi_encode().into();
239    let decimals_encoded: Bytes = decimalsCall {}.abi_encode().into();
240    let total_supply_encoded: Bytes = totalSupplyCall {}.abi_encode().into();
241    let mut token_infos = Vec::with_capacity(tokens.len());
242    for token in tokens {
243        let token_info = query_token_info(
244            evm,
245            *token,
246            name_encoded.clone(),
247            symbol_encoded.clone(),
248            decimals_encoded.clone(),
249            total_supply_encoded.clone(),
250        )?;
251        token_infos.push(token_info);
252    }
253
254    Ok(token_infos)
255}
256
257/// Parses ERC20 Transfer event data
258///
259/// # Arguments
260/// * `topics` - Event topics containing:
261///   - [0]: Transfer event signature
262///   - [1]: From address (indexed)
263///   - [2]: To address (indexed)
264/// * `data` - ABI-encoded transfer amount
265///
266/// # Returns
267/// * `Some((from, to, amount))` if valid Transfer event
268/// * `None` if invalid format or zero amount
269pub fn parse_transfer_log(
270    topics: &[FixedBytes<32>],
271    data: &[u8],
272) -> Option<(Address, Address, U256)> {
273    if topics.len() < 3 || topics[0] != ERC20_TRANSFER_EVENT_SIGNATURE {
274        return None;
275    }
276    let amount = U256::from_be_slice(data);
277    if !amount.is_zero() {
278        Some((
279            Address::from_slice(&topics[1].as_slice()[12..]),
280            Address::from_slice(&topics[2].as_slice()[12..]),
281            amount,
282        ))
283    } else {
284        None
285    }
286}