šŸ¶
Solidity

Hardhat: Get Events from Transaction Receipts

By Ondřej DolanskĆ½ on 12/02/2024

Learn how to efficiently retrieve and decode event logs emitted within a transaction receipt using Hardhat, a popular Ethereum development framework.

Hardhat: Get Events from Transaction Receipts

Table of Contents

Introduction

When working with smart contracts in a Hardhat development environment, it's often crucial to interact with events emitted during transactions. This guide demonstrates how to retrieve and analyze events from transaction receipts using ethers.js, a popular Ethereum JavaScript library. We'll cover two approaches: directly accessing the events array in the transaction receipt and utilizing the convenience of OpenZeppelin's test helpers for streamlined event assertion in your tests.

Step-by-Step Guide

To get events from a transaction receipt in Hardhat using ethers.js:

  1. Wait for the transaction to be mined:

    const tx = await contract.someFunction();
    const receipt = await tx.wait(); 
  2. Access the events array in the receipt:

    console.log(receipt.events); 
  3. Filter for specific events by name:

    const transferEvents = receipt.events.filter((x) => x.event === 'Transfer');
  4. Access event arguments:

    const from = transferEvents[0].args.from;
    const to = transferEvents[0].args.to;
    const value = transferEvents[0].args.value;

Using OpenZeppelin test helpers:

  1. Install the package:

    npm install @openzeppelin/test-helpers
  2. Import and use expectEvent in your test:

    const { expectEvent } = require('@openzeppelin/test-helpers');
    
    const receipt = await contract.someFunction();
    expectEvent(receipt, 'Transfer', { from: user1.address, to: user2.address });

This approach simplifies event assertion and provides a cleaner way to test your contract's events.

Code Example

This code defines a simple ERC20 token contract named "MyToken" and a corresponding test suite. The contract initializes with an initial supply of tokens minted to the deployer. The test suite verifies the token transfer functionality by transferring tokens between accounts and then demonstrates two methods for accessing and verifying events emitted during the transfer: manual filtering of transaction receipt events and using the expectEvent helper from OpenZeppelin Test Helpers.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor() ERC20("MyToken", "MTK") {
        _mint(msg.sender, 1000 * 10 ** decimals());
    }
}

// test/MyToken.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { expectEvent } = require("@openzeppelin/test-helpers");

describe("MyToken", function () {
    let token;
    let owner;
    let addr1;
    let addr2;

    beforeEach(async function () {
        [owner, addr1, addr2] = await ethers.getSigners();

        const Token = await ethers.getContractFactory("MyToken");
        token = await Token.deploy();
        await token.deployed();
    });

    it("Should transfer tokens between accounts", async function () {
        // Transfer 50 tokens from owner to addr1
        const amount = 50;
        const tx = await token.transfer(addr1.address, amount);
        const receipt = await tx.wait();

        // Access and log all events
        console.log("All events in the receipt:", receipt.events);

        // Filter for Transfer events
        const transferEvents = receipt.events.filter(
            (x) => x.event === "Transfer"
        );
        console.log("Transfer events:", transferEvents);

        // Access event arguments
        const from = transferEvents[0].args.from;
        const to = transferEvents[0].args.to;
        const value = transferEvents[0].args.value;

        expect(from).to.equal(owner.address);
        expect(to).to.equal(addr1.address);
        expect(value).to.equal(amount);

        // Using expectEvent from OpenZeppelin test helpers
        await expectEvent(
            token.transfer(addr2.address, amount),
            "Transfer",
            { from: owner.address, to: addr2.address }
        );
    });
});

Explanation:

  1. Contract: A simple MyToken contract is defined, inheriting from OpenZeppelin's ERC20 contract.
  2. Test Setup:
    • Import necessary libraries: chai for assertions, ethers for interacting with the Ethereum network, and expectEvent from @openzeppelin/test-helpers.
    • Define variables for the token contract, owner, and two additional addresses.
    • In the beforeEach hook:
      • Get signers (accounts) for interacting with the contract.
      • Deploy the MyToken contract.
  3. Test Case:
    • it("Should transfer tokens between accounts", ...): This test case checks the token transfer functionality.
    • Transfer tokens:
      • token.transfer(addr1.address, amount): Transfer amount tokens from the owner to addr1.
      • tx.wait(): Wait for the transaction to be mined and get the transaction receipt.
    • Access and log events:
      • console.log(receipt.events): Log all events emitted in the transaction.
      • Filter for Transfer events and log them.
    • Access event arguments:
      • Extract from, to, and value from the first Transfer event.
      • Use expect assertions to verify the correctness of the event arguments.
    • Using expectEvent:
      • expectEvent(token.transfer(...), "Transfer", ...): This line demonstrates how to use expectEvent to assert that a Transfer event was emitted with the expected arguments.

This example provides a comprehensive overview of how to get events from a transaction receipt in Hardhat using both manual filtering and the expectEvent helper function.

Additional Notes

General:

  • Event Naming: Use descriptive event names to clearly indicate their purpose. This improves code readability and makes debugging easier.
  • Event Arguments: Carefully consider the data you include as event arguments. This data should be sufficient for off-chain applications to understand what happened on-chain.
  • Gas Optimization: While events are helpful for off-chain applications, storing data on-chain consumes gas. Be mindful of the amount of data you emit in events, especially for frequently triggered events.

Directly Accessing Events:

  • Event Order: Events within a transaction receipt are ordered. If multiple events of the same type are emitted in a single transaction, you can access them by their index in the events array.
  • Error Handling: When filtering for specific events, it's good practice to check if the filtered array is empty to avoid errors when accessing elements.

Using OpenZeppelin Test Helpers:

  • Simplified Assertions: expectEvent provides a more readable and concise way to assert event emissions compared to manually filtering and checking event arguments.
  • Advanced Matching: expectEvent allows for more complex matching criteria, such as partial argument matching or using regular expressions. Refer to the OpenZeppelin documentation for more details.

Beyond the Basics:

  • Real-time Event Listening: For real-time event monitoring, you can use the ethers.js event listener functionality. This allows you to subscribe to specific events and execute code when they are emitted.
  • Indexing and Querying Events: For more complex event handling and analysis, consider using an event indexing and querying service like The Graph. These services allow you to efficiently query historical event data.

By understanding these concepts and techniques, you can effectively work with events in your Hardhat projects and build powerful decentralized applications.

Summary

This document outlines two methods for accessing and verifying events emitted by smart contracts during testing with Hardhat and ethers.js:

Method 1: Direct Access from Transaction Receipt

  1. Mine Transaction: Execute the contract function and wait for the transaction to be mined using tx.wait().
  2. Access Events Array: The transaction receipt (receipt) contains an events array holding all emitted events.
  3. Filter Events: Use array methods like filter() to select specific events based on their event property (e.g., 'Transfer').
  4. Extract Arguments: Access event arguments directly from the filtered event object using their respective names (e.g., event.args.from).

Method 2: Simplified Assertion with OpenZeppelin Test Helpers

  1. Install Package: Install the @openzeppelin/test-helpers package.
  2. Import and Use expectEvent: Import the expectEvent function and provide the transaction receipt, expected event name, and expected arguments. This simplifies event assertion and provides cleaner test code.

Benefits of OpenZeppelin Test Helpers:

  • Simplifies event assertion.
  • Improves code readability.
  • Provides a more structured approach to testing events.

Conclusion

In conclusion, understanding how to retrieve and analyze events emitted from your smart contracts is crucial for building interactive and transparent decentralized applications. By utilizing the methods outlined in this guide, developers can effectively work with events in their Hardhat projects, ensuring the proper functioning and auditability of their smart contracts. Whether you choose to directly access events from the transaction receipt or leverage the convenience of OpenZeppelin's test helpers, mastering event handling will undoubtedly prove invaluable throughout your blockchain development journey.

References

Were You Able to Follow the Instructions?

šŸ˜Love it!
šŸ˜ŠYes
šŸ˜Meh-gical
šŸ˜žNo
šŸ¤®Clickbait