Skip to content

Commit 4290827

Browse files
authored
Merge pull request #5164 from rimrakhimov/rimrakhimov/force-verification
Enforce contract verification
2 parents 3e32ba0 + 425dbf4 commit 4290827

File tree

7 files changed

+196
-8
lines changed

7 files changed

+196
-8
lines changed

.changeset/pink-goats-jam.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nomicfoundation/hardhat-verify": patch
3+
---
4+
5+
Added 'force' flag to allow verification of partially verified contracts (thanks @rimrakhimov!)

packages/hardhat-verify/src/index.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface VerifyTaskArgs {
5252
constructorArgs?: string;
5353
libraries?: string;
5454
contract?: string;
55+
force: boolean;
5556
listNetworks: boolean;
5657
}
5758

@@ -61,6 +62,7 @@ interface VerifySubtaskArgs {
6162
constructorArguments: string[];
6263
libraries: LibraryToAddress;
6364
contract?: string;
65+
force?: boolean;
6466
}
6567

6668
export interface VerificationResponse {
@@ -115,6 +117,11 @@ task(TASK_VERIFY, "Verifies a contract on Etherscan or Sourcify")
115117
"Fully qualified name of the contract to verify. Skips automatic detection of the contract. " +
116118
"Use if the deployed bytecode matches more than one contract in your project"
117119
)
120+
.addFlag(
121+
"force",
122+
"Enforce contract verification even if the contract is already verified. " +
123+
"Use to re-verify partially verified contracts on Blockscout"
124+
)
118125
.addFlag("listNetworks", "Print the list of supported networks")
119126
.setAction(async (taskArgs: VerifyTaskArgs, { run }) => {
120127
if (taskArgs.listNetworks) {
@@ -266,9 +273,16 @@ subtask(TASK_VERIFY_VERIFY)
266273
.addOptionalParam("constructorArguments", undefined, [], types.any)
267274
.addOptionalParam("libraries", undefined, {}, types.any)
268275
.addOptionalParam("contract")
276+
.addFlag("force")
269277
.setAction(
270278
async (
271-
{ address, constructorArguments, libraries, contract }: VerifySubtaskArgs,
279+
{
280+
address,
281+
constructorArguments,
282+
libraries,
283+
contract,
284+
force,
285+
}: VerifySubtaskArgs,
272286
{ run, config }
273287
) => {
274288
// This can only happen if the subtask is invoked from within Hardhat by a user script or another task.
@@ -286,6 +300,7 @@ subtask(TASK_VERIFY_VERIFY)
286300
constructorArgsParams: constructorArguments,
287301
libraries,
288302
contract,
303+
force,
289304
});
290305
}
291306

packages/hardhat-verify/src/internal/errors.ts

+8
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,11 @@ ${undetectableLibraries.map((x) => ` * ${x}`).join("\n")}`
458458
}`);
459459
}
460460
}
461+
462+
export class ContractAlreadyVerifiedError extends HardhatVerifyError {
463+
constructor(contractFQN: string, contractAddress: string) {
464+
super(`The block explorer's API responded that the contract ${contractFQN} at ${contractAddress} is already verified.
465+
This can happen if you used the '--force' flag. However, re-verification of contracts might not be supported
466+
by the explorer (e.g., Etherscan), or the contract may have already been verified with a full match.`);
467+
}
468+
}

packages/hardhat-verify/src/internal/etherscan.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ContractStatusPollingInvalidStatusCodeError,
1212
ContractVerificationMissingBytecodeError,
1313
ContractVerificationInvalidStatusCodeError,
14+
ContractAlreadyVerifiedError,
1415
HardhatVerifyError,
1516
MissingApiKeyError,
1617
ContractStatusPollingResponseNotOkError,
@@ -139,6 +140,7 @@ export class Etherscan {
139140
* @throws {ContractVerificationRequestError} if there is an error on the request.
140141
* @throws {ContractVerificationInvalidStatusCodeError} if the API returns an invalid status code.
141142
* @throws {ContractVerificationMissingBytecodeError} if the bytecode is not found on the block explorer.
143+
* @throws {ContractAlreadyVerifiedError} if the bytecode is already verified.
142144
* @throws {HardhatVerifyError} if the response status is not OK.
143145
*/
144146
public async verify(
@@ -184,6 +186,10 @@ export class Etherscan {
184186
);
185187
}
186188

189+
if (etherscanResponse.isAlreadyVerified()) {
190+
throw new ContractAlreadyVerifiedError(contractName, contractAddress);
191+
}
192+
187193
if (!etherscanResponse.isOk()) {
188194
throw new HardhatVerifyError(etherscanResponse.message);
189195
}
@@ -192,7 +198,8 @@ export class Etherscan {
192198
} catch (e) {
193199
if (
194200
e instanceof ContractVerificationInvalidStatusCodeError ||
195-
e instanceof ContractVerificationMissingBytecodeError
201+
e instanceof ContractVerificationMissingBytecodeError ||
202+
e instanceof ContractAlreadyVerifiedError
196203
) {
197204
throw e;
198205
}
@@ -239,7 +246,10 @@ export class Etherscan {
239246
return await this.getVerificationStatus(guid);
240247
}
241248

242-
if (etherscanResponse.isFailure()) {
249+
if (
250+
etherscanResponse.isFailure() ||
251+
etherscanResponse.isAlreadyVerified()
252+
) {
243253
return etherscanResponse;
244254
}
245255

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

309+
public isAlreadyVerified() {
310+
return (
311+
// returned by blockscout
312+
this.message.startsWith("Smart-contract already verified") ||
313+
// returned by etherscan
314+
this.message.startsWith("Contract source code already verified") ||
315+
this.message.startsWith("Already Verified")
316+
);
317+
}
318+
299319
public isOk() {
300320
return this.status === 1;
301321
}

packages/hardhat-verify/src/internal/tasks/etherscan.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
InvalidContractNameError,
2727
UnexpectedNumberOfFilesError,
2828
VerificationAPIUnexpectedMessageError,
29+
ContractAlreadyVerifiedError,
2930
} from "../errors";
3031
import { Etherscan } from "../etherscan";
3132
import { Bytecode } from "../solc/bytecode";
@@ -50,6 +51,7 @@ interface VerificationArgs {
5051
constructorArgs: string[];
5152
libraries: LibraryToAddress;
5253
contractFQN?: string;
54+
force: boolean;
5355
}
5456

5557
interface GetMinimalInputArgs {
@@ -76,12 +78,14 @@ subtask(TASK_VERIFY_ETHERSCAN)
7678
.addOptionalParam("constructorArgs")
7779
.addOptionalParam("libraries", undefined, undefined, types.any)
7880
.addOptionalParam("contract")
81+
.addFlag("force")
7982
.setAction(async (taskArgs: VerifyTaskArgs, { config, network, run }) => {
8083
const {
8184
address,
8285
constructorArgs,
8386
libraries,
8487
contractFQN,
88+
force,
8589
}: VerificationArgs = await run(
8690
TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS,
8791
taskArgs
@@ -99,9 +103,9 @@ subtask(TASK_VERIFY_ETHERSCAN)
99103
);
100104

101105
const isVerified = await etherscan.isVerified(address);
102-
if (isVerified) {
106+
if (!force && isVerified) {
103107
const contractURL = etherscan.getContractUrl(address);
104-
console.log(`The contract ${address} has already been verified on Etherscan.
108+
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.
105109
${contractURL}`);
106110
return;
107111
}
@@ -200,13 +204,15 @@ subtask(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS)
200204
.addOptionalParam("constructorArgs", undefined, undefined, types.inputFile)
201205
.addOptionalParam("libraries", undefined, undefined, types.any)
202206
.addOptionalParam("contract")
207+
.addFlag("force")
203208
.setAction(
204209
async ({
205210
address,
206211
constructorArgsParams,
207212
constructorArgs: constructorArgsModule,
208213
contract,
209214
libraries: librariesModule,
215+
force,
210216
}: VerifyTaskArgs): Promise<VerificationArgs> => {
211217
if (address === undefined) {
212218
throw new MissingAddressError();
@@ -238,6 +244,7 @@ subtask(TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS)
238244
constructorArgs,
239245
libraries,
240246
contractFQN: contract,
247+
force,
241248
};
242249
}
243250
);
@@ -294,16 +301,17 @@ subtask(TASK_VERIFY_ETHERSCAN_ATTEMPT_VERIFICATION)
294301
// Ensure the linking information is present in the compiler input;
295302
compilerInput.settings.libraries = contractInformation.libraries;
296303

304+
const contractFQN = `${contractInformation.sourceName}:${contractInformation.contractName}`;
297305
const { message: guid } = await verificationInterface.verify(
298306
address,
299307
JSON.stringify(compilerInput),
300-
`${contractInformation.sourceName}:${contractInformation.contractName}`,
308+
contractFQN,
301309
`v${contractInformation.solcLongVersion}`,
302310
encodedConstructorArguments
303311
);
304312

305313
console.log(`Successfully submitted source code for contract
306-
${contractInformation.sourceName}:${contractInformation.contractName} at ${address}
314+
${contractFQN} at ${address}
307315
for verification on the block explorer. Waiting for verification result...
308316
`);
309317

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

323+
// Etherscan answers with already verified message only when checking returned guid
324+
if (verificationStatus.isAlreadyVerified()) {
325+
throw new ContractAlreadyVerifiedError(contractFQN, address);
326+
}
327+
315328
if (!(verificationStatus.isFailure() || verificationStatus.isSuccess())) {
316329
// Reaching this point shouldn't be possible unless the API is behaving in a new way.
317330
throw new VerificationAPIUnexpectedMessageError(

packages/hardhat-verify/test/integration/index.ts

+127-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ describe("verify task integration tests", () => {
9191
});
9292

9393
expect(logStub).to.be.calledOnceWith(
94-
`The contract ${address} has already been verified on Etherscan.
94+
`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.
9595
https://hardhat.etherscan.io/address/${address}#code`
9696
);
9797
logStub.restore();
@@ -629,6 +629,132 @@ https://hardhat.etherscan.io/address/${bothLibsContractAddress}#code\n`);
629629
await this.hre.run(TASK_CLEAN);
630630
});
631631
});
632+
633+
describe("with a verified contract and '--force' flag", () => {
634+
let simpleContractAddress: string;
635+
before(async function () {
636+
await this.hre.run(TASK_COMPILE, { force: true, quiet: true });
637+
simpleContractAddress = await deployContract(
638+
"SimpleContract",
639+
[],
640+
this.hre
641+
);
642+
});
643+
644+
beforeEach(() => {
645+
interceptIsVerified({ message: "OK", result: [{ SourceCode: "code" }] });
646+
});
647+
648+
it("should validate a partially verified contract", async function () {
649+
interceptVerify({
650+
status: 1,
651+
result: "ezq878u486pzijkvvmerl6a9mzwhv6sefgvqi5tkwceejc7tvn",
652+
});
653+
interceptGetStatus(() => {
654+
return {
655+
status: 1,
656+
result: "Pass - Verified",
657+
};
658+
});
659+
const logStub = sinon.stub(console, "log");
660+
661+
const taskResponse = await this.hre.run(TASK_VERIFY, {
662+
address: simpleContractAddress,
663+
constructorArgsParams: [],
664+
force: true,
665+
});
666+
667+
assert.equal(logStub.callCount, 2);
668+
expect(logStub.getCall(0)).to.be
669+
.calledWith(`Successfully submitted source code for contract
670+
contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress}
671+
for verification on the block explorer. Waiting for verification result...
672+
`);
673+
expect(logStub.getCall(1)).to.be
674+
.calledWith(`Successfully verified contract SimpleContract on the block explorer.
675+
https://hardhat.etherscan.io/address/${simpleContractAddress}#code\n`);
676+
logStub.restore();
677+
assert.isUndefined(taskResponse);
678+
});
679+
680+
it("should throw if the verification response status is 'already verified' (blockscout full matched)", async function () {
681+
interceptVerify({
682+
status: 0,
683+
result: "Smart-contract already verified.",
684+
});
685+
686+
await expect(
687+
this.hre.run(TASK_VERIFY_ETHERSCAN, {
688+
address: simpleContractAddress,
689+
constructorArgsParams: [],
690+
force: true,
691+
})
692+
).to.be.rejectedWith(
693+
new RegExp(
694+
`The block explorer's API responded that the contract contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress} is already verified.`
695+
)
696+
);
697+
});
698+
699+
// If contract was actually verified, Etherscan returns an error on the verification request.
700+
it("should throw if the verification response status is 'already verified' (etherscan manually verified)", async function () {
701+
interceptVerify({
702+
status: 0,
703+
result: "Contract source code already verified",
704+
});
705+
706+
await expect(
707+
this.hre.run(TASK_VERIFY_ETHERSCAN, {
708+
address: simpleContractAddress,
709+
constructorArgsParams: [],
710+
force: true,
711+
})
712+
).to.be.rejectedWith(
713+
new RegExp(
714+
`The block explorer's API responded that the contract contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress} is already verified.`
715+
)
716+
);
717+
});
718+
719+
// If contract was verified via matching a deployed bytecode of another contract,
720+
// Etherscan returns an error only on ve get verification status response.
721+
it("should throw if the get verification status is 'already verified' (etherscan automatically verified)", async function () {
722+
interceptVerify({
723+
status: 1,
724+
result: "ezq878u486pzijkvvmerl6a9mzwhv6sefgvqi5tkwceejc7tvn",
725+
});
726+
interceptGetStatus(() => {
727+
return {
728+
status: 0,
729+
result: "Already Verified",
730+
};
731+
});
732+
const logStub = sinon.stub(console, "log");
733+
734+
await expect(
735+
this.hre.run(TASK_VERIFY_ETHERSCAN, {
736+
address: simpleContractAddress,
737+
constructorArgsParams: [],
738+
force: true,
739+
})
740+
).to.be.rejectedWith(
741+
new RegExp(
742+
`The block explorer's API responded that the contract contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress} is already verified.`
743+
)
744+
);
745+
746+
expect(logStub).to.be
747+
.calledOnceWith(`Successfully submitted source code for contract
748+
contracts/SimpleContract.sol:SimpleContract at ${simpleContractAddress}
749+
for verification on the block explorer. Waiting for verification result...
750+
`);
751+
logStub.restore();
752+
});
753+
754+
after(async function () {
755+
await this.hre.run(TASK_CLEAN);
756+
});
757+
});
632758
});
633759

634760
describe("verify task Sourcify's integration tests", () => {

packages/hardhat-verify/test/unit/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ describe("verify task", () => {
6767
ConstructorLib: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
6868
},
6969
contractFQN: "contracts/TestContract.sol:TestContract",
70+
force: false,
7071
};
7172
const processedArgs = await this.hre.run(
7273
TASK_VERIFY_ETHERSCAN_RESOLVE_ARGUMENTS,

0 commit comments

Comments
 (0)