Migrating to Hardhat Ignition from hardhat-deploy
Migrating from hardhat-deploy to Hardhat Ignition
For several years, the hardhat-deploy
community plugin has been a go-to solution for deploying smart contracts within the Hardhat community. Recently, we introduced Hardhat Ignition, a declarative system for deploying smart contracts on Ethereum, which aims to pick up the torch fromhardhat-deploy
and expand Hardhat's deployment features.
This guide will walk you through migrating your Hardhat project from hardhat-deploy
to Hardhat Ignition.
Installing Hardhat Ignition
To get started, we’ll uninstall the hardhat-deploy
plugin and install the Hardhat Ignition one by executing the following steps:
- Remove the
hardhat-deploy
packages from your project:
npm uninstall hardhat-deploy hardhat-deploy-ethers
2. Install the Hardhat Ignition package and hardhat-network-helpers
to provide additional testing support as a replacement for hardhat-deploy
functionality like EVM snapshots:
npm install --save-dev @nomicfoundation/hardhat-ignition-ethers @nomicfoundation/hardhat-network-helpers
3. Update the project’s hardhat.config
file to remove hardhat-deploy
and hardhat-deploy-ethers
and instead import Hardhat Ignition:
- import "hardhat-deploy";
- import "hardhat-deploy-ethers";
+ import "@nomicfoundation/hardhat-ignition-ethers";
Convert deployment scripts to Ignition Modules
hardhat-deploy
represents contract deployments as JavaScript or TypeScript files under the ./deploy/
folder. Hardhat Ignition follows a similar pattern with deployments encapsulated as modules; these are JS/TS files stored under the ./ignition/modules
directory. Each hardhat-deploy
deploy file will be converted or merged into a Hardhat Ignition module.
Let’s first create the required folder structure under the root of your project:
mkdir ignition
mkdir ignition/modules
Now, let’s work through converting a simple hardhat-deploy
script for this example Token
contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Token {
uint256 public totalSupply = 1000000;
address public owner;
mapping(address => uint256) balances;
constructor(address _owner) {
balances[_owner] = totalSupply;
owner = _owner;
}
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Not enough tokens");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
A hardhat-deploy
deploy function for the Token
contract might look like this:
// ./deploy/001_deploy_token.ts
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { DeployFunction } from "hardhat-deploy/types";
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployments, getNamedAccounts } = hre;
const { deploy } = deployments;
/*
The deploy function uses the hardhat-deploy named accounts feature
to set the deployment's `from` and `args` parameters.
*/
const { deployer, tokenOwner } = await getNamedAccounts();
await deploy("Token", {
from: deployer,
args: [tokenOwner],
log: true,
});
};
export default func;
Using an Ignition Module, the equivalent account access code would look like this:
// ./ignition/modules/Token.ts
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
/*
The callback passed to `buildModule()` provides a module builder object `m`
as a parameter. Through this builder object, you access the Module API.
For instance, you can deploy contracts via `m.contract()`.
*/
export default buildModule("TokenModule", (m) => {
/*
Instead of named accounts, you get access to the configured accounts
through the `getAccount()` method.
*/
const deployer = m.getAccount(0);
const tokenOwner = m.getAccount(1);
/*
Deploy `Token` by calling `contract()` with the constructor arguments
as the second argument. The account to use for the deployment transaction
is set through `from` in the third argument, which is an options object.
*/
const token = m.contract("Token", [tokenOwner], {
from: deployer,
});
/*
The call to `m.contract()` returns a future that can be used in other `m.contract()`
calls (e.g. as a constructor argument, where the future will resolve to the
deployed address), but it can also be returned from the module. Contract
futures that are returned from the module can be leveraged in Hardhat tests
and scripts, as will be shown later.
*/
return { token };
});
The conversion to an Ignition module can be tested by running the module against Hardhat Network:
npx hardhat ignition deploy ./ignition/modules/Token.ts
Which, if working correctly, will output the contract’s deployed address:
You are running Hardhat Ignition against an in-process instance of Hardhat Network.
This will execute the deployment, but the results will be lost.
You can use --network <network-name> to deploy to a different network.
Hardhat Ignition 🚀
Deploying [ TokenModule ]
Batch #1
Executed Token#Token
[ Token ] successfully deployed 🚀
Deployed Addresses
Token#Token - 0x5FbDB2315678afecb367f032d93F642f64180aa3
To learn more, check out the detailed guide on writing Hardhat Ignition modules, which showcases all available features.
Migrating tests that rely on hardhat-deploy fixtures
Let’s go over the process of rewriting Hardhat tests that rely on hardhat-deploy
fixture functionality. Using hardhat-deploy
, calls to fixture()
deploy everything under the ./deploy
and create a snapshot in the in-memory Hardhat node at the end of the first run. Subsequent calls to fixture()
revert to the saved snapshot, avoiding rerunning the deployment transactions and thus saving time.
To do this, hardhat-deploy-ethers
enhances the Hardhat ethers
object with a getContract
method that will return contract instances from the fixture snapshot.
import { expect } from "chai";
import { Contract } from "ethers";
import { ethers, deployments, getNamedAccounts } from "hardhat";
describe("Token contract", function () {
it("should assign the total supply of tokens to the owner", async function () {
// Create fixture snapshot
await deployments.fixture();
// This will get an instance from the snapshot
const token: Contract = await ethers.getContract("Token");
const { tokenOwner } = await getNamedAccounts();
expect(await token.balanceOf(tokenOwner)).to.equal(
await token.totalSupply()
);
});
});
Hardhat Ignition, in conjunction with the hardhat-network-helpers
plugin, also allows you to use fixtures:
import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers";
import { expect } from "chai";
import { ethers, ignition } from "hardhat";
import TokenModule from "../ignition/modules/Token";
describe("Token contract", function () {
async function deployTokenFixture() {
/*
Hardhat Ignition adds an `ignition` object to the Hardhat Runtime Environment
that exposes a `deploy()` method. The `deploy()` method takes an Ignition
module and returns the results of the Ignition module, where each
returned future has been converted into an *ethers* contract instance.
*/
const { token } = await ignition.deploy(TokenModule);
return { token }
}
it("should assign the total supply of tokens to the owner", async function () {
/*
The snapshot feature of `hardhat-deploy` fixtures is replicated
by the call to the `hardhat-network-helpers` function `loadFixture()`.
For a given fixture function, `loadFixture()` will snapshot the in-memory
Hardhat node, and will revert to the snapshot if called with the same
function again.
*/
const { token } = await loadFixture(deployTokenFixture);
const [, tokenOwner] = await ethers.getSigners();
expect(await token.balanceOf(tokenOwner)).to.equal(await token.totalSupply());
});
});
Once converted, tests can be checked in the standard way:
npx hardhat test
Feedback welcome
We would love your feedback if you run into any issues or have any feature requests! Please open an issue on Github.