Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce contract verification #5164

Merged
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
5 changes: 5 additions & 0 deletions .changeset/pink-goats-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-verify": patch
---

Added 'force' flag to allow verification of partially verified contracts (thanks @rimrakhimov!)
17 changes: 16 additions & 1 deletion packages/hardhat-verify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface VerifyTaskArgs {
constructorArgs?: string;
libraries?: string;
contract?: string;
force: boolean;
listNetworks: boolean;
}

Expand All @@ -61,6 +62,7 @@ interface VerifySubtaskArgs {
constructorArguments: string[];
libraries: LibraryToAddress;
contract?: string;
force?: boolean;
}

export interface VerificationResponse {
Expand Down Expand Up @@ -115,6 +117,11 @@ task(TASK_VERIFY, "Verifies a contract on Etherscan or Sourcify")
"Fully qualified name of the contract to verify. Skips automatic detection of the contract. " +
"Use if the deployed bytecode matches more than one contract in your project"
)
.addFlag(
"force",
"Enforce contract verification even if the contract is already verified. " +
"Use to re-verify partially verified contracts on Blockscout"
)
.addFlag("listNetworks", "Print the list of supported networks")
.setAction(async (taskArgs: VerifyTaskArgs, { run }) => {
if (taskArgs.listNetworks) {
Expand Down Expand Up @@ -266,9 +273,16 @@ subtask(TASK_VERIFY_VERIFY)
.addOptionalParam("constructorArguments", undefined, [], types.any)
.addOptionalParam("libraries", undefined, {}, types.any)
.addOptionalParam("contract")
.addFlag("force")
.setAction(
async (
{ address, constructorArguments, libraries, contract }: VerifySubtaskArgs,
{
address,
constructorArguments,
libraries,
contract,
force,
}: VerifySubtaskArgs,
{ run, config }
) => {
// This can only happen if the subtask is invoked from within Hardhat by a user script or another task.
Expand All @@ -286,6 +300,7 @@ subtask(TASK_VERIFY_VERIFY)
constructorArgsParams: constructorArguments,
libraries,
contract,
force,
});
}

Expand Down
8 changes: 8 additions & 0 deletions packages/hardhat-verify/src/internal/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,3 +458,11 @@ ${undetectableLibraries.map((x) => ` * ${x}`).join("\n")}`
}`);
}
}

export class ContractAlreadyVerifiedError extends HardhatVerifyError {
constructor(contractFQN: string, contractAddress: string) {
super(`The block explorer's API responded that the contract ${contractFQN} at ${contractAddress} is already verified.
This can happen if you used the '--force' flag. However, re-verification of contracts might not be supported
by the explorer (e.g., Etherscan), or the contract may have already been verified with a full match.`);
}
}
24 changes: 22 additions & 2 deletions packages/hardhat-verify/src/internal/etherscan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ContractStatusPollingInvalidStatusCodeError,
ContractVerificationMissingBytecodeError,
ContractVerificationInvalidStatusCodeError,
ContractAlreadyVerifiedError,
HardhatVerifyError,
MissingApiKeyError,
ContractStatusPollingResponseNotOkError,
Expand Down Expand Up @@ -139,6 +140,7 @@ export class Etherscan {
* @throws {ContractVerificationRequestError} if there is an error on the request.
* @throws {ContractVerificationInvalidStatusCodeError} if the API returns an invalid status code.
* @throws {ContractVerificationMissingBytecodeError} if the bytecode is not found on the block explorer.
* @throws {ContractAlreadyVerifiedError} if the bytecode is already verified.
* @throws {HardhatVerifyError} if the response status is not OK.
*/
public async verify(
Expand Down Expand Up @@ -184,6 +186,10 @@ export class Etherscan {
);
}

if (etherscanResponse.isAlreadyVerified()) {
throw new ContractAlreadyVerifiedError(contractName, contractAddress);
}

if (!etherscanResponse.isOk()) {
throw new HardhatVerifyError(etherscanResponse.message);
}
Expand All @@ -192,7 +198,8 @@ export class Etherscan {
} catch (e) {
if (
e instanceof ContractVerificationInvalidStatusCodeError ||
e instanceof ContractVerificationMissingBytecodeError
e instanceof ContractVerificationMissingBytecodeError ||
e instanceof ContractAlreadyVerifiedError
) {
throw e;
}
Expand Down Expand Up @@ -239,7 +246,10 @@ export class Etherscan {
return await this.getVerificationStatus(guid);
}

if (etherscanResponse.isFailure()) {
if (
etherscanResponse.isFailure() ||
etherscanResponse.isAlreadyVerified()
) {
return etherscanResponse;
}

Expand Down Expand Up @@ -296,6 +306,16 @@ class EtherscanResponse implements ValidationResponse {
return this.message.startsWith("Unable to locate ContractCode at");
}

public isAlreadyVerified() {
return (
// returned by blockscout
this.message.startsWith("Smart-contract already verified") ||
// returned by etherscan
this.message.startsWith("Contract source code already verified") ||
this.message.startsWith("Already Verified")
);
}

public isOk() {
return this.status === 1;
}
Expand Down
21 changes: 17 additions & 4 deletions packages/hardhat-verify/src/internal/tasks/etherscan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
InvalidContractNameError,
UnexpectedNumberOfFilesError,
VerificationAPIUnexpectedMessageError,
ContractAlreadyVerifiedError,
} from "../errors";
import { Etherscan } from "../etherscan";
import { Bytecode } from "../solc/bytecode";
Expand All @@ -50,6 +51,7 @@ interface VerificationArgs {
constructorArgs: string[];
libraries: LibraryToAddress;
contractFQN?: string;
force: boolean;
}

interface GetMinimalInputArgs {
Expand All @@ -76,12 +78,14 @@ subtask(TASK_VERIFY_ETHERSCAN)
.addOptionalParam("constructorArgs")
.addOptionalParam("libraries", undefined, undefined, types.any)
.addOptionalParam("contract")
.addFlag("force")
.setAction(async (taskArgs: VerifyTaskArgs, { config, network, run }) => {
const {
address,
constructorArgs,
libraries,
contractFQN,
force,
}: VerificationArgs = await run(
TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS,
taskArgs
Expand All @@ -99,9 +103,9 @@ subtask(TASK_VERIFY_ETHERSCAN)
);

const isVerified = await etherscan.isVerified(address);
if (isVerified) {
if (!force && isVerified) {
const contractURL = etherscan.getContractUrl(address);
console.log(`The contract ${address} has already been verified on Etherscan.
console.log(`The contract ${address} has already been verified on the block explorer. If you're trying to verify a partially verified contract, please use the --force flag.
${contractURL}`);
return;
}
Expand Down Expand Up @@ -200,13 +204,15 @@ subtask(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS)
.addOptionalParam("constructorArgs", undefined, undefined, types.inputFile)
.addOptionalParam("libraries", undefined, undefined, types.any)
.addOptionalParam("contract")
.addFlag("force")
.setAction(
async ({
address,
constructorArgsParams,
constructorArgs: constructorArgsModule,
contract,
libraries: librariesModule,
force,
}: VerifyTaskArgs): Promise<VerificationArgs> => {
if (address === undefined) {
throw new MissingAddressError();
Expand Down Expand Up @@ -238,6 +244,7 @@ subtask(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS)
constructorArgs,
libraries,
contractFQN: contract,
force,
};
}
);
Expand Down Expand Up @@ -294,16 +301,17 @@ subtask(TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION)
// Ensure the linking information is present in the compiler input;
compilerInput.settings.libraries = contractInformation.libraries;

const contractFQN = `${contractInformation.sourceName}:${contractInformation.contractName}`;
const { message: guid } = await verificationInterface.verify(
address,
JSON.stringify(compilerInput),
`${contractInformation.sourceName}:${contractInformation.contractName}`,
contractFQN,
`v${contractInformation.solcLongVersion}`,
encodedConstructorArguments
);

console.log(`Successfully submitted source code for contract
${contractInformation.sourceName}:${contractInformation.contractName} at ${address}
${contractFQN} at ${address}
for verification on the block explorer. Waiting for verification result...
`);

Expand All @@ -312,6 +320,11 @@ for verification on the block explorer. Waiting for verification result...
const verificationStatus =
await verificationInterface.getVerificationStatus(guid);

// Etherscan answers with already verified message only when checking returned guid
if (verificationStatus.isAlreadyVerified()) {
throw new ContractAlreadyVerifiedError(contractFQN, address);
}

if (!(verificationStatus.isFailure() || verificationStatus.isSuccess())) {
// Reaching this point shouldn't be possible unless the API is behaving in a new way.
throw new VerificationAPIUnexpectedMessageError(
Expand Down
128 changes: 127 additions & 1 deletion packages/hardhat-verify/test/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe("verify task integration tests", () => {
});

expect(logStub).to.be.calledOnceWith(
`The contract ${address} has already been verified on Etherscan.
`The contract ${address} has already been verified on the block explorer. If you're trying to verify a partially verified contract, please use the --force flag.
https://hardhat.etherscan.io/address/${address}#code`
);
logStub.restore();
Expand Down Expand Up @@ -629,6 +629,132 @@ https://hardhat.etherscan.io/address/${bothLibsContractAddress}#code\n`);
await this.hre.run(TASK_CLEAN);
});
});

describe("with a verified contract and '--force' flag", () => {
let simpleContractAddress: string;
before(async function () {
await this.hre.run(TASK_COMPILE, { force: true, quiet: true });
simpleContractAddress = await deployContract(
"SimpleContract",
[],
this.hre
);
});

beforeEach(() => {
interceptIsVerified({ message: "OK", result: [{ SourceCode: "code" }] });
});

it("should validate a partially verified contract", async function () {
interceptVerify({
status: 1,
result: "ezq878u486pzijkvvmerl6a9mzwhv6sefgvqi5tkwceejc7tvn",
});
interceptGetStatus(() => {
return {
status: 1,
result: "Pass - Verified",
};
});
const logStub = sinon.stub(console, "log");

const taskResponse = await this.hre.run(TASK_VERIFY, {
address: simpleContractAddress,
constructorArgsParams: [],
force: true,
});

assert.equal(logStub.callCount, 2);
expect(logStub.getCall(0)).to.be
.calledWith(`Successfully submitted source code for contract
contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress}
for verification on the block explorer. Waiting for verification result...
`);
expect(logStub.getCall(1)).to.be
.calledWith(`Successfully verified contract SimpleContract on the block explorer.
https://hardhat.etherscan.io/address/${simpleContractAddress}#code\n`);
logStub.restore();
assert.isUndefined(taskResponse);
});

it("should throw if the verification response status is 'already verified' (blockscout full matched)", async function () {
interceptVerify({
status: 0,
result: "Smart-contract already verified.",
});

await expect(
this.hre.run(TASK_VERIFY_ETHERSCAN, {
address: simpleContractAddress,
constructorArgsParams: [],
force: true,
})
).to.be.rejectedWith(
new RegExp(
`The block explorer's API responded that the contract contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress} is already verified.`
)
);
});

// If contract was actually verified, Etherscan returns an error on the verification request.
it("should throw if the verification response status is 'already verified' (etherscan manually verified)", async function () {
interceptVerify({
status: 0,
result: "Contract source code already verified",
});

await expect(
this.hre.run(TASK_VERIFY_ETHERSCAN, {
address: simpleContractAddress,
constructorArgsParams: [],
force: true,
})
).to.be.rejectedWith(
new RegExp(
`The block explorer's API responded that the contract contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress} is already verified.`
)
);
});

// If contract was verified via matching a deployed bytecode of another contract,
// Etherscan returns an error only on ve get verification status response.
it("should throw if the get verification status is 'already verified' (etherscan automatically verified)", async function () {
interceptVerify({
status: 1,
result: "ezq878u486pzijkvvmerl6a9mzwhv6sefgvqi5tkwceejc7tvn",
});
interceptGetStatus(() => {
return {
status: 0,
result: "Already Verified",
};
});
const logStub = sinon.stub(console, "log");

await expect(
this.hre.run(TASK_VERIFY_ETHERSCAN, {
address: simpleContractAddress,
constructorArgsParams: [],
force: true,
})
).to.be.rejectedWith(
new RegExp(
`The block explorer's API responded that the contract contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress} is already verified.`
)
);

expect(logStub).to.be
.calledOnceWith(`Successfully submitted source code for contract
contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress}
for verification on the block explorer. Waiting for verification result...
`);
logStub.restore();
});

after(async function () {
await this.hre.run(TASK_CLEAN);
});
});
});

describe("verify task Sourcify's integration tests", () => {
Expand Down
1 change: 1 addition & 0 deletions packages/hardhat-verify/test/unit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ describe("verify task", () => {
ConstructorLib: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
},
contractFQN: "contracts/TestContract.sol:TestContract",
force: false,
};
const processedArgs = await this.hre.run(
TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS,
Expand Down
Loading