Migration Guide: Hardhat 2 to Hardhat 3
July 23, 2026 · View on GitHub
Prerequisite: Migrate your Hardhat project to Hardhat 3 first. See the official Hardhat 3 migration guide.
Breaking Changes
- No automatic
hre.upgrades- Must call factory function explicitly - Factory functions are async - Require
awaitand network connection - Import changes - Import factory functions, not just the plugin
- Updated peerDependencies -
hardhatand@nomicfoundation/hardhat-etherspeer dependency versions have been updated for Hardhat 3.@nomicfoundation/hardhat-ethersandethersare now optional peer dependencies, needed only if you use ethers.
Install Dependencies
If upgrading from a previous version, ensure these packages are in your devDependencies:
npm install --save-dev hardhat @nomicfoundation/hardhat-ethers
Using viem? Install
@nomicfoundation/hardhat-viemandviem, register@nomicfoundation/hardhat-viemin your config'spluginsarray, and import the upgrades plugin and the API from@openzeppelin/hardhat-upgrades/viem. Its functions take contract names and return viem contract instances. See Usage with viem.
Migration
Update Config
In Hardhat 3, plugins must be explicitly added to the plugins array in your config:
Before (Hardhat 2):
import '@openzeppelin/hardhat-upgrades';
After (Hardhat 3):
import { defineConfig } from 'hardhat/config';
import hardhatUpgrades from '@openzeppelin/hardhat-upgrades';
export default defineConfig({
plugins: [hardhatUpgrades],
// ... rest of config
});
If you use the verify task, also add @nomicfoundation/hardhat-verify and configure Hardhat's verify.etherscan.apiKey setting:
import { configVariable, defineConfig } from 'hardhat/config';
import hardhatVerify from '@nomicfoundation/hardhat-verify';
import hardhatUpgrades from '@openzeppelin/hardhat-upgrades';
export default defineConfig({
plugins: [hardhatVerify, hardhatUpgrades],
verify: {
etherscan: {
apiKey: configVariable('ETHERSCAN_API_KEY'),
},
},
});
Update Imports
Before:
import '@openzeppelin/hardhat-upgrades';
After:
import { upgrades, defender } from '@openzeppelin/hardhat-upgrades';
All functions are now exported by the API. Import upgrades for standard functions, or defender for Defender-specific functions.
Update Usage
Before:
await hre.upgrades.deployProxy(MyContract, []);
After:
const connection = await hre.network.create();
const upgradesApi = await upgrades(hre, connection);
await upgradesApi.deployProxy(MyContract, []);
Important:
- Both
upgradesanddefenderreceivehreandconnectionas parameters:upgrades(hre, connection)ordefender(hre, connection) - Share the connection across multiple operations; do not create a new one each time
- In tests, create the connection once in a
beforeblock or use top-level await (ESM)
Examples
Scripts
import hre from 'hardhat';
import { upgrades } from '@openzeppelin/hardhat-upgrades';
async function main() {
const connection = await hre.network.create();
const { ethers } = connection;
const upgradesApi = await upgrades(hre, connection);
const MyContract = await ethers.getContractFactory('MyContract');
const proxy = await upgradesApi.deployProxy(MyContract, []);
const MyContractV2 = await ethers.getContractFactory('MyContractV2');
await upgradesApi.upgradeProxy(proxy, MyContractV2);
}
main();
Tasks
import { task } from 'hardhat/config';
import { upgrades } from '@openzeppelin/hardhat-upgrades';
task('deploy', async (args, hre) => {
const connection = await hre.network.create();
const { ethers } = connection;
const upgradesApi = await upgrades(hre, connection);
const MyContract = await ethers.getContractFactory('MyContract');
await upgradesApi.deployProxy(MyContract, []);
});
Tests
Important: In Hardhat 3, ethers comes from the connection, not hre.ethers. Share the connection across all tests in a suite.
Before (Hardhat 2):
import hre from 'hardhat';
import '@openzeppelin/hardhat-upgrades';
describe('MyContract', () => {
it('should deploy', async () => {
const MyContract = await hre.ethers.getContractFactory('MyContract');
const proxy = await hre.upgrades.deployProxy(MyContract, []);
});
});
After (Hardhat 3) - with before hook:
import hre from 'hardhat';
import { upgrades } from '@openzeppelin/hardhat-upgrades';
describe('MyContract', () => {
let upgradesApi;
let ethers;
before(async () => {
const connection = await hre.network.create();
({ ethers } = connection);
upgradesApi = await upgrades(hre, connection);
});
it('should deploy', async () => {
const MyContract = await ethers.getContractFactory('MyContract');
const proxy = await upgradesApi.deployProxy(MyContract, []);
});
});
After (Hardhat 3) - with ESM top-level await:
import hre from 'hardhat';
import { upgrades, defender } from '@openzeppelin/hardhat-upgrades';
const connection = await hre.network.create();
const { ethers } = connection;
const upgradesApi = await upgrades(hre, connection);
const defenderApi = await defender(hre, connection);
describe('MyContract', () => {
it('should deploy', async () => {
const MyContract = await ethers.getContractFactory('MyContract');
const proxy = await upgradesApi.deployProxy(MyContract, []);
});
});
Note: Both upgrades and defender receive hre and connection as parameters.
Hardhat 3 also supports tests written in Solidity. See Solidity tests for setup with @openzeppelin/foundry-upgrades.
Verify Task (Optional)
If your Hardhat config file's verify.etherscan.apiKey setting uses configVariable('ETHERSCAN_API_KEY'), set ETHERSCAN_API_KEY before running Hardhat (or use a provider such as @nomicfoundation/hardhat-keystore):
ETHERSCAN_API_KEY=... npx hardhat verify --network mainnet PROXY_ADDRESS
The upgrades plugin extends hardhat-verify's verify task for proxy addresses.
Note that you do not need to include constructor arguments when verifying if your implementation contract only uses initializers. However, if your implementation contract has an actual constructor with arguments (such as to set immutable variables), then include constructor arguments according to Hardhat's documentation for verifying a contract.
Checklist
- If you use ethers, install
@nomicfoundation/hardhat-ethersandethers(now optional peer dependencies; not needed if you use viem) - If you use viem, install
@nomicfoundation/hardhat-viemandviem, addhardhatViemtoplugins, and import both the upgrades plugin and the API from@openzeppelin/hardhat-upgrades/viem - Add
hardhatUpgradestopluginsinhardhat.config.ts - If using
verify, addhardhatVerifytoplugins, install@nomicfoundation/hardhat-verify, and configure Hardhat'sverify.etherscan.apiKeysetting - Replace
import '@openzeppelin/hardhat-upgrades'→import { upgrades, defender } from '@openzeppelin/hardhat-upgrades'in scripts/tests - Add
const connection = await hre.network.create();(share connection across operations, don't create new ones) - Replace
hre.ethers→ethersfrom connection (const { ethers } = connection) - Replace
hre.upgrades.method()→ call methods fromconst upgradesApi = await upgrades(hre, connection) - Replace
hre.defender.method()→ call methods fromconst defenderApi = await defender(hre, connection) - Update all scripts, tasks, and tests