Back to top
Published on -
January 11, 2024
Written by:

How Do You Construct Smart Contract Events?

In this blog post, we'll learn about smart contract events covering everything from their purpose to their implementation.

Smart contracts on Ethereum and Ethereum virtual machine-compatible (EVM) blockchains communicate with the outside world primarily through transactions and events. While transactions execute the contract's logic and modify the state, events are an efficient and expressive way to log information about these state changes.

In this blog post, we'll learn about smart contract events covering everything from their purpose to their implementation.

TL;DR
  • ​​Smart contracts on Ethereum use events to communicate state changes to external applications.
  • Events are logged in the blockchain and can have indexed or non-indexed parameters.
  • To construct events in Solidity, declare the event, emit it within a function, and capture it using external applications. Consistency in event naming and parameter usage is important for easy aggregation by DApps and third party services.

This blogpost includes a guided tutorial on how to construct and emit events in a smart contract in Solidity, and then interact with them using the MultiBaas blockchain middleware in 5-10 minutes.

What are events?

Events in Solidity are a way to emit data from a smart contract that can be captured by external applications, such as front end or backend web or mobile applications. They allow smart contracts to emit structured logs that capture specific data about state changes, like a token transfer or a change of ownership, providing a way to notify external systems and applications about important events within the blockchain.

Events are useful for observing the effects of a blockchain state change. Since the blockchain is eventually consistent, the alternative would be to poll the blockchain for updates. For example, for a DApp that displays token balances, you would have to query the balance for every account on every new block, which is extremely inefficient. By monitoring events specific to a smart contract, you can observe or be notified of exactly which account balances have changed. Another way to think of events is as time series data. In this context, all of the considerations around storing, aggregating, etc. of time series data can apply.

Smart contracts are not interactive, and so don’t offer many techniques to communicate to the outside, be it users or the blockchain. In fact, pretty much the only way is through emitting events and thereby creating logs on blockchain.

This is how an example event looks like:

event Deposit(
   address indexed _from,
   bytes32 indexed _id,
   uint _value
);

There are three parameters here, two of type indexed, one not indexed.

Limitations of indexed parameters:

  1. There can be up to 3 parameters that are indexed.
  2. If the type of an indexed parameter is larger than 32 bytes (i.e. string and bytes), the actual data isn’t stored, but rather a Keccak256 hash of the data is stored.
Indexed vs. non-indexed event parameters

Events are abstractions on top of the EVM's low-level logging functionality opcodes LOG0 to LOG4. The opcode number depends on the number of topics the event declares using the indexed keyword. A topic is just a variable that we want to be included in the event and tells Solidity we want to be able to filter on the variable as well. [reference]

Topics are indexed parameters to an event. topic[0] always refers to the hash of the event itself, and can have up to 3 indexed arguments, which will each be reflected in the topics.

The LOG0 to LOG4 opcodes create log records. [reference]

Each log record consists of both topics and data. Topics are 32-byte (256 bit) “words” that are used to describe what’s going on in an event. Different opcodes (LOG0 … LOG4) are needed to describe the number of topics that need to be included in the log record. A LOG0 entry includes the ABI-encoded event signature and the event data, but it doesn't have any additional indexed topics. LOG1 includes one topic, LOG2 includes two topics, LOG3 includes three topics, and LOG4 includes four topics. Therefore, the maximum number of topics that can be included in a single log record is four. [reference].

[image reference]

The first two columns of this table show you the bytecode associated with this operation, and the opcode that describes it. Then, there is a graph indicating what values of the stack the operation uses. The columns further to the right explain how they are used.
Let’s take a look at what information a log contains in Etherscan:

  • Address is a hexadecimal representation of an Ethereum address that refers to the address of the contract or account.
  • Name is the name of the function within a smart contract followed by function signature. It suggests a function called "Transfer" that takes three parameters: "from" (an address), "to" (another address), and "value" (a uint256 type, representing some numerical value).
  • Topics are the indexed parameters associated with an event. The first topic (index 0) is a function selector, and the second topic (index 1) represents the event "Transfer". The subsequent topics represent addresses and values related to the event.
  • Data refers to the ABI encoded non-indexed parameters of the event. In this case, the "value" is set to 0, indicating that the event "Transfer" occurred with a value of 0.

This is how Transaction Explorer looks like on MultiBaas, we can see all the event details including original function signature, decoded event parameters, link to emitting contract, and function arguments.

Hashed event parameters

You can add the indexed attribute to up to three parameters which adds them to a special data structure known as “topics” instead of the data part of the log. If you use arrays (including string and bytes) as indexed arguments, its Keccak-256 hash is stored as a topic instead, this is because a topic can only hold a single word (32 bytes). [reference]

All parameters without the indexed attribute are ABI-encoded into the data part of the log. [reference]

Manual decoding of event parameters

When an event has non-indexed parameters and you want to interpret the data logged in the event, you need to manually decode them. This involves ABI encoding the parameters (converting them to the standardized format) and then using the contract's ABI (Application Binary Interface) to decode them back into their original types.

For example: Let's say you have an event with a non-indexed string parameter. To decode it, you would ABI encode the string and then use the contract's ABI to decode it back into the original string. MultiBaas type conversion feature provides users with the ability, on a per-method per-input/output parameter basis, to adjust how the value is interpreted on its way to the blockchain, or adjust the return value on its way back from the blockchain and therefore automatically decode event parameters for you.

Constructing Events in Solidity

In this mini tutorial we will show you how to declare an event, emit an event using MultiBaas, and listen for events.

Let’s first setup our hardhat work environment. Hardhat is an open-source development environment and tool suite for Ethereum smart contract development and deployment. To install, set up the environment and create a new Hardhat project, then follow steps on the Hardhat docs.

Write a smart contract

In your Hardhat project, delete the file under the contracts folder and create a new file with the name Token.sol. This is a simple smart contract that implements a token that can be transferred.

We will create a token called PandaPeso with a symbol $PANDA. You can change the name of the token into whatever you want. This smart contract implements a token that can be transferred, there is a fixed total supply that can’t be changed. Read through the code below (it’s extensively commented) and add it to your newly created Token.sol file:

// SPDX-License-Identifier: UNLICENSED
// Solidity files have to start with this pragma.
// It will be used by the Solidity compiler to validate its version.
pragma solidity ^0.8.0;
// This is the main building block for smart contracts.
contract Token {
    // Some string type variables to identify the token.
    string public name = "PandaPeso";
    string public symbol = "PANDA";
    // The fixed amount of tokens, stored in an unsigned integer type variable.
    uint256 public totalSupply = 69420;
    // An address type variable is used to store ethereum accounts.
    address public owner;
    // A mapping is a key/value map. Here we store each account's balance.
    mapping(address => uint256) balances;
    // The Transfer event helps off-chain applications understand
    // what happens within your contract.
    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    /**
     * Contract initialization.
     */
    constructor() {
        // The totalSupply is assigned to the transaction sender, which is the
        // account that is deploying the contract.
        balances[msg.sender] = totalSupply;
        owner = msg.sender;
    }
    /**
     * A function to transfer tokens.
     *
     * The `external` modifier makes a function *only* callable from *outside*
     * the contract.
     */
    function transfer(address to, uint256 amount) external {
        // Check if the transaction sender has enough tokens.
        // If `require`'s first argument evaluates to `false` then the
        // transaction will revert.
        require(balances[msg.sender] >= amount, "Not enough tokens");

        // Transfer the amount.
        balances[msg.sender] -= amount;
        balances[to] += amount;

        // Notify off-chain applications of the transfer.
        emit Transfer(msg.sender, to, amount);
    }
    /**
     * Read only function to retrieve the token balance of a given account.
     *
     * The `view` modifier indicates that it doesn't modify the contract's
     * state, which allows us to call it without executing a transaction.
     */
    function balanceOf(address account) external view returns (uint256) {
        return balances[account];
    }
}

Deploy the smart contract on the testnet

Now, we could write a deploy script and deploy the contract from the terminal, but there is an easier way using MultiBaas. MultiBaas will help us easily deploy and interact with our smart contract without writing extra code!

Let’s first create an account – head to MultiBaas and sign up for an account. Create a new deployment on the Sepolia testnet.

Important: You can’t change the network later. You can only create a new deployment and reupload the smart contract. Select the free deployment type and click on Create.

Now that we created our deployment, go ahead and log in to your deployment. What we’re going to do now is upload Token.sol. Click on + New Contract to upload a smart contract file. MultiBaas will compile it for you.

We will also need some Sepolia test ETH to pay for the gas. You can get it from the Infura faucet here.

After we have successfully uploaded a smart contract we will deploy it. Make sure your wallet is connected and just click on Deploy to confirm the transaction from the wallet pop-up. We can see how much gas the contract deployment cost, as well as the contract address.

Emit an event on the testnet using MultiBaas

Let’s trigger an event by calling a contract function that emits an event. At your contract page, if you scroll down you can see the Events section and that it’s empty right now. Now, let’s trigger the event. At the Methods section, trigger our Transfer event by transferring some PANDA tokens. Add a new wallet address to which you’d like to send tokens and the amount of tokens you want to send. Finally, click on Send Method and confirm the transaction. MultiBaas will listen for the event and catch it.

If we follow the transaction hash we will get to see our triggered event at the bottom of the page with the indexed parameters _from, _to, and _value:

We can see the same transaction hash on Etherscan as well. Here we can see the logs:

And that is how you emit an event. Easy, right? For any questions, please join our Discord.

Gas cost of emitting events

Emitting events does consume gas, but it's significantly less expensive than storage operations. “The reason that a log operation is cheap is because the log data isn’t really stored in the blockchain. Logs, in principle, can be recalculated on the fly as necessary. Miners, in particular, can simply throw away the log data, because future calculations can’t access past logs anyway.

The network as a whole does not bear the cost of logs. Only the API service nodes need to actually process, store, and index the logs.

So the cost structure of logging is just the minimal cost to prevent log spamming.” [reference]

How many events can be emitted in a function?

Technically, a function can emit multiple events. The primary limitation here is the block gas limit. As long as the total gas used by the function, including all the events, stays under the block's gas limit, you're good to go.

What goes in smart contract state vs. events

The rule of thumb:

  • Smart Contract: Data that needs to be accessed within the smart contract's functions or made available to other smart contracts needs to be stored in the contract's data and variables. Examples include an account's token balance or the owner of an NFT.
  • Events: Events can be used to log actions or state changes that occur within the smart contract that would be useful for an external application to track but are not necessary for the contract's functions to execute. An example would be a transfer of tokens. It is useful for a user application to show the transfer history but the contract internally only needs to know the current balance. It's best not to overload your contract with too many events, but rather focus on emitting events that are of primary importance or would be relevant for your application
Conclusion

In essence, smart contract events in Ethereum act as a messaging system between contracts and the outside world, crucial for notifying external applications of specific blockchain events.

Further reading

For those who want to dive deeper into the logging functionality of events, this blog post is an excellent resource: How Solidity Events Are Implemented — Diving Into The Ethereum VM Part 6