Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/tame-socks-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@nomicfoundation/hardhat-ignition": minor
"@nomicfoundation/ignition-core": minor
"@nomicfoundation/hardhat-ignition-ethers": minor
"@nomicfoundation/hardhat-ignition-viem": minor
"@nomicfoundation/ignition-ui": minor
---

Added support for verifying on all enabled verification services (e.g. Sourcify) ([#7538](https://github.com/NomicFoundation/hardhat/issues/7538)).
2 changes: 1 addition & 1 deletion v-next/hardhat-ignition/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const hardhatIgnitionPlugin: HardhatPlugin = {
})
.addFlag({
name: "verify",
description: "Verify the deployment on Etherscan",
description: "Verify the deployment on all enabled verifiers",
})
.addFlag({
name: "writeLocalhostDeployment",
Expand Down
74 changes: 61 additions & 13 deletions v-next/hardhat-ignition/src/internal/tasks/verify.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { VerificationProvidersConfig } from "hardhat/types/config";
import type { HardhatRuntimeEnvironment } from "hardhat/types/hre";
import type { NewTaskActionFunction } from "hardhat/types/tasks";

import path from "node:path";

import { ensureError } from "@nomicfoundation/hardhat-utils/error";
import { capitalize } from "@nomicfoundation/hardhat-utils/string";
import { verifyContract } from "@nomicfoundation/hardhat-verify/verify";
import { getVerificationInformation } from "@nomicfoundation/ignition-core";
import chalk from "chalk";

interface TaskVerifyArguments {
deploymentId: string;
Expand All @@ -15,6 +19,37 @@ const verifyTask: NewTaskActionFunction<TaskVerifyArguments> = async (
{ deploymentId, force },
hre: HardhatRuntimeEnvironment,
) => {
await verify(
{ deploymentId, force },
hre,
verifyContract,
getVerificationInformation,
);
};

// Exported for testing
export async function verify(
{ deploymentId, force }: TaskVerifyArguments,
hre: HardhatRuntimeEnvironment,
verifyContractFn: typeof verifyContract,
getVerificationInformationFn: typeof getVerificationInformation,
): Promise<void> {
const allProviders: Array<keyof VerificationProvidersConfig> = [
"etherscan",
"blockscout",
"sourcify",
];

const enabledProviders = allProviders.filter(
(provider) => hre.config.verify[provider].enabled,
);

if (enabledProviders.length === 0) {
console.warn(chalk.yellow("\n⚠️ No verification providers are enabled."));

Comment thread
kanej marked this conversation as resolved.
return;
}

const deploymentDir = path.join(
hre.config.paths.ignition,
"deployments",
Expand All @@ -23,28 +58,41 @@ const verifyTask: NewTaskActionFunction<TaskVerifyArguments> = async (

const connection = await hre.network.connect();

Comment thread
kanej marked this conversation as resolved.
for await (const contractInfo of getVerificationInformation(deploymentDir)) {
for await (const contractInfo of getVerificationInformationFn(
deploymentDir,
)) {
if (typeof contractInfo === "string") {
console.log(
`Could not resolve contract artifacts for contract "${contractInfo}". Skipping verification.`,
`Could not resolve contract artifacts for contract "${contractInfo}". Skipping verification.\n`,
);
console.log("");

continue;
}

console.log(
`Verifying contract "${contractInfo.contract}" for network ${connection.networkName}...`,
`\nVerifying contract "${contractInfo.contract}" for network ${connection.networkName}...`,
);

await verifyContract(
{
...contractInfo,
force,
provider: "etherscan",
},
hre,
);
for (const provider of enabledProviders) {
try {
console.log(chalk.cyan.bold(`\n=== ${capitalize(provider)} ===`));

await verifyContractFn(
{
...contractInfo,
force,
provider,
},
hre,
);
} catch (error) {
ensureError(error);

console.error(chalk.red(error.message));
process.exitCode = 1;
}
Comment thread
kanej marked this conversation as resolved.
}
}
};
}

export default verifyTask;
252 changes: 252 additions & 0 deletions v-next/hardhat-ignition/test/tasks/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import type { VerifyContractArgs } from "@nomicfoundation/hardhat-verify/verify";
import type { VerifyResult } from "@nomicfoundation/ignition-core";
import type { VerificationProvidersConfig } from "hardhat/types/config";
import type { HardhatRuntimeEnvironment } from "hardhat/types/hre";

import { assert } from "chai";
import sinon from "sinon";

import { verify } from "../../src/internal/tasks/verify.js";
import { useEphemeralIgnitionProject } from "../test-helpers/use-ignition-project.js";

describe("ignition verify task", () => {
useEphemeralIgnitionProject("minimal");

// TODO: replace with disableConsole() once converted to `node:test`
let consoleLogStub: sinon.SinonStub;
let consoleWarnStub: sinon.SinonStub;
let consoleErrorStub: sinon.SinonStub;

beforeEach(() => {
consoleLogStub = sinon.stub(console, "log");
consoleWarnStub = sinon.stub(console, "warn");
consoleErrorStub = sinon.stub(console, "error");
});

afterEach(() => {
consoleLogStub.restore();
consoleWarnStub.restore();
consoleErrorStub.restore();

process.exitCode = undefined;
});

const exampleVerifyInfo: VerifyResult = {
address: "0x1234567890123456789012345678901234567890",
constructorArgs: [],
libraries: {},
contract: "contracts/Foo.sol:Foo",
creationTxHash:
"0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
};

it("should verify on all enabled providers", async function () {
const verifyCallsByProvider: Array<keyof VerificationProvidersConfig> = [];

const mockVerifyContract = async (
args: VerifyContractArgs,
_hre: HardhatRuntimeEnvironment,
) => {
if (args.provider !== undefined) {
verifyCallsByProvider.push(args.provider);
}

return true;
};

const mockGetVerificationInformation = async function* () {
yield exampleVerifyInfo;
};

await verify(
{ deploymentId: "test-deployment", force: false },
this.hre,
mockVerifyContract,
mockGetVerificationInformation,
);

assert.equal(verifyCallsByProvider.length, 3);
assert.include(verifyCallsByProvider, "etherscan");
assert.include(verifyCallsByProvider, "blockscout");
assert.include(verifyCallsByProvider, "sourcify");

assert.equal(process.exitCode, undefined);
});

it("should continue verification on other providers when one fails", async function () {
const verifyCallsByProvider: Array<keyof VerificationProvidersConfig> = [];

const mockVerifyContract = async (
args: VerifyContractArgs,
_hre: HardhatRuntimeEnvironment,
) => {
if (args.provider !== undefined) {
verifyCallsByProvider.push(args.provider);

if (args.provider === "blockscout") {
throw new Error("Blockscout verification failed");
}
}

return true;
};

const mockGetVerificationInformation = async function* () {
yield exampleVerifyInfo;
};

await verify(
{ deploymentId: "test-deployment", force: false },
this.hre,
mockVerifyContract,
mockGetVerificationInformation,
);

assert.equal(verifyCallsByProvider.length, 3);
assert.include(verifyCallsByProvider, "etherscan");
assert.include(verifyCallsByProvider, "blockscout");
assert.include(verifyCallsByProvider, "sourcify");

assert.equal(process.exitCode, 1);
});

it("should not verify when no providers are enabled", async function () {
let verifyContractCalled = false;

await verify(
{ deploymentId: "test-deployment", force: false },
{
...this.hre,
config: {
...this.hre.config,
verify: {
etherscan: { enabled: false, apiKey: "" as any },
blockscout: { enabled: false },
sourcify: { enabled: false },
},
},
},
async () => {
verifyContractCalled = true;

return true;
},
async function* () {},
);

assert.isFalse(verifyContractCalled);
});

it("should verify multiple contracts on all enabled providers", async function () {
const verifyCalls: Array<{ provider: string; contract: string }> = [];

const mockVerifyContract = async (
args: VerifyContractArgs,
_hre: HardhatRuntimeEnvironment,
) => {
if (args.contract === undefined) {
assert.fail("Expected contract to be passed");
}

if (args.provider !== undefined) {
verifyCalls.push({ provider: args.provider, contract: args.contract });
}

return true;
};

const mockGetVerificationInformation = async function* () {
yield {
address: "0x1111111111111111111111111111111111111111",
constructorArgs: [],
libraries: {},
contract: "contracts/Foo.sol:Foo",
creationTxHash:
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
};
yield {
address: "0x2222222222222222222222222222222222222222",
constructorArgs: [123],
libraries: {},
contract: "contracts/Bar.sol:Bar",
creationTxHash:
"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
};
};

await verify(
{ deploymentId: "test-deployment", force: false },
this.hre,
mockVerifyContract,
mockGetVerificationInformation,
);

// 2 contracts × 3 providers = 6 calls
assert.equal(verifyCalls.length, 6);

// Each contract should be verified on each provider
assert.deepEqual(verifyCalls, [
{ provider: "etherscan", contract: "contracts/Foo.sol:Foo" },
{ provider: "blockscout", contract: "contracts/Foo.sol:Foo" },
{ provider: "sourcify", contract: "contracts/Foo.sol:Foo" },
{ provider: "etherscan", contract: "contracts/Bar.sol:Bar" },
{ provider: "blockscout", contract: "contracts/Bar.sol:Bar" },
{ provider: "sourcify", contract: "contracts/Bar.sol:Bar" },
]);
});

it("should pass force flag through to each provider call", async function () {
const forceValues: boolean[] = [];

const mockVerifyContract = async (
args: VerifyContractArgs,
_hre: HardhatRuntimeEnvironment,
) => {
if (args.force === undefined) {
assert.fail("Expected force to be passed");
}

forceValues.push(args.force);

return true;
};

const mockGetVerificationInformation = async function* () {
yield exampleVerifyInfo;
};

await verify(
{ deploymentId: "test-deployment", force: true },
this.hre,
mockVerifyContract,
mockGetVerificationInformation,
);

assert.equal(forceValues.length, 3);
assert.isTrue(
forceValues.every((f) => f === true),
"Expected all verify calls to have force: true",
);
});

it("should not verify if contracts artifacts cannot be resolved", async function () {
let verifyContractCalled = false;

const mockGetVerificationInformation = async function* () {
yield "contracts/Foo.sol:Foo";
};

await verify(
{ deploymentId: "test-deployment", force: false },
this.hre,
async () => {
verifyContractCalled = true;

return true;
},
mockGetVerificationInformation,
);

assert.isFalse(verifyContractCalled);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ export type StaticCallStrategyGenerator = AsyncGenerator<
* process of requestsing `OnchainInteraction`s, which differs whether if the request
* has already being resolved or not. See below for more details.
*
* A strategy that performs multiple transactions must ensure the last transaction
* encapsulates the semantic changes of the strategy. For example, a multi-sig
* strategy should ensure that the final transaction is the one that performs the
* deployment or create2 invocation.
*
* There are two types of request, which follow a different protocol:
*
* - `OnchainInteractionRequest`: This request is used to perform an onchain
Expand Down
Loading
Loading