Waffle Documentation¶

Waffle is a library for writing and testing smart contracts.
Sweeter, simpler and faster than Truffle.
Works with ethers-js.
Philosophy:¶
Simpler: Minimalistic, few dependencies.
Sweeter: Nice syntax, easy to extend.
Faster: Focus on the speed of tests execution.
Features:¶
Sweet set of chai matchers,
Easy contract importing from npm modules,
Fast compilation with native and dockerized solc,
Typescript compatible,
Fixtures that help write fast and maintainable test suites,
Well documented.
Versions and ethers compatibility¶
Use version
0.2.3
with ethers 3 and solidity 4,Use version
1.2.0
with ethers 4 and solidity 4,Use version
2.*.*
with ethers 4, solidity 4, 5 and ability to use native or dockerized solc.Use version
3.*.*
with ethers 5, solidity 4, 5, 6 and ability to use native, dockerized solc or dockerized vyper.
Getting Started¶
Installation¶
To get started, install ethereum-waffle
:
yarn add --dev ethereum-waffle
npm install --save-dev ethereum-waffle
Add external dependency¶
Add an external library by installing it with yarn or npm:
yarn add @openzeppelin/contracts@^4.6.0 -D
npm install @openzeppelin/contracts@^4.6.0 -D
Writing a contract¶
Below is an example contract written in Solidity. Place it in src/BasicToken.sol
file of your project:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// Example class - a mock class using delivering from ERC20
contract BasicToken is ERC20 {
constructor(uint256 initialBalance) ERC20("Basic", "BSC") {
_mint(msg.sender, initialBalance);
}
}
Compiling the contract¶
Add the following entry in the package.json
of your project :
Note
Since Waffle 3.0.0 it recognises waffle.json
as default configuration file. If your configuration file is called
waffle.json
, it’s possible to use just waffle
to build contracts.
{
"scripts": {
"build": "waffle"
}
}
{
"scripts": {
"build": "waffle waffle.json"
}
}
In the waffle.json
file of your project add the following entry:
{
"compilerType": "solcjs",
"compilerVersion": "0.8.13",
"sourceDirectory": "./src",
"outputDirectory": "./build"
}
Then run the following command:
yarn build
npm run build
You should see that Waffle compiled your contract and placed the resulting JSON
output inside the build
directory.
If you want to know more about how to configure Waffle, see Configuration.
Flattener¶
To flat your smart contracts run:
npx waffle flatten
In configuration file you can add optional field with path to flatten files:
{
"flattenOutputDirectory": "./custom_flatten"
}
Writing tests¶
After you have successfully authored a Smart Contract you can now think about testing it. Fortunately for you, Waffle is packed with tools that help with that.
Tests in waffle are written using Mocha alongside with
Chai. You can use a different test environment,
but Waffle matchers only work with chai
.
Run:
yarn add --dev mocha chai
npm install --save-dev mocha chai
Note
If you are using Typescript don’t forget to add ts-node
, typescript
and @types/mocha
to your devDependencies
. Also, make sure to add a tsconfig.json
, and within it, set "esModuleInterop"
and "resolveJsonModule"
to true
.
Below is an example test file for the contract above written with Waffle. Place it under test/BasicToken.test.ts
file in your project directory:
import {expect, use} from 'chai';
import {Contract} from 'ethers';
import {deployContract, MockProvider, solidity} from 'ethereum-waffle';
import BasicToken from '../build/BasicToken.json';
use(solidity);
describe('BasicToken', () => {
const [wallet, walletTo] = new MockProvider().getWallets();
let token: Contract;
beforeEach(async () => {
token = await deployContract(wallet, BasicToken, [1000]);
});
it('Assigns initial balance', async () => {
expect(await token.balanceOf(wallet.address)).to.equal(1000);
});
it('Transfer adds amount to destination account', async () => {
await token.transfer(walletTo.address, 7);
expect(await token.balanceOf(walletTo.address)).to.equal(7);
});
it('Transfer emits event', async () => {
await expect(token.transfer(walletTo.address, 7))
.to.emit(token, 'Transfer')
.withArgs(wallet.address, walletTo.address, 7);
});
it('Can not transfer above the amount', async () => {
await expect(token.transfer(walletTo.address, 1007)).to.be.reverted;
});
it('Can not transfer from empty account', async () => {
const tokenFromOtherWallet = token.connect(walletTo);
await expect(tokenFromOtherWallet.transfer(wallet.address, 1))
.to.be.reverted;
});
it('Calls totalSupply on BasicToken contract', async () => {
await token.totalSupply();
expect('totalSupply').to.be.calledOnContract(token);
});
it('Calls balanceOf with sender address on BasicToken contract', async () => {
await token.balanceOf(wallet.address);
expect('balanceOf').to.be.calledOnContractWith(token, [wallet.address]);
});
});
Running tests¶
Update your package.json
file to include:
{
"scripts": {
"build": "waffle",
"test": "NODE_ENV=test mocha",
}
}
Next, add a .mocharc.json
file in your root directory:
{
"spec": "test/**/*.test.{js,ts}"
}
{
"require": "ts-node/register/transpile-only",
"spec": "test/**/*.test.{js,ts}"
}
And run:
yarn test
npm test
You should see the following output:
BasicToken
✓ Assigns initial balance (67ms)
✓ Transfer adds amount to destination account (524ms)
✓ Transfer emits event (309ms)
✓ Can not transfer above the amount (44ms)
✓ Can not transfer from empty account (78ms)
✓ Calls totalSupply on BasicToken contract (43ms)
✓ Calls balanceOf with sender address on BasicToken contract (45ms)
7 passing (5s)
If you want to know more about testing with Waffle, see Basic testing.
Compilation¶
Using third party libraries¶
One of the nice things about Waffle is that it enables you to import third party
libraries when writing your smart contracts. All you need to do is install the
library from npm
.
For example you can install the popular @openzeppelin/contracts
package:
yarn add @openzeppelin/contracts
npm install @openzeppelin/contracts
After installing a library, you can import it into your Solidity code:
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC721/ERC721Full.sol";
If you are using a custom node_modules
location you can configure Waffle
to recognize it. Change the nodeModulesDirectory
setting in your .waffle.json
file:
{
"nodeModulesDirectory": "path/to/node_modules"
}
To read more about configuring Waffle, see Configuration.
Reducing compile times¶
By default, Waffle uses solc-js for compiling smart contracts. The package provides JavaScript bindings for the Solidity compiler. It is slow, but easy to use and install in the JS ecosystem.
Because we value speed and flexibility, we provide some alternatives that you can use with Waffle. There are two other options:
Installing solc directly on your computer, see Using native solc
Using solc installed in a docker container, see Using dockerized solc
Using native solc¶
This is the fastest option but comes with some downsides. The system wide installation means that you are stuck with a single Solidity version across all of your projects. Additionally it might be complicated to install the old versions of the compiler using this method.
We recommend this option if you only care about the latest solidity version.
You can find detailed installation instructions for native solc
in the
Solidity documentation.
Note
You need to install version compatible with your source files.
Change the compilerType
setting in your .waffle.json
file:
{
"compilerType": "native"
}
To read more about configuring Waffle, see Configuration.
When compiling your smart contracts, Waffle will now use the native solc installation.
Using dockerized solc¶
This is the recommended option if you want flexibility when it comes to the compiler version. It is pretty easy to set up, especially if you have Docker installed.
If you don’t have docker visit the Docker documentation to learn how to install it.
After you’ve installed docker you can install the Solidity compiler. Pull the docker container tagged with the version you are interested in, for example for version 0.4.24:
docker pull ethereum/solc:0.4.24
Then, change the compilerType
setting in your .waffle.json
file:
{
"compilerType": "dockerized-solc",
"compilerVersion": "0.4.24"
}
If no compilerVersion
is specified the docker tag pulled defaults to
latest
. To read more about configuring Waffle, see Configuration.
When compiling your smart contracts, Waffle will now use the docker image you pulled.
Using dockerized vyper¶
This is the option if you have contracts in Vyper. You will need Docker installed.
To install docker visit the Docker documentation to learn how to do it.
To install dockerized Vyper pull the docker container tagged with the version you are interested in, for example for version 0.1.0:
docker pull vyperlang/vyper:0.1.0
Then, change the compilerType
setting in your .waffle.json
file:
{
"compilerType": "dockerized-vyper",
"compilerVersion": "0.1.0"
}
If no compilerVersion
is specified the docker tag pulled defaults to
latest
. To read more about configuring Waffle, see Configuration.
When compiling your smart contracts, Waffle will now use the docker image you pulled.
Basic testing¶
Creating a provider¶
Creating a mock provider for your tests is super simple.
import { MockProvider } from 'ethereum-waffle';
const provider = new MockProvider();
This class takes an optional MockProviderOptions
parameter in the constructor. Then the ganacheOptions
from MockProviderOptions
are passed to the underlying ganache-core implementation. You can read more about the options here.
Getting wallets¶
To obtain wallets that have been prefunded with eth use the provider
import { MockProvider } from 'ethereum-waffle';
const provider = new MockProvider();
const [wallet, otherWallet] = provider.getWallets();
// or use a shorthand
const [wallet, otherWallet] = new MockProvider().getWallets();
By default this method returns 10 wallets. You can modify the returned wallets, by changing MockProvider configuration.
import { MockProvider } from 'ethereum-waffle';
const provider = new MockProvider({
ganacheOptions: {
accounts: [{balance: 'BALANCE IN WEI', secretKey: 'PRIVATE KEY'}]
}
});
const wallets = provider.getWallets();
import { MockProvider } from 'ethereum-waffle';
const provider = new MockProvider({
accounts: [{balance: 'BALANCE IN WEI', secretKey: 'PRIVATE KEY'}]
});
const wallets = provider.getWallets();
You can also get an empty random wallet by calling:
import { MockProvider } from 'ethereum-waffle';
const provider = new MockProvider();
const wallet = provider.createEmptyWallet();
Deploying contracts¶
- Once you compile your contracts using waffle, you can deploy them in your javascript code. It accepts three arguments:
wallet to send the deploy transaction
contract information (abi and bytecode)
contract constructor arguments
Deploy a contract:
import BasicTokenMock from "build/BasicTokenMock.json";
token = await deployContract(wallet, BasicTokenMock, [wallet.address, 1000]);
The contract information can be one of the following formats:
interface StandardContractJSON {
abi: any;
evm: {bytecode: {object: any}};
}
interface SimpleContractJSON {
abi: any[];
bytecode: string;
}
Linking¶
Link a library:
myLibrary = await deployContract(wallet, MyLibrary, []);
link(LibraryConsumer, 'contracts/MyLibrary.sol:MyLibrary', myLibrary.address);
libraryConsumer = await deployContract(wallet, LibraryConsumer, []);
Note
You need to use a fully qualified name as the second parameter of the link function (path to the file relative to the root of the project, followed by a colon and the contract name).
Chai matchers¶
A set of sweet chai matchers, makes your test easy to write and read. Before you can start using the matchers, you have to tell chai to use the solidity plugin:
import chai from "chai";
import { solidity } from "ethereum-waffle";
chai.use(solidity);
Below is the list of available matchers:
Bignumbers¶
Testing equality of big numbers:
expect(await token.balanceOf(wallet.address)).to.equal(993);
Available matchers for BigNumbers are: equal, eq, above, gt, gte, below, lt, lte, least, most, within, closeTo.
expect(BigNumber.from(100)).to.be.within(BigNumber.from(99), BigNumber.from(101));
expect(BigNumber.from(100)).to.be.closeTo(BigNumber.from(101), 10);
Emitting events¶
Testing what events were emitted with what arguments:
Note
You must await
the expect
in order for the matcher to execute properly. Failing to await
will cause the assertion to pass whether or not the event is actually emitted!
await expect(token.transfer(walletTo.address, 7))
.to.emit(token, 'Transfer')
.withArgs(wallet.address, walletTo.address, 7);
Note
The matcher will match indexed
event parameters of type string
or bytes
even if the expected argument is not hashed using keccack256
first.
Testing with indexed bytes or string parameters. These two examples are equivalent
await expect(contract.addAddress("street", "city"))
.to.emit(contract, 'AddAddress')
.withArgs("street", "city");
const hashedStreet = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("street"));
const hashedCity = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("city"));
await expect(contract.addAddress(street, city))
.to.emit(contract, 'AddAddress')
.withArgs(hashedStreet, hashedCity);
If you are using Waffle version 3.4.4
or lower, you can’t chain emit
matchers like in example below.
await expect(tx)
.to.emit(contract, 'One')
.to.emit(contract, 'Two');
This feature is available in Waffle version 4.
Testing the argument names in the event:
await expect(token.transfer(walletTo.address, 7))
.to.emit(token, 'Transfer')
.withNamedArgs({
from: wallet.address,
to: walletTo.address,
amount: 7
});
A subset of arguments in an event can be tested by only including the desired arguments:
await expect(token.transfer(walletTo.address, "8000000000000000000"))
.to.emit(token, 'Transfer')
.withNamedArgs({
amount: "8000000000000000000"
});
This feature will be available in Waffle version 4.
Events declared and emitted in a library:
If your contract is using a Solidity Library
that declares and emits events and you want to test your contract for emitting those events, you’ll have to specifically point to the library. You can use attach
method on the library object. An example snippet for this case:
await expect(contract.function())
.to.emit(library.attach(contract.address), 'MyEvent');
where library
is the ethers object of the Solidity Library in which MyEvent
is declared.
Called on contract¶
Testing if function was called on the provided contract:
await token.balanceOf(wallet.address)
expect('balanceOf').to.be.calledOnContract(token);
Called on contract with arguments¶
Testing if function with certain arguments was called on provided contract:
await token.balanceOf(wallet.address)
expect('balanceOf').to.be.calledOnContractWith(token, [wallet.address]);
Revert¶
Testing if transaction was reverted:
await expect(token.transfer(walletTo.address, 1007)).to.be.reverted;
Revert with message¶
Testing if transaction was reverted with certain message:
await expect(token.transfer(walletTo.address, 1007))
.to.be.revertedWith('Insufficient funds');
You can also test if revert message matches to a regular expression:
await expect(token.checkRole('ADMIN'))
.to.be.revertedWith(/AccessControl: account .* is missing role .*/);
Change ether balance¶
Testing whether the transaction changes the balance of the account:
await expect(() => wallet.sendTransaction({to: walletTo.address, value: 200}))
.to.changeEtherBalance(walletTo, 200);
await expect(await wallet.sendTransaction({to: walletTo.address, value: 200}))
.to.changeEtherBalance(walletTo, 200);
expect
for changeEtherBalance
gets one of the following parameters:
transaction call : () => Promise<TransactionResponse>
transaction response : TransactionResponse
Note
changeEtherBalance
won’t work if there is more than one transaction mined in the block.
The transaction call should be passed to the expect
as a callback (we need to check the balance before the call) or as a transaction response.
The matcher can accept numbers, strings and BigNumbers as a balance change, while the account should be specified either as a Wallet or a Contract.
changeEtherBalance
ignores transaction fees by default:
// Default behavior
await expect(await wallet.sendTransaction({to: walletTo.address, value: 200}))
.to.changeEtherBalance(wallet, -200);
// To include the transaction fee use:
await expect(await wallet.sendTransaction({to: walletTo.address, gasPrice: 1, value: 200}))
.to.changeEtherBalance(wallet, -21200, {includeFee: true});
Note
changeEtherBalance
calls should not be chained. If you need to check changes of the balance for multiple accounts, you should use the changeEtherBalances
matcher.
Change ether balance (multiple accounts)¶
Testing whether the transaction changes balance of multiple accounts:
await expect(() => wallet.sendTransaction({to: walletTo.address, value: 200}))
.to.changeEtherBalances([wallet, walletTo], [-200, 200]);
await expect(await wallet.sendTransaction({to: walletTo.address, value: 200}))
.to.changeEtherBalances([wallet, walletTo], [-200, 200]);
Note
changeEtherBalances
calls won’t work if there is more than one transaction mined in the block.
Change token balance¶
Testing whether the transfer changes the balance of the account:
await expect(() => token.transfer(walletTo.address, 200))
.to.changeTokenBalance(token, walletTo, 200);
await expect(() => token.transferFrom(wallet.address, walletTo.address, 200))
.to.changeTokenBalance(token, walletTo, 200);
Note
The transfer call should be passed to the expect
as a callback (we need to check the balance before the call).
The matcher can accept numbers, strings and BigNumbers as a balance change, while the account should be specified either as a Wallet or a Contract.
Note
changeTokenBalance
calls should not be chained. If you need to check changes of the balance for multiple accounts, you should use the changeTokenBalances
matcher.
Change token balance (multiple accounts)¶
Testing whether the transfer changes balance for multiple accounts:
await expect(() => token.transfer(walletTo.address, 200))
.to.changeTokenBalances(token, [wallet, walletTo], [-200, 200]);
Proper address¶
Testing if a string is a proper address:
expect('0x28FAA621c3348823D6c6548981a19716bcDc740e').to.be.properAddress;
Proper private key¶
Testing if a string is a proper private key:
expect('0x706618637b8ca922f6290ce1ecd4c31247e9ab75cf0530a0ac95c0332173d7c5').to.be.properPrivateKey;
Proper hex¶
Testing if a string is a proper hex value of given length:
expect('0x70').to.be.properHex(2);
Hex Equal¶
Testing if a string is a proper hex with value equal to the given hex value. Case insensitive and strips leading zeros:
expect('0x00012AB').to.hexEqual('0x12ab');
Deprecated matchers¶
Change balance¶
Deprecated since version 3.1.2: Use changeEtherBalance()
instead.
Testing whether the transaction changes the balance of the account:
await expect(() => wallet.sendTransaction({to: walletTo.address, gasPrice: 0, value: 200}))
.to.changeBalance(walletTo, 200);
await expect(await wallet.sendTransaction({to: walletTo.address, gasPrice: 0, value: 200}))
.to.changeBalance(walletTo, 200);
expect
for changeBalance
gets one of the following parameters:
transaction call : () => Promise<TransactionResponse>
transaction response : TransactionResponse
Note
changeBalance
won’t work if there is more than one transaction mined in the block.
The transaction call should be passed to the expect
as a callback (we need to check the balance before the call) or as a transaction response.
The matcher can accept numbers, strings and BigNumbers as a balance change, while the account should be specified either as a Wallet or a Contract.
Note
changeBalance
calls should not be chained. If you need to check changes of the balance for multiple accounts, you should use the changeBalances
matcher.
Change balance (multiple accounts)¶
Deprecated since version 3.1.2: Use changeEtherBalances()
instead.
Testing whether the transaction changes balance of multiple accounts:
await expect(() => wallet.sendTransaction({to: walletTo.address, gasPrice: 0, value: 200}))
.to.changeBalances([wallet, walletTo], [-200, 200]);
await expect(await wallet.sendTransaction({to: walletTo.address, gasPrice: 0, value: 200}))
.to.changeBalances([wallet, walletTo], [-200, 200]);
Note
changeBalances
calls won’t work if there is more than one transaction mined in the block.
Fixtures¶
When testing code dependent on smart contracts, it is often useful to have a specific scenario play out before every test. For example, when testing an ERC20 token, one might want to check whether each and every address can or cannot perform transfers. Before each of those tests however, you have to deploy the ERC20 contract and maybe transfer some funds.
The repeated deployment of contracts might slow down the test significantly. This is why Waffle allows you to create fixtures - testing scenarios that are executed once and then remembered by making snapshots of the blockchain. This speeds up the tests considerably.
Example:
import {expect} from 'chai';
import {loadFixture, deployContract} from 'ethereum-waffle';
import BasicTokenMock from './build/BasicTokenMock';
describe('Fixtures', () => {
async function fixture([wallet, other], provider) {
const token = await deployContract(wallet, BasicTokenMock, [
wallet.address, 1000
]);
return {token, wallet, other};
}
it('Assigns initial balance', async () => {
const {token, wallet} = await loadFixture(fixture);
expect(await token.balanceOf(wallet.address)).to.equal(1000);
});
it('Transfer adds amount to destination account', async () => {
const {token, other} = await loadFixture(fixture);
await token.transfer(other.address, 7);
expect(await token.balanceOf(other.address)).to.equal(7);
});
});
import {expect} from 'chai';
import {loadFixture, deployContract} from 'ethereum-waffle';
import BasicTokenMock from './build/BasicTokenMock';
describe('Fixtures', () => {
async function fixture(provider, [wallet, other]) {
const token = await deployContract(wallet, BasicTokenMock, [
wallet.address, 1000
]);
return {token, wallet, other};
}
it('Assigns initial balance', async () => {
const {token, wallet} = await loadFixture(fixture);
expect(await token.balanceOf(wallet.address)).to.equal(1000);
});
it('Transfer adds amount to destination account', async () => {
const {token, other} = await loadFixture(fixture);
await token.transfer(other.address, 7);
expect(await token.balanceOf(other.address)).to.equal(7);
});
});
Fixtures receive a provider and an array of wallets as an argument. By default, the wallets are obtained by calling getWallets
and the provider by createMockProvider
. You can, however, override those by using a custom fixture loader.
import {createFixtureLoader} from 'ethereum-waffle';
const loadFixture = createFixtureLoader(myWallets, myProvider);
// later in tests
await loadFixture((myWallets, myProvider) => {
// fixture implementation
});
import {createFixtureLoader} from 'ethereum-waffle';
const loadFixture = createFixtureLoader(myProvider, myWallets);
// later in tests
await loadFixture((myProvider, myWallets) => {
// fixture implementation
});
Mock contract¶
Mocking your smart contract dependencies.
Usage¶
Create an instance of a mock contract providing the ABI
of the smart contract you want to mock:
import {deployMockContract} from '@ethereum-waffle/mock-contract';
...
const mockContract = await deployMockContract(wallet, contractAbi);
You can also choose the deployment address of the mock contract with the options argument:
const mockContract = await deployMockContract(wallet, contractAbi, {
address: deploymentAddress,
overrride: false // optional, specifies if the contract should be overwritten
})
The mock contract can now be integrated into other contracts by using the address
attribute.
Return values for mocked functions can be set using:
await mockContract.mock.<nameOfMethod>.returns(<value>)
await mockContract.mock.<nameOfMethod>.withArgs(<arguments>).returns(<value>)
Methods can also be set up to be reverted using:
await mockContract.mock.<nameOfMethod>.reverts()
await mockContract.mock.<nameOfMethod>.revertsWithReason(<reason>)
await mockContract.mock.<nameOfMethod>.withArgs(<arguments>).reverts()
await mockContract.mock.<nameOfMethod>.withArgs(<arguments>).revertsWithReason(<reason>)
Example¶
The example below illustrates how mock-contract
can be used to test the very simple AmIRichAlready
contract.
pragma solidity ^0.6.0;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
}
contract AmIRichAlready {
IERC20 private tokenContract;
uint private constant RICHNESS = 1000000 * 10 ** 18;
constructor (IERC20 _tokenContract) public {
tokenContract = _tokenContract;
}
function check() public view returns (bool) {
uint balance = tokenContract.balanceOf(msg.sender);
return balance > RICHNESS;
}
}
We are mostly interested in the tokenContract.balanceOf
call.
Mock contract will be used to mock exactly this call with values that are relevant for the return of the check()
method.
const {use, expect} = require('chai');
const {ContractFactory, utils} = require('ethers');
const {MockProvider} = require('@ethereum-waffle/provider');
const {waffleChai} = require('@ethereum-waffle/chai');
const {deployMockContract} = require('@ethereum-waffle/mock-contract');
const IERC20 = require('../build/IERC20');
const AmIRichAlready = require('../build/AmIRichAlready');
use(waffleChai);
describe('Am I Rich Already', () => {
async function setup() {
const [sender, receiver] = new MockProvider().getWallets();
const mockERC20 = await deployMockContract(sender, IERC20.abi);
const contractFactory = new ContractFactory(AmIRichAlready.abi, AmIRichAlready.bytecode, sender);
const contract = await contractFactory.deploy(mockERC20.address);
return {sender, receiver, contract, mockERC20};
}
it('returns false if the wallet has less then 1000000 coins', async () => {
const {contract, mockERC20} = await setup();
await mockERC20.mock.balanceOf.returns(utils.parseEther('999999'));
expect(await contract.check()).to.be.equal(false);
});
it('returns true if the wallet has more than 1000000 coins', async () => {
const {contract, mockERC20} = await setup();
await mockERC20.mock.balanceOf.returns(utils.parseEther('1000001'));
expect(await contract.check()).to.equal(true);
});
it('reverts if the ERC20 reverts', async () => {
const {contract, mockERC20} = await setup();
await mockERC20.mock.balanceOf.reverts();
await expect(contract.check()).to.be.revertedWith('Mock revert');
});
it('returns 1000001 coins for my address and 0 otherwise', async () => {
const {contract, mockERC20, sender, receiver} = await setup();
await mockERC20.mock.balanceOf.returns('0');
await mockERC20.mock.balanceOf.withArgs(sender.address).returns(utils.parseEther('1000001'));
expect(await contract.check()).to.equal(true);
expect(await contract.connect(receiver.address).check()).to.equal(false);
});
});
Mocking multiple calls¶
Mock contract allows to queue multiple mock calls to the same function. This can only be done if the function is not pure or view. That’s because the mock call queue is stored on the blockchain and we need to modify it.
await mockContract.mock.<nameOfMethod>.returns(<value1>).returns(<value2>);
await mockContract.<nameOfMethod>() // returns <value1>
await mockContract.<nameOfMethod>() // returns <value2>
Just like with regular mock calls, the queue can be set up to revert or return a specified value. It can also be set up to return different values for different arguments.
await mockContract.mock.<nameOfMethod>.returns(<value1>).returns(<value2>);
await mockContract.mock.<nameOfMethod>.withArgs(<arguments1>).returns(<value3>);
await mockContract.<nameOfMethod>() // returns <value1>
await mockContract.<nameOfMethod>() // returns <value2>
await mockContract.<nameOfMethod>(<arguments1>) // returns <value3>
Keep in mind that the mocked revert must be at the end of the queue, because it prevents the contract from updating the queue.
await mockContract.mock.<nameOfMethod>.returns(<value1>).returns(<value2>).reverts();
await mockContract.<nameOfMethod>() // returns <value1>
await mockContract.<nameOfMethod>() // returns <value2>
await mockContract.<nameOfMethod>() // reverts
When the queue is empty, the mock contract will return the last value from the queue and each time the you set up a new queue, the old one is overwritten.
await mockContract.mock.<nameOfMethod>.returns(<value1>).returns(<value2>);
await mockContract.<nameOfMethod>() // returns <value1>
await mockContract.<nameOfMethod>() // returns <value2>
await mockContract.<nameOfMethod>() // returns <value2>
await mockContract.mock.<nameOfMethod>.returns(<value1>).returns(<value2>);
await mockContract.mock.<nameOfMethod>.returns(<value3>).returns(<value4>);
await mockContract.<nameOfMethod>() // returns <value3>
await mockContract.<nameOfMethod>() // returns <value4>
Mocking receive function¶
The receive
function of the mocked Smart Contract can be mocked to revert. It cannot however be mocked to return a specified value, because of gas limitations when calling another contract using send
and transfer
.
Receive mock example¶
pragma solidity ^0.6.0;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
fallback() external payable;
receive() external payable;
}
contract EtherForward {
IERC20 private tokenContract;
constructor (IERC20 _tokenContract) public {
tokenContract = _tokenContract;
}
function forward() public payable {
payable(tokenContract).transfer(msg.value);
}
}
(...)
it('use the receive function normally', async () => {
const {contract, mockERC20} = await setup();
expect (
await mockERC20.provider.getBalance(mockERC20.address)
).to.be.equal(0);
await contract.forward({value: 7})
expect (
await mockERC20.provider.getBalance(mockERC20.address)
).to.be.equal(7);
});
it('can mock the receive function to revert', async () => {
const {contract, mockERC20} = await setup();
await mockERC20.mock.receive.revertsWithReason('Receive function rejected')
await expect(
contract.forward({value: 7})
).to.be.revertedWith('Receive function rejected')
expect (
await mockERC20.provider.getBalance(mockERC20.address)
).to.be.equal(0);
});
(...)
ENS¶
Dependencies¶
Make sure to install additional peer dependencies needed to use Waffle ENS tools.
yarn add --dev @ensdomains/ens@^0.4.4 @ensdomains/resolver@^0.2.4
npm install --save-dev @ensdomains/ens@^0.4.4 @ensdomains/resolver@^0.2.4
Creating a ENS¶
Creating a simple ENS for testing with ENS
.
import {MockProvider} from '@ethereum-waffle/provider';
import {deployENS, ENS} from '@ethereum-waffle/ens';
const provider = new MockProvider();
const [wallet] = provider.getWallets();
const ens: ENS = await deployENS(wallet);
This class takes a wallet
in the constructor. The wallet
available in MockProvider
class in package @ethereum-waffle/provider
.
Setup ENS¶
Note
The feature was introduced in Waffle 3.0.0
Also, if you use MockProvider
, you can use setupENS()
function in MockProvider
,
to create and setup simple ENS
.
import {MockProvider} from '@ethereum-waffle/provider';
const provider = new MockProvider();
await provider.setupENS();
await provider.ens.createTopLevelDomain('test');
setupENS()
method employs the last of the provider’s wallets
by default, but you can pass your own wallet
as an argument for setupENS(wallet)
.
Also setupENS()
method saves ENS address in the provider’s networks.
Creating top level domain¶
Use createTopLevelDomain
function to create a top level domain:
await ens.createTopLevelDomain('test');
Creating sub domain¶
Use createSubDomain
function for creating a sub domain:
await ens.createSubDomain('ethworks.test');
Creating sub domain with options¶
Note
The feature was introduced in Waffle 3.0.0
It is also possible to create a sub domain recursively, if the top domain doesn’t exist, by specifying the appropriate option:
await ens.createSubDomain('waffle.ethworks.tld', {recursive: true});
Setting address¶
Use setAddress
function for setting address for the domain:
await ensBuilder.setAddress('vlad.ethworks.test', '0x001...03');
Setting address with options¶
Note
The feature was introduced in Waffle 3.0.0
It is also possible to set an address for domain recursively, if the domain doesn’t exist, by specifying the appropriate option:
await ens.setAddress('vlad.waffle.ethworks.tld', '0x001...03', {recursive: true});
Use setAddressWithReverse
function for setting address for the domain and make this domain reverse. Add recursive option if the domain doesn’t exist:
await ens.setAddressWithReverse('vlad.ethworks.tld', wallet, {recursive: true});
Configuration¶
Configuration file¶
While Waffle works well enough without any configurations, advanced users might want to excert more control over what happens when they use Waffle in their projects.
This is why we made it very easy to configure Waffle to meet your needs. All
you need to do is create a waffle.json
file inside your project and
point waffle to it.
First create your waffle.json
configuration file:
{
"compilerType": "solcjs",
"compilerVersion": "0.6.2",
"sourceDirectory": "./src",
"outputDirectory": "./build"
}
Note
Each configuration is optional.
Afterwards update your package.json
build script:
Note
Since Waffle 3.0.0 it recognises waffle.json
as the default configuration file. If your configuration file is called
waffle.json
, it’s possible to use just waffle
to build contracts.
{
"scripts": {
"build": "waffle"
}
}
{
"scripts": {
"build": "waffle waffle.json"
}
}
Configuration options:
sourceDirectory¶
You can specify a custom path to the directory containing your smart contracts.
Waffle uses ./contracts
as the default value for sourceDirectory
.
The path you provide will be resolved relative to the current working directory.
Example:
{
"sourceDirectory": "./custom/path/to/contracts"
}
outputDirectory¶
You can specify a custom path to the directory to which Waffle saves the
compilation output. Waffle uses ./build
as the default value for
outputDirectory
. The path you provide will be resolved relative to the
current working directory.
Example:
{
"outputDirectory": "./custom/path/to/output"
}
nodeModulesDirectory¶
You can specify a custom path to the node_modules
folder which Waffle
will use to resolve third party dependencies. Waffle uses ./node_modules
as the default value for nodeModulesDirectory
. The path you provide
will be resolved relative to the current working directory.
For more information about third party libraries, see Using third party libraries.
Example:
{
"nodeModulesDirectory": "./custom/path/to/node_modules"
}
cacheDirectory¶
When compiling using solcjs
and using a non-default compilerVersion
Waffle downloads the necessary solcjs
binary from a remote server. This file
is cached to speed up subsequent runs. You can specify a custom path to the directory
in which caches are saved. Waffle uses ./cache
as the default value for
cacheDirectory
. The path you provide will be resolved relative to the current
working directory.
Example:
{
"cacheDirectory": "./custom/path/to/cache"
}
compilerType¶
Specifies the compiler to use. For more information see: Reducing compile times. Allowed values:
solcjs
native
dockerized-solc
dockerized-vyper
Waffle uses solcjs
as the default value for
compilerType
.
Example:
{
"compilerType": "dockerized-solc"
}
compilerVersion¶
Specifies the version of the compiler. Should be a semver string like
0.5.9
. You can use it with "compilerType": "solcjs"
or
"compilerType": "dockerized-solc"
.
When using "compilerType": "solcjs"
you can also specify the exact
commit that will be used or a path to a specific solc module dependency.
To find a specific commit please consult the list of available solc versions.
Examples:
{
"compilerType": "dockerized-solc",
"compilerVersion": "0.4.24"
}
{
"compilerType": "solcjs",
"compilerVersion": "v0.4.24+commit.e67f0147"
}
{
"compilerType": "solcjs",
"compilerVersion": "./node_modules/solc"
}
compilerAllowedPaths¶
The solc
compiler has restrictions on paths it can access for security
reasons. The value of compilerAllowedPaths
will be passed as a command
line argument: solc --allow-paths <VALUE>
.
This is especially useful if you are doing a monorepo setup with Lerna, see: Usage with Lernajs.
Example:
{
"compilerAllowedPaths": ["../contracts"]
}
compilerOptions¶
You can customize the behaviour of solc
by providing custom settings for
it. All of the information is provided in the Solidity documentation. Value of the compilerOptions
configuration setting will be passed to solc
as settings
.
For detailed list of options go to solidity documentation (sections: ‘Setting the EVM version to target’, ‘Target options’ and ‘Compiler Input and Output JSON Description’).
Example:
{
"compilerOptions": {
"evmVersion": "constantinople"
}
}
outputType¶
See: KLAB compatibility.
outputHumanReadableAbi¶
Waffle supports Human Readable Abi.
In order to enable its output, you need to set outputHumanReadableAbi
to true
in your config file:
{
"outputHumanReadableAbi": true
}
For the compiled contracts you will now see the following in the output:
{
"humanReadableAbi": [
"constructor(uint256 argOne)",
"event Bar(bool argOne, uint256 indexed argTwo)",
"event FooEvent()",
"function noArgs() view returns(uint200)",
"function oneArg(bool argOne)",
"function threeArgs(string argOne, bool argTwo, uint256[] argThree) view returns(bool, uint256)",
"function twoReturns(bool argOne) view returns(bool, uint256)"
]
}
typechain¶
Waffle supports typechain artifacts generation.
To enable typed artifacts generation you should set typechainEnabled
property to true
.
You are also able to define target folder for your artifacts by defining typechainOutputDir
property, which is set to ./types
by default.
Property typechainOutputDir
is a path relative to outputDirectory.
Example:
{
"typechainEnabled": true,
"typechainOutputDir": "typechain"
}
Other configuration file formats¶
Waffle supports the following configuration file formats:
JSON:
{
"sourceDirectory": "./src/contracts",
}
JavaScript:
module.exports = {
sourceDirectory: './src/contracts'
}
The configuration can even be a promise
module.exports = Promise.resolve({
sourceDirectory: './src/contracts'
})
Hint
This is a powerful feature if you want to asynchronously load different compliation configurations in different environments. For example, you can use native solc in CI for faster compilation, while deciding the exact solc-js version locally based on the contract versions being used. Since many of those operations are asynchronous, you’ll most likely be returning a Promise to waffle to handle.
Setting Solidity compiler version¶
See compilerVersion.
Usage with Truffle¶
Waffle output should be compatible with Truffle by default.
Custom compiler options¶
See compilerOptions.
KLAB compatibility¶
The default compilation process is not compatible with KLAB (a formal verification tool, see: https://github.com/dapphub/klab). To compile contracts to work with KLAB one must:
Set appropriate compiler options, i.e.:
compilerOptions: {
outputSelection: {
"*": {
"*": [ "evm.bytecode.object", "evm.deployedBytecode.object",
"abi" ,
"evm.bytecode.sourceMap", "evm.deployedBytecode.sourceMap" ],
"": [ "ast" ]
},
}
}
2. Set appropriate output type. We support two types: one (default) generates a single file for each contract and the other (KLAB friendly) generates one file (Combined-Json.json) combining all contracts. The latter type does not meet (in contrary to the first one) all official solidity standards since KLAB requirements are slightly modified. Set the output in the config file:
Possible options are:
‘multiple’: a single file for each contract;
‘combined’: one KLAB friendly file;
‘all’: generates both above outputs;
‘minimal’: a single file for each contract with minimal information (just abi and bytecode).
Note
‘minimal’ option was introduced in Waffle 3.0.0.
{
"outputType": "combined"
}
An example of full KLAB friendly config file:
module.exports = {
compilerType: process.env.WAFFLE_COMPILER,
outputType: 'all',
compilerOptions: {
outputSelection: {
"*": {
"*": [ "evm.bytecode.object", "evm.deployedBytecode.object",
"abi" ,
"evm.bytecode.sourceMap", "evm.deployedBytecode.sourceMap" ],
"": [ "ast" ]
},
}
}
};
Monorepo¶
Waffle works well with mono-repositories. It is enough to set up a common nodeModulesDirectory
in the configuration file to make it work.
We recommend using yarn workspaces and wsrun for monorepo management.
Usage with Lernajs¶
Waffle works with lerna, but requires additional configuration. When lerna cross-links npm packages in monorepo, it creates symbolic links to the original catalog. They lead to sources files located beyond allowed paths. This process breaks compilation with native solc.
If you see the following message in your monorepo setup:
contracts/Contract.sol:4:1: ParserError: Source ".../monorepo/node_modules/YourProjectContracts/contracts/Contract.sol" not found: File outside of allowed directories.
import "YourProjectContracts/contracts/Contract.sol";
you probably need to add allowedPath to your waffle configuration.
Assuming you have the following setup:
/monorepo
/YourProjectContracts
/contracts
/YourProjectDapp
/contracts
Add to waffle configuration in YourProjectDapp:
{
"compilerAllowedPaths": ["../YourProjectContracts"]
}
That should solve a problem.
Currently Waffle does not support a similar feature for dockerized solc.
Migration guides¶
Migration from Waffle 2.2.0 to Waffle 2.3.0¶
Created monorepo¶
Waffle was internally migrated to a monorepo. Thanks to this, you can now use parts of waffle individually. We provide the following packages:
ethereum-waffle
- core package exporting everythingethereum-compiler
- compile your contracts programmaticallyethereum-chai
- chai matchers for better unit testingethereum-provider
- mock provider to interact with an in-memory blockchain
Created MockProvider class¶
We added MockProvider class. It changed the creation of the provider.
Waffle 2.2.0
await createMockProvider(options);
Waffle 2.3.0
provider = new MockProvider();
Reorganise getWallets() method¶
Waffle 2.2.0
await getWallets(provider);
Waffle 2.3.0
new MockProvider().getWallets()
Migration from Waffle 2.3.0 to Waffle 2.4.0¶
Renamed configuration options¶
We renamed configuration options to compile contracts:
sourcesPath
- renamed to sourceDirectorytargetPath
- renamed to outputDirectorynpmPath
- renamed to nodeModulesDirectorycompiler
- renamed to compilerTypedocker-tag
- replaced by compilerVersionsolcVersion
- replaced by compilerVersionlegacyOutput
- removed, setting it to false gave no effectallowedPaths
- renamed to compilerAllowedPathsganacheOptions
- removed, wasn’t used by the compiler
Migration from Waffle 2.5.* to Waffle 3.0.0¶
There are some new functionality and some slight refactoring and improved paradigms in Waffle v3.
Removed deprecated APIs from the provider¶
In Waffle 3.0.0
we remove deprecated APIs from the provider, such as createMockProvider
and getGanacheOptions
.
Swapped arguments for Fixture¶
In Waffle 3.0.0
we swapped arguments for Fixture, because the provider argument is very rarely used compared to wallets.
So such implementation should be more convenient for users.
Waffle 2.5.0
function createFixtureLoader(overrideProvider?: MockProvider, overrideWallets?: Wallet[]);
Waffle 3.0.0
function createFixtureLoader(overrideWallets?: Wallet[], overrideProvider?: MockProvider);
Waffle 2.5.0
import {expect} from 'chai';
import {loadFixture, deployContract} from 'ethereum-waffle';
import BasicTokenMock from './build/BasicTokenMock';
describe('Fixtures', () => {
async function fixture(provider, [wallet, other]) {
const token = await deployContract(wallet, BasicTokenMock, [
wallet.address, 1000
]);
return {token, wallet, other};
}
it('Assigns initial balance', async () => {
const {token, wallet} = await loadFixture(fixture);
expect(await token.balanceOf(wallet.address)).to.equal(1000);
});
});
Waffle 3.0.0
import {expect} from 'chai';
import {loadFixture, deployContract} from 'ethereum-waffle';
import BasicTokenMock from './build/BasicTokenMock';
describe('Fixtures', () => {
async function fixture([wallet, other], provider) {
const token = await deployContract(wallet, BasicTokenMock, [
wallet.address, 1000
]);
return {token, wallet, other};
}
it('Assigns initial balance', async () => {
const {token, wallet} = await loadFixture(fixture);
expect(await token.balanceOf(wallet.address)).to.equal(1000);
});
});
Added automatic recognising waffle.json config without cli argument¶
Waffle recognises waffle.json
as the default configuration file. If your configuration file is called
waffle.json
, it’s possible to use just waffle
to build contracts.
In Waffle 2.5.0, If the argument has not been provided, the Waffle uses the default configuration.
Waffle 2.5.0
{
"scripts": {
"build": "waffle waffle.json"
}
}
Waffle 3.0.0
{
"scripts": {
"build": "waffle"
}
}
Introduced MockProviderOptions¶
We added MockProviderOptions. It will be convenient in the future, when the provider may need some
options other than ganacheOptions
.
Waffle 2.5.0
import {expect} from 'chai';
import {Wallet} from 'ethers';
import {MockProvider} from 'ethereum-waffle';
import {deployToken} from './BasicToken';
describe('INTEGRATION: MockProvider', () => {
it('accepts options', () => {
const original = Wallet.createRandom();
const provider = new MockProvider({
accounts: [{balance: '100', secretKey: original.privateKey}]
});
const wallets = provider.getWallets();
expect(wallets.length).to.equal(1);
expect(wallets[0].address).to.equal(original.address);
});
});
Waffle 3.0.0
import {expect} from 'chai';
import {Wallet} from 'ethers';
import {MockProvider} from 'ethereum-waffle';
import {deployToken} from './BasicToken';
describe('INTEGRATION: MockProvider', () => {
it('accepts options', () => {
const original = Wallet.createRandom();
const provider = new MockProvider({
ganacheOptions: {
accounts: [{balance: '100', secretKey: original.privateKey}]
}
});
const wallets = provider.getWallets();
expect(wallets.length).to.equal(1);
expect(wallets[0].address).to.equal(original.address);
});
});
Dropped support for contract interface¶
We dropped support for contract interface because it duplicated contract ABI. Also interface
is a keyword in typescript,
so we decided not to use this field. Now we support just contract.abi
.
Waffle 2.5.0
{
"abi": [
...
],
"interface: [
...
],
"evm": {
...
},
"bytecode": "..."
}
Waffle 3.0.0
{
"abi": [
{...}
],
"evm": {
...
},
"bytecode": "..."
}
Migration from Waffle 3.4.0 to Waffle 4.0.0-alpha¶
Dependencies upgrades¶
The main difference between Waffle 3.4.0 and Waffle 4.0.0-alpha is about dependencies that Waffle packages use. We updated the following dependencies:
typechain
- bumped version from ^2.0.0 to ^9.0.0. Now every Waffle package uses the same version of the package. Also the package was moved to thepeerDependencies
section - you now need to installtypechain
manually when using Waffle.ethers
- bumped version from to ^5.5.4. Now every Waffle package uses the same version of the package. Also the package was moved to thepeerDependencies
section - you now need to installethers
manually when using Waffle.solc
- the package is used bywaffle-compiler
package to provide the default option for compiling Solidity code. Was moved to thepeerDependencies
section and has no version restrictions - you now have to installsolc
manually when using Waffle.Deprecated
ganache-core
package has been replaced withganache
version ^7.0.3. It causes slight differences in the parameters ofMockProvider
from@ethereum-waffle/provider
. Now theMockProvider
usesberlin
hardfork by default.
Changes to MockProvider
parameters¶
Previous (optional) parameters of MockProvider
included override options for the Ganache provider:
interface MockProviderOptions {
ganacheOptions: {
account_keys_path?: string;
accounts?: object[];
allowUnlimitedContractSize?: boolean;
blockTime?: number;
db_path?: string;
debug?: boolean;
default_balance_ether?: number;
fork?: string | object;
fork_block_number?: string | number;
forkCacheSize?: number;
gasLimit?: string | number;
gasPrice?: string;
hardfork?: "byzantium" | "constantinople" | "petersburg" | "istanbul" | "muirGlacier";
hd_path?: string;
locked?: boolean;
logger?: {
log(msg: string): void;
};
mnemonic?: string;
network_id?: number;
networkId?: number;
port?: number;
seed?: any;
time?: Date;
total_accounts?: number;
unlocked_accounts?: string[];
verbose?: boolean;
vmErrorsOnRPCResponse?: boolean;
ws?: boolean;
}
}
Current ganacheOptions
parameter are documented here.
Typechain changes¶
If you used type generation (typechainEnabled
option set to true
in waffle.json
), you need to update your code to conform to the new naming convention used by typechain
. Contract factories now have postfix __factory
instead of Factory
. For example, MyContractFactory
becomes MyContract__factory
. Example refactoring:
const contractConstructorArgs: [string, string] = [bob.address, charlie.address];
-const contract = await deployContract(alice, MyContractFactory, contractConstructorArgs);
+const contract = await deployContract(alice, MyContract__factory, contractConstructorArgs);
@ethereum-waffle/jest
deprecated¶
We stopped supporting @ethereum-waffle/jest
. From now on the package is deprecated and will be removed in the future. The suggested test framework to use with Waffle
is mocha
combined with chai
and @ethereum-waffle/chai
package. If you want to migrate from jest to mocha in your project, please follow the guide below.
Setup
mocha
- this may include the following steps:
installing
mocha
andchai
packages.if you are using
typescript
, installing@types/mocha
,@types/chai
andts-node
packages.updating your
test
script (the common pattern for typescript is"test": "mocha -r ts-node/register/transpile-only 'test/**/*.test.ts'"
).updating your tests to use
mocha
syntax.
Replace
@ethereum-waffle/jest
with@ethereum-waffle/chai
. Below is little table that contains the list of all the matchers provided by@ethereum-waffle/jest
and their replacements in@ethereum-waffle/chai
.
@ethereum-waffle/jest |
@ethereum-waffle/chai |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Time-based tests¶
If your tests rely on manually setting timestamp on the blockchain using evm_mine, you need to use evm_setTime alongside.
For example:
export async function mineBlock(provider: providers.Web3Provider, timestamp: number) {
+provider.send('evm_setTime', [timestamp * 1000])
await provider.send('evm_mine', [timestamp])
}
Tests relying on setting gasPrice¶
Since London hardfork, baseFeePerGas
is replacing gasPrice
.
If your tests are relying on setting gasPrice
with changeBalance
matcher, you will have to update them.
Before
await expect(() =>
sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
})
).to.changeBalance(sender, -200);
After
const TX_GAS = 21000;
const BASE_FEE_PER_GAS = 875000000
const gasFees = BASE_FEE_PER_GAS * TX_GAS;
await expect(() =>
sender.sendTransaction({
to: receiver.address,
gasPrice: BASE_FEE_PER_GAS,
value: 200
})
).to.changeBalance(sender, -(gasFees + 200));
Currently there is no way to set gasPrice
to 0
in Ganache
.
Instead of (deprecated) matcher changeBalance
, new matcher changeEtherBalance
can be used instead - which handles transaction fee calculation automatically.
Custom wallet mnemonic¶
Ganache has a build-in set of Wallets with positive Ether balance. In the new Ganache, you should not override the wallet config, otherwise you might end up with Wallets with no Ether balance.
const provider = new MockProvider({
ganacheOptions: {
// Remove this, if exists:
wallet: {
mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn'
}
}
})
Chaining matchers¶
Now when testing events on a smart contract you can conveniently chain matchers. It can be especially useful when testing events.
const tx = await contract.emitEventsOneAndTwo();
await expect(tx)
.to.emit(contract, 'One').withArgs(
1,
'One'
)
.to.emit(contract, 'Two').withArgs(
2,
'Two'
)
.to.not.emit(contract, 'Three');
changeEtherBalance
, changeEtherBalances
, changeTokenbalance
and changeTokenBalances
matchers also support chaining:
await token.approve(complex.address, 100);
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
await expect(tx)
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
.and.to.emit(complex, 'TransferredEther').withArgs(200)
.and.to.emit(complex, 'TransferredTokens').withArgs(100);
Although you may find it more convenient to write multiple expects. The test below is equivalent to the one above:
await token.approve(complex.address, 100);
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
await expect(tx).to.changeTokenBalances(token, [sender, receiver], [-100, 100]);
await expect(tx).to.changeEtherBalances([sender, receiver], [-200, 200]);
await expect(tx).to.emit(complex, 'TransferredEther').withArgs(200);
await expect(tx).to.emit(complex, 'TransferredTokens').withArgs(100);
Note that in both cases you can use chai
negation not
. In a case of a single expect everything after not
is negated.
await token.approve(complex.address, 100);
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
await expect(expect(tx)
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
.and.not
.to.emit(complex, 'UnusedEvent') // This is negated
.and.to.changeEtherBalances([sender, receiver], [-100, 100]) // This is negated as well
Custom errors¶
Custom errors were introduced in Solidity v0.8.4. It is a convenient and gas-efficient way to explain to users why an operation failed. Custom errors are defined in a similar way as events:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// Insufficient balance for transfer. Needed `required` but only
/// `available` available.
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);
contract TestToken {
mapping(address => uint) balance;
function transfer(address to, uint256 amount) public {
if (amount > balance[msg.sender])
// Error call using named parameters. Equivalent to
// revert InsufficientBalance(balance[msg.sender], amount);
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
balance[msg.sender] -= amount;
balance[to] += amount;
}
// ...
}
When using Waffle v4.0.0-alpha.* with Hardhat, you can test transactions being reverted with custom errors as well. Using the .revertedWith
matcher you can capture the custom error’s name (expect(tx).to.be.revertedWith('InsufficientBalance')
). If you want to access arguments of a custom error you should use .withArgs
matcher after the .revertedWith
matcher.
await expect(token.transfer(receiver, 100))
.to.be.revertedWith('InsufficientBalance')
.withArgs(0, 100);