Migrating from Hardhat 2
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.
Install Dependencies
If upgrading from a previous version, ensure these packages are in your devDependencies:
npm install --save-dev hardhat @nomicfoundation/hardhat-ethersUsing viem? You can install both @nomicfoundation/hardhat-ethers and @nomicfoundation/hardhat-viem. The upgrades plugin uses ethers internally; your own scripts and tests can still use 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 (or use
hre.network.getOrCreate(), which reuses a connection per network) - 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_ADDRESSThe 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
- Install
@nomicfoundation/hardhat-ethers— required even if your project uses viem (install both if needed) - 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