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 everything

  • ethereum-compiler - compile your contracts programmatically

  • ethereum-chai - chai matchers for better unit testing

  • ethereum-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:

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 the peerDependencies section - you now need to install typechain 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 the peerDependencies section - you now need to install ethers manually when using Waffle.

  • solc - the package is used by waffle-compiler package to provide the default option for compiling Solidity code. Was moved to the peerDependencies section and has no version restrictions - you now have to install solc manually when using Waffle.

  • Deprecated ganache-core package has been replaced with ganache version ^7.0.3. It causes slight differences in the parameters of MockProvider from @ethereum-waffle/provider. Now the MockProvider uses berlin 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.

  1. Setup mocha - this may include the following steps:

  • installing mocha and chai packages.

  • if you are using typescript, installing @types/mocha, @types/chai and ts-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.

  1. 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.

Matchers replacements

@ethereum-waffle/jest

@ethereum-waffle/chai

.toChangeBalance(wallet, balanceChange)

.to.changeEtherBalance(wallet, balanceChange)

.toChangeBalances(wallets, balanceChanges)

.to.changeEtherBalances(wallets, balanceChanges)

.toBeGtBN(value)

.to.be.gt(value)

.toBeLtBN(value)

.to.be.lt(value)

.toBeGteBN(value)

.to.be.gte(value)

.toBeLteBN(value)

.to.be.lte(value)

.toEqBN(value)

.to.equal(value)

.toHaveEmitted(contract, eventName)

.to.emit(contract, eventName)

.toHaveEmittedWith(contract, eventName, args)

.to.emit(contract, eventName).withArgs(...args)

.toThrowErrorMatchingSnapshot()

.to.be.eventually.rejectedWith(expected, message)

.toBeProperPrivateKey()

.to.be.properPrivateKey

.toBeProperAddress()

.to.be.properAddress

.toBeReverted()

.to.be.reverted

.toBeRevertedWith(revertReason)

.to.be.revertedWith(reason)

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);