Migrating from hardhat-deploy to Hardhat Ignition

John Kane
Nomic Foundation
Published in
5 min readApr 1, 2024

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-deployand 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:

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

--

--

John Kane
Nomic Foundation

Hardhat Ignition team lead at the Nomic Foundation