Introduction
Events are crucial in the Ethereum Virtual Machine (EVM) networks. Interestingly, half of the transactions to smart contracts on Scroll are to non-decoded contracts on Dune. To unlock full data insights, you need to submit the ABI (Application Binary Interface) to access decoded tables on Dune but they can still be limited and there handy functions to decode data.
Events are a critical communication mechanism for smart contracts, primarily serving as a bridge between the blockchain and front-end applications. They are logs stored in a specialized data structure within the blockchain, allowing efficient access and filtering by external applications like decentralized apps (dApps) or off-chain services. Each emitted event is intrinsically linked to the smart contract that generated it.
Dune stands out as one of the most prominent blockchain data platforms. With an account, you can gain access to vast amounts of data at your fingertips. Many important contracts are already decoded, providing human-readable data that can be easily analyzed.
The decoding functions
While you might typically rely on the chain.logs_decoded
table to gather logs information, these tables often provide only a superficial overview. They frequently capture just a brief detail about the event being emitted, rather than the complete parameter details.
Anatomy of an EVM transaction
A typical EVM transaction can be sent to two primary types of addresses:
Externally Owned Accounts (EOA) - for simple operations like sending native currency
Smart Contracts - which can execute more complex operations
{
"to": "0xd29687c813d741e2f938f4ac377128810e217b1b",
// [...]
}
When a function executes successfully, the contract is likely to emit events (or logs). This data is stored in tables like scroll.logs
▼Logs Schema Attributes
The logs table typically includes the following key attributes:
block_time
: Timestampblock_number
: Numeric block identifiercontract_address
: Address of the contracttopic0-3
: Indexed event parametersdata
: Additional event datatx_hash
: Transaction hashAnd several other metadata field
A raw log entry might look cryptic:
{
"contract_address": "0xd29687c813d741e2f938f4ac377128810e217b1b",
"topic0":"0xdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a724",
"topic1":"0x000000000000000000000000588faac6b3aa91bb4cd8127ab357515716541f1b",
"topic2":"",
"topic3":"",
"data":"0x00000000000000000000000000000000000000000000000496bda902a80a80000000000000000000000000000000000000000000000000000000000000000000",
}
Decoding Functions
Dune provides two primary decoding functions:
decode_evm_event()
: Decodes logs or eventsdecode_evm_function_call()
: Decodes internal functions or traces
The article focus on the event decoding function.
A typical decoding query follows this structure:
SELECT
*
FROM TABLE(
decode_evm_event(
abi => '{EVENT ABI}',
input => TABLE(
SELECT block_date, topic0, topic1, topic2, topic3, data, tx_hash, tx_from, contract_address
FROM arbitrum.logs
WHERE
contract_address IN
({CONTRACTS})
AND
topic0 = {EVENT SIGNATURE}
)
)
)
To get the event ABI, you need to obtain the contract ABI and find the event functions. You can do this through various methods:
Using a blockchain explorer
Compiling the contract
A typical event looks like this:
{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"voter","type":"address"},{"indexed":false,"internalType":"uint256","name":"proposalId","type":"uint256"},{"indexed":false,"internalType":"uint8","name":"support","type":"uint8"},{"indexed":false,"internalType":"uint256","name":"weight","type":"uint256"},{"indexed":false,"internalType":"string","name":"reason","type":"string"}],"name":"VoteCast","type":"event"}
Concrete Example: Scroll Token (SCR) Transfer Events
Here's a concrete example of generating a query to return Transfer events for the Scroll Token (SCR):
SELECT
block_time,
"from",
"to",
value / POWER(10, 18) AS value_
FROM TABLE(
DECODE_EVM_EVENT(
abi => '{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"}',
input => TABLE(
SELECT
*
FROM scroll.logs
WHERE
contract_address IN (0xd29687c813d741e2f938f4ac377128810e217b1b) -- SCR Token Address
AND topic0 = 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef -- Event Signature
),
null_on_error => FALSE
)
)
Some event attributes are reserved SQL words, such as "from" or "to". To use these, you need to enclose them in double quotes in the attribute name. If you use single quotation marks, you will get the literal attribute.
The results you obtain will provide detailed information about the event.
For each case, you need to consider specific nuances. In this example, it's necessary to divide the "value" attribute by the 18 decimal places of the token to obtain the integer token number.
Now you have all the data for a specific event, which allows you to tweak the query to:
Track how a variable evolves over time
Explore various aspects of your contract
Query Creation Using Python
After building several dashboards using this technique, I found myself doing repetitive work:
Going to the contract address
Ensuring the contract is decoded
Finding event signatures
Copying and pasting into query templates
This process was time-consuming. I realized there must be a better way, so I asked Claude to help me develop a Python script to automate this process.
Key features of the script
Load and parse ABI files
Generate event signatures
Create query templates
Support for multiple blockchain analysis scenarios
Before running the script, remember to install the required dependencies:
pip install keccak
python -m pip install "eth-hash[pycryptodome]"
After creating a virtual environment to use Python without conflicts and installing the required packages, copy the provided script and save it with a descriptive name like evm_abi_parser.py
.
import json
import os
import sys
from eth_utils import keccak
class EVMABIParser:
"""
A comprehensive tool for parsing and working with Ethereum ABI files.
"""
def __init__(self, file_path=None):
"""
Initialize the ABI parser with an optional file path.
Args:
file_path (str, optional): Path to the ABI JSON file.
"""
self.abi_contents = None
self.parsed_abi = None
if file_path:
self.load_abi_from_file(file_path)
def load_abi_from_file(self, file_path):
"""
Load ABI from a JSON file.
Args:
file_path (str): Path to the ABI JSON file.
Returns:
bool: True if loading was successful, False otherwise.
"""
try:
# Expand user home directory if needed
file_path = os.path.expanduser(file_path)
# Check if file exists
if not os.path.exists(file_path):
print(f"Error: File '{file_path}' does not exist.")
return False
# Check file extension
if not file_path.lower().endswith(('.json', '.jsonc')):
print("Error: File must be a JSON or JSONC file.")
return False
# Read and parse the file
with open(file_path, 'r', encoding='utf-8') as file:
try:
self.abi_contents = json.load(file)
# Validate ABI structure
if not isinstance(self.abi_contents, list):
print("Error: ABI must be a JSON array.")
return False
self.parsed_abi = self.parse_abi()
return True
except json.JSONDecodeError as e:
print(f"JSON Parsing Error: {e}")
return False
except Exception as e:
print(f"Unexpected error reading file: {e}")
return False
def parse_abi(self):
"""
Parse the ABI contents and extract functions and events.
Returns:
dict: A dictionary with lists of functions and events.
"""
functions = []
events = []
for item in self.abi_contents:
# Validate each ABI item
if not isinstance(item, dict):
continue
if item.get("type") == "function":
functions.append({
"name": item.get("name", "Unnamed Function"),
"inputs": item.get("inputs", []),
"outputs": item.get("outputs", []),
"stateMutability": item.get("stateMutability", "Unknown")
})
elif item.get("type") == "event":
events.append({
"name": item.get("name", "Unnamed Event"),
"inputs": item.get("inputs", []),
"anonymous": item.get("anonymous", False)
})
return {"functions": functions, "events": events}
@staticmethod
def generate_event_signatures(events):
"""
Generate text and hex signatures for events using Keccak-256.
Args:
events (list): List of event dictionaries.
Returns:
list: List of dictionaries with event signatures.
"""
event_signatures = []
for event in events:
# Generate text signature
input_types = [inp.get('type', '') for inp in event.get('inputs', [])]
text_signature = f"{event['name']}({','.join(input_types)})"
# Generate full topic0 using Keccak-256 of the text signature
# Use eth_utils.keccak for correct Keccak-256 implementation
topic0 = '0x' + keccak(text_signature.encode()).hex()
event_signatures.append({
'name': event['name'],
'text_signature': text_signature,
'topic0': topic0
})
return event_signatures
def display_event_signatures(self):
"""
Display event signatures in a formatted manner.
"""
if not self.parsed_abi or not self.parsed_abi['events']:
print("No events found in the ABI.")
return
event_signatures = self.generate_event_signatures(self.parsed_abi['events'])
print("\n--- Event Signatures ---")
for sig in event_signatures:
print(f"Event: {sig['name']}")
print(f" Text Signature: {sig['text_signature']}")
print(f" Topic0: {sig['topic0']}\n")
def generate_query_template(self, event_name, contract_addresses=None):
"""
Generate a query template for a specific event.
Args:
event_name (str): Name of the event.
contract_addresses (list, optional): List of contract addresses.
Returns:
str: SQL query template or None if event not found.
"""
# Find the event in the parsed ABI
target_event = None
for event in self.abi_contents: # Use self.abi_contents instead of self.parsed_abi['events']
if event.get('name') == event_name and event.get('type') == 'event':
target_event = event
break
if not target_event:
print(f"Event '{event_name}' not found in the ABI.")
return None
# Generate text signature to get topic0
signatures = self.generate_event_signatures([target_event])
topic0 = signatures[0]['topic0']
# Construct query template
contract_address_clause = (
f"contract_address IN ({', '.join(str(addr) for addr in contract_addresses)})"
if contract_addresses
else "1=1"
)
query_template = f"""
SELECT
*
FROM TABLE(
decode_evm_event(
abi => '{json.dumps(target_event)}',
input => TABLE(
SELECT block_date, topic0, topic1, topic2, topic3, data, tx_hash, tx_from, contract_address
FROM scroll.logs
WHERE
{contract_address_clause}
AND
topic0 = {topic0}
)
)
)
"""
return query_template
def main():
"""
Main function to interact with the EVM ABI Parser.
"""
print("EVM ABI Parser and Query Generator")
while True:
print("\nChoose an option:")
print("1. Load ABI file")
print("2. Display Event Signatures")
print("3. Generate Query Template")
print("4. Exit")
choice = input("Enter your choice (1-4): ").strip()
if choice == '1':
file_path = input("Enter the path to your ABI JSON file: ").strip()
parser = EVMABIParser(file_path)
elif choice == '2':
if not parser.parsed_abi:
print("Please load an ABI file first.")
continue
parser.display_event_signatures()
elif choice == '3':
if not parser.parsed_abi:
print("Please load an ABI file first.")
continue
event_name = input("Enter the event name: ").strip()
include_addresses = input("Do you want to specify contract addresses? (y/n): ").strip().lower()
contract_addresses = None
if include_addresses == 'y':
addresses_input = input("Enter contract addresses (comma-separated): ").strip()
contract_addresses = [addr.strip() for addr in addresses_input.split(',')]
query = parser.generate_query_template(event_name, contract_addresses)
if query:
print("\n--- Query Template ---")
print(query)
elif choice == '4':
print("Exiting the script.")
break
else:
print("Invalid choice. Please try again.")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\nOperation cancelled by user.")
sys.exit(0)
Before running the Python script:
Copy the contract's ABI to your clipboard
Save the ABI as a JSON file
Load this file when executing the script
If the contract you are calling is a proxy you still need to use the underlying contract ABI for the proxy, not the proxy ABI.
When we run our query, we can now see the detailed results:
With the base query in place, we now have a powerful starting point to extract useful information about blockchain events. For example, we can modify the query to analyze the distribution of SCR token claims during the Scroll airdrop over time:
Final Thoughts
This script represents an initial proof of concept and requires further refinement as it's not battle-tested. I found that some contracts couldn't be properly decoded like the dmail contract, in which I wasn’t able to get the events as the query generated returned an error on Dune.
Still, I think this script will be helpful, as these functions have been instrumental in my analysis of the Scroll airdrop and the dashboard for governance in Scroll. Among the things that need to be added is support for internal functions using the decode_evm_function_call()
.