Try it Live Run EventLog examples in the interactive playground
Conceptual Guide - For API reference and method documentation, see EventLog API .
Ethereum event logs are structured outputs emitted by smart contracts during execution. They enable efficient off-chain indexing and querying of on-chain activity without scanning all transaction data.
Structure
An event log contains:
Address - Contract that emitted the log (20 bytes)
Topics - Up to 4 indexed parameters (32 bytes each)
Data - Non-indexed parameters (variable length)
Metadata - Block number, transaction hash, log index
Topics and Indexed Parameters
Topics enable efficient log filtering via bloom filters. The first topic (topic0) is the event signature hash:
event Transfer ( address indexed from , address indexed to , uint256 value );
This compiles to:
topic0 - keccak256("Transfer(address,address,uint256)")
topic1 - from address (padded to 32 bytes)
topic2 - to address (padded to 32 bytes)
data - value (not indexed, in log data field)
Creating and Parsing Event Logs
import { EventLog , Address , Hash , Hex } from 'tevm' ;
// ERC-20 Transfer event signature
const TRANSFER_SIG = Hash (
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
);
// Addresses as topic hashes (left-padded to 32 bytes)
const fromAddress = Address ( '0xa1b2c3d4e5f67890abcdef1234567890abcdef12' );
const toAddress = Address ( '0xfedcba0987654321fedcba0987654321fedcba09' );
// Create topic hashes (addresses are right-aligned in 32-byte topics)
const fromHash = Hash (
'0x000000000000000000000000a1b2c3d4e5f67890abcdef1234567890abcdef12'
);
const toHash = Hash (
'0x000000000000000000000000fedcba0987654321fedcba0987654321fedcba09'
);
// Create log
const log = EventLog ({
address: Address ( '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2' ),
topics: [ TRANSFER_SIG , fromHash , toHash ],
data: Hex . toBytes ( '0x0000000000000000000000000000000000000000000000000de0b6b3a7640000' ), // 1 ether
blockNumber: 18000000 n ,
transactionHash: Hash ( '0xabc123...' ),
logIndex: 5 ,
});
// Access signature
const signature = log . getTopic0 ();
console . log ( Hash . toHex ( signature )); // 0xddf252ad...
// Access indexed parameters
const [ from , to ] = log . getIndexedTopics ();
console . log ( 'From:' , Hash . toHex ( from ));
console . log ( 'To:' , Hash . toHex ( to ));
// Decode value from data
const value = new DataView ( log . data . buffer ). getBigUint64 ( 24 , false );
console . log ( 'Value:' , value ); // 1000000000000000000n
Event Signature Hashing
Event signatures are computed by hashing the canonical event declaration:
import { Hash , Hex } from 'tevm' ;
// Compute event signature
function eventSignature ( declaration : string ) : Hash {
const bytes = new TextEncoder (). encode ( declaration );
return Hash ( keccak256 ( bytes ));
}
// ERC-20 events
const TRANSFER = eventSignature ( 'Transfer(address,address,uint256)' );
const APPROVAL = eventSignature ( 'Approval(address,address,uint256)' );
// ERC-721 events
const NFT_TRANSFER = eventSignature ( 'Transfer(address,address,uint256)' );
// Note: Same signature as ERC-20 Transfer (different semantics, same structure)
console . log ( Hash . toHex ( TRANSFER ));
// 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
Event signatures must use canonical types: uint256 (not uint), address (20 bytes), no spaces. Incorrect signatures will not match emitted events.
Filtering Event Logs
Address Filtering
import { EventLog , Address } from 'tevm' ;
const usdcAddress = Address ( '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' );
const daiAddress = Address ( '0x6B175474E89094C44Da98b954EedeAC495271d0F' );
// Single address filter
const usdcLogs = allLogs . filter ( log => log . matchesAddress ( usdcAddress ));
// Multiple addresses (OR logic)
const stablecoinLogs = allLogs . filter ( log =>
log . matchesAddress ([ usdcAddress , daiAddress ])
);
// Using filterLogs
const filtered = EventLog . filterLogs ( allLogs , {
address: [ usdcAddress , daiAddress ],
});
Topic Filtering
import { EventLog , Hash } from 'tevm' ;
const TRANSFER_SIG = Hash ( '0xddf252ad...' );
const userHash = Hash ( '0x000000000000000000000000a1b2c3d4...' );
// Match specific signature
const transfers = allLogs . filter ( log =>
log . matchesTopics ([ TRANSFER_SIG ])
);
// Match signature + indexed parameter
const transfersToUser = allLogs . filter ( log =>
log . matchesTopics ([
TRANSFER_SIG , // topic0: event signature
null , // topic1: any from address
userHash , // topic2: specific to address
])
);
// Multiple options per topic (OR logic)
const transfersBetweenUsers = allLogs . filter ( log =>
log . matchesTopics ([
TRANSFER_SIG ,
[ user1Hash , user2Hash , user3Hash ], // from: any of these
[ user1Hash , user2Hash , user3Hash ], // to: any of these
])
);
Complete Filter
import { EventLog } from 'tevm' ;
const filtered = EventLog . filterLogs ( allLogs , {
address: [ usdcAddress , daiAddress , wethAddress ],
topics: [
TRANSFER_SIG , // Event signature
null , // from: any
userAddressHash , // to: user
],
fromBlock: 18000000 n ,
toBlock: 18500000 n ,
});
// Sort chronologically
const sorted = EventLog . sortLogs ( filtered );
Block headers contain a 2048-bit bloom filter enabling efficient log queries without scanning all transactions.
How Bloom Filters Work
For each log, hash the address and each topic
Set 3 bits in the bloom filter per hash
To query: check if all required bits are set
False positives possible, false negatives impossible
import { BloomFilter , Address , Hash } from 'tevm' ;
// Create block bloom filter
const bloom = BloomFilter . create ();
// Add log components
bloom . add ( Address . toBytes ( contractAddress ));
bloom . add ( topic0 );
bloom . add ( topic1 );
// Query bloom filter (fast, but may have false positives)
if ( bloom . contains ( Address . toBytes ( targetAddress ))) {
// Block MAY contain logs from this address
// Must check actual logs to confirm
}
// Export to header format
const bloomBytes = bloom . toBytes (); // 256 bytes (2048 bits)
See BloomFilter documentation for details.
ABI Decoding Integration
Event data requires ABI decoding for non-indexed parameters:
import { EventLog , Abi , Address , Hash } from 'tevm' ;
// Event ABI
const transferAbi = {
name: 'Transfer' ,
type: 'event' ,
inputs: [
{ name: 'from' , type: 'address' , indexed: true },
{ name: 'to' , type: 'address' , indexed: true },
{ name: 'value' , type: 'uint256' , indexed: false },
],
};
// Decode event
function decodeTransfer ( log : EventLog ) {
const [ fromHash , toHash ] = log . getIndexedTopics ();
// Extract addresses from topic hashes (last 20 bytes)
const from = Address ( fromHash . slice ( 12 ));
const to = Address ( toHash . slice ( 12 ));
// Decode value from data
const decoded = Abi . decodeParameters (
[{ type: 'uint256' }],
log . data
);
const value = decoded [ 0 ] as bigint ;
return { from , to , value };
}
const transfer = decodeTransfer ( transferLog );
console . log ( `Transfer: ${ transfer . value } ` );
console . log ( `From: ${ Address . toHex ( transfer . from ) } ` );
console . log ( `To: ${ Address . toHex ( transfer . to ) } ` );
Anonymous Events
Anonymous events omit the signature hash (topic0), allowing 4 indexed parameters instead of 3:
event AnonymousTransfer (
address indexed from ,
address indexed to ,
uint256 indexed tokenId ,
uint256 indexed price
) anonymous;
This produces:
topic0 - from (first indexed parameter, NOT signature)
topic1 - to
topic2 - tokenId
topic3 - price
data - Empty (all parameters indexed)
import { EventLog } from 'tevm' ;
const anonymousLog = EventLog ({
address: contractAddress ,
topics: [ fromHash , toHash , tokenIdHash , priceHash ], // 4 indexed params
data: Hex ( '0x' ), // No non-indexed data
});
// getTopic0 returns first indexed parameter (NOT signature)
const firstIndexed = anonymousLog . getTopic0 ();
// getIndexedTopics returns remaining 3 parameters
const remaining = anonymousLog . getIndexedTopics (); // [to, tokenId, price]
Complete Example: ERC-20 Transfer Tracking
import { EventLog , Address , Hash , Hex } from 'tevm' ;
// Event signature
const TRANSFER_SIG = Hash (
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
);
// Track user balance changes
const userAddress = Address ( '0xa1b2c3d4e5f67890abcdef1234567890abcdef12' );
const userHash = Hash (
'0x000000000000000000000000a1b2c3d4e5f67890abcdef1234567890abcdef12'
);
// Filter incoming transfers
const incoming = EventLog . filterLogs ( allLogs , {
address: usdcAddress ,
topics: [
TRANSFER_SIG ,
null , // from: any
userHash , // to: user
],
fromBlock: 18000000 n ,
toBlock: 18500000 n ,
});
// Filter outgoing transfers
const outgoing = EventLog . filterLogs ( allLogs , {
address: usdcAddress ,
topics: [
TRANSFER_SIG ,
userHash , // from: user
null , // to: any
],
fromBlock: 18000000 n ,
toBlock: 18500000 n ,
});
// Calculate net change
let netChange = 0 n ;
for ( const log of incoming ) {
const value = new DataView ( log . data . buffer ). getBigUint64 ( 24 , false );
netChange += value ;
}
for ( const log of outgoing ) {
const value = new DataView ( log . data . buffer ). getBigUint64 ( 24 , false );
netChange -= value ;
}
console . log ( `Net change: ${ netChange } ` );
console . log ( `Incoming: ${ incoming . length } transfers` );
console . log ( `Outgoing: ${ outgoing . length } transfers` );
Chain Reorganizations
Logs can be marked as removed: true when chain reorganizations invalidate their blocks:
import { EventLog } from 'tevm' ;
// Filter active logs only
const activeLogs = allLogs . filter ( log => ! log . isRemoved ());
// Detect and handle reorgs
function handleReorg ( newLogs : EventLog []) {
const removed = newLogs . filter ( log => log . wasRemoved ());
if ( removed . length > 0 ) {
console . log ( `Reorg detected: ${ removed . length } logs invalidated` );
// Reprocess affected blocks
const affectedBlocks = new Set (
removed . map ( log => log . blockNumber ). filter ( n => n !== undefined )
);
for ( const blockNum of affectedBlocks ) {
console . log ( ` Block ${ blockNum } invalidated` );
// Re-fetch and reprocess block
}
}
}
Bloom Filter False Positives
Bloom filters enable fast queries but produce false positives (10-20% typical):
// Block bloom indicates potential match
if ( blockHeader . logsBloom . contains ( targetAddress )) {
// Fetch block logs and verify
const blockLogs = await provider . getLogs ({
blockHash: blockHeader . hash ,
address: targetAddress ,
});
// Actual logs may be empty (false positive)
console . log ( `Block has ${ blockLogs . length } actual logs` );
}
Efficient Filtering
// Inefficient: Filter in JavaScript
const filtered = allLogs . filter ( log =>
log . matchesAddress ( targetAddress ) &&
log . matchesTopics ([ TRANSFER_SIG , null , userHash ])
);
// Efficient: Use filterLogs (optimized internally)
const filtered = EventLog . filterLogs ( allLogs , {
address: targetAddress ,
topics: [ TRANSFER_SIG , null , userHash ],
});
Resources
Next Steps