Estimated time: 6 minutes read
What are Solidity events?
Solidity events are data structures that are stored in the transaction log, a special section of the blockchain that is accessible as long as the block containing the transaction that emitted the event remains in place.
Each event is associated with the address of the smart contract that emitted it.
Solidity events are used to record data that is accessible for reading by external clients (they can even subscribe to specific events to receive new occurrences), such as decentralized applications or a centralized backend.
Since events cannot be accessed from within smart contracts, their storage cost on the blockchain (gas) is relatively low. For this reason, they are used to store a wide variety of information that reflects milestones achieved during the execution of smart contracts. They are also sometimes used to complement data stored within the smart contracts themselves, recording large data structures at a low cost.
How are events created in Solidity?
Events are defined syntactically using the keyword “event”, allowing parameters similar to custom errors in Solidity, and can be declared at the file level or within conventional contracts, interfaces, and even libraries.
An example of an event emitted in an ERC721 token contract would be the following:
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
It is observed that in the definition of parameters, the keyword `indexed` can be added. This allows them to be placed in the special topics section instead of the common data section. These parameters, considered event topics, can be used as filters by external clients when searching for events emitted on the blockchain, making the task easier and faster.
Each topic can only store 32-byte words; if referenced types are used as indexed parameters (e.g., a string), a hash (keccak-256) will be stored instead.
An event can contain up to 4 topics, with topic 0 by default always being the hash of the “event signature” (the event name and the types of its parameters); this way, filtering by the event name is always allowed.
Thus, there is space to define up to 3 indexed parameters in each event. It would be possible to omit topic 0 in order to have up to 4 indexed parameters. This can be achieved through anonymous events.
For the previous example:
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId) anonymous;
When emitting an anonymous event, there will be no record of its name, making filtering and searching for information on the blockchain more difficult. In general, this is not considered a good practice, as it goes against the principle of transparency inherent to the technology.
The other parameters that are not indexed are stored in the event data section of the blockchain in “ABI-encoded” format. These parameters cannot be used as filters in searches.
I recommend reading this Solidity event guide to dive deeper into the technical details.
How to debug events in Hardhat?
The correct emission of events programmed in smart contracts should be checked in the test suites developed.
To debug events using Hardhat, several extensions of the Chai tool are available.
To check that the event from the example is emitted with certain parameters:
it('emits a transfer event', async function () {
await expect(contract.safeTransferFrom(owner, receiver, tokenId))
.to.emit(contract, 'Transfer').withArgs(owner, receiver, tokenId);
});
If you don’t want to specifically check the value of a particular argument, you can use the predicate “anyValue” instead:
it('emits a transfer event', async function () {
await expect(contract.safeTransferFrom(owner, receiver, tokenId))
.to.emit(contract, 'Transfer').withArgs(owner, receiver, anyValue);
});
The functionality of predicates in the Chai tool allows evaluating whether a value meets a certain condition to be considered valid. The predicate “anyValue” always returns “true.”
The following function would check using a predicate if the “tokenId” of the “Transfer” event is even:
function isEven(x) {
return x % 2n === 0n;
}
it('emits a transfer event', async function () {
await expect(contract.safeTransferFrom(owner, receiver, tokenId))
.to.emit(contract, 'Transfer').withArgs(owner, receiver, isEven);
});
This particularity of predicates could also be useful for capturing specific values emitted in an event and assigning them to variables within the tests.
In the following example, the address of a contract created in the call to the smart contract “createContract” is captured through the emitted event “ContractCreated”:
let contractAddress;
getAddress = (address) => {
contractAddress = address;
return true;
}
await expect(contract.createContract())
.to.emit(contract, "ContractCreated")
.withArgs(getAddress, anyValue, anyValue, anyValue);
Another use case would be to ensure that a specific event is not emitted:
it('emits a transfer event', async function () {
await expect(contract.safeTransferFrom(owner, receiver, tokenId))
.to.not.emit(contract, 'Transfer');
});
How to access events from external clients?
There are numerous mechanisms to access information stored on the blockchain through events emitted in smart contracts.
For DApps, depending on the client library used to access the network, specific methods will be available.
The general JSON-RPC call to access emitted events is `eth_getLogs`.
Filtering elements include, among others, the initial and final blocks, the address of the contract emitting the event, and the indexed parameters.
Since this is a call that could return a large amount of information (depending on the filters used), many providers limit the number of results returned or the maximum duration of the query (for example, check the conditions of Alchemy or Infura).
There are other alternatives to access this information, such as those provided by block explorers through their APIs or services from an “indexer” like The Graph.
Other calls and APIs even provide subscription services to receive information about newly emitted events in real-time.
Below are several ways to access events through the viem library in a Hardhat script. It is recommended to follow the guide on integrating viem.
Indexed event parameters are used to filter the results:
let manager = await hre.viem.deployContract("MembershipManager", []);
const groupId = 1;
let hash;
// Calls contract method that emits a ContractCreated event
// Event signature is "event ContractCreated(uint256 indexed id,uint256 indexed groupId,string name)"
hash = await manager.write.createContract([1, groupId, "contract1"]);
await publicClient.waitForTransactionReceipt({ hash });
hash = await manager.write.createContract([2, groupId, "contract2"]);
await publicClient.waitForTransactionReceipt({ hash });
// This method just retrieves the latest emitted event
let contractCreated = await manager.getEvents.ContractCreated();
// This method retrieves every filtered event by groupId parameter
parentCreated = await publicClient.getContractEvents({
address: manager.address,
abi: membershipManagerAbi,
eventName: 'ContractCreated',
fromBlock: 'earliest',
toBlock: 'latest',
args: {
groupId,
}
})
// This method retrieves every filtered event by groupId parameter
parentCreated = await publicClient.getLogs({
address: manager.address,
fromBlock: 'earliest',
toBlock: 'latest',
args: {
groupId,
},
event: parseAbiItem('event ContractCreated(uint256 indexed id,uint256 indexed groupId,string name)')
});
Conclusion: Information Recording to Ensure Reliability and Decentralization
Emitting events in blockchain smart contracts is a mechanism to inform DApps and other external clients about the outcome of transaction executions.
It is also very useful for storing information in a decentralized manner beyond the state of smart contracts, with low gas consumption.
Today, the main drawback lies in the high resource consumption that can be involved in searching for events on the blockchain, depending on the filters used. This can impose limitations both on the provider that enables read access to the blockchain node and on the DApp or external client that must process the returned information.
To mitigate these difficulties, aggregators and indexers have emerged that preprocess the information to return more focused and precise results.
Another potential issue that may arise in the future is the possible purging of events on the blockchain or the implementation of changes that are incompatible with current mechanisms.
Below are some proposals in this regard:
- EIP-7668 (remove bloom filters): A proposal to remove events from the blockchain due to the high cost of accumulated storage over time. It seems to have been rejected.
- EIP-7745 (two-dimensional log filter data structure): A proposal to retain events on the blockchain by improving data structures to optimize the space used. It is currently a work draft.
Do you regularly use events in your smart contracts? What do you use them for? Feel free to share your comments and experiences.
Contact me if you want to develop a decentralized application capable of collecting all its information directly from the blockchain through events and other mechanisms. Thank you very much!