#Overview
@nomicfoundation/hardhat-chai-matchers adds Ethereum-specific capabilities to the Chai assertion library, making your smart contract tests easy to write and read.
Among other things, you can assert that a contract fired certain events, or that it exhibited a specific revert, or that a transaction resulted in specific changes to a wallet's Ether or token balance.
WARNING
The hardhat-chai-matchers
plugin is designed to work with hardhat-ethers
. Attempting to use it in conjunction with hardhat-viem
results in compatibility issues.
# Installation
npm install --save-dev @nomicfoundation/hardhat-chai-matchers
npm install --save-dev @nomicfoundation/hardhat-chai-matchers
yarn add --dev @nomicfoundation/hardhat-chai-matchers
# How can I use it?
Simply require("@nomicfoundation/hardhat-chai-matchers")
in your Hardhat config and then the assertions will be available in your code.
A few other helpers, such as argument predicates and panic code constants, must be imported explicitly. These are discussed below.
# Why would I want to use it?
#Events
You can easily write tests to verify that your contract emitted a certain event. For example, await expect(contract.call()).to.emit(contract, "Event")
would detect the event emitted by the following Solidity code:
contract C {
event Event();
function call () public {
emit Event();
}
}
Note that the await
is required before an expect(...).to.emit(...)
, because the verification requires the retrieval of the event logs from the Ethereum node, which is an asynchronous operation. Without that initial await
, your test may run to completion before the Ethereum transaction even completes.
Also note that the first argument to emit()
is the contract which emits the event. If your contract calls another contract, and you want to detect an event from the inner contract, you need to pass in the inner contract to emit()
.
#Events with Arguments
Solidity events can contain arguments, and you can assert the presence of certain argument values in an event that was emitted. For example, to assert that an event emits a certain unsigned integer value:
await expect(contract.call())
.to.emit(contract, "Uint")
.withArgs(3);
Sometimes you may want to assert the value of the second argument of an event, but you want to permit any value for the first argument. This is easy with withArgs
because it supports not just specific values but also predicates. For example, to skip checking the first argument but assert the value of the second:
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
await expect(contract.call())
.to.emit(contract, "TwoUints")
.withArgs(anyValue, 3);
Predicates are simply functions that, when called, indicate whether the value should be considered successfully matched or not. The function will receive the value as its input, but it need not use it. For example, the anyValue
predicate is simply () => true
.
This package provides the predicates anyValue
and anyUint
, but you can easily create your own:
function isEven(x: bigint): boolean {
return x % 2n === 0n;
}
await expect(contract.emitUint(2))
.to.emit(contract, "Uint")
.withArgs(isEven);
#Reverts
You can also easily write tests that assert whether a contract call reverted (or not) and what sort of error data to expect along with the revert.
The most simple case asserts that a revert happened:
await expect(contract.call()).to.be.reverted;
Or, conversely, that one didn't:
await expect(contract.call()).not.to.be.reverted;
A revert may also include some error data, such as a string, a panic code, or a custom error, and this package provides matchers for all of them.
The revertedWith
matcher allows you to assert that a revert's error data does or doesn't match a specific string:
await expect(contract.call()).to.be.revertedWith("Some revert message");
await expect(contract.call()).not.to.be.revertedWith("Another revert message");
The revertedWithPanic
matcher allows you to assert that a revert did or didn't occur with a specific panic code. You can match a panic code via its integer value (including via hexadecimal notation, such as 0x12
) or via the PANIC_CODES
dictionary exported from this package:
const { PANIC_CODES } = require("@nomicfoundation/hardhat-chai-matchers/panic");
await expect(contract.divideBy(0)).to.be.revertedWithPanic(
PANIC_CODES.DIVISION_BY_ZERO
);
await expect(contract.divideBy(1)).not.to.be.revertedWithPanic(
PANIC_CODES.DIVISION_BY_ZERO
);
You can omit the panic code in order to assert that the transaction reverted with any panic code.
The revertedWithCustomError
matcher allows you to assert that a transaction reverted with a specific custom error:
await expect(contract.call()).to.be.revertedWithCustomError(
contract,
"SomeCustomError"
);
Just as with events, the first argument to this matcher must specify the contract that defines the custom error. If you're expecting an error from a nested call to a different contract, then you'll need to pass that different contract as the first argument.
Further, just as events can have arguments, so too can custom error objects, and, just as with events, you can assert the values of these arguments. To do this, use the same .withArgs()
matcher, and the same predicate system:
await expect(contract.call())
.to.be.revertedWithCustomError(contract, "SomeCustomError")
.withArgs(anyValue, "some error data string");
Finally, you can assert that a call reverted without any error data (neither a reason string, nor a panic code, nor a custom error):
await expect(contract.call()).to.be.revertedWithoutReason();
#Big Numbers
Working with Ethereum smart contracts in JavaScript can be annoying due Ethereum's 256-bit native integer size. Contracts returning integer values can yield numbers greater than JavaScript's maximum safe integer value, and writing assertions about the expectations of such values can be difficult without prior familiarity with the 3rd-party big integer library used by your web3 framework.
This package enhances the standard numerical equality matchers (equal
, above
, within
, etc) such that you can seamlessly mix and match contract return values with regular Number
s. For example:
expect(await token.balanceOf(someAddress)).to.equal(1);
These matchers support not just the native JavaScript Number
, but also BigInt
, bn.js, and bignumber.js.
#Balance Changes
Oftentimes, a transaction you're testing will be expected to have some effect on a wallet's balance, either its balance of Ether or its balance of some ERC-20 token. Another set of matchers allows you to verify that a transaction resulted in such a balance change:
await expect(() =>
sender.sendTransaction({ to: someAddress, value: 200 })
).to.changeEtherBalance(sender, "-200");
await expect(token.transfer(account, 1)).to.changeTokenBalance(
token,
account,
1
);
Further, you can also check these conditions for multiple addresses at the same time:
await expect(() =>
sender.sendTransaction({ to: receiver, value: 200 })
).to.changeEtherBalances([sender, receiver], [-200, 200]);
await expect(token.transferFrom(sender, receiver, 1)).to.changeTokenBalances(
token,
[sender, receiver],
[-1, 1]
);
#Miscellaneous String Checks
Sometimes you may also need to verify that hexadecimal string data is appropriate for the context it's used in. A handful of other matchers help you with this:
The properHex
matcher asserts that the given string consists only of valid hexadecimal characters and that its length (the number of hexadecimal digits) matches its second argument:
expect("0x1234").to.be.properHex(4);
The properAddress
matcher asserts that the given string is a hexadecimal value of the proper length (40 hexadecimal digits):
expect("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266").to.be.a.properAddress;
The properPrivateKey
matcher asserts that the given string is a hexadecimal value of the proper length:
expect("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80").to
.be.a.properPrivateKey;
Finally, the hexEqual
matcher accepts two hexadecimal strings and compares their numerical values, regardless of leading zeroes or upper/lower case digits:
expect("0x00012AB").to.hexEqual("0x12ab");
# Known limitations
At the moment, some of these chai matchers only work correctly when Hardhat is running in automine mode. See this issue for more details.
# Dig Deeper
For a full listing of all of the matchers supported by this package, see the reference documentation.