Skip to content

Migration of hardhat-upgrade to plugin to use hardhat v3#1194

Open
afa7789 wants to merge 101 commits intonextfrom
migrate/hardhat-3
Open

Migration of hardhat-upgrade to plugin to use hardhat v3#1194
afa7789 wants to merge 101 commits intonextfrom
migrate/hardhat-3

Conversation

@afa7789
Copy link

@afa7789 afa7789 commented Oct 20, 2025

⚠️ Changeset and versioning must be reviewed so we don't break versioning for users of previous version too ( hh2). ⚠️

Migration to Hardhat 3: ESM & Plugin Hooks

This PR migrates @openzeppelin/hardhat-upgrades to Hardhat 3, converting the package to ESM and adopting the new plugin hooks architecture. This is a major breaking change that replaces the legacy extendEnvironment pattern with HardhatPlugin hook handlers for config and Solidity initialization. Users must now utilize explicit async factory functions—await upgrades(hre, connection) and defender()—and manage network connectivity via await hre.network.connect(), as ethers is now obtained through the connection rather than the HRE.

  • Technical Implementation & Compatibility

The package has been fully converted to ESM (type: module) using Node16 resolution. To ensure compatibility with Babel and NYC coverage, import...with { type: 'json' } statements were replaced with createRequire() across 15 files. Dependencies are updated to hardhat@^3.0.16, ethers v6, and the latest @nomicfoundation plugins.

Testing infrastructure now includes Solidity test support via the foundry-upgrades repository. To stabilize the environment, example projects were isolated using pnpm link protocols and local AVA configurations. CI/CD workflows were modified to initialize git submodules before installation and now include foundry-upgrades in npmFilesToBuild.

  • Documentation & Known Constraints

A comprehensive MIGRATION.md has been added with before/after examples, and the README now reflects Hardhat 3 patterns. Please note that full hardhat-verify integration and certain namespace layouts are currently pending an upstream fix in Hardhat ([PR #7744](NomicFoundation/hardhat#7744)), which also impacts the provided example projects. While core functionality is tested and passing, some edge cases in verification may require further refinement following the upstream merge. We also need a new feature from the nomic team to allow access to their internal libraries for us to use them like in hh2, at the verify part.

Summary by CodeRabbit

  • New Features

    • Added Hardhat 3 support with ESM module structure and plugin hooks
    • Updated tooling to Node 22 and ethers v6.14.0
    • Introduced example projects for Transparent and UUPS proxy patterns
  • Documentation

    • Added migration guide for Hardhat 2 to 3 upgrade
    • Updated README with TypeScript-first approach and connection-sharing patterns
    • Documented Hardhat 3 API changes and configuration updates
  • Chores

    • Updated build configuration and dependencies for Hardhat 3 compatibility

@@ -1,29 +1,37 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// TODO figure out what is the newer, TASK_COMPILE_SOLIDITY and TASK_COMPILE_SOLIDITY_COMPILE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are Hardhat internal subtasks, where Hardhat calls TASK_COMPILE_SOLIDITY -> TASK_COMPILE_SOLIDITY_COMPILE_JOBS -> TASK_COMPILE_SOLIDITY_COMPILE_JOB -> TASK_COMPILE_SOLIDITY_COMPILE. And only certain parts of those need to be overridden in our plugin. See https://github.com/NomicFoundation/hardhat/blob/09951afd78e49465f7949478919c9217a12e8338/packages/hardhat-core/src/builtin-tasks/compile.ts#L1366 for the v2 internal subtasks.

We may need to find the equivalents in v3.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On it already.

it seems that there is no such function named subtask anymore, so I'm trying to find what is the alternative right now, so we can figure out how to do what we did before but in hardhat3.

@socket-security
Copy link

socket-security bot commented Oct 22, 2025

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

Ignoring alerts on:

  • yargs-parser@18.1.3
  • yargs@15.4.1
  • y18n@4.0.3
  • yargs-parser@20.2.9
  • yocto-queue@0.1.0
  • resolve-pkg-maps@1.0.0
  • yargs-unparser@2.0.0
  • decamelize@4.0.0
  • yargs@13.3.2
  • wrap-ansi@5.1.0
  • yargs-parser@13.1.2
  • yallist@5.0.0
  • @isaacs/fs-minipass@4.0.1
  • rfdc@1.4.1
  • rimraf@5.0.10
  • escalade@3.2.0
  • undici-types@6.21.0
  • node-gyp-build@4.8.4
  • resolve.exports@2.0.3
  • https-proxy-agent@7.0.6
  • chokidar@4.0.3
  • fast-glob@3.3.3
  • @streamparser/json@0.0.22
  • readdirp@4.1.2
  • @streamparser/json-node@0.0.22
  • foreground-child@3.3.1
  • cbor2@1.12.0
  • consola@3.4.2
  • @noble/secp256k1@1.7.2
  • bn.js@5.2.2
  • ignore@7.0.5
  • @types/estree@1.0.8
  • acorn@8.15.0
  • forge-std@1.9.7
  • dotenv@16.6.1
  • ws@8.18.3
  • agent-base@7.1.4
  • zod@3.25.76
  • follow-redirects@1.15.11
  • @sentry/core@9.46.0
  • esmock@2.7.3
  • debug@4.4.3
  • tsx@4.20.6
  • undici@6.22.0
  • ci-info@4.3.1
  • semver@7.7.3
  • get-tsconfig@4.12.0
  • @types/node@20.19.21
  • @esbuild/win32-x64@0.25.11
  • @esbuild/darwin-arm64@0.25.11
  • @esbuild/freebsd-arm64@0.25.11
  • @esbuild/openbsd-arm64@0.25.11
  • esbuild@0.25.11
  • @esbuild/netbsd-arm64@0.25.11
  • @esbuild/openharmony-arm64@0.25.11
  • @esbuild/sunos-x64@0.25.11
  • @esbuild/linux-loong64@0.25.11
  • @esbuild/android-x64@0.25.11
  • @esbuild/linux-ia32@0.25.11
  • @esbuild/android-arm64@0.25.11
  • @esbuild/linux-riscv64@0.25.11
  • @esbuild/android-arm@0.25.11
  • @esbuild/linux-arm@0.25.11
  • @esbuild/darwin-x64@0.25.11
  • @esbuild/linux-x64@0.25.11
  • @esbuild/linux-ppc64@0.25.11
  • @esbuild/linux-s390x@0.25.11
  • @esbuild/aix-ppc64@0.25.11
  • @esbuild/linux-mips64el@0.25.11
  • @esbuild/freebsd-x64@0.25.11
  • @esbuild/linux-arm64@0.25.11
  • @esbuild/netbsd-x64@0.25.11
  • @esbuild/openbsd-x64@0.25.11
  • @esbuild/win32-arm64@0.25.11
  • @esbuild/win32-ia32@0.25.11
  • tsx@4.21.0
  • fast-equals@5.4.0
  • @openzeppelin/foundry-upgrades@0.4.1-alpha.0
  • @nomicfoundation/hardhat-vendored@3.0.1
  • @nomicfoundation/hardhat-utils@4.0.0
  • @nomicfoundation/hardhat-errors@3.0.7
  • @nomicfoundation/hardhat-zod-utils@3.0.2
  • @openzeppelin/contracts-upgradeable@5.6.1
  • chownr@3.0.0
  • glob@10.4.5
  • unicorn-magic@0.3.0
  • path-type@6.0.0
  • is-unicode-supported@2.1.0
  • acorn-walk@8.3.4
  • write-file-atomic@6.0.0
  • package-json-from-dist@1.0.1
  • mocha@10.8.2
  • elliptic@6.6.1
  • p-map@7.0.3
  • obliterator@2.0.5
  • @types/ms@2.1.0
  • nopt@8.1.0
  • @mapbox/node-pre-gyp@2.0.0
  • globby@14.1.0
  • stacktrace-parser@0.1.11
  • memoize@10.1.0
  • @types/sinon@17.0.4
  • @ethersproject/logger@5.8.0
  • @ethersproject/bignumber@5.8.0
  • @ethersproject/properties@5.8.0
  • @ethersproject/constants@5.8.0
  • @ethersproject/address@5.8.0
  • @ethersproject/base64@5.8.0
  • @ethersproject/networks@5.8.0
  • @ethersproject/rlp@5.8.0
  • @ethersproject/strings@5.8.0
  • @ethersproject/transactions@5.8.0
  • @ethersproject/bytes@5.8.0
  • @ethersproject/keccak256@5.8.0
  • @ethersproject/signing-key@5.8.0
  • @ethersproject/abi@5.8.0
  • @ethersproject/abstract-provider@5.8.0
  • @ethersproject/abstract-signer@5.8.0
  • @ethersproject/hash@5.8.0
  • @ethersproject/web@5.8.0
  • find-up-simple@1.0.1
  • abbrev@3.0.1
  • @vercel/nft@0.29.4
  • emittery@1.2.0
  • ava@6.4.1
  • picomatch@4.0.3
  • @sinonjs/samsam@8.0.3
  • fdir@6.5.0
  • cbor@10.0.11
  • cbor@10.0.3
  • @rollup/pluginutils@5.3.0
  • solidity-ast@0.4.61
  • tinyglobby@0.2.15
  • chalk@5.6.2
  • ansi-regex@6.2.2
  • ansi-styles@6.2.3
  • strip-ansi@7.1.2
  • strip-ansi@5.2.0
  • get-east-asian-width@1.4.0
  • pretty-ms@9.3.0
  • minizlib@3.1.0
  • tar@7.5.1
  • detect-libc@2.1.2
  • emoji-regex@10.6.0
  • ethers@6.15.0
  • ethers@6.16.0
  • @nomicfoundation/edr-darwin-x64@0.11.3
  • @nomicfoundation/edr-darwin-x64@0.12.0-next.24
  • @nomicfoundation/edr-win32-x64-msvc@0.11.3
  • @nomicfoundation/edr-win32-x64-msvc@0.12.0-next.24
  • @nomicfoundation/edr-linux-arm64-gnu@0.11.3
  • @nomicfoundation/edr-linux-arm64-gnu@0.12.0-next.24
  • @nomicfoundation/edr-linux-arm64-musl@0.11.3
  • @nomicfoundation/edr-linux-arm64-musl@0.12.0-next.24
  • @nomicfoundation/edr-darwin-arm64@0.11.3
  • @nomicfoundation/edr-darwin-arm64@0.12.0-next.24
  • @nomicfoundation/edr@0.11.3
  • @nomicfoundation/edr@0.12.0-next.24
  • @nomicfoundation/edr-linux-x64-musl@0.11.3
  • @nomicfoundation/edr-linux-x64-musl@0.12.0-next.24
  • @nomicfoundation/edr-linux-x64-gnu@0.11.3
  • @nomicfoundation/edr-linux-x64-gnu@0.12.0-next.24
  • hardhat@2.26.3
  • hardhat@3.1.10
  • @nomicfoundation/hardhat-verify@3.0.11
  • @nomicfoundation/hardhat-ethers@4.0.5
  • @nomicfoundation/hardhat-ethers@3.1.0

View full report

@socket-security
Copy link

socket-security bot commented Nov 13, 2025

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request migrates the OpenZeppelin Hardhat Upgrades plugin from Hardhat 2 to Hardhat 3, upgrading Node.js from 20 to 22, converting the codebase to ES modules, introducing a connection-based API architecture, adding Hardhat hooks for config and Solidity compilation, and including comprehensive examples and test fixtures for upgrade workflows.

Changes

Cohort / File(s) Summary
Infrastructure & Node.js Updates
.node-version, .github/actions/setup/action.yml
Updated Node.js from version 20 to 22.x in both runtime configuration and GitHub Actions setup.
Workflow Configuration
.github/workflows/changeset.yml, .github/workflows/checks.yml, .github/workflows/publish.yml, .github/workflows/version.yml
Added submodules: recursive to checkout steps across multiple workflows to enable recursive submodule fetching during CI/CD.
Linting Configuration
.eslintrc.js
Updated ECMAScript version from 2020 to 2022; expanded ignorePatterns to array; enhanced ava.config.js override with additional test file patterns and strict no-unused-vars rules.
Plugin Entry & Types
packages/plugin-hardhat/src/index.ts, packages/plugin-hardhat/src/types.ts, packages/plugin-hardhat/src/type-extensions.ts
Restructured main plugin from monolithic builder pattern to modular hook-based system; moved type augmentation from hardhat/types/runtime to hardhat/types/hre; added comprehensive HardhatUpgrades and DefenderHardhatUpgrades type definitions.
New Hook System
packages/plugin-hardhat/src/hooks/config.ts, packages/plugin-hardhat/src/hooks/solidity.ts
Added Hardhat 3 plugin hooks for config (ensuring storageLayout in compiler output) and Solidity compilation (preprocessing, validation, AST injection for Foundry compatibility); solidity.ts includes injectAstIntoArtifacts utility for artifact enhancement.
Connection-Based API Factory
packages/plugin-hardhat/src/utils/factory.ts
New factory module providing upgrades() and defender() functions that initialize connection-based upgrade APIs; includes Defender deprecation warnings and dynamic import of upgrade operations.
Core Upgrade Operations - Connection Refactored
packages/plugin-hardhat/src/deploy-proxy.ts, packages/plugin-hardhat/src/upgrade-proxy.ts, packages/plugin-hardhat/src/deploy-beacon.ts, packages/plugin-hardhat/src/deploy-beacon-proxy.ts, packages/plugin-hardhat/src/prepare-upgrade.ts, packages/plugin-hardhat/src/validate-*.ts, packages/plugin-hardhat/src/force-import.ts
All proxy/beacon deployment and upgrade functions refactored to accept and use NetworkConnection parameter instead of relying solely on HardhatRuntimeEnvironment; provider and factory resolution now derived from connection object.
Defender Integration
packages/plugin-hardhat/src/defender/*.ts
Updated Defender-related modules (client.ts, deploy.ts, get-approval-process.ts, propose-upgrade-with-approval.ts, utils.ts) to accept NetworkConnection parameter and derive network/provider data from connection; includes build-info retrieval adjustments for Hardhat 3's project-prefixed sources.
Utility & Helper Functions
packages/plugin-hardhat/src/utils/attach-abi.ts, packages/plugin-hardhat/src/utils/factories.ts, packages/plugin-hardhat/src/utils/deploy-impl.ts, packages/plugin-hardhat/src/utils/simulate-deploy.ts, packages/plugin-hardhat/src/utils/artifacts.ts, packages/plugin-hardhat/src/utils/npmFilesToBuild.ts
Refactored to use NetworkConnection; added new artifact utilities (getBuildInfo, getCombinedBuildInfo, CombinedBuildInfo interface) for Hardhat 3 compatibility; introduced OZfilesToBuild() function for standard OpenZeppelin file inclusion.
Module Resolution Updates
packages/plugin-hardhat/src/utils/index.ts and related files
Updated all internal imports to use explicit .js extensions and index.js re-exports for ES module compatibility.
Test Utilities & Mocking
packages/plugin-hardhat/src/test-utils/mock-deploy.ts
New utility function mockDeploy for testing deployment flows with mocked transactions.
Configuration Files
packages/plugin-hardhat/hardhat.config.ts, packages/plugin-hardhat/ava.config.js, packages/plugin-hardhat/package.json, packages/plugin-hardhat/tsconfig.json
Migrated from CommonJS to ESM exports; updated Hardhat to v3.1.5, ethers to ^4.0.2; added ESM-compatible build/test scripts; updated TypeScript compiler options for Node16 modules; bumped version to 4.0.0.
Hardhat Config Removal
packages/plugin-hardhat/hardhat.config.js
Removed previous CommonJS Hardhat config (compiler overrides, dependency imports).
Solidity Test Contracts
packages/plugin-hardhat/contracts/foundry/*.sol
Added comprehensive test contracts: GreeterInitializable, GreeterProxiable, GreeterV2, GreeterV2Proxiable, HasOwner, MyContractName, NoLicense, Unlicensed, UnrecognizedLicense, UpgradeInterfaceVersions, Validations, WithConstructor for testing upgrade patterns and validations.
Documentation & Guides
packages/plugin-hardhat/README.md, packages/plugin-hardhat/CHANGELOG.md, packages/plugin-hardhat/MIGRATION.md
Updated README with TypeScript-first guidance, ESM import examples, connection sharing patterns; added v4.0.0 CHANGELOG entry detailing Hardhat 3 migration and breaking changes; new MIGRATION.md guide for Hardhat 2→3 upgrade with before/after code examples.
Example Projects - TokenTransparent
packages/plugin-hardhat/example/TokenTransparent/*
Complete upgrade example migrating from Ownable to AccessControl via Transparent proxy: includes TokenV1.sol, TokenV2.sol, deployment script, upgrade script, verification script, tests (both Foundry .t.sol and AVA .test.ts), configuration, and comprehensive README.
Example Projects - TokenUUPS
packages/plugin-hardhat/example/TokenUUPS/*
Complete upgrade example using UUPS proxy pattern with AccessControl and role-based upgrade authorization: mirrors TokenTransparent structure with UUPS-specific implementations (_authorizeUpgrade, UPGRADER_ROLE).
Test File Migrations (ESM & Connection)
packages/plugin-hardhat/test/*.js, packages/plugin-hardhat/test/solidity/*.sol
Migrated ~80 test files from CommonJS (require) to ESM (import); all tests refactored to use upgradesFactory(hre, connection) pattern; contract factory references updated to fully-qualified paths (e.g., 'contracts/Greeter.sol:GreeterProxiable'); replaced proxyquire with esmock for mocking; added comprehensive Solidity test suite (Upgrades.t.sol) covering UUPS, Transparent, Beacon proxies and validation scenarios.
Test Script Changes
packages/plugin-hardhat/scripts/test.sh, packages/plugin-hardhat/run_failed.sh
Enhanced test script to distinguish and run Solidity vs JavaScript tests separately; added new failed test runner script.
Snapshot Updates
packages/plugin-hardhat/test/namespaced.js.md
Updated snapshot references to use project/ prefix for build-info paths (Hardhat 3 compliance).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The PR represents a major architectural migration with high heterogeneity: ESM conversion across 100+ files, connection-based API refactoring throughout core modules, new hook system implementation, Defender integration updates, comprehensive example projects, and extensive test file migrations. While many test file changes follow repetitive patterns (reducing per-file review burden), the core plugin logic changes, new hooks system, and API factory require careful analysis of control flow and backward compatibility implications.

Possibly related PRs

Poem

🐰 Hop along to Hardhat three!
Connection-based, ESM so free,
Hooks and factories in place,
TypeScript-first, a cleaner space,
Upgrades dance with tokens bright! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main change: migration of the hardhat-upgrades plugin to support Hardhat v3, which aligns with the substantial refactoring shown across the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch migrate/hardhat-3

Comment @coderabbitai help to get the list of available commands and usage tips.

@afa7789 afa7789 changed the base branch from master to next January 27, 2026 15:07
@afa7789 afa7789 marked this pull request as ready for review January 27, 2026 15:07
packages changes and initial config in accord to migration documentation.
importing errors + a few todos, change of Imports + new functions  to use hardhat3 library in plugin files.
…xtensions for Hardhat compatibility

( still have to test it )
- Updated deploy and upgrade functions across multiple files to accept a NetworkConnection parameter.
…solidity, etherscan-api, and verify-proxy files
@ericglau
Copy link
Member

@SocketSecurity ignore-all
These look like low-severity heuristic alerts on common dependencies (mostly dev/test tooling).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/plugin-hardhat/test/import-with-deploy-v5.js (1)

91-91: ⚠️ Potential issue | 🟡 Minor

Incorrect assertion: comparing boolean to string.

The expression implAddrUpgraded2 !== implAddrUpgraded evaluates to a boolean, so t.not(boolean, implAddrUpgraded2) compares a boolean against a string address—this will always pass regardless of the actual values.

The intended assertion is likely to verify the implementation address changed after upgrading to a different contract.

🐛 Proposed fix
-  t.not(implAddrUpgraded2 !== implAddrUpgraded, implAddrUpgraded2);
+  t.not(implAddrUpgraded2, implAddrUpgraded, 'implementation address should change after upgrading to different contract');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/import-with-deploy-v5.js` at line 91, The
assertion currently compares a boolean to a string (t.not(implAddrUpgraded2 !==
implAddrUpgraded, implAddrUpgraded2)); change it to directly assert the two
addresses are different by calling t.not(implAddrUpgraded2, implAddrUpgraded)
(or the equivalent t.notEqual) so the test actually verifies that
implAddrUpgraded2 and implAddrUpgraded are not the same.
packages/plugin-hardhat/test/propose-upgrade-with-approval-uups.js (1)

95-98: ⚠️ Potential issue | 🟡 Minor

Use proposal2.txResponse.wait() for the second assertion path.

At Line 97, the test waits on proposal.txResponse again, so it doesn’t validate that the second call’s response object is usable.

✅ Suggested fix
-  const txReceipt2 = await proposal.txResponse.wait();
+  const txReceipt2 = await proposal2.txResponse.wait();
   t.is(txReceipt2.contractAddress, txReceipt.contractAddress);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/propose-upgrade-with-approval-uups.js` around
lines 95 - 98, The test currently waits on proposal.txResponse twice instead of
waiting on the second proposal's response; change the second wait to call
proposal2.txResponse.wait() so the assertion verifies proposal2.txResponse is
usable (update the code around the t.is(proposal2.txResponse.hash,
proposal.txResponse.hash) block to call await proposal2.txResponse.wait() when
assigning txReceipt2 and then assert txReceipt2.contractAddress against
txReceipt.contractAddress).
packages/plugin-hardhat/test/beacon-upgrade-validation.js (1)

25-28: ⚠️ Potential issue | 🔴 Critical

The test expects the wrong error; upgradeProxy is not intended for beacon proxies.

The test calls upgradeProxy(greeter, Invalid) on a beacon proxy and expects the error 'Contract Invalid is not upgrade safe'. However, based on the beacon-upgrade-block-proxy.js test suite, upgradeProxy should reject beacon proxies outright with the error 'Beacon proxies are not supported with the current function' before validating the implementation.

The test should either:

  1. Use upgradeBeacon(beacon, Invalid) instead to properly test beacon upgrades (consistent with all other beacon tests), or
  2. Change the expected error message to match what upgradeProxy actually throws for beacon proxies

All other beacon upgrade tests use upgradeBeacon for beacon upgrades and upgradeProxy is only called in beacon-upgrade-block-proxy.js to verify it explicitly rejects beacon proxies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/beacon-upgrade-validation.js` around lines 25 -
28, The test is calling upgrades.upgradeProxy(greeter, Invalid) on a beacon
proxy but should use the beacon upgrade flow; replace the call to
upgrades.upgradeProxy with upgrades.upgradeBeacon(beacon, Invalid) (keep the
greeter/beacon/Invalid identifiers) and update the t.throwsAsync expectation to
assert the implementation-safety error ('Contract Invalid is not upgrade safe')
as in other beacon tests; alternatively, if you prefer to assert upgradeProxy
rejecting beacon proxies, change only the expected error string to 'Beacon
proxies are not supported with the current function' when calling
upgradeProxy(greeter, Invalid).
packages/plugin-hardhat/src/verify-proxy.ts (1)

514-523: ⚠️ Potential issue | 🟠 Major

Add a timeout/cap to proxy-link polling loop.

The while (responseBody.result === 'Pending in queue') loop has no max attempts or timeout. If Etherscan stays pending, the command can hang indefinitely.

🛠️ Suggested reliability guard
-    while (responseBody.result === 'Pending in queue') {
-      await delay(3000);
-      responseBody = await checkProxyVerificationStatus(etherscan, guid);
-    }
+    const maxAttempts = 40;
+    for (let attempts = 0; responseBody.result === 'Pending in queue' && attempts < maxAttempts; attempts++) {
+      await delay(3000);
+      responseBody = await checkProxyVerificationStatus(etherscan, guid);
+    }
+    if (responseBody.result === 'Pending in queue') {
+      recordError(`Timed out while linking proxy ${proxyAddress} with implementation`, errorReport);
+      return;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/src/verify-proxy.ts` around lines 514 - 523, The
polling loop that checks proxy verification status (starting after
callEtherscanApi returns RESPONSE_OK and guid is set, using
checkProxyVerificationStatus and delay) has no max attempts or timeout and can
hang indefinitely; modify the loop to include a retry limit and/or elapsed-time
timeout (e.g., MAX_ATTEMPTS or MAX_POLL_MS), increment a counter or track
startTime before the while (responseBody.result === 'Pending in queue') loop,
break and throw a clear error via the same error-handling path if the cap is
exceeded, and ensure any resources or promises are cleaned up—update references
in this block that use guid, responseBody, checkProxyVerificationStatus, delay,
and RESPONSE_OK.
♻️ Duplicate comments (1)
packages/plugin-hardhat/test/prepare-upgrade-txresponse.js (1)

4-6: ⚠️ Potential issue | 🔴 Critical

Move upgradesFactory import above executable code (ESM syntax blocker).

import on Line 6 appears after top-level executable statements on Lines 4-5. In ESM, this can fail module parsing/loading.

🐛 Proposed fix
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
 
 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
#!/bin/bash
set -euo pipefail

target="packages/plugin-hardhat/test/prepare-upgrade-txresponse.js"

# Verifies no static import appears after first executable top-level statement.
awk '
BEGIN { seen_code=0; bad=0 }
{
  line=$0
  if (line ~ /^[[:space:]]*$/) next
  if (line ~ /^[[:space:]]*\/\/.*/) next
  if (line ~ /^[[:space:]]*import[[:space:]].*;[[:space:]]*$/) {
    if (seen_code==1) {
      printf("Invalid import order at line %d: %s\n", NR, line)
      bad=1
    }
    next
  }
  seen_code=1
}
END { exit bad }
' "$target"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/prepare-upgrade-txresponse.js` around lines 4 -
6, The file has a static ESM import ("upgradesFactory" from
'@openzeppelin/hardhat-upgrades') placed after top-level executable statements
(the calls creating const connection and const { ethers } using
hre.network.connect), which breaks ESM parsing; move the import line for
upgradesFactory to the top of the module before any top-level execution (i.e.,
place the import above the code that calls hre.network.connect and before const
connection / const { ethers } are defined) so that import order follows ESM
rules and the module parses correctly.
🧹 Nitpick comments (30)
packages/plugin-hardhat/test/transparent-v4-change-admin-happy-path.js (1)

15-25: Prefer test context over module-scoped mutable upgrades.

This works, but storing upgrades in t.context improves isolation and future-proofs for additional concurrent tests.

Suggested refactor
-let upgrades;
-
 test.before(async t => {
-  upgrades = await upgradesFactory(hre, connection);
+  t.context.upgrades = await upgradesFactory(hre, connection);
   t.context.Greeter = await ethers.getContractFactory('Greeter');
   t.context.ProxyAdmin = await ethers.getContractFactory(ProxyAdmin.abi, ProxyAdmin.bytecode);
   t.context.TransparentUpgradableProxy = await ethers.getContractFactory(
     TransparentUpgradableProxy.abi,
     TransparentUpgradableProxy.bytecode,
   );
 });
 test('changeProxyAdmin', async t => {
-  const { Greeter, ProxyAdmin, TransparentUpgradableProxy } = t.context;
+  const { Greeter, ProxyAdmin, TransparentUpgradableProxy, upgrades } = t.context;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/transparent-v4-change-admin-happy-path.js`
around lines 15 - 25, The module-scoped mutable variable `upgrades` should be
moved into the per-test context to improve isolation: in the `test.before` hook
replace assigning to the top-level `upgrades` with `t.context.upgrades = await
upgradesFactory(hre, connection)` (and keep `t.context.Greeter = await
ethers.getContractFactory('Greeter')`), then update any references elsewhere in
the file to use `t.context.upgrades` instead of the module-scoped `upgrades`
(search for uses of the `upgrades` symbol and the initialization call to
`upgradesFactory`).
packages/plugin-hardhat/test/transparent-linked-libraries.js (3)

14-20: Reorder lifecycle hooks to match execution order.

test.after.always is defined before test.before, which is counterintuitive. Placing test.before first improves readability by reflecting the actual execution sequence.

♻️ Proposed fix to reorder hooks
-test.after.always(async () => {
-  await connection.close();
-});
-
 test.before(async () => {
   upgrades = await upgradesFactory(hre, connection);
 });
+
+test.after.always(async () => {
+  await connection.close();
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/transparent-linked-libraries.js` around lines 14
- 20, Move the test lifecycle hooks so their declarations reflect execution
order: place the test.before(async () => { upgrades = await upgradesFactory(hre,
connection); }); block before the test.after.always(async () => { await
connection.close(); }); block; this keeps test.before, test.after.always and the
use of upgradesFactory/connection organized and improves readability.

1-10: Move import statement to the top of the file with other imports.

The import { upgrades as upgradesFactory } on line 6 is placed after executable statements (lines 4-5). While ESM hoists imports, mixing them with top-level await statements reduces readability and violates conventional module structure.

♻️ Proposed fix to group imports at the top
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
 
 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
 const artifacts = hre.artifacts;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/transparent-linked-libraries.js` around lines 1
- 10, Move the import statement for upgrades so all imports are grouped at the
top of the module: relocate "import { upgrades as upgradesFactory } from
'@openzeppelin/hardhat-upgrades';" to be before any executable top-level code
(before the top-level await on "const connection = await hre.network.connect();"
and before "const { ethers } = connection;") to keep module imports together and
restore conventional module structure.

116-119: Replace deprecated substr with substring.

String.prototype.substr is deprecated. Use substring for the same behavior with start and end indices.

♻️ Proposed fix to use substring
       for (const fixup of fixups) {
         bytecode =
-          bytecode.substr(0, 2 + fixup.start * 2) +
-          addr.substr(2) +
-          bytecode.substr(2 + (fixup.start + fixup.length) * 2);
+          bytecode.substring(0, 2 + fixup.start * 2) +
+          addr.substring(2) +
+          bytecode.substring(2 + (fixup.start + fixup.length) * 2);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/transparent-linked-libraries.js` around lines
116 - 119, Replace deprecated String.prototype.substr calls in the bytecode
assembly expression by using String.prototype.substring: change
bytecode.substr(0, 2 + fixup.start * 2) to bytecode.substring(0, 2 + fixup.start
* 2), replace addr.substr(2) with addr.substring(2), and replace
bytecode.substr(2 + (fixup.start + fixup.length) * 2) with bytecode.substring(2
+ (fixup.start + fixup.length) * 2). Ensure you reference the existing variables
bytecode, addr, fixup, and fixup.length when making these substitutions so the
start/end indices preserve the same slicing semantics.
packages/plugin-hardhat/test/beacon-initializers.js (1)

35-38: Consider whether kind: 'beacon' is necessary here.

When using deployBeaconProxy, specifying kind: 'beacon' appears redundant since the function already implies beacon deployment. The key option here is initializer to resolve the overloaded function. If this is intentional for explicitness or required by the API, feel free to keep it.

♻️ Optional simplification
   const instance = await upgrades.deployBeaconProxy(beacon, InitializerOverloaded, [42], {
-    kind: 'beacon',
     initializer: 'initialize(uint256)',
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/beacon-initializers.js` around lines 35 - 38,
The options object passed to upgrades.deployBeaconProxy includes a redundant
kind: 'beacon' entry; update the call to rely on upgrades.deployBeaconProxy's
beacon semantics and remove the kind property, keeping only the initializer:
'initialize(uint256)' option to disambiguate the overloaded
InitializerOverloaded initializer.
packages/plugin-hardhat/test/transparent-happy-path.js (1)

1-9: Consider moving the import statement before executable code for conventional ordering.

ESM hoists imports regardless of source position, so the code works correctly. However, placing the import on line 6 after the top-level await statements (lines 4-5) is unconventional and may confuse readers about execution order.

♻️ Suggested reordering for clarity
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';

 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';

 /** `@type` {import('@openzeppelin/hardhat-upgrades').HardhatUpgrades} */
 let upgrades;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/transparent-happy-path.js` around lines 1 - 9,
The import of upgradesFactory from '@openzeppelin/hardhat-upgrades' appears
after top-level executable code (the await hre.network.connect() and const {
ethers } = connection), which is unconventional; move the statement "import {
upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';" (and any
other imports) above the top-level await and any executable lines so imports are
grouped at the top (keeping the declarations like "const connection = await
hre.network.connect();" and "let upgrades;" below them) to clarify execution
order and reader expectations.
packages/plugin-hardhat/test/constructor.js (1)

23-23: Optional: deduplicate repeated proxy option literals.

The repeated { constructorArgs: [...], initializer: false } blocks make future edits noisier than necessary. Consider extracting small helpers/constants for readability and consistency.

Proposed refactor
+const noInitializer = { initializer: false };
+
+function deployWithConstructorArg(WithConstructor, arg) {
+  return upgrades.deployProxy(WithConstructor, [], { constructorArgs: [arg], ...noInitializer });
+}
+
+function upgradeWithConstructorArg(proxy, WithConstructor, arg) {
+  return upgrades.upgradeProxy(proxy, WithConstructor, { constructorArgs: [arg], ...noInitializer });
+}

Also applies to: 27-27, 37-37, 41-41, 51-51, 55-55, 65-65, 69-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/constructor.js` at line 23, Multiple tests call
upgrades.deployProxy(WithConstructor, ..., { constructorArgs: [...],
initializer: false }) which duplicates the same literal; refactor by extracting
a small helper or constant (e.g., PROXY_CONSTRUCTOR_OPTIONS or a function
proxyOptions(constructorArgs)) and use it in all calls (references:
upgrades.deployProxy and WithConstructor) to avoid repetition and make edits
simpler and consistent across the test file.
packages/plugin-hardhat/test/transparent-deploy-overload.js (1)

1-6: Move the import statement to the top of the file for clarity.

Line 6's import statement appears after the top-level await on lines 4-5. While ESM hoists imports so this works correctly, the unconventional ordering harms readability. Group all imports together at the top.

♻️ Suggested reordering
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';

 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/transparent-deploy-overload.js` around lines 1 -
6, Move the trailing import of "upgrades as upgradesFactory" from the bottom of
the top-level statements into the import group at the top of the file so all
imports are together; specifically, relocate the import of
'@openzeppelin/hardhat-upgrades' (exports renamed as upgradesFactory) to sit
above the top-level await that creates connection (the expression using
hre.network.connect and const { ethers } = connection) to improve readability
and maintain conventional ordering.
packages/plugin-hardhat/test/implementation-functions.js (2)

1-13: Import statement placed after executable code.

Line 6 has an import statement after the top-level await on lines 4-5. While valid ESM, grouping all imports at the top improves readability and aligns with common conventions.

Suggested reordering
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';

 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/implementation-functions.js` around lines 1 -
13, Top-level imports must be grouped before any executable code: move the
import of "upgrades as upgradesFactory" (from '@openzeppelin/hardhat-upgrades')
so it's declared with the other imports at the top of the file, then run the
top-level await that creates "connection" and destructures "ethers"; ensure
"upgrades" remains declared as the typed variable but only initialized later if
needed. This keeps identifiers like connection, hre, ethers, upgradesFactory and
the upgrades declaration in the same logical order and removes the
import-after-execution pattern.

169-216: Unused signer variable in multiple tests.

The signer variable is declared but never used in five tests:

  • Line 169 (validate upgrade uups - happy path)
  • Line 179 (validate upgrade uups - incompatible storage)
  • Line 191 (validate upgrade uups - incompatible storage - forced)
  • Line 201 (validate upgrade uups - wrong kind)
  • Line 211 (validate upgrade uups - no upgrade function)

Either remove these unused declarations or use the signer if it was intended for the deployProxy calls.

Remove unused signer declarations
 test('validate upgrade uups - happy path', async t => {
   const { GreeterProxiable, GreeterV2Proxiable } = t.context;

-  const signer = await ethers.provider.getSigner();
   const greeter = await upgrades.deployProxy(GreeterProxiable, ['Hola mundo!'], {
     kind: 'uups',
   });
   await upgrades.validateUpgrade(greeter, GreeterV2Proxiable);
 });

 test('validate upgrade uups - incompatible storage', async t => {
   const { GreeterProxiable, GreeterStorageConflictProxiable } = t.context;

-  const signer = await ethers.provider.getSigner();
   const greeter = await upgrades.deployProxy(GreeterProxiable, ['Hola mundo!'], {
     kind: 'uups',
   });
   await t.throwsAsync(() => upgrades.validateUpgrade(greeter, GreeterStorageConflictProxiable), {
     message: /(New storage layout is incompatible)/,
   });
 });

 test('validate upgrade uups - incompatible storage - forced', async t => {
   const { GreeterProxiable, GreeterStorageConflictProxiable } = t.context;

-  const signer = await ethers.provider.getSigner();
   const greeter = await upgrades.deployProxy(GreeterProxiable, ['Hola mundo!'], {
     kind: 'uups',
   });
   await upgrades.validateUpgrade(greeter, GreeterStorageConflictProxiable, { unsafeSkipStorageCheck: true });
 });

 test('validate upgrade uups - wrong kind', async t => {
   const { GreeterProxiable, GreeterV2 } = t.context;

-  const signer = await ethers.provider.getSigner();
   const greeter = await upgrades.deployProxy(GreeterProxiable, ['Hola mundo!']);
   await t.throwsAsync(() => upgrades.validateUpgrade(greeter, GreeterV2), {
     message: /(Requested an upgrade of kind transparent but proxy is uups)/,
   });
 });

 test('validate upgrade uups - no upgrade function', async t => {
   const { GreeterProxiable, GreeterV2 } = t.context;

-  const signer = await ethers.provider.getSigner();
   const greeter = await upgrades.deployProxy(GreeterProxiable, ['Hola mundo!']);
   await t.throwsAsync(() => upgrades.validateUpgrade(greeter, GreeterV2, { kind: 'uups' }), {
     message: getUpgradeUnsafeRegex('GreeterV2'),
   });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/implementation-functions.js` around lines 169 -
216, Remove the unused signer local in the affected tests or actually use it
when calling deployProxy; specifically, in the tests referencing
GreeterProxiable/GreeterV2/GreeterStorageConflictProxiable (tests named
"validate upgrade uups - happy path", "validate upgrade uups - incompatible
storage", "validate upgrade uups - incompatible storage - forced", "validate
upgrade uups - wrong kind", and "validate upgrade uups - no upgrade function")
either delete the line that declares const signer = await
ethers.provider.getSigner(); or pass that signer into upgrades.deployProxy(...)
if the deploy was intended to use an explicit signer, ensuring references to the
variable (signer) are removed or used consistently across the functions
deployProxy and validateUpgrade calls.
packages/plugin-hardhat/test/transparent-transfer-admin-ownership-happy-path.js (1)

31-31: Prefer removing the temporary mixed-language inline note.

Line [31] includes a migration/history comment in Portuguese; this can be simplified to keep test code neutral and maintainable.

Proposed cleanup
-  const admin = await ethers.getContractAt(OWNABLE_ABI, adminAddress); // ← mudou aqui de hre.ethers para ethers
+  const admin = await ethers.getContractAt(OWNABLE_ABI, adminAddress);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/plugin-hardhat/test/transparent-transfer-admin-ownership-happy-path.js`
at line 31, Remove the temporary mixed-language inline note after the statement
creating the admin contract instance: the comment following "const admin = await
ethers.getContractAt(OWNABLE_ABI, adminAddress);" should be deleted (no
replacement comment needed) so the test remains language-neutral and
maintainable; locate this in the test where variables/admin, ethers,
OWNABLE_ABI, and adminAddress are used and simply remove the Portuguese remark.
packages/plugin-hardhat/test/uups-initial-owner.js (2)

4-6: Consider grouping all imports at the top of the file.

The import on line 6 is placed after the top-level await statements. While ESM hoists imports so this works correctly, it reduces readability. Moving the import to lines 1-2 with other imports would follow conventional module organization.

♻️ Suggested reordering
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';

 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/uups-initial-owner.js` around lines 4 - 6, Move
the import of upgrades (import { upgrades as upgradesFactory } from
'@openzeppelin/hardhat-upgrades') to the top of the module alongside other
imports so it appears before any top-level awaits; specifically, place that
import above the call to hre.network.connect() (which invokes
hre.network.connect() and uses ethers) to improve readability and follow
conventional module organization while keeping the existing symbols unchanged
(hre.network.connect, upgradesFactory).

23-24: Unused variable signer.

The signer variable on line 24 is retrieved but never used in this test. If it's not needed for future assertions, consider removing it to keep the test focused.

♻️ Suggested removal
 test('uups with initialOwner option', async t => {
   const { Greeter } = t.context;

   const initialOwner = await ethers.provider.getSigner(1);
-  const signer = await ethers.provider.getSigner();

   await t.throwsAsync(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/uups-initial-owner.js` around lines 23 - 24, The
test defines an unused variable signer (const signer = await
ethers.provider.getSigner();) which should be removed if not used; delete the
unused signer declaration or, if intended for future assertions, actually use
signer in the test assertions. Locate the pair of declarations (initialOwner and
signer) and remove the signer line (or replace its usage accordingly) to
eliminate the unused-variable warning.
packages/plugin-hardhat/test/uups-unknown-upgrade-interface.js (1)

1-9: Move import statement before top-level await for clarity.

Line 6's import is placed after the top-level await on lines 4-5. While ESM hoists imports (so it executes correctly), this ordering is misleading—readers may think the connection is established before the import runs. Group all imports together at the top for readability.

🔧 Suggested reordering
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';

 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/uups-unknown-upgrade-interface.js` around lines
1 - 9, Move the import for upgradesFactory (and any other imports) above the
top-level await so all imports are grouped at the top; specifically, place
"import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';"
before using "await hre.network.connect()" (which creates "connection" and
"ethers") to avoid the misleading ordering—ensure imports appear before the
top-level await that instantiates "connection" and before the "let upgrades"
declaration.
packages/plugin-hardhat/test/transparent-happy-path-with-call.js (1)

8-9: Remove duplicated JSDoc annotation.

The HardhatUpgrades type annotation is repeated twice; keep a single declaration to reduce noise.

Proposed cleanup
-/** `@type` {import('@openzeppelin/hardhat-upgrades').HardhatUpgrades} */
 /** `@type` {import('@openzeppelin/hardhat-upgrades').HardhatUpgrades} */
 let upgrades;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/transparent-happy-path-with-call.js` around
lines 8 - 9, There are two identical JSDoc type annotations for HardhatUpgrades
(/** `@type` {import('@openzeppelin/hardhat-upgrades').HardhatUpgrades} */)
duplicated; remove the extra duplicate so only a single JSDoc declaration
remains at the top of the test file to avoid noise and redundancy.
packages/plugin-hardhat/test/uups-initializers.js (1)

1-9: Move import statement to top with other imports.

The import on line 6 is placed after the await hre.network.connect() code, but ESM hoists all static imports to execute before any module code. The current placement is misleading—group it with the other imports at lines 1-2 for clarity.

✨ Suggested reordering
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';

 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/uups-initializers.js` around lines 1 - 9, Move
the misplaced static import so it's grouped with the other top-of-file imports:
relocate "import { upgrades as upgradesFactory } from
'@openzeppelin/hardhat-upgrades';" to the top alongside "import test from
'ava';" and "import hre from 'hardhat';", leaving the await
hre.network.connect() call and the subsequent "const { ethers } = connection;"
line unchanged; this fixes the incorrect ordering relative to the runtime call
to hre.network.connect() and avoids confusing ESM hoisting behavior.
packages/plugin-hardhat/test/transparent-happy-path-with-enums.js (1)

1-9: Consider moving import statement to the top with other imports.

Line 6's import statement appears after the top-level await expressions, which is misleading. While ESM hoists imports (so they execute before lines 4-5 regardless), placing the import after executable code can confuse readers who may assume sequential execution.

📝 Suggested reordering
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
 
 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
 
 /** `@type` {import('@openzeppelin/hardhat-upgrades').HardhatUpgrades} */
 let upgrades;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/transparent-happy-path-with-enums.js` around
lines 1 - 9, Move the import "import { upgrades as upgradesFactory } from
'@openzeppelin/hardhat-upgrades';" so it sits with the other top-level imports
before any executable statements (e.g., before the top-level await connecting to
hre.network); update the file to import upgradesFactory at the top alongside
test, hre and any other imports to avoid placing an import after "const
connection = await hre.network.connect()" and make the import order clear.
packages/plugin-hardhat/test/uups-happy-path.js (1)

26-35: Strengthen upgrade-path assertions for better regression safety.

The current flow executes correctly, but adding address-level assertions would make the happy-path test more robust (proxy address unchanged after upgradeProxy, implementation address different after prepareUpgrade).

Suggested test hardening diff
 test('happy path', async t => {
   const { Greeter, GreeterV2, GreeterV3 } = t.context;
   const signer = await ethers.provider.getSigner();

   const greeter = await upgrades.deployProxy(Greeter, ['Hello, Hardhat!'], { kind: 'uups' });
+  const proxyAddr = await greeter.getAddress();

   const greeter2 = await upgrades.upgradeProxy(greeter, GreeterV2);
   await greeter2.waitForDeployment();
+  t.is(await greeter2.getAddress(), proxyAddr);
   await greeter2.resetGreeting();

-  const greeter3ImplAddr = await upgrades.prepareUpgrade(await greeter.getAddress(), GreeterV3);
+  const greeter3ImplAddr = await upgrades.prepareUpgrade(proxyAddr, GreeterV3);
+  t.not(greeter3ImplAddr, proxyAddr);
   const greeter3 = GreeterV3.attach(greeter3ImplAddr);
   const version3 = await greeter3.version();
   t.is(version3, 'V3');
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/uups-happy-path.js` around lines 26 - 35, Add
address-level assertions to ensure the proxy address remains the same after
upgrades and the implementation address changes after prepareUpgrade: after
deploying via upgrades.deployProxy (greeter) assert greeter.getAddress() equals
greeter2.getAddress(); after calling upgrades.prepareUpgrade(store the returned
greeter3ImplAddr) assert greeter3ImplAddr is not equal to the current
implementation address of greeter (or the implementation address before
prepareUpgrade); use the existing variables greeter, greeter2, greeter3ImplAddr
and the upgrade helpers upgrades.upgradeProxy and upgrades.prepareUpgrade to
locate where to insert these assertions.
packages/plugin-hardhat/test/namespaced.js (1)

4-5: Move connection creation into the hook to avoid module-load side effects.

Line 4 opens the network connection during module evaluation. Prefer doing this in test.before so setup/teardown lifecycle is fully controlled by AVA hooks.

♻️ Suggested refactor
-const connection = await hre.network.connect();
-const { ethers } = connection;
+let connection;
+let ethers;
 import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
@@
 test.after.always(async () => {
-  await connection.close();
+  if (connection) {
+    await connection.close();
+  }
 });
@@
 test.before(async t => {
+  connection = await hre.network.connect();
+  ({ ethers } = connection);
   upgrades = await upgradesFactory(hre, connection);

Also applies to: 11-13, 16-16

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/namespaced.js` around lines 4 - 5, The module
currently opens the network connection at load time by calling
hre.network.connect() and destructuring ethers from connection; move that async
connection setup into an AVA lifecycle hook (test.before) and store the
resulting connection/ethers in a variable accessible to tests so setup/teardown
are controlled; update any other top-level calls to
hre.network.connect()/destructuring (the occurrences around the connection,
ethers and any direct uses at lines referenced) to instead read from the
variables initialized in test.before and ensure disconnection/cleanup (e.g., in
test.after) is performed.
packages/plugin-hardhat/test/defender-contract-instance.js (1)

78-90: Consider cleaning up inner connections created by the stubbed connect.

The connectStub creates a new connection via originalConnect() each time it's called (line 81). These inner connections are not explicitly closed when the test completes—only the top-level connection is closed in test.after.always. For a local Hardhat network in tests this is likely harmless, but if you want to be thorough about resource cleanup:

♻️ Optional: Track and close inner connections
+  const innerConnections = [];
   const connectStub = sinon.stub(hre.network, 'connect').callsFake(async () => {
     const connection = await originalConnect();
+    innerConnections.push(connection);
     const originalGetTransaction = connection.ethers.provider.getTransaction.bind(connection.ethers.provider);
     sinon.stub(connection.ethers.provider, 'getTransaction').callsFake(async hash => {
       if (hash === second.deploymentTransaction().hash) {
         return second.deploymentTransaction();
       }
       return originalGetTransaction(hash);
     });
     return connection;
   });

   // ... rest of test ...

   connectStub.restore();
+  for (const conn of innerConnections) {
+    await conn.close();
+  }
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/defender-contract-instance.js` around lines 78 -
90, connectStub creates inner connections by calling originalConnect() that
aren't closed; record each inner connection (push to an array inside the
connectStub when you create the connection) and then in the existing
test.after.always teardown iterate that array and call the same close method
used for the top-level connection (e.g., connection.close()) and clear the
array; update the connectStub restore code to still restore the stub
(sinon.restore or connectStub.restore()) so both stubbing and inner-connection
resources are cleaned up (referencing connectStub, originalConnect, connection,
and test.after.always).
packages/plugin-hardhat/test/transparent-transfer-admin-ownership-wrong-signer.js (1)

1-6: Move import statement above top-level await for clarity.

The import on line 6 is placed after the top-level await statements on lines 4-5. While ESM hoists imports (so this executes correctly), the ordering is unconventional and visually suggests a dependency that doesn't exist. Group all imports together at the top for readability.

🧹 Suggested reordering
 import test from 'ava';
 import hre from 'hardhat';
+import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';

 const connection = await hre.network.connect();
 const { ethers } = connection;
-import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/plugin-hardhat/test/transparent-transfer-admin-ownership-wrong-signer.js`
around lines 1 - 6, Move the import for "upgrades as upgradesFactory" to join
the other top-level imports so all import statements are grouped before any
top-level await; specifically, reorder the file so the imports (import test,
import hre, import { upgrades as upgradesFactory }) appear before the top-level
await that calls hre.network.connect() and destructures ethers from connection
to improve clarity and conventional ordering.
packages/plugin-hardhat/test/propose-upgrade-with-approval-beacon.js (1)

39-48: Consider aligning mock setup with other proposal tests.

This esmock setup only provides local mocks and only mocks getDeployClient. Other similar tests (e.g., propose-upgrade-with-approval.js) also mock getNetworkClient and use global mocks to ensure consistent mocking throughout the module tree. While this may work for tests that verify blocking behavior, consider aligning the mock strategy for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/propose-upgrade-with-approval-beacon.js` around
lines 39 - 48, The esmock setup only overrides getNetwork and getDeployClient;
update it to mirror other proposal tests by also mocking getNetworkClient (from
'../dist/defender/utils.js' or '../dist/defender/client.js' as appropriate) and
enable global mocking so mocks apply consistently across the module tree;
specifically, when creating the module via
esmock('./dist/defender/propose-upgrade-with-approval.js', {...}) include a
getNetworkClient mock alongside getDeployClient and getNetwork (reference
symbols: otherDefenderUtils, getNetwork, getDeployClient, getNetworkClient, and
module) and switch to the same global/mock strategy used in the other test file
so behavior is consistent.
packages/plugin-hardhat/test/defender-deploy-proxy.js (1)

4-4: Remove stale commented-out defender scaffolding.

Lines 4, 16, and 25 are leftover comments from the old path and add noise in the migrated test.

🧹 Suggested cleanup
-//  defender as defenderFactory
@@
-/** `@type` {import('@openzeppelin/hardhat-upgrades').DefenderHardhatUpgrades} */
-// let defender;
@@
 test.before(async () => {
   upgrades = await upgradesFactory(hre, connection);
-  // defender = await defenderFactory(hre, connection);
 });

Also applies to: 16-16, 25-25

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/defender-deploy-proxy.js` at line 4, Remove the
stale commented-out Defender scaffolding comments (e.g. lines containing "// 
defender as defenderFactory" and other leftover comments referencing the old
defender path) from the test file; locate the occurrences of that exact comment
text and similar defender-related commented lines in the test (they appear in
multiple places) and delete them so the migrated test no longer contains noisy
leftover comments.
packages/plugin-hardhat/test/beacon-upgrade-validation.js (1)

27-31: Consider using expectations object to verify error message content.

The third argument to t.throwsAsync is the assertion failure message, not the expected error message. If the intent is to verify the thrown error contains "Contract Invalid is not upgrade safe", use the expectations object instead:

💡 Optional improvement to verify error message
   await t.throwsAsync(
     () => upgrades.upgradeProxy(greeter, Invalid),
-    undefined,
-    'Contract `Invalid` is not upgrade safe',
+    { message: /Contract `Invalid` is not upgrade safe/ },
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/beacon-upgrade-validation.js` around lines 27 -
31, The test currently passes the expected error message as the third argument
to t.throwsAsync (which is the assertion failure message) instead of as an
expectations object; update the call to t.throwsAsync that wraps
upgrades.upgradeProxy(greeter, Invalid) so the second parameter is an
expectations object verifying the thrown error message (e.g. { message:
/Contract `Invalid` is not upgrade safe/ }) and remove the current
third-argument string; locate the call by the symbols t.throwsAsync,
upgrades.upgradeProxy, greeter, and Invalid.
packages/core/src/cli/validate/build-info-file.test.ts (1)

386-401: Add a HH3 test for malformed (present-but-empty) .output.json.

Current coverage checks missing output file, but not a present file missing compiler output payload.

✅ Suggested test case
+test.serial('Hardhat 3 format with malformed .output.json reports missing Solidity compiler output', async t => {
+  await fs.mkdir('hh3-format-invalid-output', { recursive: true });
+
+  await fs.writeFile(
+    'hh3-format-invalid-output/solc-0_8_0-abc123.json',
+    JSON.stringify({
+      _format: 'hh3-sol-build-info-1',
+      input: BUILD_INFO.input,
+      solcVersion: '0.8.9',
+    }),
+  );
+
+  await fs.writeFile('hh3-format-invalid-output/solc-0_8_0-abc123.output.json', JSON.stringify({}));
+
+  const error = await t.throwsAsync(getBuildInfoFiles('hh3-format-invalid-output'));
+  t.true(error?.message.includes('missing Solidity compiler output'));
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/cli/validate/build-info-file.test.ts` around lines 386 -
401, Add a new test similar to "Hardhat 3 format with missing .output.json
suggests hardhat compile" that verifies a present-but-empty HH3 output file
triggers the same error: create a directory (e.g., "hh3-format-empty-output"),
write the hh3 build-info file (with _format: 'hh3-sol-build-info-1') and then
write the corresponding ".output.json" file containing an empty object or a JSON
lacking the compiler output payload, then call
getBuildInfoFiles('hh3-format-empty-output') and assert it throws with a message
that includes either "could not be read" or "missing Solidity compiler output"
and also includes "hardhat compile"; use the same test harness (test.serial) and
error assertions as the existing test to keep behavior consistent with the
missing-file case.
packages/plugin-hardhat/test/uups-upgrade-storage.js (1)

17-20: Bind signer explicitly in test setup to consolidate account configuration.

getSigner() is fetched in each test (lines 25, 36) but never used. Meanwhile, getContractFactory() relies on implicit default signer selection. Move signer initialization to test.before and pass it explicitly to both factory calls, eliminating the redundant fetches and making account assignment clear.

♻️ Proposed refactor
 test.before(async t => {
   upgrades = await upgradesFactory(hre, connection);
+  const signer = await ethers.getSigners().then(signers => signers[0]);
-  t.context.Greeter = await ethers.getContractFactory('contracts/Greeter.sol:GreeterProxiable');
+  t.context.Greeter = await ethers.getContractFactory('contracts/Greeter.sol:GreeterProxiable', signer);
   t.context.GreeterStorageConflict = await ethers.getContractFactory(
     'contracts/InvalidGreeter.sol:GreeterStorageConflictProxiable',
+    signer,
   );
 });
@@
-  const signer = await ethers.provider.getSigner();
   const greeter = await upgrades.deployProxy(Greeter, ['Hola mundo!'], { kind: 'uups' });
@@
-  const signer = await ethers.provider.getSigner();
   const greeter = await upgrades.deployProxy(Greeter, ['Hola mundo!'], { kind: 'uups' });

Also applies to: 25-25, 36-36

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/uups-upgrade-storage.js` around lines 17 - 20,
Move signer initialization into the shared setup (test.before) and use it
explicitly when creating contract factories so account assignment is
deterministic: call getSigner() once in test.before (save as e.g. signer) and
pass that signer to ethers.getContractFactory for both 'GreeterProxiable' and
'GreeterStorageConflictProxiable' instead of relying on implicit default
signers; remove the redundant per-test getSigner() calls (lines where
getSigner() is fetched but unused) so tests use the single explicit signer for
deployments and interactions.
packages/plugin-hardhat/examples/BoxUUPS/tsconfig.json (1)

1-13: Consider a shared base tsconfig for both examples.

This config matches the BoxTransparent config closely. A shared base (extended by each example) would reduce future drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/examples/BoxUUPS/tsconfig.json` around lines 1 - 13,
The two example tsconfig.json files (e.g., the BoxUUPS tsconfig.json with
"compilerOptions" like "target":"ES2020", "module":"Node16",
"moduleResolution":"node16" and includes
["./scripts","./test","./hardhat.config.ts"]) should be refactored to use a
shared base config to avoid drift: create a root/shared base tsconfig.json
containing the common "compilerOptions" and common exclude/include defaults,
then update the example tsconfig.json files to remove duplicated compilerOptions
and instead use "extends": "<path-to-shared-tsconfig>" and only override any
example-specific keys (like "include") in the example configs; ensure the symbol
names to change are the example tsconfig.json files and the shared base tsconfig
that they extend.
.github/actions/setup/action.yml (1)

17-19: Cache key is broader than necessary and may hurt hit rate.

Line 17 hashes every package.json, so unrelated workspace edits can invalidate the Solidity compiler cache. Consider keying primarily on lockfile + Hardhat version inputs.

💡 Suggested key refinement
-        key: hardhat-compilers-${{ runner.os }}-${{ hashFiles('**/package.json', '**/yarn.lock') }}
+        key: hardhat-compilers-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/setup/action.yml around lines 17 - 19, The cache key is too
broad because it hashes every package.json; update the cache key string (the
YAML "key" entry value) to hash only lockfiles and include the Hardhat version
input (e.g. use hashFiles('**/yarn.lock','**/package-lock.json') and append ${{
inputs.hardhat-version }}), and update the "restore-keys" entry to match the
more specific prefix (e.g. hardhat-compilers-${{ runner.os }}-). Ensure both the
key and restore-keys lines in .github/actions/setup/action.yml are adjusted
accordingly.
packages/plugin-hardhat/src/index.ts (1)

12-17: Consider deduplicating plugin-module wrapper logic.

The repeated then(m => ({ default: m.default })) pattern is easy to drift over time.

♻️ Proposed refactor
+const toPluginModule = <T extends { default: HardhatPlugin }>(loader: () => Promise<T>) =>
+  loader().then(m => ({ default: m.default }));
+
 const plugin: HardhatPlugin = {
   id: '@openzeppelin/hardhat-upgrades',
@@
-  dependencies: () => [import('@nomicfoundation/hardhat-ethers').then(m => ({ default: m.default }))],
+  dependencies: () => [toPluginModule(() => import('@nomicfoundation/hardhat-ethers'))],
@@
-      condition: () => [import('@nomicfoundation/hardhat-verify').then(m => ({ default: m.default }))],
+      condition: () => [toPluginModule(() => import('@nomicfoundation/hardhat-verify'))],
       plugin: () => import('./verify-plugin.js'),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/src/index.ts` around lines 12 - 17, The duplicated
module-wrapper pattern (then(m => ({ default: m.default })) used inside
dependencies and conditionalDependencies) should be extracted into a small
helper to avoid drift: add a utility function (e.g., wrapDefaultImport or
importDefault) and replace the inline then(...) usages in the dependencies array
and the conditionalDependencies[*].condition and plugin entries to call that
helper instead; update references in packages/plugin-hardhat/src/index.ts where
dependencies, conditionalDependencies, and plugin currently inline the wrapper
so they now call the shared function.
packages/plugin-hardhat/test/beacon-000-externally-deployed.js (1)

33-47: Prefer removing ad-hoc console logs from tests.

These logs add noise in CI output; assertions already capture failure context.

🧹 Proposed cleanup
-  console.log('Deployed Greeter at:', await greeter.getAddress());
@@
-  console.log('Deployed Beacon at:', await beacon.getAddress());
@@
-    console.log('Upgrade error message:', e.message);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/beacon-000-externally-deployed.js` around lines
33 - 47, Remove the ad-hoc console.log statements added to the test: delete the
console.log('Deployed Greeter at:', await greeter.getAddress());
console.log('Deployed Beacon at:', await beacon.getAddress()); and
console.log('Upgrade error message:', e.message); keep the existing logic around
Beacon.deploy, beacon.waitForDeployment, and the try/catch that calls
upgrades.upgradeBeacon and asserts t.true(e.message.includes(IS_NOT_REGISTERED))
so the test remains informative without noisy CI output.

Comment on lines +418 to +421
t.true(
error?.message.includes(
'Build info from invalid-hh2-build-info/invalid.json must include Solidity compiler input, output, and solcVersion',
),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This assertion is path-separator brittle.

The test hardcodes invalid-hh2-build-info/invalid.json, which can be unstable across environments. Assert on stable substrings (message core + format token) instead.

🔧 Proposed fix
-  t.true(
-    error?.message.includes(
-      'Build info from invalid-hh2-build-info/invalid.json must include Solidity compiler input, output, and solcVersion',
-    ),
-  );
+  t.true(error?.message.includes('must include Solidity compiler input, output, and solcVersion'));
+  t.true(error?.message.includes('Got format: hh-sol-build-info-1'));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
t.true(
error?.message.includes(
'Build info from invalid-hh2-build-info/invalid.json must include Solidity compiler input, output, and solcVersion',
),
t.true(error?.message.includes('must include Solidity compiler input, output, and solcVersion'));
t.true(error?.message.includes('Got format: hh-sol-build-info-1'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/cli/validate/build-info-file.test.ts` around lines 418 -
421, The test currently asserts error?.message.includes('Build info from
invalid-hh2-build-info/invalid.json must include Solidity compiler input,
output, and solcVersion'), which is brittle due to path separators; update the
assertion around error?.message.includes(...) to check for stable substrings
instead (e.g., the core message "must include Solidity compiler input, output,
and solcVersion" plus a format token like "invalid.json" or the filename only)
rather than the full path; locate the assertion that uses
error?.message.includes in build-info-file.test.ts and replace the hardcoded
path check with two contains checks (core message and filename) or normalize the
path before comparing.

Comment on lines +216 to +230
const outputJson = await readJSON(outputFilePath);
outputData = outputJson.output ?? outputJson;
} catch (error) {
throw new ValidateCommandError(
`Build info file ${buildInfoFilePath} does not contain output, and output file ${outputFilePath} could not be read.`,
() => HH3_BUILD_INFO_HELP,
);
}

if (outputData === undefined) {
const { dir, name } = path.parse(buildInfoFilePath);
const outputFilePath = path.join(dir, `${name}.output.json`);

try {
const outputJson = await readJSON(outputFilePath);
outputData = outputJson.output ?? outputJson;
} catch (error) {
throw new ValidateCommandError(
`Build info file ${buildInfoFilePath} does not contain output, and output file ${outputFilePath} could not be read.`,
);
}

if (outputData === undefined) {
throw new ValidateCommandError(
`Build info file ${buildInfoFilePath} does not contain output, and output file ${outputFilePath} is missing Solidity compiler output.`,
);
}
throw new ValidateCommandError(
`Build info file ${buildInfoFilePath} does not contain output, and output file ${outputFilePath} is missing Solidity compiler output.`,
() => HH3_BUILD_INFO_HELP,
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Malformed HH3 .output.json can be accepted as valid output.

outputData = outputJson.output ?? outputJson treats {} (or other non-SolcOutput objects) as valid output, so invalid output files can bypass this check and fail later with less actionable errors.

🔧 Proposed fix
       try {
         const outputJson = await readJSON(outputFilePath);
-        outputData = outputJson.output ?? outputJson;
+        const candidateOutput = outputJson.output ?? outputJson;
+        if (
+          candidateOutput === null ||
+          typeof candidateOutput !== 'object' ||
+          (!('sources' in candidateOutput) && !('contracts' in candidateOutput))
+        ) {
+          throw new ValidateCommandError(
+            `Build info file ${buildInfoFilePath} does not contain output, and output file ${outputFilePath} is missing Solidity compiler output.`,
+            () => HH3_BUILD_INFO_HELP,
+          );
+        }
+        outputData = candidateOutput as SolcOutput;
       } catch (error) {
         throw new ValidateCommandError(
           `Build info file ${buildInfoFilePath} does not contain output, and output file ${outputFilePath} could not be read.`,
           () => HH3_BUILD_INFO_HELP,
         );
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/cli/validate/build-info-file.ts` around lines 216 - 230,
The current assignment outputData = outputJson.output ?? outputJson lets empty
or malformed JSON (e.g., {}) pass as valid; update the logic in the block that
reads outputFilePath so that after reading outputJson you verify the
shape/contents before assigning to outputData (check that either
outputJson.output exists and is a non-empty object with expected SolcOutput keys
like "contracts" or "sources", or if falling back to outputJson itself it
likewise contains those keys); if the validation fails, throw the existing
ValidateCommandError (using buildInfoFilePath, outputFilePath and
HH3_BUILD_INFO_HELP) instead of accepting the malformed object—look for the
readJSON call, the outputData assignment, and the two places that throw
ValidateCommandError to implement this guard.

Comment on lines +233 to +237
const userSourceNameMap: Record<string, string> | undefined = buildInfoJson.userSourceNameMap;
if (userSourceNameMap !== undefined) {
const canonicalToUser: Record<string, string> = {};
for (const [userSource, canonicalSource] of Object.entries(userSourceNameMap)) {
canonicalToUser[canonicalSource] = userSource;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard userSourceNameMap shape before iterating it.

If userSourceNameMap is null or malformed, Object.entries can throw a raw runtime error instead of returning a ValidateCommandError.

🔧 Proposed fix
-    const userSourceNameMap: Record<string, string> | undefined = buildInfoJson.userSourceNameMap;
+    const userSourceNameMap = buildInfoJson.userSourceNameMap as unknown;
     if (userSourceNameMap !== undefined) {
+      if (userSourceNameMap === null || typeof userSourceNameMap !== 'object') {
+        throw new ValidateCommandError(
+          `Build info file ${buildInfoFilePath} has an invalid userSourceNameMap.`,
+          () => HH3_BUILD_INFO_HELP,
+        );
+      }
       const canonicalToUser: Record<string, string> = {};
-      for (const [userSource, canonicalSource] of Object.entries(userSourceNameMap)) {
+      for (const [userSource, canonicalSource] of Object.entries(userSourceNameMap as Record<string, string>)) {
         canonicalToUser[canonicalSource] = userSource;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/cli/validate/build-info-file.ts` around lines 233 - 237,
Guard the shape of buildInfoJson.userSourceNameMap before iterating: ensure
userSourceNameMap is a non-null plain object (e.g. typeof userSourceNameMap ===
"object" && userSourceNameMap !== null && !Array.isArray(userSourceNameMap)) and
that each entry's key/value are strings; if the check fails, return or throw a
ValidateCommandError with a clear message instead of proceeding to build
canonicalToUser and using Object.entries. Locate the block referencing
userSourceNameMap, canonicalToUser and buildInfoJson and add the shape checks
and error creation early to prevent Object.entries from throwing on
null/malformed data.

Comment on lines +12 to +21
const connection = await hre.network.connect();
const { ethers } = connection;
const upgradesApi = await upgrades(hre, connection);

const BoxV2 = await ethers.getContractFactory('BoxV2');
const box = await upgradesApi.upgradeProxy(PROXY_ADDRESS, BoxV2);
await box.waitForDeployment();

console.log('Box upgraded at:', await box.getAddress());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine the actual file
echo "=== File: 2-upgrade.ts ==="
cat -n packages/plugin-hardhat/examples/BoxTransparent/scripts/2-upgrade.ts

Repository: OpenZeppelin/openzeppelin-upgrades

Length of output: 1048


🏁 Script executed:

#!/bin/bash
# Check connection lifecycle patterns across examples
echo "=== hre.network.connect() usage ==="
rg -nP --type=ts -C2 'hre\.network\.connect\s*\(' packages/plugin-hardhat/examples

echo ""
echo "=== connection.close() usage ==="
rg -nP --type=ts -C2 'connection\.close\s*\(' packages/plugin-hardhat/examples

Repository: OpenZeppelin/openzeppelin-upgrades

Length of output: 4217


🏁 Script executed:

#!/bin/bash
# Let's check all files in the BoxTransparent examples directory
echo "=== Files in BoxTransparent/scripts ==="
find packages/plugin-hardhat/examples/BoxTransparent/scripts -type f

Repository: OpenZeppelin/openzeppelin-upgrades

Length of output: 254


Close the Hardhat connection in a finally block.

The script opens a NetworkConnection but never closes it. This pattern is missing across all script examples; test files properly close connections using connection.close(), and scripts should follow the same resource-cleanup discipline to avoid leaving open handles.

♻️ Proposed fix
 async function main() {
   if (PROXY_ADDRESS === '0x...') {
     console.error('Please set PROXY_ADDRESS to your deployed proxy address');
     process.exit(1);
   }

-  const connection = await hre.network.connect();
-  const { ethers } = connection;
-  const upgradesApi = await upgrades(hre, connection);
-
-  const BoxV2 = await ethers.getContractFactory('BoxV2');
-  const box = await upgradesApi.upgradeProxy(PROXY_ADDRESS, BoxV2);
-  await box.waitForDeployment();
-
-  console.log('Box upgraded at:', await box.getAddress());
+  const connection = await hre.network.connect();
+  try {
+    const { ethers } = connection;
+    const upgradesApi = await upgrades(hre, connection);
+
+    const BoxV2 = await ethers.getContractFactory('BoxV2');
+    const box = await upgradesApi.upgradeProxy(PROXY_ADDRESS, BoxV2);
+    await box.waitForDeployment();
+
+    console.log('Box upgraded at:', await box.getAddress());
+  } finally {
+    await connection.close();
+  }

Note: The same issue affects 1-deploy.ts and both scripts in the BoxUUPS example. Consider applying this fix consistently across all script files.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const connection = await hre.network.connect();
const { ethers } = connection;
const upgradesApi = await upgrades(hre, connection);
const BoxV2 = await ethers.getContractFactory('BoxV2');
const box = await upgradesApi.upgradeProxy(PROXY_ADDRESS, BoxV2);
await box.waitForDeployment();
console.log('Box upgraded at:', await box.getAddress());
}
const connection = await hre.network.connect();
try {
const { ethers } = connection;
const upgradesApi = await upgrades(hre, connection);
const BoxV2 = await ethers.getContractFactory('BoxV2');
const box = await upgradesApi.upgradeProxy(PROXY_ADDRESS, BoxV2);
await box.waitForDeployment();
console.log('Box upgraded at:', await box.getAddress());
} finally {
await connection.close();
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/examples/BoxTransparent/scripts/2-upgrade.ts` around
lines 12 - 21, The script opens a Hardhat NetworkConnection via
hre.network.connect() (variable connection) but never closes it; wrap the main
upgrade logic (getting ethers, calling upgrades(hre, connection),
ethers.getContractFactory('BoxV2'), upgradesApi.upgradeProxy(...),
box.waitForDeployment(), console.log) in a try/finally and call
connection.close() from the finally block to ensure the NetworkConnection is
always closed (apply the same pattern to 1-deploy.ts and the BoxUUPS scripts);
reference the connection variable and ensure any thrown errors still let finally
run to call connection.close().

Comment on lines +16 to +17
"ethers": "^6.8.1",
"hardhat": "^3.0.0",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Hardhat/Ethers versions across package.json files =="
fd '^package\.json$' -t f | while read -r f; do
  python - <<'PY' "$f"
import json,sys
p=sys.argv[1]
with open(p) as fh:
    j=json.load(fh)
deps={}
for k in ("dependencies","devDependencies","peerDependencies"):
    deps.update(j.get(k,{}))
h=deps.get("hardhat")
e=deps.get("ethers")
oz=deps.get("@openzeppelin/hardhat-upgrades")
if h or e or oz:
    print(f"{p}: hardhat={h} ethers={e} oz-upgrades={oz}")
PY
done

echo
echo "== plugin-hardhat peerDependencies (if present) =="
fd '^package\.json$' -t f -x grep -l "plugin-hardhat" {} \; | head -1 | while read -r f; do
  python - <<'PY' "$f"
import json,sys
p=sys.argv[1]
with open(p) as fh:
    j=json.load(fh)
print(f"File: {p}")
print(json.dumps(j.get("peerDependencies",{}), indent=2))
PY
done

Repository: OpenZeppelin/openzeppelin-upgrades

Length of output: 691


🏁 Script executed:

# Verify the target file exists and read lines 16-17
echo "=== Target file content ==="
if [ -f "packages/plugin-hardhat/examples/BoxUUPS/package.json" ]; then
  echo "File found. Content around lines 16-17:"
  sed -n '14,19p' "packages/plugin-hardhat/examples/BoxUUPS/package.json" | cat -n
else
  echo "File not found at expected path"
  fd 'BoxUUPS.*package.json'
fi

Repository: OpenZeppelin/openzeppelin-upgrades

Length of output: 391


🏁 Script executed:

#!/bin/bash

echo "=== Checking lock files for baseline versions ==="
if [ -f "package-lock.json" ]; then
  echo "Root package-lock.json - hardhat entries:"
  jq '.packages[] | select(.name == "hardhat") | .version' package-lock.json 2>/dev/null | head -5 || rg 'hardhat.*3\.0\.' package-lock.json | head -3
fi

if [ -f "pnpm-lock.yaml" ]; then
  echo "pnpm-lock.yaml found - checking hardhat versions:"
  rg 'hardhat@' pnpm-lock.yaml | head -5
fi

echo
echo "=== Checking main plugin-hardhat package.json details ==="
cat packages/plugin-hardhat/package.json | head -30

echo
echo "=== Comparing both example directories ==="
echo "BoxTransparent package.json (lines 16-17):"
sed -n '14,19p' packages/plugin-hardhat/examples/BoxTransparent/package.json | cat -n

echo
echo "BoxUUPS package.json (lines 16-17):"
sed -n '14,19p' packages/plugin-hardhat/examples/BoxUUPS/package.json | cat -n

Repository: OpenZeppelin/openzeppelin-upgrades

Length of output: 1722


🏁 Script executed:

# Check if there's any documentation or CI config mentioning baseline versions
echo "=== Searching for version baseline references ==="
rg -i "baseline|migration.*version|tested.*with" --type md --type json --type yaml --type txt . 2>/dev/null | head -20

Repository: OpenZeppelin/openzeppelin-upgrades

Length of output: 819


Example dependencies have outdated ethers version compared to main plugin.

The examples at lines 16-17 use ethers ^6.8.1, which lags behind the main plugin-hardhat (^6.14.0) by 6 minor versions. This can cause drift and inconsistent behavior.

The hardhat version suggestion to ^3.0.16 could not be verified against the codebase baseline; all packages consistently use hardhat ^3.0.0. Recommend updating ethers to align with the main plugin:

🔧 Suggested fix
-    "ethers": "^6.8.1",
+    "ethers": "^6.14.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"ethers": "^6.8.1",
"hardhat": "^3.0.0",
"ethers": "^6.14.0",
"hardhat": "^3.0.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/examples/BoxUUPS/package.json` around lines 16 - 17,
Update the example's package.json dependency for ethers to match the main plugin
version: replace the current "ethers": "^6.8.1" entry with the same version used
by the plugin ("^6.14.0") so the example and main package remain aligned; leave
the existing hardhat entry as-is ("^3.0.0") to maintain consistency across
packages.

Comment on lines +12 to +18
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);

const artifactsBuildInfo = require('@openzeppelin/upgrades-core/artifacts/build-info-v5.json');
import { AbiCoder } from 'ethers';
import * as defenderUtils from '../dist/defender/utils.js';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix ESM import ordering: static imports must precede all module-level code.

The import { AbiCoder } and import * as defenderUtils statements (lines 17-18) appear after the const require = createRequire(...) and require(...) calls (lines 14, 16). In ESM, all static imports must come before any other module-level statements.

Proposed fix
 import { getCombinedBuildInfo } from '../dist/utils/artifacts.js';
-import { createRequire } from 'node:module';
-
-const require = createRequire(import.meta.url);
-
-const artifactsBuildInfo = require('@openzeppelin/upgrades-core/artifacts/build-info-v5.json');
 import { AbiCoder } from 'ethers';
 import * as defenderUtils from '../dist/defender/utils.js';
+import { createRequire } from 'node:module';
+
+const require = createRequire(import.meta.url);
+
+const artifactsBuildInfo = require('@openzeppelin/upgrades-core/artifacts/build-info-v5.json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/defender-deploy.js` around lines 12 - 18, Move
all static import statements to the top of the module so they precede any
module-level code: ensure the lines "import { createRequire } from
'node:module';", "import { AbiCoder } from 'ethers';" and "import * as
defenderUtils from '../dist/defender/utils.js';" appear before the "const
require = createRequire(import.meta.url);" and the "const artifactsBuildInfo =
require(...)" call; alternatively replace the JSON require with an ESM import if
preferred, but at minimum reorder to place imports (createRequire, AbiCoder,
defenderUtils) before createRequire/require usage.

Comment on lines +4 to +9
const connection = await hre.network.connect();
const { ethers } = connection;
// import { defender as defenderFactory } from '@openzeppelin/hardhat-upgrades';
import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
import sinon from 'sinon';
import esmock from 'esmock';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix ESM import ordering: static imports must precede top-level await.

Lines 4-5 execute top-level await before the static imports on lines 7-9. Move the connection setup below all imports.

Proposed fix
 import test from 'ava';
 import hre from 'hardhat';
-
-const connection = await hre.network.connect();
-const { ethers } = connection;
-// import { defender as defenderFactory } from '@openzeppelin/hardhat-upgrades';
 import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
 import sinon from 'sinon';
 import esmock from 'esmock';
+
+const connection = await hre.network.connect();
+const { ethers } = connection;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const connection = await hre.network.connect();
const { ethers } = connection;
// import { defender as defenderFactory } from '@openzeppelin/hardhat-upgrades';
import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
import sinon from 'sinon';
import esmock from 'esmock';
import test from 'ava';
import hre from 'hardhat';
import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
import sinon from 'sinon';
import esmock from 'esmock';
const connection = await hre.network.connect();
const { ethers } = connection;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/propose-upgrade-with-approval-beacon.js` around
lines 4 - 9, Top-level await is executed before static imports; move the dynamic
connection setup so that all static imports (the lines importing
upgradesFactory, sinon, esmock and any other imports) appear first, then call
const connection = await hre.network.connect(); const { ethers } = connection; —
i.e., relocate the connection/ethers assignment below the import statements
(ensure symbols connection, hre.network.connect and ethers remain unchanged and
referenced after the imports).


const proposalId = 'mocked proposal id';
const proposalUrl = 'https://example.com';
const approvalProcessId = '123'; // ← ADICIONEI
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove development comments before merging.

Lines 16 and 69 contain Portuguese comments (← ADICIONEI, ← MUDEI AQUI: usar transparent proxy ao invés de beacon) that appear to be development notes. These should be removed or translated to English before merging.

Proposed fix
-const approvalProcessId = '123'; // ← ADICIONEI
+const approvalProcessId = '123';
-  // ← MUDEI AQUI: usar transparent proxy ao invés de beacon
   t.context.greeter = await upgrades.deployProxy(t.context.Greeter, { kind: 'transparent' });

Also applies to: 69-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/propose-upgrade-with-approval-transparent.js` at
line 16, Remove the leftover Portuguese development comments from the test by
deleting the inline comment text after the approvalProcessId constant (the "←
ADICIONEI" note) and the other Portuguese note near the proxy change (the "←
MUDEI AQUI: usar transparent proxy ao invés de beacon") so the file contains
only code or English comments; locate the statements by the variable name
approvalProcessId and the nearby proxy-change comment and either remove or
replace them with a concise English comment before merging.

Comment on lines +4 to +9
const connection = await hre.network.connect();
const { ethers } = connection;
import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
import sinon from 'sinon';
import esmock from 'esmock';
import { mockDeploy } from '../dist/test-utils/mock-deploy.js';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix ESM import ordering: static imports must precede top-level await.

Lines 4-5 execute top-level await before the static imports on lines 6-9. Move the connection setup below all imports.

Proposed fix
 import test from 'ava';
 import hre from 'hardhat';
-
-const connection = await hre.network.connect();
-const { ethers } = connection;
 import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
 import sinon from 'sinon';
 import esmock from 'esmock';
 import { mockDeploy } from '../dist/test-utils/mock-deploy.js';
+
+const connection = await hre.network.connect();
+const { ethers } = connection;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/propose-upgrade-with-approval-unsafeAllow.js`
around lines 4 - 9, The file currently calls top-level await via const
connection = await hre.network.connect() and const { ethers } = connection
before static imports (e.g., imports of upgradesFactory, sinon, esmock,
mockDeploy); move the top-level await lines (the connection and ethers
extraction using hre.network.connect) to after all static import statements so
that static imports (imports of '@openzeppelin/hardhat-upgrades', 'sinon',
'esmock', and '../dist/test-utils/mock-deploy.js') appear first and the
asynchronous network connection is performed subsequently.

Comment on lines +4 to +6
const connection = await hre.network.connect();
const { ethers } = connection;
import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix ESM import ordering: static imports must precede top-level await.

In ESM, all import declarations must appear at the top of the module before any other code, including top-level await. Move the connection setup below all imports.

Proposed fix
 import test from 'ava';
 import hre from 'hardhat';
-
-const connection = await hre.network.connect();
-const { ethers } = connection;
 import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
+
+const connection = await hre.network.connect();
+const { ethers } = connection;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const connection = await hre.network.connect();
const { ethers } = connection;
import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
import test from 'ava';
import hre from 'hardhat';
import { upgrades as upgradesFactory } from '@openzeppelin/hardhat-upgrades';
const connection = await hre.network.connect();
const { ethers } = connection;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-hardhat/test/uups-deploy-overload.js` around lines 4 - 6, The
file uses top-level await before static imports: move all import declarations
(e.g., "import { upgrades as upgradesFactory } from
'@openzeppelin/hardhat-upgrades';" and any other import statements including
ethers if using static import) to the very top of the module, then place the
top-level await call that sets up the Hardhat connection (const connection =
await hre.network.connect();) and the subsequent const { ethers } = connection;
after those imports; ensure no top-level code runs before the import
declarations so ESM ordering rules are satisfied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants