Events
Solidity events let contracts log to the blockchain. Cheaper than storage; indexed for off-chain searching. Front-ends and indexers (TheGraph, Subsquid) subscribe to these to build user-facing data.
Define, emit, index, subscribe
EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract Marketplace {
// 1) Define events — 'indexed' fields are searchable; max 3 per event
event ItemListed(uint256 indexed id, address indexed seller, uint256 price);
event ItemSold (uint256 indexed id, address indexed buyer, uint256 price);
event ItemDelisted(uint256 indexed id);
// Bulk events — for analytics
event Stat(string indexed kind, uint256 value);
struct Item { address seller; uint256 price; bool active; }
mapping(uint256 => Item) public items;
uint256 public nextId;
function list(uint256 price) external {
require(price > 0, "price must be > 0");
uint256 id = ++nextId;
items[id] = Item(msg.sender, price, true);
emit ItemListed(id, msg.sender, price);
}
function buy(uint256 id) external payable {
Item storage it = items[id];
require(it.active, "not for sale");
require(msg.value == it.price, "wrong amount");
it.active = false;
(bool ok, ) = it.seller.call{value: msg.value}("");
require(ok, "payment failed");
emit ItemSold(id, msg.sender, it.price);
}
function delist(uint256 id) external {
require(items[id].seller == msg.sender, "not seller");
items[id].active = false;
emit ItemDelisted(id);
}
}
// === Off-chain — ethers.js v6 ===
import { ethers } from 'ethers';
const provider = new ethers.JsonRpcProvider(RPC_URL);
const contract = new ethers.Contract(ADDRESS, ABI, provider);
// 2) Listen for new events
contract.on('ItemListed', (id, seller, price, event) => {
console.log('listed', id, seller, ethers.formatEther(price));
});
// 3) Listen with a filter — only the seller you care about
const filter = contract.filters.ItemListed(null, sellerAddress);
contract.on(filter, (id, seller, price) => { /* … */ });
// 4) Query historical events
const from = (await provider.getBlockNumber()) - 50_000;
const events = await contract.queryFilter('ItemListed', from, 'latest');
for (const e of events) {
console.log(e.args.id, e.args.price);
}
// 5) Decode a raw log (e.g. when reading from a tx receipt)
const receipt = await provider.getTransactionReceipt(txHash);
for (const log of receipt.logs) {
try {
const parsed = contract.interface.parseLog(log);
if (parsed?.name === 'ItemSold') console.log('sold', parsed.args);
} catch { /* not our event */ }
}
// === Indexing at scale — TheGraph subgraph ===
// subgraph.yaml
// dataSources:
// - kind: ethereum/contract
// name: Marketplace
// network: mainnet
// source:
// address: '0x...'
// abi: 'Marketplace'
// startBlock: 12345678
// mapping:
// kind: ethereum/events
// apiVersion: 0.0.7
// language: wasm/assemblyscript
// entities: [Listing, Sale]
// eventHandlers:
// - event: 'ItemListed(indexed uint256, indexed address, uint256)'
// handler: 'handleListed'
// - event: 'ItemSold(indexed uint256, indexed address, uint256)'
// handler: 'handleSold'
// file: ./src/mapping.ts
// === Gas hints ===
// • Events cost ~375 gas + 8 gas/byte + 375 per indexed topic
// • Storing the same data in state is ~20k gas per write — events are 50x cheaper
// • Don't put primary data in events — they're discoverable but not contract-readable
// === Best practices ===
// • Index the fields users + dashboards filter on (id, owner, kind)
// • Include enough context that off-chain subscribers don't need a follow-up read
// • Emit on every state change worth observing — UIs depend on it
// • Version events when ABIs change (NewEventV2) — don't reuse a signature
Why it matters
Events are the contract’s API to the off-chain world. Get them right and your UI / indexer needs ~zero RPC calls; get them wrong and every component re-fetches state by hand. Index the fields users filter on; emit on every state change.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
event Transfer(address indexed from, address indexed to, uint256 amount);
function send(address to, uint256 amount) external {
// …
emit Transfer(msg.sender, to, amount);
}
Try it Yourself »
Exercise
Emit an event named Transfer.
Transfer(msg.sender, to, amount);
Four letters.
Discussion
Loading…