A guide to emitting events in Solidity
An in-depth guide to events in Solidity and events on the EVM/Ethereum smart contracts
Table of Contents for A guide to emitting events in Solidity
Events are a very useful feature of smart contracts on the ethereum blockchain.
You can emit events (which can be shown when looking at a transaction on sites like etherscan, or listened to by JS libraries such as web3/ethers).
Emitted events are stored on the blockchain, and are easy to access/read when looking at a smart contract event - but smart contracts themselves cannot see emitted events (for their own contract or other contracts).
There are two main parts - defining it and emitting it.
How to define an event in Solidity
define the type of event. This has two parts:
- the event name itself (
yourEventName
in the following example) - and an optional list of parameters (with
indexed
oranonomous
modifiers), such as a number (uint) in the following case:
event YourEventName(uint anEventParameter);
event TransferSomething(address indexed from, address indexed to, uint256 amount);
The second example event (above) has 3 params - two are address
, one is a uint256
.
The two address ones also have indexed
(more on that later).
Where to define events in Solidity - these normally go at the top of your smart contract.
Note: each param for an event is stored as a separate 32 byte bit of data. So storing less than uint256 (e.g. uint8 or even bool) doesn’t save data.
Under the hood it calls opcodes such as log4
- which logs 4 ‘topics’. The first topic is the keccak256 hash of the event signature. Each topic is 32 bytes. (there are other opcodes such as log3
etc)
Using (emitting) an event in Solidity
Once an event is defined, you can emit it in your functions:
emit yourEventName(1234);
Full example of a smart contract using events in Solidity
pragma solidity ^0.8.16;
contract CryptoGuideDevExample {
uint256 public myCounter = 0;
event Increment(address whoIncremented, uint256 currentValue);
function addToCounter() public {
emit Increment(msg.sender, myCount);
value += 1;
}
}
In the Solidity example above, when addToCounter()
is called, it will emit the Increment
event. that event contains two parameters - who incremented it (the address of who called the function), and the current value (before incrementing the value)
What are indexed events
You can add the indexed
keyword when defining event parameters in Solidity:
event MyEvent(
uint256 indexed amount,
address from,
address indexed to
);
This makes it much easier for you to search for events (in an off-chain application).
You can have up to three indexed parameters for a Solidity event.
The indexed params are stored in a concept known as topics instead of the normal data part of the event log.
This can help to access them later (you can search for them).
If there is an indexed param where the type is a reference type (which is larger than 32 bytes - for example strings), then what gets stored as the topic is the keccack256 hash of the data.
Note: non indexed attributes are encoded (abi-encoded) and stored on the data part of the event log.
How to get the keccak256 hash selector for an event
You can use the event.selector
(e.g. myEvent.selector
), which returns a bytes32
hash.
For a guide on function selectors, see here
How the event logging functions work (deep dive)
There are five EVM opcodes for storing event logs: log0
, log1,
log2,
log3,
log4`.
(note: when you write solidity you don’t need to use them, you just call emit YourEvent(param1)
, but under the hood it will translate it to these opcodes. For just learning Solidity you don’t need to read this deep dive section).
log1
(costs 375 gas) has 2 params - offset
, size
. This adds a event log record, with no topics. (remember: topics = indexed params).
log1
costs more : 750 gas. And it adds another param, for the first (and only topic) which is a 32 byte value.
log2
costs 1125 gas, log3
costs 1500 and finally log4
costs 1875.
(note: it also costs gas to use the memory, at 3 gas per byte, and gas per byte of log data at 8 gas).
The offsets are used for the main data
(non indexed params).
Strings/bytes can only be more than 32 bytes in event logs if they’re indexed. As as explained above, then the keccack256 digest is stored (which is a pointer to where the data exists).
If you work in Solidity, like most of us, then you can have at most 3 topics (topics = indexed params). But really the EVM supports 4 topics. Solidity uses the first topic as the event’s signature. (this is indexed - so this is why you can filter by event type).
The simplest way to add an event log is with log0
. This logs data
, but has no topics
(indexed params).
log0(0xfefefe);
// log0(offset, size);
// log without topics and data mem[offset...(offset+size))
This will log data (from offset
, to offset+size
)
But if you want to start using topics, then it gets more interesting
Remember, topics are 32 bytes of data. You send the data (offset, size), and also 32 bytes for each topic.
e.g. log2
would have 3 args - first is for the data offset/size, then two 32 byte topics
log2(0xfefefe, 0x00000001, 0x11111110);
Spotted a typo or have a suggestion to make this crypto dev article better? Please let me know!
Previous post
📙 Solidity Auditing online quiz
Learn how to audit smart contracts by looking at some example code and trying to find the bugs
⛽ Solidity Gas Optimizations Guide
How to optimize and reduce gas usage in your smart contracts in Solidity
🧪 Guide to testing with Foundry
Guide to adding testing for your Solidity contracts, using the Foundry and Forge tools
📌 Guide to UTXO
UTXO and the UTXO set (used by blockchains such as Bitcoin) explained
📐 Solidity Assembly Guide
Introduction guide to using assembly in your Solidity smart contracts
📦 Ethereum EOF format explained
Information explaining what the upcoming Ethereum EOF format is all about