Migrating to Hardhat Ignition from hardhat-deploy

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