State changing smart contract functions cannot return any values when they are called by an externally owned address, they return a transaction hash. However, we still need a way to provide important data regarding that function call to the outside world – for example, to a web3 application that called such a function on behalf of an externally owned user account.

Events allow us to log data to the transaction log. The transaction log is a specific data structure on the blockchain that is associated with the address of the smart contract. The transaction log also acts as a cheap data storage on the blockchain. Storing data in state variables is very expensive, emitting an event (and therefore storing the data in the transaction log) is however very cheap.

It is important to note that the transaction log cannot be accessed from within your smart contract. A client application can subscribe and listen to events and it is also possible to query the transaction log for specific events using libraries like web3.js or ethers.js. A common use case is to listen to specific events and to update the user interface accordingly.

Declaring and emitting events in a Solidity smart contract:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract Events {
    mapping (address => uint) private balances; 

    event Deposit(address indexed user, uint amount);   

    function deposit() payable public {
        require(msg.value > 0, "No ETH was transferred");

        balances[msg.sender] += msg.value;

        emit Deposit(msg.sender, msg.value);
    }
}

We declare an event using the “event” keyword and we can also provide a list of arguments. To log an event (as in our deposit function), we are using the “emit” keyword, the name of the event we want to log and the required argument values – in our case, the address that called the deposit function and the amount that was deposited.

The transaction log…

The value of those arguments is stored in the transaction log. With the first argument (user), we are specifying the “indexed” keyword. That’s what’s called an indexed argument and it allows us to search all entries in the transaction log for a specific address. The second argument (amount) is a normal argument, which cannot be searched for.

Later on, a client application, could ask for all events that have been logged on contract 0x456… and that contain the address 0x123… , but we cannot search the transaction log for events that contain deposits of, let’s say 1 ETH.

Indexed event parameters…

We are allowed to define a maximum of 3 indexed parameters. Indexed parameters are stored on a specific location in the transaction log, called topics. Each topic is limited to 32 bytes (one word). All other parameters are ABI-encoded and stored on the data section of the transaction log.

To interpret those encoded event arguments, when we are querying the transaction log from a client application, we need to know their data types. With that information, we can easily decode the argument values. But, usually that’s not a problem, because we should always be able to access the ABI of whatever smart contract we are working with and by using a library like ethers.js or web3.js, all the complex tasks will be handled for us.

One final thing about topics: The event signature itself is also stored in the transaction log as a topic.

Example of a transaction log…

Deploy the sample contract listed above to the Goerli network from Remix. Once the contract has been successfully deployed, call the “deposit” function, check the transaction details on https://goerli.etherscan.io and click on the “Logs” tab.

You should see something similar as in the image below:

https://goerli.etherscan.io/tx/0x6cb913d83da91f338c3cd5af033aea09d316239a737f71a4e08a514991a7fd62#eventlog

The transaction log displays the address of the associated smart contract (the sample contract you just deployed), below you can see 2 topics and at the bottom you see the “Data” section of the transaction log that contains the value of the amount parameter – 5 wei in our example.

The second topic is the address (20 bytes) of the account that called the “deposit” function prefixed with 24 leading zeroes (12 bytes) to provide 32 bytes / 1 word data structure for the topic.

The first topic is the hash of the event signature. To be exact, it is the keccack256 hash of the following string: Deposit(address,uint256)

To get the exact same hash value as listed for topic 0, we need to respect upper and lower case characters for the event name. Also, we are not allowed to specify any whitespaces, argument names or the indexed keyword in the argument list. Furthermore, instead of writing uint for our data type (as in the smart contract), we need to be exact and use uint256 instead.

But, honestly, you don’t need to care about all those details, because a client-side library like ethers.js or web3.js will do all that work for you. It is just important to know, that the first topic will always be the hash of your event signature. Topic1 will contain the data of your first indexed parameter, Topic2 will contain the data of your second indexed parameter, and Topic3 will contain the data of your third indexed parameter (if you have that many indexed parameters).