Skip to content

Conversation

@SyedAsadKazmi
Copy link

Summary

This PR fixes a critical bug in Etherscan contract verification where detailed failure messages prevented the automatic retry with full solc input, causing verification to fail even though it would succeed with the full compiler input.

Problem

When verifying contracts on Etherscan, users were experiencing failures with the error:

HHE80024: The contract verification status polling encountered an error: 
"Fail - Unable to verify. Compiled contract deployment bytecode does NOT match 
the transaction deployment bytecode.". Verification may still succeed.

However, the same verification would succeed on Blockscout, which properly retried with the full solc input after the minimal input failed.

Root Cause

There were two bugs in the Etherscan verification status polling logic:

  1. Bug 1 - Exact String Matching: The isFailure() method used exact string matching (===) instead of prefix matching (startsWith()), so it couldn't recognize failure messages that included additional details.

    // Before (line 386)
    public isFailure(): boolean {
      return this.message === "Fail - Unable to verify";  // ❌ Exact match only
    }
  2. Bug 2 - Incorrect Logic Order: The polling logic checked isOk() before checking isFailure(), causing it to throw an error before recognizing legitimate failures. When Etherscan returns a failure, the status code is 0, making isOk() return false and immediately throwing an error.

    // Before (lines 323-328)
    if (!etherscanResponse.isOk()) {  // ❌ Throws error before checking isFailure()
      throw new HardhatError(
        HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_STATUS_POLLING_FAILED,
        { message: etherscanResponse.message },
      );
    }

This prevented the automatic retry mechanism from kicking in, even though the verification logic was designed to retry with full solc input on failure.

Solution

Change 1: Use Prefix Matching for Failure Detection

Changed the isFailure() method to use startsWith() to properly detect failure messages with additional details:

// After (line 386)
public isFailure(): boolean {
  return this.message.startsWith("Fail - Unable to verify");  // ✅ Prefix match
}

Change 2: Reorder Logic to Check Success/Failure First

Reordered the polling logic to check for recognized success/failure states before throwing errors:

// After (lines 323-328)
if (etherscanResponse.isFailure() || etherscanResponse.isSuccess()) {
  return {
    success: etherscanResponse.isSuccess(),
    message: etherscanResponse.message,
  };
}

if (!etherscanResponse.isOk()) {
  throw new HardhatError(
    HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_VERIFICATION_STATUS_POLLING_FAILED,
    { message: etherscanResponse.message },
  );
}

Impact

Before

=== Etherscan ===
📤 Submitted source code for verification...
⏳ Waiting for verification result...
❌ HHE80024: The contract verification status polling encountered an error...
(Verification stops here, never retries with full input)

=== Blockscout ===
📤 Submitted source code for verification...
⏳ Waiting for verification result...
The initial verification attempt failed using the minimal compiler input.
Trying again with the full solc input...
✅ Contract verified successfully on Blockscout!

After

=== Etherscan ===
📤 Submitted source code for verification...
⏳ Waiting for verification result...
The initial verification attempt failed using the minimal compiler input.
Trying again with the full solc input...
📤 Submitted source code for verification...
⏳ Waiting for verification result...
✅ Contract verified successfully on Etherscan!

=== Blockscout ===
(Already verified)

Testing

Tested with a Counter contract deployed to Sepolia:

Files Changed

  • v-next/hardhat-verify/src/internal/etherscan.ts
    • Fixed isFailure() method to use prefix matching
    • Reordered pollVerificationStatus() logic to check success/failure before throwing errors

Breaking Changes

None. This is a bug fix that makes Etherscan verification work as originally intended.

Related Issues

Fixes the issue where Etherscan verification fails with:

  • "Compiled contract deployment bytecode does NOT match the transaction deployment bytecode"
  • While the same contract successfully verifies on Blockscout using full solc input

This aligns Etherscan verification behavior with Blockscout, ensuring both providers properly utilize the two-step verification process (minimal input first, then full input on failure).

@changeset-bot
Copy link

changeset-bot bot commented Oct 14, 2025

⚠️ No Changeset found

Latest commit: d919e1a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@SyedAsadKazmi
Copy link
Author

@michalbrabec, @alcuadrado, any updates regarding this PR?

@kanej kanej requested a review from schaable October 27, 2025 09:49
@alcuadrado
Copy link
Member

Hey @SyedAsadKazmi, sorry for the delay. We'll release some etherscan related fixes next, and will take a look at it after. Thanks for sending the PR!

@SyedAsadKazmi
Copy link
Author

Noted, @alcuadrado. Thanks!

@schaable
Copy link
Member

Hi @SyedAsadKazmi, could you share the steps to reproduce that specific error?
If your local contracts don’t match the deployed bytecode, it’s usually caught by:

HHE80009: The address contains a contract whose bytecode does not match any of your local contracts.

The order of these checks in the verify and pollVerificationStatus functions is intentional, it helps ensure any change in the Etherscan API doesn’t go unnoticed. That might actually be what’s happening here.

I’ll need the sample contracts you used to trigger this scenario. I tested with the Counter contract you shared and was able to verify it successfully, so it could be related to your compiler settings or the way you’re verifying (for example, via the programmatic API). Could you share the exact contract and detailed steps to reproduce the issue?

@SyedAsadKazmi
Copy link
Author

Hello @schaable,

It's reproducible unless we use the exact same settings in default and production profiles, like:

solidity: {
  profiles: {
    default: {
      version: "0.8.24",
      settings: {
        optimizer: {
          enabled: true,
          runs: 1_000,
        },
      }
    },
    production: {
      version: "0.8.24",
      settings: {
        optimizer: {
          enabled: true,
          runs: 1_000,
        },
      }
    },
  },
},

When you run npx hardhat compile without specifying the profile, then the compilation is done using the default, but during the verification process, it's picking the production profile by default.

So, generally speaking, the problem is more related to verify picking the production profile by default, as ideally it should pick the default profile. But, if it's supposed to be designed this way only, then using the full solc input on first failure (i.e., minimal input first, then full input on failure), solves the problem and also aligns the Etherscan verification behavior with that of the Blockscout.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants