6

Here is a very small repo to show the issue: https://github.com/adamdry/ethers-event-issue

But I'll explain it here too. This is my contract:

//SPDX-License-Identifier: UNLICENSED;
pragma solidity 0.8.4;

contract ContractA {

    event TokensMinted(uint amount);

    function mint(uint amount) public {
        emit TokensMinted(amount);
    }

}

And this is my test code:

import * as chai from 'chai'
import { BigNumber, ContractTransaction } from 'ethers'
import { ethers } from 'hardhat'
import { ContractA, ContractAFactory } from '../typechain'

const expect = chai.expect

describe("Example test", function () {
    it("should fire the event", async function () {
        const [owner] = await ethers.getSigners();

        const contractAFactory = (await ethers.getContractFactory(
            'ContractA',
            owner,
        )) as ContractAFactory

        const contractA: ContractA = await contractAFactory.deploy()

        contractA.on('TokensMinted', (amount: BigNumber) => {
            // THIS LINE NEVER GETS HIT
            console.log('###########')
        })

        const contractTx: ContractTransaction = await contractA.mint(123)
        const contractReceipt: ContractReceipt = await contractTx.wait()

        for (const event of contractReceipt.events!) {
            console.log(JSON.stringify(event))
        }
    });
});

I was expecting the ########### to get printed to the console however it doesn't so the listener function isn't being executed for some reason.

If I dig into the ContractReceipt the correct event data is there:

{
  "transactionIndex": 0,
  "blockNumber": 2,
  "transactionHash": "0x55d118548c8200e5e6c19759d9aab56cb2e6a274186a92643de776d617d51e1a",
  "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
  "topics": [
    "0x772f66a00a405709c30e7f18feadcc8f123b20c09c7260165d3eec36c9f21372"
  ],
  "data": "0x000000000000000000000000000000000000000000000000000000000000007b",
  "logIndex": 0,
  "blockHash": "0x808e6949118509b5a9e482e84cf47921a2fcffbcd943ebbd8ce4f6671469ee01",
  "args": [
    {
      "type": "BigNumber",
      "hex": "0x7b"
    }
  ],
  "event": "TokensMinted",
  "eventSignature": "TokensMinted(uint256)"
}
Force Hero
  • 2,674
  • 3
  • 19
  • 35

2 Answers2

8

The full answer is here: https://github.com/nomiclabs/hardhat/issues/1692#issuecomment-905674692

But to summarise, the reason this doesn't work is that ethers.js, by default, uses polling to get events, and the polling interval is 4 seconds. If you add this at the end of your test:

await new Promise(res => setTimeout(() => res(null), 5000));

the event should fire.

However! You can also adjust the polling interval of a given contract like this:

// at the time of this writing, ethers' default polling interval is
// 4000 ms. here we turn it down in order to speed up this test.
// see also
// https://github.com/ethers-io/ethers.js/issues/615#issuecomment-848991047
const provider = greeter.provider as EthersProviderWrapper;
provider.pollingInterval = 100;

As seen here: https://github.com/nomiclabs/hardhat/blob/master/packages/hardhat-ethers/test/index.ts#L642

However! (again) If you want to get the results from an event, the below method requires no changes to the polling or any other "time" based solutions, which in my experience can cause flakey tests:

it('testcase', async() => {
  const tx = await contract.transfer(...args); // 100ms
  const rc = await tx.wait(); // 0ms, as tx is already confirmed
  const event = rc.events.find(event => event.event === 'Transfer');
  const [from, to, value] = event.args;
  console.log(from, to, value);
})

Here is my TypeScriptyfied version (on my own contract with a slightly different event and args):

const contractTx: ContractTransaction = await tokenA.mint(owner.address, 500)
const contractReceipt: ContractReceipt = await contractTx.wait()
const event = contractReceipt.events?.find(event => event.event === 'TokensMinted')
const amountMintedFromEvent: BigNumber = event?.args!['amount']

This is the event declaration in solidity that goes with the above:

event TokensMinted(uint amount);
Force Hero
  • 2,674
  • 3
  • 19
  • 35
0

In case you need to do it only inside the tests, an alternative would be to use Hardhats Chai expect().to.emit() syntax. Docs here

import { expect } from "chai";
import { ethers } from "hardhat";

//...

expect(contractA.mint(123)).to.emit(contractA, "TokensMinted")
  .withArgs(123)

Keep in mind that you still want to use a timeout pooling if you need to fire another event in sequence in the same test. Something like this.

      expect(contractA.mint(123)).to.emit(contractA, "TokensMinted")
        .withArgs(123)

      // wait 5s due to hardhat pooling of 4s
      await new Promise(resolve => setTimeout(resolve, 5000));

      // if you remove the wait above, it will try to burn tokens with 0 balance
      await constractA.burn(123)
   
      // "wait" for the burn to happen
      await new Promise(resolve => setTimeout(resolve, 5000));
      
      // now we can check balances
      expect(contractA.getBalance()).to.be(0)
dav1app
  • 819
  • 8
  • 7