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 thepeerDependenciessection - you now need to installtypechainmanually 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 thepeerDependenciessection - you now need to installethersmanually when using Waffle.solc- the package is used bywaffle-compilerpackage to provide the default option for compiling Solidity code. Was moved to thepeerDependenciessection and has no version restrictions - you now have to installsolcmanually when using Waffle.Deprecated
ganache-corepackage has been replaced withganacheversion ^7.0.3. It causes slight differences in the parameters ofMockProviderfrom@ethereum-waffle/provider. Now theMockProviderusesberlinhardfork 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
mochaandchaipackages.if you are using
typescript, installing@types/mocha,@types/chaiandts-nodepackages.updating your
testscript (the common pattern for typescript is"test": "mocha -r ts-node/register/transpile-only 'test/**/*.test.ts'").updating your tests to use
mochasyntax.
Replace
@ethereum-waffle/jestwith@ethereum-waffle/chai. Below is little table that contains the list of all the matchers provided by@ethereum-waffle/jestand 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);