Skill v1.0.1
currentAutomated scan100/1003 files
version: "1.0.1" name: hardhat-deploy-migration description: Comprehensive guide for migrating projects from hardhat-deploy v1 to v2, including dependency updates, configuration restructuring, deploy script conversion, test updates, and troubleshooting
SKILL: Migrate hardhat-deploy v1 to v2
This guide provides comprehensive instructions for migrating projects from hardhat-deploy v1 to v2. It's designed for AI assistants to understand and execute the migration process systematically.
Note: For complete working examples, see the template-ethereum-contracts repository which demonstrates a full hardhat-deploy v2 setup.
Table of Contents
- Introduction
- Prerequisites Check
- Architecture Comparison
- Step-by-Step Migration
- Common Patterns & Examples
- Troubleshooting Guide
- Migration Checklist
- Advanced Topics
Introduction
Overview
hardhat-deploy v2 is a complete rewrite that requires Hardhat 3.x and introduces significant architectural changes:
- ESM Modules: v2 uses native ES modules (
import/export) instead of CommonJS - Rocketh Integration: v2 integrates with the rocketh ecosystem for deployment management
- Configuration Changes: Named accounts moved from hardhat.config.ts to rocketh/config.ts
- Plugin System: Enhanced extensibility through rocketh extensions
Key Differences Summary
| Aspect | v1 Pattern | v2 Pattern | |
|---|---|---|---|
| Hardhat version | 2.x | 3.x (specifically ^3.1.5) | |
| Module system | CommonJS (require/module.exports) | ESM (import/export) | |
| Named accounts | namedAccounts in hardhat.config.ts | rocketh/config.ts | |
| Deploy function | deployments.deploy(name, {...}) | deploy(name, {account: ..., artifact: ...}) | |
| Deployer param | from: address | account: address | |
| Solidity config | solidity: "0.8.x" | solidity: {profiles: {default: {version: "..."}}} | |
| Test fixtures | deployments.createFixture() | Custom fixture with loadAndExecuteDeploymentsFromFiles() | |
| Contract interaction | ethers.getContract() | env.get() + env.execute() |
When to Stay on v1
If you have a production project using hardhat-deploy v1 with Hardhat 2.x, it's often better to stay on v1:
npm uninstall hardhat-deploynpm install hardhat-deploy@1
v1 continues to receive security fixes but won't get new features.
Prerequisites Check
Before starting the migration, verify your environment meets these requirements:
Checklist
# Check Node.js version (requires 22+ for v2)node --version# Check Hardhat version (requires 3.x+ for v2)npx hardhat --version# Check if project is using CommonJS or ESMgrep -q '"type": "module"' package.json && echo "ESM" || echo "CommonJS"
Required Versions
- Node.js: 22 or higher
- Hardhat: 3.x or higher (specifically
^3.1.5recommended) - TypeScript: 5.x (for TypeScript projects)
Pre-Migration Assessment
Check for these v1 patterns in your project:
// In hardhat.config.ts- namedAccounts configuration- require() statements- module.exports- solidity: "0.8.x" format// In deploy scripts- async function (hre) { ... }- hre.deployments.deploy()- hre.getNamedAccounts()- from: parameter- log: true parameter// In tests- deployments.createFixture()- ethers.getContract()- getUnnamedAccounts()
Architecture Comparison
v1 Architecture
graph TDA[hardhat.config.ts] -->|contains| B[namedAccounts]A -->|contains| C[solidity config]A -->|requires| D[hardhat-deploy]E[deploy/*.ts] -->|imports| F[HardhatRuntimeEnvironment]F -->|provides| G[getNamedAccounts]F -->|provides| H[deployments]I[test/*.ts] -->|uses| J[deployments.createFixture]J -->|calls| K[deployments.fixture]K -->|gets| L[ethers.getContract]
Key Files in v1:
hardhat.config.ts- Single configuration filedeploy/*.ts- Deploy scripts with HRE patterntest/*.ts- Tests using deployments fixtureutils/network.ts- Network configuration helper
v2 Architecture
graph TDA[hardhat.config.ts] -->|imports plugins| B[HardhatDeploy]A -->|uses helpers| C[addNetworksFromEnv]A -->|contains| D[solidity profiles]E[rocketh/config.ts] -->|contains| F[accounts config]E -->|contains| G[extensions]H[rocketh/deploy.ts] -->|exports| I[deployScript]H -->|exports| J[artifacts]K[rocketh/environment.ts] -->|exports| L[loadEnvironmentFromHardhat]K -->|exports| M[loadAndExecuteDeploymentsFromFiles]N[deploy/*.ts] -->|imports| IN -->|uses| JO[test/*.ts] -->|imports| MO -->|uses| env.get and env.execute
Key Files in v2:
hardhat.config.ts- Hardhat configuration (no named accounts)rocketh/config.ts- Named accounts and extensionsrocketh/deploy.ts- Deploy script setuprocketh/environment.ts- Environment setup for tests/scriptsdeploy/*.ts- Deploy scripts with new patterntest/*.ts- Tests using new fixture pattern
Step-by-Step Migration
Step 1: Update Dependencies
Update your package.json to use Hardhat 3.x and hardhat-deploy v2:
v1 package.json example:
{"devDependencies": {"hardhat": "^2.22.18","hardhat-deploy": "^0.14.0","hardhat-deploy-ethers": "^0.4.2","hardhat-deploy-tenderly": "^1.0.0","ethers": "^6.13.5","hardhat-deploy": "^0.14.0"}}
v2 package.json example: (see template-ethereum-contracts/package.json)
{"type": "module","devDependencies": {"hardhat": "^3.1.4","hardhat-deploy": "^2.0.0","rocketh": "^0.17.15","@rocketh/deploy": "^0.17.9","@rocketh/read-execute": "^0.17.9","@rocketh/node": "^0.17.18","@rocketh/proxy": "^0.17.13","@rocketh/signer": "^0.17.9","viem": "^2.45.0","earl": "^2.0.0","@nomicfoundation/hardhat-viem": "^3.0.1","@nomicfoundation/hardhat-node-test-runner": "^3.0.8","@nomicfoundation/hardhat-network-helpers": "^3.0.3","@nomicfoundation/hardhat-keystore": "^3.0.3"}}
Transformation Rules:
- Add
"type": "module"at the top level - Update
hardhatto^3.1.4or higher - Update
hardhat-deployto^2.0.0or higher - Remove
hardhat-deploy-ethersandhardhat-deploy-tenderly - Add rocketh packages:
rocketh,@rocketh/deploy,@rocketh/read-execute,@rocketh/node,@rocketh/signer - Add optional packages:
@rocketh/proxy,@rocketh/export,@rocketh/verifier,@rocketh/doc - Add
viemfor contract interactions - Add
earlfor assertions (for node:test) - Add Hardhat 3.x plugins
Install dependencies:
pnpm install
Step 2: Restructure Configuration
2.1 Convert hardhat.config.ts
v1 hardhat.config.ts example:
import "dotenv/config";import { HardhatUserConfig } from "hardhat/types";import "@nomicfoundation/hardhat-chai-matchers";import "@nomicfoundation/hardhat-ethers";import "@typechain/hardhat";import "hardhat-deploy";import "hardhat-deploy-ethers";import "hardhat-deploy-tenderly";import { node_url, accounts, addForkConfiguration } from "./utils/network";const config: HardhatUserConfig = {solidity: {compilers: [{version: "0.8.17",settings: {optimizer: {enabled: true,runs: 2000,},},},],},namedAccounts: {deployer: 0,simpleERC20Beneficiary: 1,},networks: addForkConfiguration({hardhat: {initialBaseFeePerGas: 0,},localhost: {url: node_url("localhost"),accounts: accounts(),},mainnet: {url: node_url("mainnet"),accounts: accounts("mainnet"),},sepolia: {url: node_url("sepolia"),accounts: accounts("sepolia"),},}),paths: {sources: "src",},mocha: {timeout: 0,},external: process.env.HARDHAT_FORK? {deployments: {hardhat: ["deployments/" + process.env.HARDHAT_FORK],localhost: ["deployments/" + process.env.HARDHAT_FORK],},}: undefined,};export default config;
v2 hardhat.config.ts example: (see template-ethereum-contracts/hardhat.config.ts)
import type { HardhatUserConfig } from "hardhat/config";import HardhatNodeTestRunner from "@nomicfoundation/hardhat-node-test-runner";import HardhatViem from "@nomicfoundation/hardhat-viem";import HardhatNetworkHelpers from "@nomicfoundation/hardhat-network-helpers";import HardhatKeystore from "@nomicfoundation/hardhat-keystore";import HardhatDeploy from "hardhat-deploy";import {addForkConfiguration,addNetworksFromEnv,addNetworksFromKnownList,} from "hardhat-deploy/helpers";const config: HardhatUserConfig = {plugins: [HardhatNodeTestRunner,HardhatViem,HardhatNetworkHelpers,HardhatKeystore,HardhatDeploy,],solidity: {profiles: {default: {version: "0.8.17",},production: {version: "0.8.17",settings: {optimizer: {enabled: true,runs: 999999,},},},},},networks:// This add the fork configuration for chosen networkaddForkConfiguration(// this add a network config for all known chain using kebab-cases names// Note that MNEMONIC_<network> (or MNEMONIC if the other is not set) will// be used for account// Similarly ETH_NODE_URI_<network> will be used for rpcUrl// Note that if you set these env variable to have the value: "SECRET" it will be like using:// configVariable('SECRET_ETH_NODE_URI_<network>')// configVariable('SECRET_MNEMONIC_<network>')addNetworksFromKnownList(// this add network for each respective env var found (ETH_NODE_URI_<network>)// it will also read MNEMONIC_<network> to populate the accounts// And like above it will use configVariable if set to SECRETaddNetworksFromEnv(// and you can add in your specific network here{default: {type: "edr-simulated",chainType: "l1",accounts: {mnemonic: process.env.MNEMONIC || undefined,},},},),),),paths: {sources: ["src"],},generateTypedArtifacts: {destinations: [{folder: "./generated",mode: "typescript",},],},};export default config;
Transformation Rules:
- Change from
import 'hardhat-deploy'toimport HardhatDeploy from 'hardhat-deploy' - Remove
namedAccountssection entirely - Convert
solidity.compilerstosolidity.profiles - Add
pluginsarray with imported plugins - Use helper functions from
hardhat-deploy/helpersfor network configuration - Add
generateTypedArtifactsconfiguration - Remove
mochatimeout configuration (not needed in v2) - Remove
external.deploymentsconfiguration (handled differently) - Delete
utils/network.tsfile (no longer needed)
2.5 Update tsconfig.json for ESM
v1 tsconfig.json example:
{"compilerOptions": {"target": "es5","module": "commonjs","strict": true,"esModuleInterop": true,"moduleResolution": "node","forceConsistentCasingInFileNames": true,"outDir": "dist"},"include": ["hardhat.config.ts","./scripts","./deploy","./test","typechain/**/*"]}
v2 tsconfig.json example:
{"compilerOptions": {"lib": ["es2023"],"module": "node16","target": "es2022","moduleResolution": "node16","strict": true,"esModuleInterop": true,"skipLibCheck": true,"sourceMap": true,"declaration": true,"declarationMap": true,"outDir": "dist","rootDir": "."},"include": ["deploy", "generated"]}
Create scripts/tsconfig.json:
{"extends": "../tsconfig.json","compilerOptions": {"noEmit": true,"rootDir": ".."},"include": ["**/*", "../generated/**/*", "../rocketh/**/*"],"exclude": []}
Create test/tsconfig.json:
{"extends": "../tsconfig.json","compilerOptions": {"noEmit": true,"rootDir": ".."},"include": ["**/*","../generated/**/*","../rocketh/**/*","../hardhat.config.ts"],"exclude": []}
Transformation Rules for tsconfig.json:
- Update
modulefromcommonjstonode16 - Update
targetfromes5toes2022 - Update
moduleResolutionfromnodetonode16 - Add
lib: ["es2023"] - Add
skipLibCheck: true - Add
sourceMap: true,declaration: true,declarationMap: true - Change
includeto only["deploy", "generated"] - Create
scripts/tsconfig.jsonextending the main config - Create
test/tsconfig.jsonextending the main config
2.2 Create rocketh/config.ts
New file: rocketh/config.ts example: (see template-ethereum-contracts/rocketh/config.ts)
// ----------------------------------------------------------------------------// Typed Config// ----------------------------------------------------------------------------import type {EnhancedEnvironment,UnknownDeployments,UserConfig,} from "rocketh/types";// this one provide a protocol supporting private key as accountimport { privateKey } from "@rocketh/signer";// we define our config and export it as "config"export const config = {accounts: {deployer: {default: 0,},simpleERC20Beneficiary: {default: 1,},},data: {},signerProtocols: {privateKey,},} as const satisfies UserConfig;// then we import each extensions we are interested in using in our deploy script or elsewhere// this one provide a deploy functionimport * as deployExtension from "@rocketh/deploy";// this one provide read,execute functionsimport * as readExecuteExtension from "@rocketh/read-execute";// this one provide a deployViaProxy function that let you declaratively// deploy proxy based contractsimport * as deployProxyExtension from "@rocketh/proxy";// this one provide a viem handle to clients and contractsimport * as viemExtension from "@rocketh/viem";// and export them as a unified objectconst extensions = {...deployExtension,...readExecuteExtension,...deployProxyExtension,...viemExtension,};export { extensions };// then we also export the types that our config ehibit so other can use ittype Extensions = typeof extensions;type Accounts = typeof config.accounts;type Data = typeof config.data;type Environment = EnhancedEnvironment<Accounts,Data,UnknownDeployments,Extensions>;export type { Extensions, Accounts, Data, Environment };
Transformation Rules:
- Create
rockethdirectory:mkdir rocketh - Move
namedAccountsfrom hardhat.config.ts torocketh/config.tsunderaccounts - Import required rocketh extensions
- Export extensions as unified object
- Export TypeScript types for type safety
2.3 Create rocketh/deploy.ts
New file: rocketh/deploy.ts example: (see template-ethereum-contracts/rocketh/deploy.ts)
import {type Accounts,type Data,type Extensions,extensions,} from "./config.js";// ----------------------------------------------------------------------------// we re-export the artifacts, so they are easily available from the aliasimport * as artifacts from "../generated/artifacts/index.js";export { artifacts };// ----------------------------------------------------------------------------// we create the rocketh functions we need by passing the extensions to the// setup functionimport { setupDeployScripts } from "rocketh";const { deployScript } = setupDeployScripts<Extensions, Accounts, Data>(extensions,);export { deployScript };
Transformation Rules:
- Import config and extensions from
./config.js - Import generated artifacts from
../generated/artifacts/index.js - Setup deploy scripts using
setupDeployScriptsfrom rocketh - Export
deployScriptandartifacts
2.4 Create rocketh/environment.ts
New file: rocketh/environment.ts example: (see template-ethereum-contracts/rocketh/environment.ts)
import {type Accounts,type Data,type Extensions,extensions,} from "./config.js";import { setupEnvironmentFromFiles } from "@rocketh/node";import { setupHardhatDeploy } from "hardhat-deploy/helpers";// useful for test and scripts, uses file-systemconst { loadAndExecuteDeploymentsFromFiles } = setupEnvironmentFromFiles<Extensions,Accounts,Data>(extensions);const { loadEnvironmentFromHardhat } = setupHardhatDeploy<Extensions,Accounts,Data>(extensions);export { loadEnvironmentFromHardhat, loadAndExecuteDeploymentsFromFiles };
Transformation Rules:
- Import config and extensions from
./config.js - Setup environment functions from
@rocketh/nodeandhardhat-deploy/helpers - Export
loadEnvironmentFromHardhatfor scripts - Export
loadAndExecuteDeploymentsFromFilesfor tests
Step 3: Convert Deploy Scripts
3.1 Simple Deployment
v1 deploy script example:
import { HardhatRuntimeEnvironment } from "hardhat/types";import { DeployFunction } from "hardhat-deploy/types";import { parseEther } from "ethers";const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {const { deployments, getNamedAccounts } = hre;const { deploy } = deployments;const { deployer, simpleERC20Beneficiary } = await getNamedAccounts();await deploy("SimpleERC20", {from: deployer,args: [simpleERC20Beneficiary, parseEther("1000000000")],log: true,autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks});};export default func;func.tags = ["SimpleERC20"];
v2 deploy script example: (see template-ethereum-contracts/deploy/001_deploy_greetings_registry.ts)
import { deployScript, artifacts } from "../rocketh/deploy.js";import { parseEther } from "viem";export default deployScript(async (env) => {const { deployer, simpleERC20Beneficiary } = env.namedAccounts;await env.deploy("SimpleERC20", {artifact: artifacts.SimpleERC20,account: deployer,args: [simpleERC20Beneficiary, parseEther("1000000000")],});},{tags: ["SimpleERC20"],},);
Transformation Rules:
- Remove
HardhatRuntimeEnvironmentandDeployFunctionimports - Import
deployScriptandartifactsfrom../rocketh/deploy.js - Change
parseEtherfrometherstoviem - Wrap function in
deployScript()call - Change parameter from
(hre)to(env) - Replace
hre.getNamedAccounts()with directenv.namedAccountsaccess - Replace
hre.deployments.deploy()withenv.deploy() - Change
from:toaccount: - Add explicit
artifact:parameter - Remove
log:andautoMine:parameters (not needed in v2) - Move tags to second argument object instead of
func.tags
3.2 Proxy Deployment
v1 proxy deploy script example:
import { HardhatRuntimeEnvironment } from "hardhat/types";import { DeployFunction } from "hardhat-deploy/types";const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {const { deployer } = await hre.getNamedAccounts();const { deploy } = hre.deployments;const useProxy = !hre.network.live;// proxy only in non-live network (localhost and hardhat network) enabling HCR (Hot Contract Replacement)// in live network, proxy is disabled and constructor is invokedawait deploy("GreetingsRegistry", {from: deployer,proxy: useProxy && "postUpgrade",args: [2],log: true,autoMine: true, // speed up deployment on local network (ganache, hardhat), no effect on live networks});return !useProxy; // when live network, record the script as executed to prevent rexecution};export default func;func.id = "deploy_greetings_registry"; // id required to prevent reexecutionfunc.tags = ["GreetingsRegistry"];
v2 proxy deploy script example: (see template-ethereum-contracts/deploy/002_deploy_greetings_registry.ts)
import { deployScript, artifacts } from "../rocketh/deploy.js";import { parseEther } from "viem";export default deployScript(async (env) => {const { deployer } = env.namedAccounts;const useProxy = !env.tags.live;// proxy only in non-live network (localhost and hardhat network) enabling HCR (Hot Contract Replacement)// in live network, proxy is disabled and constructor is invokedawait env.deployViaProxy("GreetingsRegistry",{account: deployer,artifact: artifacts.GreetingsRegistry,args: ["2"],},{proxyDisabled: !useProxy,execute: "postUpgrade",},);return !useProxy; // when live network, record the script as executed to prevent rexecution},{tags: ["GreetingsRegistry"],id: "deploy_greetings_registry", // id required to prevent reexecution},);
Transformation Rules for Proxy Deployment:
- Change
proxy: useProxy && 'postUpgrade'toenv.deployViaProxy()call - Move proxy configuration to second argument object
- Use
proxyDisabled: !useProxyinstead of conditional proxy - Use
execute: 'postUpgrade'instead of proxy type - Replace
hre.network.livewithenv.tags.live
Step 4: Convert Tests
4.1 Test with Deployments
v1 test example:
import { expect } from "chai";import {ethers,deployments,getUnnamedAccounts,getNamedAccounts,} from "hardhat";import { IERC20 } from "../typechain-types";import { setupUser, setupUsers } from "./utils";const setup = deployments.createFixture(async () => {await deployments.fixture("SimpleERC20");const { simpleERC20Beneficiary } = await getNamedAccounts();const contracts = {SimpleERC20: await ethers.getContract<IERC20>("SimpleERC20"),};const users = await setupUsers(await getUnnamedAccounts(), contracts);return {...contracts,users,simpleERC20Beneficiary: await setupUser(simpleERC20Beneficiary, contracts),};});describe("SimpleERC20", function () {it("transfer fails", async function () {const { users } = await setup();await expect(users[0].SimpleERC20.transfer(users[1].address, 1),).to.be.revertedWith("NOT_ENOUGH_TOKENS");});it("transfer succeed", async function () {const { users, simpleERC20Beneficiary, SimpleERC20 } = await setup();await simpleERC20Beneficiary.SimpleERC20.transfer(users[1].address, 1);await expect(simpleERC20Beneficiary.SimpleERC20.transfer(users[1].address, 1),).to.emit(SimpleERC20, "Transfer").withArgs(simpleERC20Beneficiary.address, users[1].address, 1);});});
v2 test example: (see template-ethereum-contracts/test/GreetingsRegistry.test.ts)
import { expect } from "earl";import { describe, it } from "node:test";import { network } from "hardhat";import { EthereumProvider } from "hardhat/types/providers";import { loadAndExecuteDeploymentsFromFiles } from "../rocketh/environment.js";import { Abi_SimpleERC20 } from "../generated/abis/SimpleERC20.js";function setupFixtures(provider: EthereumProvider) {return {async deployAll() {const env = await loadAndExecuteDeploymentsFromFiles({provider: provider,});// Deployment are inherently untyped since they can vary from// network or even be different from current artifacts so here// we type them manually assuming the artifact is still matchingconst SimpleERC20 = env.get<Abi_SimpleERC20>("SimpleERC20");return {env,SimpleERC20,namedAccounts: env.namedAccounts,unnamedAccounts: env.unnamedAccounts,};},};}const { provider, networkHelpers } = await network.connect();const { deployAll } = setupFixtures(provider);describe("SimpleERC20", function () {it("transfer fails", async function () {const { env, SimpleERC20, unnamedAccounts } =await networkHelpers.loadFixture(deployAll);await expect(env.execute(SimpleERC20, {account: unnamedAccounts[0],functionName: "transfer",args: [unnamedAccounts[1], 1n],}),).toBeRejectedWith("NOT_ENOUGH_TOKENS");});it("transfer succeed", async function () {const { env, SimpleERC20, unnamedAccounts, namedAccounts } =await networkHelpers.loadFixture(deployAll);await env.execute(SimpleERC20, {account: namedAccounts.simpleERC20Beneficiary,functionName: "transfer",args: [unnamedAccounts[1], 1n],});env.execute(SimpleERC20, {account: namedAccounts.simpleERC20Beneficiary,functionName: "transfer",args: [unnamedAccounts[1], 1n],});// TODO// expect(...).toEmit(SimpleERC20, 'Transfer')// .withArgs(simpleERC20Beneficiary.address, users[1].address, 1));});});
Transformation Rules for Tests:
- Change test runner from
mochatonode:test - Change assertion library from
chaitoearl(or keep chai if preferred) - Import
networkfrom 'hardhat' - Create custom fixture function using
loadAndExecuteDeploymentsFromFiles() - Replace
deployments.createFixture()with custom fixture - Replace
deployments.fixture()withloadAndExecuteDeploymentsFromFiles() - Replace
ethers.getContract()withenv.get<Abi_Type>() - Import ABI types from generated artifacts:
import {Abi_SimpleERC20} from '../generated/abis/SimpleERC20.js' - Replace
getUnnamedAccounts()withenv.unnamedAccounts - Replace contract method calls with
env.execute():
- Old:
users[0].SimpleERC20.transfer(users[1].address, 1) - New:
env.execute(SimpleERC20, {account: unnamedAccounts[0], functionName: 'transfer', args: [unnamedAccounts[1], 1n]})
- Use
BigIntliterals (1n) instead of Numbers (1) for amounts - Use
networkHelpers.loadFixture()instead of direct fixture call
4.2 Test Utilities Update
v1 test utils example:
import { BaseContract } from "ethers";import hre from "hardhat";const { ethers } = hre;export async function setupUsers<T extends { [contractName: string]: BaseContract },>(addresses: string[], contracts: T): Promise<({ address: string } & T)[]> {const users: ({ address: string } & T)[] = [];for (const address of addresses) {users.push(await setupUser(address, contracts));}return users;}export async function setupUser<T extends { [contractName: string]: BaseContract },>(address: string, contracts: T): Promise<{ address: string } & T> {// eslint-disable-next-line @typescript-eslint/no-explicit-anyconst user: any = { address };for (const key of Object.keys(contracts)) {user[key] = contracts[key].connect(await ethers.getSigner(address));}return user as { address: string } & T;}
v2 test utils example: (see template-ethereum-contracts/test/utils/index.ts)
import { Abi_GreetingsRegistry } from "../../generated/abis/GreetingsRegistry.js";import { loadAndExecuteDeploymentsFromFiles } from "../../rocketh/environment.js";import { EthereumProvider } from "hardhat/types/providers";export function setupFixtures(provider: EthereumProvider) {return {async deployAll() {const env = await loadAndExecuteDeploymentsFromFiles({provider: provider,});// Deployment are inherently untyped since they can vary from// network or even be different from current artifacts so here// we type them manually assuming the artifact is still matchingconst GreetingsRegistry =env.get<Abi_GreetingsRegistry>("GreetingsRegistry");return {env,GreetingsRegistry,namedAccounts: env.namedAccounts,unnamedAccounts: env.unnamedAccounts,};},};}
Transformation Rules for Test Utils:
- Remove
setupUsersandsetupUserfunctions (not needed in v2) - Create
setupFixturesfunction that returns deployment setup - Use
loadAndExecuteDeploymentsFromFiles()for deployment - Import ABI types from generated artifacts
Step 5: Update Scripts
v1 script pattern:
import hre from "hardhat";async function main() {const { deployments, getNamedAccounts } = hre;const { deployer } = await getNamedAccounts();const MyContract = await deployments.get("MyContract");const contract = await ethers.getContractAt("MyContract", MyContract.address);await contract.someFunction();}main().catch((error) => {console.error(error);process.exit(1);});
v2 script pattern:
import hre from "hardhat";import { loadEnvironmentFromHardhat } from "./rocketh/environment.js";import { Abi_MyContract } from "./generated/abis/MyContract.js";async function main() {const env = await loadEnvironmentFromHardhat({ hre });const MyContract = env.get<Abi_MyContract>("MyContract");await env.execute(MyContract, {account: env.namedAccounts.deployer,functionName: "someFunction",args: [],});}main().catch((error) => {console.error(error);process.exit(1);});
Transformation Rules for Scripts:
- Import
loadEnvironmentFromHardhatfrom./rocketh/environment.js - Import ABI types from generated artifacts
- Use
loadEnvironmentFromHardhat({hre})instead of direct HRE access - Replace
ethers.getContract()withenv.get<Abi_Type>() - Replace contract method calls with
env.execute()
Step 6: Update package.json Scripts
v1 package.json scripts example:
{"scripts": {"prepare": "hardhat typechain","compile": "hardhat compile","void:deploy": "hardhat deploy --report-gas","test": "cross-env HARDHAT_DEPLOY_FIXTURE=true HARDHAT_COMPILE=true mocha --bail --recursive test","gas": "cross-env REPORT_GAS=true hardhat test","coverage": "cross-env HARDHAT_DEPLOY_FIXTURE=true hardhat coverage","dev:node": "cross-env MINING_INTERVAL=\"3000,5000\" hardhat node --hostname 0.0.0.0","dev": "cross-env MINING_INTERVAL=\"3000,5000\" hardhat node --hostname 0.0.0.0 --watch","local:dev": "hardhat --network localhost deploy --watch","execute": "node ./_scripts.js run","deploy": "node ./_scripts.js deploy","verify": "node ./_scripts.js verify","export": "node ./_scripts.js export"}}
v2 package.json scripts example: (see template-ethereum-contracts/package.json)
{"scripts": {"prepare": "set-defaults .vscode && pnpm compile","local_node": "ldenv -d localhost hardhat node","compile": "hardhat compile","compile:watch": "as-soon -w src pnpm compile","fork:execute": "ldenv tsx @=HARDHAT_FORK=@@MODE @@","fork:deploy": "pnpm compile --build-profile production && ldenv hardhat @=HARDHAT_FORK=@@MODE deploy @@","deploy:dev": "ldenv -d localhost pnpm :deploy+export @@","deploy:watch": "wait-on ./generated && ldenv -m localhost pnpm as-soon -w generated -w deploy pnpm run deploy:dev @@MODE @@","test": "hardhat test","test:watch": "wait-on ./generated && as-soon -w generated -w test hardhat test --no-compile","typescript:watch": "as-soon -w js pnpm typescript","format:check": "prettier --check .","format": "prettier --write .","lint": "slippy src/**/*.sol","docgen": "ldenv -m default pnpm run deploy @@MODE --save-deployments true --skip-prompts ~~ pnpm rocketh-doc -e @@MODE --except-suffix _Implementation,_Proxy,_Router,_Route ~~ @@","execute": "ldenv -n HARDHAT_NETWORK tsx @@","deploy": "pnpm compile --build-profile production && ldenv hardhat --network @@MODE deploy @@","verify": "ldenv rocketh-verify -e @@MODE @@","export": "ldenv rocketh-export -e @@MODE @@","typescript": "tsc"}}
Transformation Rules for Scripts:
- Remove
hardhat typechain(no longer needed, artifacts generated automatically) - Update test command to use
hardhat test(no need for mocha directly) - Remove
_scripts.jspatterns - Use
ldenvfor environment-aware commands - Add
compile:watchusingas-soon - Add
deploy:watchusingas-soonandwait-on - Use rocketh commands:
rocketh-verify,rocketh-export,rocketh-doc - Add
typescriptscript for TypeScript compilation - Use
@@MODEplaceholder for network/environment
Common Patterns & Examples
Pattern 1: Simple Contract Deployment
v1:
module.exports = async ({ getNamedAccounts, deployments }) => {const { deploy } = deployments;const { deployer } = await getNamedAccounts();await deploy("MyContract", {from: deployer,args: ["Hello"],log: true,});};module.exports.tags = ["MyContract"];
v2:
import { deployScript, artifacts } from "../rocketh/deploy.js";export default deployScript(async ({ deploy, namedAccounts }) => {const { deployer } = namedAccounts;await deploy("MyContract", {account: deployer,artifact: artifacts.MyContract,args: ["Hello"],});},{ tags: ["MyContract"] },);
Pattern 2: Contract Deployment with Constructor Arguments
v1:
const { deploy } = deployments;const { deployer, tokenOwner } = await getNamedAccounts();await deploy("Token", {from: deployer,args: [tokenOwner, ethers.utils.parseEther("1000000"), "My Token", "MTK"],log: true,});
v2:
import { deployScript, artifacts } from "../rocketh/deploy.js";import { parseEther } from "viem";export default deployScript(async ({ deploy, namedAccounts }) => {const { deployer, tokenOwner } = namedAccounts;await deploy("Token", {account: deployer,artifact: artifacts.Token,args: [tokenOwner, parseEther("1000000"), "My Token", "MTK"],});},{ tags: ["Token"] },);
Pattern 3: Proxy Deployment
v1:
await deploy("MyContract", {from: deployer,proxy: {proxyContract: "OpenZeppelinTransparentProxy",viaAdminContract: "DefaultProxyAdmin",},args: [initArg],log: true,});
v2:
import * as proxyExtension from "@rocketh/proxy";// Add to extensions in rocketh/config.tsconst extensions = {...deployExtension,...proxyExtension,};// Then in deploy script:await env.deployViaProxy("MyContract",{account: deployer,artifact: artifacts.MyContract,args: [initArg],},{proxyKind: "Transparent",},);
Pattern 4: Reading Existing Deployments
v1:
const { deployer } = await getNamedAccounts();const existing = await deployments.get("MyContract");console.log("Contract address:", existing.address);
v2:
const MyContract = env.get<Abi_MyContract>("MyContract");console.log("Contract address:", MyContract.address);
Pattern 5: Contract Interaction in Tests
v1:
const MyContract = await ethers.getContract("MyContract");await MyContract.setValue(42);const value = await MyContract.getValue();expect(value).to.equal(42);
v2:
import { Abi_MyContract } from "../generated/abis/MyContract.js";const MyContract = env.get<Abi_MyContract>("MyContract");await env.execute(MyContract, {account: env.namedAccounts.deployer,functionName: "setValue",args: [42n],});const value = await env.read(MyContract, {functionName: "getValue",args: [],});expect(value).toEqual(42n);
Pattern 6: Getting Deployments by Tag
v1:
const deploymentsList = await deployments.getAll();const myDeployments = Object.values(deploymentsList);
v2:
const env = await loadAndExecuteDeploymentsFromFiles({provider: provider,tags: ["MyTag"],});
Pattern 7: Conditional Deployment
v1:
const useProxy = !hre.network.live;await deploy("MyContract", {from: deployer,proxy: useProxy && "postUpgrade",args: [initArg],});
v2:
const useProxy = !env.tags.live;await env.deployViaProxy("MyContract",{account: deployer,artifact: artifacts.MyContract,args: [initArg],},{proxyDisabled: !useProxy,execute: "postUpgrade",},);
Pattern 8: Network-Specific Configuration
v1:
const networkName = hre.network.name;if (networkName === "mainnet") {// mainnet-specific logic} else {// testnet logic}
v2:
const networkName = hre.network.name;if (env.tags.live) {// live network logic} else {// local/dev network logic}
Troubleshooting Guide
Error: "namedAccounts is not supported"
Cause: You still have namedAccounts in your hardhat.config.ts file.
Solution: Remove the namedAccounts section from hardhat.config.ts and move it to rocketh/config.ts:
// hardhat.config.ts - REMOVE THISnamedAccounts: {deployer: 0,admin: 1,},// rocketh/config.ts - ADD THISexport const config = {accounts: {deployer: {default: 0,},admin: {default: 1,},},} as const satisfies UserConfig;
Error: "deployments.deploy is not a function"
Cause: In v2, deploy is available directly in the environment, not through deployments.
Solution: Change your deploy script to use the new pattern:
Before (v1):
const {deploy} = hre.deployments;await deploy("Contract", {...});
After (v2):
import { deployScript, artifacts } from "../rocketh/deploy.js";export default deployScript(async ({ deploy }) => {await deploy("Contract", {artifact: artifacts.Contract,account: deployer,args: [],});}, {});
Error: "from is not a valid parameter"
Cause: v2 uses account: instead of from:.
Solution: Change all from: parameters to account::
Before:
await deploy("Contract", {from: deployer,args: [],});
After:
await deploy("Contract", {account: deployer,args: [],});
Error: Import errors with .js extensions
Cause: ESM modules require explicit file extensions for local imports.
Solution: Add .js extension to all local imports:
Before:
import { deployScript, artifacts } from "../rocketh/deploy";import { loadEnvironmentFromHardhat } from "./rocketh/environment";
After:
import { deployScript, artifacts } from "../rocketh/deploy.js";import { loadEnvironmentFromHardhat } from "./rocketh/environment.js";
Error: Type errors with artifacts
Cause: v2 uses explicit ABI types from generated artifacts.
Solution: Import ABI types and use them with env.get():
Before:
const MyContract = await ethers.getContract("MyContract");
After:
import { Abi_MyContract } from "../generated/abis/MyContract.js";const MyContract = env.get<Abi_MyContract>("MyContract");
Error: "HardhatDeploy is not a constructor"
Cause: Incorrect import of HardhatDeploy plugin.
Solution: Use default import:
Before:
import { HardhatDeploy } from "hardhat-deploy";
After:
import HardhatDeploy from "hardhat-deploy";
Error: Test fixtures not working
Cause: v2 uses a different fixture pattern.
Solution: Create custom fixture using loadAndExecuteDeploymentsFromFiles():
import {loadAndExecuteDeploymentsFromFiles} from '../rocketh/environment.js';import {network} from 'hardhat';const {provider, networkHelpers} = await network.connect();function setupFixtures(provider) {return {async deployAll() {const env = await loadAndExecuteDeploymentsFromFiles({provider: provider,});const MyContract = env.get<Abi_MyContract>("MyContract");return {env, MyContract, ...};},};}const {deployAll} = setupFixtures(provider);// In testconst {env, MyContract} = await networkHelpers.loadFixture(deployAll);
Error: "env.execute is not a function"
Cause: You're trying to use v2 pattern but haven't imported the correct extensions.
Solution: Ensure you have imported and set up the rocketh extensions:
// rocketh/config.tsimport * as readExecuteExtension from "@rocketh/read-execute";const extensions = {...deployExtension,...readExecuteExtension, // This provides execute function};export { extensions };
Error: Network configuration not working
Cause: v2 uses helper functions for network configuration.
Solution: Use the helper functions from hardhat-deploy/helpers:
import {addForkConfiguration,addNetworksFromEnv,addNetworksFromKnownList,} from "hardhat-deploy/helpers";const config: HardhatUserConfig = {networks: addForkConfiguration(addNetworksFromKnownList(addNetworksFromEnv({hardhat: {type: "edr-simulated",chainType: "l1",},}),),),};
Error: solidity config not working
Cause: v2 uses solidity profiles instead of compilers array.
Solution: Convert to profiles format:
Before (v1):
solidity: {compilers: [{version: '0.8.17',settings: {optimizer: {enabled: true,runs: 2000,},},},],}
After (v2):
solidity: {profiles: {default: {version: '0.8.17',},production: {version: '0.8.17',settings: {optimizer: {enabled: true,runs: 999999,},},},},}
Error: Module not found for utils/network.ts
Cause: This file is no longer needed in v2.
Solution: Delete the utils/network.ts file and use the helper functions from hardhat-deploy/helpers.
Error: Hardhat tasks not working
Cause: v2 may have different task names or requirements.
Solution: Check the task documentation and ensure you're using the correct syntax:
# Compile with production profilenpx hardhat compile --build-profile production# Deploy to specific networknpx hardhat --network sepolia deploy# Run testsnpx hardhat test
Error: Proxied.sol import not found
Cause: In hardhat-deploy v2, the Proxied.sol helper contract has moved from hardhat-deploy/solc_0.8/proxy/Proxied.sol to the @rocketh/proxy package.
Solution: Update the Solidity import path in your contracts:
Before (v1):
import "hardhat-deploy/solc_0.8/proxy/Proxied.sol";
After (v2):
import "@rocketh/proxy/solc_0_8/ERC1967/Proxied.sol";
Note: The path uses solc_0_8 with an underscore, not a dot.
Migration Checklist
Use this checklist to verify your migration is complete and working correctly.
Phase 1: Dependencies
- [ ] Updated package.json with
"type": "module" - [ ] Updated
hardhatto version 3.x or higher - [ ] Updated
hardhat-deployto version 2.x or higher - [ ] Removed
hardhat-deploy-ethersandhardhat-deploy-tenderly - [ ] Added
rockethpackage - [ ] Added
@rocketh/deploy,@rocketh/read-execute,@rocketh/node - [ ] Added
@rocketh/proxy(if using proxies) - [ ] Added optional packages:
@rocketh/export,@rocketh/verifier,@rocketh/doc - [ ] Added
viempackage - [ ] Added Hardhat 3.x plugins
- [ ] Updated
tsconfig.jsonfor ESM (module: "node16", moduleResolution: "node16") - [ ] Created
scripts/tsconfig.jsonwith extended configuration - [ ] Created
test/tsconfig.jsonwith extended configuration - [ ] Ran
pnpm installsuccessfully
Phase 2: Configuration
- [ ] Converted hardhat.config.ts to use
export default - [ ] Removed
namedAccountsfrom hardhat.config.ts - [ ] Imported
HardhatDeployfrom 'hardhat-deploy' - [ ] Added plugins array with all required plugins
- [ ] Converted solidity config to use profiles
- [ ] Used helper functions for network configuration
- [ ] Added
generateTypedArtifactsconfiguration - [ ] Deleted
utils/network.tsfile - [ ] Created
rocketh/config.tswith named accounts - [ ] Added extensions to rocketh/config.ts
- [ ] Created
rocketh/deploy.tswith deployScript setup - [ ] Created
rocketh/environment.tswith environment setup
Phase 3: Deploy Scripts
- [ ] Converted all deploy scripts to use
deployScriptwrapper - [ ] Changed from
(hre)to(env)parameter - [ ] Replaced
hre.getNamedAccounts()withenv.namedAccounts - [ ] Changed
from:toaccount:in all deploy calls - [ ] Added explicit
artifact:parameter to all deploy calls - [ ] Removed
log:andautoMine:parameters - [ ] Moved tags to second argument object
- [ ] Converted proxy deployments to use
env.deployViaProxy() - [ ] Updated
hre.network.livetoenv.tags.live - [ ] Imported artifacts from
../rocketh/deploy.js
Phase 4: Tests
- [ ] Changed test runner to
node:test(or kept mocha if preferred) - [ ] Updated assertion library imports
- [ ] Created custom fixture functions
- [ ] Replaced
deployments.createFixture()with custom fixtures - [ ] Imported ABI types from generated artifacts
- [ ] Replaced
ethers.getContract()withenv.get<Abi_Type>() - [ ] Replaced
getUnnamedAccounts()withenv.unnamedAccounts - [ ] Converted contract method calls to
env.execute() - [ ] Updated test utilities
- [ ] Used
networkHelpers.loadFixture()for fixtures
Phase 5: Scripts
- [ ] Imported
loadEnvironmentFromHardhatfrom rocketh/environment - [ ] Imported ABI types from generated artifacts
- [ ] Replaced direct HRE access with
loadEnvironmentFromHardhat() - [ ] Converted contract method calls to
env.execute()
Phase 6: Package.json Scripts
- [ ] Removed
hardhat typechaincommand - [ ] Updated test command to use
hardhat test - [ ] Removed
_scripts.jspatterns - [ ] Added
ldenvcommands for environment-aware operations - [ ] Added watch commands using
as-soon - [ ] Added rocketh commands:
rocketh-verify,rocketh-export,rocketh-doc - [ ] Added TypeScript compilation script
Phase 7: Verification
- [ ] Ran
npx hardhat compilesuccessfully - [ ] Ran
npx hardhat deployon local network successfully - [ ] Ran
npx hardhat testsuccessfully - [ ] Verified deployments in
deployments/directory - [ ] Verified generated artifacts in
generated/directory - [ ] Tested on test network (if applicable)
- [ ] Verified contract interactions work correctly
- [ ] Checked type errors with
tsc --noEmit
Phase 8: Documentation
- [ ] Updated project README with v2-specific instructions
- [ ] Documented any custom rocketh extensions used
- [ ] Updated CI/CD configuration if needed
- [ ] Created migration notes for team members
Advanced Topics
Fork Testing
v2 provides enhanced fork testing through the Hardhat 3.x integration.
Setup:
import { addForkConfiguration } from "hardhat-deploy/helpers";const config: HardhatUserConfig = {networks: addForkConfiguration({hardhat: {type: "edr-simulated",chainType: "l1",},}),};
Usage:
# Fork from mainnetHARDHAT_FORK=mainnet npx hardhat test# Fork from specific blockHARDHAT_FORK=mainnet HARDHAT_FORK_NUMBER=12345678 npx hardhat test
Environment Variable Configuration
v2 makes it easy to configure networks using environment variables:
# Set RPC URL for a networkETH_NODE_URI_MAINNET=https://mainnet.infura.io/v3/YOUR_KEYETH_NODE_URI_SEPOLIA=https://sepolia.infura.io/v3/YOUR_KEY# Set mnemonic for accountsMNEMONIC="your twelve word mnemonic here"# Or use network-specific mnemonicsMNEMONIC_MAINNET="production mnemonic"MNEMONIC_SEPOLIA="testnet mnemonic"
The addNetworksFromEnv() helper will automatically create network configurations for these variables.
Customizing Rocketh Extensions
You can add custom extensions to the rocketh configuration:
// rocketh/config.tsimport * as customExtension from "./my-custom-extension";const extensions = {...deployExtension,...readExecuteExtension,...customExtension, // Add your custom extension};
// my-custom-extension.tsexport function myCustomFunction(env, options) {// Your custom logic herereturn result;}
Integration with CI/CD
GitHub Actions Example:
name: Deployon:push:branches: [main]jobs:deploy:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v3- uses: pnpm/action-setup@v2- uses: actions/setup-node@v3with:node-version: "22"cache: "pnpm"- run: pnpm install- run: pnpm compile --build-profile production- name: Deploy to Sepoliaenv:ETH_NODE_URI_SEPOLIA: ${{ secrets.SEPOLIA_RPC_URL }}MNEMONIC_SEPOLIA: ${{ secrets.SEPOLIA_MNEMONIC }}run: pnpm deploy sepolia- name: Verify Contractsenv:ETH_NODE_URI_SEPOLIA: ${{ secrets.SEPOLIA_RPC_URL }}MNEMONIC_SEPOLIA: ${{ secrets.SEPOLIA_MNEMONIC }}ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}run: pnpm verify sepolia
Multi-Contract Deployments
For complex deployments with multiple interdependent contracts:
export default deployScript(async ({ deploy, namedAccounts }) => {const { deployer } = namedAccounts;// Deploy first contractconst ContractA = await deploy("ContractA", {account: deployer,artifact: artifacts.ContractA,args: [],});// Deploy second contract with address of firstconst ContractB = await deploy("ContractB", {account: deployer,artifact: artifacts.ContractB,args: [ContractA.address],});},{ tags: ["multi"] },);
Deployment Verification
Use the verification extension to verify contracts on block explorers:
# Verify all deploymentspnpm verify sepolia# Verify specific contractpnpm rocketh-verify -e sepolia --contract MyContract
Deployment Export
Export deployments for use in other projects:
# Export deployments to JSONpnpm export sepolia# This creates a deployments.json file with all contract addresses and ABIs
Hot Contract Replacement (HCR)
v2 maintains the HCR feature from v1, allowing rapid development cycles:
export default deployScript(async ({ deploy, namedAccounts }) => {const { deployer } = namedAccounts;const useProxy = !env.tags.live;await env.deployViaProxy("MyContract",{account: deployer,artifact: artifacts.MyContract,args: [],},{proxyDisabled: !useProxy, // Only use proxy in devexecute: "postUpgrade",},);}, {});
With watch mode, any contract changes will automatically redeploy the proxy:
pnpm deploy:watch sepolia
TypeScript Type Safety
v2 provides enhanced TypeScript support through generated types:
import { Abi_MyContract } from "../generated/abis/MyContract.js";// Fully typed contract accessconst MyContract = env.get<Abi_MyContract>("MyContract");// Type-safe function calls with IntelliSenseawait env.execute(MyContract, {account: deployer,functionName: "setValue", // TypeScript will suggest available functionsargs: [42n], // TypeScript will validate argument types});// Type-safe readsconst value = await env.read(MyContract, {functionName: "getValue",args: [], // TypeScript will validate return type});// value is typed as bigint
Handling Existing Deployments
If you have existing deployments from v1, v2 can read them directly:
- Ensure your
deployments/directory structure is maintained - The
.chainfile format is compatible - Deployment JSON files work with both versions
To migrate deployment metadata to v2 format:
# Compile contractspnpm compile# Deploy to refresh metadatapnpm hardhat deploy --network <network> --reset
Additional Resources
Official Documentation
- hardhat-deploy v2 Documentation
- Setup First Project Guide
- Migration from v1 Guide
- Hardhat 3.x Documentation
- Rocketh Documentation
Example Projects
- template-ethereum-contracts - Complete working example using v2
- Basic Demo
- Diamond Demo
- Proxies Demo
Community
Summary
Migrating from hardhat-deploy v1 to v2 involves:
- Updating dependencies to Hardhat 3.x and v2 packages
- Restructuring configuration into multiple files (hardhat.config.ts, rocketh/\*.ts)
- Converting deploy scripts to use the new pattern with
deployScript - Updating tests to use the new fixture pattern
- Modernizing scripts to use
loadEnvironmentFromHardhat - Updating package.json scripts for the new workflow
The migration requires significant changes, but results in:
- Better TypeScript support
- More modular architecture
- Enhanced extensibility through rocketh
- Modern ESM modules
- Improved developer experience
Take the migration step by step, test each phase thoroughly, and refer to this guide and the template-ethereum-contracts repository for concrete examples of the transformations needed.