Testing smart contracts in Ethereum requires testing for more than just the expected return values. It is important to verify that the events that should be emitted when the contract is invoked are also occurring as expected. In this article, we’ll explore how to listen and record local Ethereum events while testing with Hardhat to ensure that our smart contracts perform as expected.
Here is a repo of the finished project.
Project Setup
Open the terminal and create a new hardhat project with npx hardhat init
and follow the setup, choosing to create a ‘js project’:

Setup contract
Rename the contacts/Lock.sol file into Bank.sol, and replace the contents with a simple Solidity contract that records a users balance, emitting events when they add and withdraw money:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
contract Bank {
event moneyAdded(uint256 amount);
event moneyWithdrawn(uint256 amount);
mapping(address => uint) userBalance;
function addMoney(uint256 amount) public {
userBalance[msg.sender] += amount;
emit moneyAdded(amount);
}
function withdrawMoney(uint256 amount) public {
require(userBalance[msg.sender] >= amount, "Not enough money");
userBalance[msg.sender] -= amount;
emit moneyWithdrawn(amount);
}
function getBalance(address user) public view returns (uint) {
return userBalance[user];
}
}
Testing
Rename the test/Lock.js file into bank.test.js and add some basic tests to make sure our smart contract works:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Bank", function () {
const eth = (amount) => ethers.utils.parseEther(String(amount));
before(async function () {
[deployer, otherAccount] = await ethers.getSigners();
Bank = await ethers.getContractFactory("Bank");
bank = await Bank.deploy();
await bank.deployed();
});
it('can increase users balance', async function () {
await bank.addMoney(eth(100));
expect(await bank.getBalance(deployer.address)).to.equal(eth(100));
});
it('can decrease users balance', async function () {
await bank.withdrawMoney(eth(100));
expect(await bank.getBalance(deployer.address)).to.equal(0);
});
});
Note: We added a helper function called eth
to shorten converting numbers to eth.
Now if you run npx hardhat test
we should see two passing tests:

Listening to events
To record the events emitted by our contract and store them to read later, we can add a helper function above our ‘describe’ statement called saveEvents
. It will take our transaction, wait for it to be mined and record the receipt. Then it will loop through each event in the receipt and capture the event title + the arguments:
const emittedEvents = [];
const saveEvents = async (tx) => {
const receipt = await tx.wait()
receipt.events.forEach(ev => {
if (ev.event) {
emittedEvents.push({
name: ev.event,
args: ev.args
});
}
});
console.log(`emittedEvents: `, emittedEvents);
}
To save emitted events, we need to store the transaction in a variable tx
and pass that to our function.
const tx = await bank.addMoney(eth(100));
saveEvents(tx);
Let’s add another test to create a bunch of transactions, saving the events from each one:
it('can calculate user deposit history from events', async function () {
saveEvents(await bank.addMoney(eth(20)));
saveEvents(await bank.addMoney(eth(30)));
saveEvents(await bank.withdrawMoney(eth(10)));
saveEvents(await bank.addMoney(eth(50)));
saveEvents(await bank.withdrawMoney(eth(40)));
});
Next we can loop through our saved events, to print a log of user deposits and total balance:
let transactionHistory = "Transaction history:\n---\n";
let userBalance = 0;
emittedEvents.forEach(event => {
if (event.name === 'moneyAdded') {
transactionHistory += `Deposited: $${parseFloat(fromEth(event.args.amount))} \n`;
userBalance += parseFloat(fromEth(event.args.amount));
} else if (event.name === 'moneyWithdrawn') {
transactionHistory += `Withdrew: $${parseFloat(fromEth(event.args.amount))} \n`;
userBalance -= parseFloat(fromEth(event.args.amount));
}
});
transactionHistory += `---\nTotal balance: $${userBalance}`;
console.log('\x1b[36m%s\x1b[0m', transactionHistory);
Note: We added a new function fromEth
which does the reverse of eth
.
Now when we run the test we get the output:

Note: To make the console text blue we used the ANSI escape code ‘\x1b[36m%s\x1b[0m’ in our console.log statement.
Our completed test file now looks like this:
const { expect } = require("chai");
const { ethers } = require("hardhat");
const emittedEvents = [];
const saveEvents = async (tx) => {
const receipt = await tx.wait()
receipt.events.forEach(ev => {
if (ev.event) {
emittedEvents.push({
name: ev.event,
args: ev.args
});
}
});
}
describe("Bank", function () {
const eth = (amount) => ethers.utils.parseEther(String(amount));
const fromEth = (amount) => ethers.utils.formatEther(String(amount));
before(async function () {
[deployer, otherAccount] = await ethers.getSigners();
Bank = await ethers.getContractFactory("Bank");
bank = await Bank.deploy();
await bank.deployed();
});
it('can increase users balance', async function () {
const tx = await bank.addMoney(eth(100));
saveEvents(tx);
expect(await bank.getBalance(deployer.address)).to.equal(eth(100));
});
it('can decrease users balance', async function () {
const tx = await bank.withdrawMoney(eth(100));
saveEvents(tx);
expect(await bank.getBalance(deployer.address)).to.equal(0);
});
it('can calculate user deposit history from events', async function () {
saveEvents(await bank.addMoney(eth(20)));
saveEvents(await bank.addMoney(eth(30)));
saveEvents(await bank.withdrawMoney(eth(10)));
saveEvents(await bank.addMoney(eth(50)));
saveEvents(await bank.withdrawMoney(eth(40)));
let transactionHistory = "Transaction history:\n---\n";
let userBalance = 0;
emittedEvents.forEach(event => {
if (event.name === 'moneyAdded') {
transactionHistory += `Deposited: $${parseFloat(fromEth(event.args.amount))} \n`;
userBalance += parseFloat(fromEth(event.args.amount));
} else if (event.name === 'moneyWithdrawn') {
transactionHistory += `Withdrew: $${parseFloat(fromEth(event.args.amount))} \n`;
userBalance -= parseFloat(fromEth(event.args.amount));
}
});
transactionHistory += `---\nTotal balance: $${userBalance}`;
console.log('\x1b[36m%s\x1b[0m', transactionHistory);
});
});
Conclusion
A major benefit of listening to and reading events is that we don’t have to spend gas reading and writing to storage on the blockchain, so we can use events as a cheap form of storage for large amounts of data.
Services such as Alchemy and Moralis provide an SDK to capture recorded events on the blockchain, but this is a handy way to emulate that in the testing environment.
Further Reading
- Hardhat documentation
- A Guide to Events and Logs in Ethereum Smart Contracts
- How to Listen to Smart Contract Events Using Ethers.js
Leave a Reply