Comprehensive guide for migrating projects from hardhat-deploy v1 to v2, including dependency updates, configuration restructuring, deploy script conversion, test updates, and troubleshooting
76
65%
Does it follow best practices?
Impact
100%
5.26xAverage score across 3 eval scenarios
Advisory
Suggest reviewing before use
Optimize this skill with Tessl
npx tessl skill review --optimize ./skills/hardhat-deploy-migration/SKILL.mdThis 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.
hardhat-deploy v2 is a complete rewrite that requires Hardhat 3.x and introduces significant architectural changes:
import/export) instead of CommonJS| 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() |
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-deploy
npm install hardhat-deploy@1v1 continues to receive security fixes but won't get new features.
Before starting the migration, verify your environment meets these requirements:
# 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 ESM
grep -q '"type": "module"' package.json && echo "ESM" || echo "CommonJS"^3.1.5 recommended)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()graph TD
A[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 helpergraph TD
A[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| I
N -->|uses| J
O[test/*.ts] -->|imports| M
O -->|uses| env.get and env.executeKey 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 patternUpdate 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:
"type": "module" at the top levelhardhat to ^3.1.4 or higherhardhat-deploy to ^2.0.0 or higherhardhat-deploy-ethers and hardhat-deploy-tenderlyrocketh, @rocketh/deploy, @rocketh/read-execute, @rocketh/node, @rocketh/signer@rocketh/proxy, @rocketh/export, @rocketh/verifier, @rocketh/docviem for contract interactionsearl for assertions (for node:test)Install dependencies:
pnpm installv1 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 network
addForkConfiguration(
// 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 SECRET
addNetworksFromEnv(
// 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:
import 'hardhat-deploy' to import HardhatDeploy from 'hardhat-deploy'namedAccounts section entirelysolidity.compilers to solidity.profilesplugins array with imported pluginshardhat-deploy/helpers for network configurationgenerateTypedArtifacts configurationmocha timeout configuration (not needed in v2)external.deployments configuration (handled differently)utils/network.ts file (no longer needed)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:
module from commonjs to node16target from es5 to es2022moduleResolution from node to node16lib: ["es2023"]skipLibCheck: truesourceMap: true, declaration: true, declarationMap: trueinclude to only ["deploy", "generated"]scripts/tsconfig.json extending the main configtest/tsconfig.json extending the main configNew 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 account
import { 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 function
import * as deployExtension from "@rocketh/deploy";
// this one provide read,execute functions
import * as readExecuteExtension from "@rocketh/read-execute";
// this one provide a deployViaProxy function that let you declaratively
// deploy proxy based contracts
import * as deployProxyExtension from "@rocketh/proxy";
// this one provide a viem handle to clients and contracts
import * as viemExtension from "@rocketh/viem";
// and export them as a unified object
const extensions = {
...deployExtension,
...readExecuteExtension,
...deployProxyExtension,
...viemExtension,
};
export { extensions };
// then we also export the types that our config ehibit so other can use it
type 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:
rocketh directory: mkdir rockethnamedAccounts from hardhat.config.ts to rocketh/config.ts under accountsNew 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 alias
import * as artifacts from "../generated/artifacts/index.js";
export { artifacts };
// ----------------------------------------------------------------------------
// we create the rocketh functions we need by passing the extensions to the
// setup function
import { setupDeployScripts } from "rocketh";
const { deployScript } = setupDeployScripts<Extensions, Accounts, Data>(
extensions,
);
export { deployScript };Transformation Rules:
./config.js../generated/artifacts/index.jssetupDeployScripts from rockethdeployScript and artifactsNew 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-system
const { loadAndExecuteDeploymentsFromFiles } = setupEnvironmentFromFiles<
Extensions,
Accounts,
Data
>(extensions);
const { loadEnvironmentFromHardhat } = setupHardhatDeploy<
Extensions,
Accounts,
Data
>(extensions);
export { loadEnvironmentFromHardhat, loadAndExecuteDeploymentsFromFiles };Transformation Rules:
./config.js@rocketh/node and hardhat-deploy/helpersloadEnvironmentFromHardhat for scriptsloadAndExecuteDeploymentsFromFiles for testsv1 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:
HardhatRuntimeEnvironment and DeployFunction importsdeployScript and artifacts from ../rocketh/deploy.jsparseEther from ethers to viemdeployScript() call(hre) to (env)hre.getNamedAccounts() with direct env.namedAccounts accesshre.deployments.deploy() with env.deploy()from: to account:artifact: parameterlog: and autoMine: parameters (not needed in v2)func.tagsv1 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 invoked
await 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 reexecution
func.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 invoked
await 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:
proxy: useProxy && 'postUpgrade' to env.deployViaProxy() callproxyDisabled: !useProxy instead of conditional proxyexecute: 'postUpgrade' instead of proxy typehre.network.live with env.tags.livev1 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 matching
const 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:
mocha to node:testchai to earl (or keep chai if preferred)network from 'hardhat'loadAndExecuteDeploymentsFromFiles()deployments.createFixture() with custom fixturedeployments.fixture() with loadAndExecuteDeploymentsFromFiles()ethers.getContract() with env.get<Abi_Type>()import {Abi_SimpleERC20} from '../generated/abis/SimpleERC20.js'getUnnamedAccounts() with env.unnamedAccountsenv.execute():users[0].SimpleERC20.transfer(users[1].address, 1)env.execute(SimpleERC20, {account: unnamedAccounts[0], functionName: 'transfer', args: [unnamedAccounts[1], 1n]})BigInt literals (1n) instead of Numbers (1) for amountsnetworkHelpers.loadFixture() instead of direct fixture callv1 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-any
const 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 matching
const GreetingsRegistry =
env.get<Abi_GreetingsRegistry>("GreetingsRegistry");
return {
env,
GreetingsRegistry,
namedAccounts: env.namedAccounts,
unnamedAccounts: env.unnamedAccounts,
};
},
};
}Transformation Rules for Test Utils:
setupUsers and setupUser functions (not needed in v2)setupFixtures function that returns deployment setuploadAndExecuteDeploymentsFromFiles() for deploymentv1 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:
loadEnvironmentFromHardhat from ./rocketh/environment.jsloadEnvironmentFromHardhat({hre}) instead of direct HRE accessethers.getContract() with env.get<Abi_Type>()env.execute()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:
hardhat typechain (no longer needed, artifacts generated automatically)hardhat test (no need for mocha directly)_scripts.js patternsldenv for environment-aware commandscompile:watch using as-soondeploy:watch using as-soon and wait-onrocketh-verify, rocketh-export, rocketh-doctypescript script for TypeScript compilation@@MODE placeholder for network/environmentv1:
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"] },
);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"] },
);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.ts
const extensions = {
...deployExtension,
...proxyExtension,
};
// Then in deploy script:
await env.deployViaProxy(
"MyContract",
{
account: deployer,
artifact: artifacts.MyContract,
args: [initArg],
},
{
proxyKind: "Transparent",
},
);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);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);v1:
const deploymentsList = await deployments.getAll();
const myDeployments = Object.values(deploymentsList);v2:
const env = await loadAndExecuteDeploymentsFromFiles({
provider: provider,
tags: ["MyTag"],
});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",
},
);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
}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 THIS
namedAccounts: {
deployer: 0,
admin: 1,
},
// rocketh/config.ts - ADD THIS
export const config = {
accounts: {
deployer: {
default: 0,
},
admin: {
default: 1,
},
},
} as const satisfies UserConfig;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: [],
});
}, {});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: [],
});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";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");Cause: Incorrect import of HardhatDeploy plugin.
Solution: Use default import:
Before:
import { HardhatDeploy } from "hardhat-deploy";After:
import HardhatDeploy from "hardhat-deploy";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 test
const {env, MyContract} = await networkHelpers.loadFixture(deployAll);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.ts
import * as readExecuteExtension from "@rocketh/read-execute";
const extensions = {
...deployExtension,
...readExecuteExtension, // This provides execute function
};
export { extensions };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",
},
}),
),
),
};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,
},
},
},
},
}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.
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 profile
npx hardhat compile --build-profile production
# Deploy to specific network
npx hardhat --network sepolia deploy
# Run tests
npx hardhat testCause: 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.
Use this checklist to verify your migration is complete and working correctly.
"type": "module"hardhat to version 3.x or higherhardhat-deploy to version 2.x or higherhardhat-deploy-ethers and hardhat-deploy-tenderlyrocketh package@rocketh/deploy, @rocketh/read-execute, @rocketh/node@rocketh/proxy (if using proxies)@rocketh/export, @rocketh/verifier, @rocketh/docviem packagetsconfig.json for ESM (module: "node16", moduleResolution: "node16")scripts/tsconfig.json with extended configurationtest/tsconfig.json with extended configurationpnpm install successfullyexport defaultnamedAccounts from hardhat.config.tsHardhatDeploy from 'hardhat-deploy'generateTypedArtifacts configurationutils/network.ts filerocketh/config.ts with named accountsrocketh/deploy.ts with deployScript setuprocketh/environment.ts with environment setupdeployScript wrapper(hre) to (env) parameterhre.getNamedAccounts() with env.namedAccountsfrom: to account: in all deploy callsartifact: parameter to all deploy callslog: and autoMine: parametersenv.deployViaProxy()hre.network.live to env.tags.live../rocketh/deploy.jsnode:test (or kept mocha if preferred)deployments.createFixture() with custom fixturesethers.getContract() with env.get<Abi_Type>()getUnnamedAccounts() with env.unnamedAccountsenv.execute()networkHelpers.loadFixture() for fixturesloadEnvironmentFromHardhat from rocketh/environmentloadEnvironmentFromHardhat()env.execute()hardhat typechain commandhardhat test_scripts.js patternsldenv commands for environment-aware operationsas-soonrocketh-verify, rocketh-export, rocketh-docnpx hardhat compile successfullynpx hardhat deploy on local network successfullynpx hardhat test successfullydeployments/ directorygenerated/ directorytsc --noEmitv2 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 mainnet
HARDHAT_FORK=mainnet npx hardhat test
# Fork from specific block
HARDHAT_FORK=mainnet HARDHAT_FORK_NUMBER=12345678 npx hardhat testv2 makes it easy to configure networks using environment variables:
# Set RPC URL for a network
ETH_NODE_URI_MAINNET=https://mainnet.infura.io/v3/YOUR_KEY
ETH_NODE_URI_SEPOLIA=https://sepolia.infura.io/v3/YOUR_KEY
# Set mnemonic for accounts
MNEMONIC="your twelve word mnemonic here"
# Or use network-specific mnemonics
MNEMONIC_MAINNET="production mnemonic"
MNEMONIC_SEPOLIA="testnet mnemonic"The addNetworksFromEnv() helper will automatically create network configurations for these variables.
You can add custom extensions to the rocketh configuration:
// rocketh/config.ts
import * as customExtension from "./my-custom-extension";
const extensions = {
...deployExtension,
...readExecuteExtension,
...customExtension, // Add your custom extension
};// my-custom-extension.ts
export function myCustomFunction(env, options) {
// Your custom logic here
return result;
}GitHub Actions Example:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: "22"
cache: "pnpm"
- run: pnpm install
- run: pnpm compile --build-profile production
- name: Deploy to Sepolia
env:
ETH_NODE_URI_SEPOLIA: ${{ secrets.SEPOLIA_RPC_URL }}
MNEMONIC_SEPOLIA: ${{ secrets.SEPOLIA_MNEMONIC }}
run: pnpm deploy sepolia
- name: Verify Contracts
env:
ETH_NODE_URI_SEPOLIA: ${{ secrets.SEPOLIA_RPC_URL }}
MNEMONIC_SEPOLIA: ${{ secrets.SEPOLIA_MNEMONIC }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
run: pnpm verify sepoliaFor complex deployments with multiple interdependent contracts:
export default deployScript(
async ({ deploy, namedAccounts }) => {
const { deployer } = namedAccounts;
// Deploy first contract
const ContractA = await deploy("ContractA", {
account: deployer,
artifact: artifacts.ContractA,
args: [],
});
// Deploy second contract with address of first
const ContractB = await deploy("ContractB", {
account: deployer,
artifact: artifacts.ContractB,
args: [ContractA.address],
});
},
{ tags: ["multi"] },
);Use the verification extension to verify contracts on block explorers:
# Verify all deployments
pnpm verify sepolia
# Verify specific contract
pnpm rocketh-verify -e sepolia --contract MyContractExport deployments for use in other projects:
# Export deployments to JSON
pnpm export sepolia
# This creates a deployments.json file with all contract addresses and ABIsv2 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 dev
execute: "postUpgrade",
},
);
}, {});With watch mode, any contract changes will automatically redeploy the proxy:
pnpm deploy:watch sepoliav2 provides enhanced TypeScript support through generated types:
import { Abi_MyContract } from "../generated/abis/MyContract.js";
// Fully typed contract access
const MyContract = env.get<Abi_MyContract>("MyContract");
// Type-safe function calls with IntelliSense
await env.execute(MyContract, {
account: deployer,
functionName: "setValue", // TypeScript will suggest available functions
args: [42n], // TypeScript will validate argument types
});
// Type-safe reads
const value = await env.read(MyContract, {
functionName: "getValue",
args: [], // TypeScript will validate return type
});
// value is typed as bigintIf you have existing deployments from v1, v2 can read them directly:
deployments/ directory structure is maintained.chain file format is compatibleTo migrate deployment metadata to v2 format:
# Compile contracts
pnpm compile
# Deploy to refresh metadata
pnpm hardhat deploy --network <network> --resetMigrating from hardhat-deploy v1 to v2 involves:
deployScriptloadEnvironmentFromHardhatThe migration requires significant changes, but results in:
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.
a1cee92
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.