A Special Kind of On-Chain Data

Up until this point of the textbook, all on-chain data has revolved around the smart contract; that is, any information that we associated as living on the blockchain was always tied to an associated contract (i.e. via a state variable). In this section, we will go over events, a different form of on-chain data that allows us to permanently store data without having it be stored within a smart contract.

Motivation: Dummy ERC-20 Token

To understand events, we will introduce the example of a "dummy" ERC-20 token called... Dummy.

contract Dummy {

    function transfer() public {}


Like all ERC-20 tokens, Dummy has the logic necessary to transfer tokens from one address to another, via the transfer function. However, rather than being able to just transfer tokens between addresses, we would also like to keep track of every transfer. Whether you are the IRS or your average on-chain data scrapper, there is good reason to track transfer events.

To start, one might track transfer events via the following principle:

Create a state variable which stores all transfer events (as a list). Furthermore, for each transfer event, append the event to the transfer event list

An updated implementation of the Dummy token with this new principle in mind might look like as follows:

contract Dummy {

    struct TransferEvent {
        address from;
        address to;
        uint amount;

    TransferEvent[] allTransfers;

    function transfer() public {
        address from = msg.sender; // Arbitrary values
        address to = address(0x0);
        uint amount = 1;
        // Transfer logic goes here
        allTransfers.push(TransferEvent(from, to, amount));


Examining each segment of this new Dummy contract in depth:

  • TransferEvent: struct which represents a transfer event. This struct holds the from address, to address, and value transfered.

  • allTransfers: a dynamic array which holds all transfer events

  • transfer: function which executes a transfer. At the end of this function, the transfer event is appended to allTransfers.

Our new implementation of the Dummy token is now able to keep track of every transfer event! However, the biggest issue with this is that this new implementation is egregiously inefficient. Storing all transfer events into a state variable is extremely costly when considering the transfer function might be called thousands of times per day. Are we doomed?


As the last section made it evident, we want a way to store transfer events without having to use any contract data. For this, we can leverage events. The syntax for events is as follows:

event eventName();

Inside the parentheses is where we can list the arguments of our event (i.e. the data that we want logged). In the case of our transfer event, we can define the following:

event Transfer(address from, address to, uint value);

Finally, now that we have our event defined, we can emit our event as follows:

emit Transfer(from, to, value);

Tying all these ideas, the following code is an updated version of our Dummy token which utilizes events to record transfers:

contract Dummy {

    event Transfer(address from, address to, uint value);
    function transfer() public {
        address from = msg.sender;
        address to = address(0x0);
        uint value = 1;
        emit Transfer(from, to, value);


Many might be wondering where events are actually stored? As mentioned previously, events are not stored as state variables; rather, events are stored as part of the transaction receipt. By storing data in this format, users are able to store on-chain data without having to utilize state variables.

Who Can Access Event Data?

Because events are stored as part of the transaction receipt, all nodes (and users with access to a node) can access events. However, this also means that contracts cannot access event data.


Although we showed how to store data in events, we have not shown how to prioritize certain types of data within an event. To understand why we might want to prioritize certain data fields in our events, consider again the Transfer event:

event Transfer(address from, address to, uint value);

Although we will get more into how events are represented when we discuss the EVM, the general structure of events is as follows:

  • Event ID

  • Event Data

With our current implementation of the Transfer event, our event data field is as follows (utilizing arbitrary values for the from, to, and value fields):


Although it is possible to extract the arguments from event data field, most can agree that the data field, as it stands, is a mess. Ideally, we want a way to structure our event data so that we can access the arguments of an event directly without having to do any sort of decoding; that is, we want to prioritize certain arguments. For this, we can mark the arguments we want to prioritize with the indexed label; the following code is an updated of our Transfer event that utilizes indexed arguments:

event Transfer(address indexed from, address indexed to, uint value);

Using indexed arguments, the new structure of our event is as follows:

  • Event ID

  • from Argument: 0x000000000000000000000000Fb2b43852f444DE82643ec58BDc125Ee4EBeDad6

  • to Argument: 0x000000000000000000000000549889FA3522717E7F1665647BA89f58DE8D277d

  • Event Data: 0x0000000000000000000000000000000000000000000000000000000000000001

Using indexed arguments, we can now clearly access the arguments of our event. Note that the structure of an event is dependent on the ordering of the event arguments.

Index Everything?

Although it seems that indexing all arguments is ideal, consider the following:

  • We can only index at most 3 arguments in an event

  • The amount of gas used in emitting an event is dependent on the number of arguments indexed. Therefore, in the case of our transfer event, although we could have indexed all three arguments, it is better from a gas perspective to index only two arguments since we know that the data field contains just the value transferred.


Events in Go Ethereum:

Last updated