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

feat: add etherscan as verification interface #1518

Merged
merged 5 commits into from
Oct 31, 2024
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
24 changes: 17 additions & 7 deletions packages/hardhat-zksync-upgradable/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,26 @@ subtask('verify:etherscan').setAction(async (args, hre, runSuper) => {
return await verify(args, hre, runSuper);
}

throw new ZkSyncUpgradablePluginError(
'This task is only available for zkSync network, use `verify:verify` instead',
);
const { verify } = await import('./verify/verify-proxy');
return await verify(args, hre, runSuper);
});

subtask('verify:verify').setAction(async (args, hre, runSuper) => {
subtask('verify:zksync-etherscan').setAction(async (args, hre, runSuper) => {
if (!hre.network.zksync) {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { verify } = await import('@openzeppelin/hardhat-upgrades/dist/verify-proxy');
return await verify(args, hre, runSuper);
throw new ZkSyncUpgradablePluginError(
'This task is only available for zkSync network, use `verify:verify` instead',
);
}

const { verify } = await import('./verify/verify-proxy');
return await verify(args, hre, runSuper);
});

subtask('verify:zksync-blockexplorer').setAction(async (args, hre, runSuper) => {
if (!hre.network.zksync) {
throw new ZkSyncUpgradablePluginError(
'This task is only available for zkSync network, use `verify:verify` instead',
);
}

const { verify } = await import('./verify/verify-proxy');
Expand Down
28 changes: 17 additions & 11 deletions packages/hardhat-zksync-verify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ Import the plugin in the hardhat.config.ts file:

`import "@matterlabs/hardhat-zksync-verify";`

Add the verifyURL property to the ZKsync Era network in the hardhat.config.ts file as shown below:

```
networks: {
sepolia: {
Expand All @@ -42,20 +40,34 @@ networks: {
url: "https://sepolia.era.zksync.dev", // The testnet RPC URL of ZKsync Era network.
ethNetwork: "sepolia", // The Ethereum Web3 RPC URL, or the identifier of the network (e.g. `mainnet` or `sepolia`)
zksync: true,
// Verification endpoint for Sepolia
verifyURL: 'https://explorer.sepolia.era.zksync.dev/contract_verification'
}
},

```
### Updates introduced in plugin version 1.7.0.

Etherscan verification is supported. To enable it, configure the etherscan property in the Hardhat configuration:

```
etherscan: {
apiKey: 'APIKEY'
}
```
If the etherscan property is configured and enabled, verification will run on Etherscan. Otherwise, the plugin will default to verifying on the ZKsync block explorer.

For more information on how to create api keys, please [visit the documentation](https://docs.zksync.network/getting-started/viewing-api-usage-statistics).
For more information on how to configre etherscan for multiple api keys, please [visit the documentation](https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify#multiple-api-keys-and-alternative-block-explorers)

| 🔧 properties | 📄 Description |
|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| zkTestnet | Arbitrary ZKsync Era network name. You can select this as the default network using the defaultNetwork property. |
| url | Field is required for all ZKsync Era and Ethereum networks used by this plugin. For ZKsync network, set it to true |
| ethNetwork | Field with the URL of the Ethereum node. |
| ethers | Provider for the network if the configuration is not provided. This field is required for all ZKsync networks used by this plugin. |
| zksync | Flag that indicates a ZKsync Era network configuration. This field is set to true for all ZKsync Era networks. |
| verifyURL | Field that points to the verification endpoint for the specific ZKsync network. This parameter is optional. |
| verifyURL | This field specifies the verification endpoint for the connected ZKsync network. From version 1.7.0, the plugin automatically resolves this endpoint based on the network configuration. If you are using a custom chain with an API compatible with the zksync block explorer, you can manually set the URL here. |
| browserVerifyURL | Introduced in version 1.7.0 of the plugin, this field automatically resolves the browser URL based on the network configuration. If you're using a custom chain, you can manually specify the URL here. |
| enableVerifyURL | Introduced in version 1.7.0 of the plugin, this flag forces verification on the ZKsync block explorer. It allows you to verify the same contract on both Etherscan and the ZKsync block explorer. |

Default values for verifyURL are:

Expand All @@ -73,12 +85,6 @@ When executed in this manner, the verification task attempts to compare the comp

With the --contract parameter you can also specify which contract from your local setup you want to verify by specifying its Fully qualified name. Fully qualified name structure looks like this: "contracts/AContract.sol:TheContract"

The following command checks the status of the verification request for the specific verification ID:

`yarn hardhat verify-status --verification-id <your verification id>`



**Constructor arguments**

If your contract was deployed with the specific constructor arguments, you need to specify them when running the verify task. For example:
Expand Down
15 changes: 10 additions & 5 deletions packages/hardhat-zksync-verify/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ export const TASK_VERIFY = 'verify';
export const TASK_VERIFY_VERIFY = 'verify:verify';
export const TASK_VERIFY_CONTRACT = 'zk:verify:contract';
export const TASK_CHECK_VERIFICATION_STATUS = 'verify-status';
export const TASK_VERIFY_ETHERSCAN = 'verify:etherscan';

export const TASK_VERIFY_ZKSYNC_EXPLORER = 'verify:zksync-blockexplorer';
export const TASK_VERIFY_ZKSYNC_ETHERSCAN = 'verify:zksync-etherscan';

export const TASK_VERIFY_RESOLVE_ARGUMENTS = 'verify:resolve-arguments';
export const TASK_VERIFY_GET_VERIFICATION_SUBTASKS = 'verify:get-verification-subtasks';

export const TASK_VERIFY_GET_CONSTRUCTOR_ARGUMENTS = 'verify:get-constructor-arguments';
export const TASK_VERIFY_GET_LIBRARIES = 'verify:get-libraries';
Expand Down Expand Up @@ -93,13 +100,11 @@ export const COMPILER_VERSION_NOT_SUPPORTED =

export const WRONG_CONSTRUCTOR_ARGUMENTS = 'types/values length mismatch';

export const PENDING_CONTRACT_INFORMATION_MESSAGE = (verificationId: number) => `
export const PENDING_CONTRACT_INFORMATION_MESSAGE = (browserUrl?: string) => `
Your verification request has been sent, but our servers are currently overloaded and we could not confirm that the verification was successful.
Please try one of the following options:
1. Use the your verification request ID (${verificationId}) to check the status of the pending verification process by typing the command 'yarn hardhat verify-status --verification-id ${verificationId}'
2. Manually check the contract's code on the zksync block explorer: https://explorer.zksync.io/
3. Run the verification process again
`;
1. Manually check the contract's code on the explorer ${browserUrl ? `: ${browserUrl}` : '.'}
2. Run the verification process again`;

export const SINGLE_FILE_CODE_FORMAT = 'solidity-single-file';
export const JSON_INPUT_CODE_FORMAT = 'solidity-standard-json-input';
Expand Down
8 changes: 8 additions & 0 deletions packages/hardhat-zksync-verify/src/explorers/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const TRYING_VERIFICATION_WITH_FULL_COMPILER_INPUT = (
contractName: string,
) => `We tried verifying your contract ${contractName} without including any unrelated one, but it failed.
Trying again with the full solc input used to compile and deploy it.
This means that unrelated contracts may be displayed on the explorer.`;

export const PROVIDED_CHAIN_IS_NOT_SUPPORTED_FOR_VERIFICATION = (chainId: number) =>
`The provided chain with id ${chainId} is not supported by default!`;
9 changes: 9 additions & 0 deletions packages/hardhat-zksync-verify/src/explorers/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ZkSyncVerifyPluginError } from '../errors';

export class ZksyncContractVerificationInvalidStatusCodeError extends ZkSyncVerifyPluginError {
constructor(url: string, statusCode: number, responseText: string) {
super(`Failed to send contract verification request.
Endpoint URL: ${url}
The HTTP server response is not ok. Status code: ${statusCode} Response text: ${responseText}`);
}
}
196 changes: 196 additions & 0 deletions packages/hardhat-zksync-verify/src/explorers/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { EthereumProvider, HardhatRuntimeEnvironment } from 'hardhat/types';
import chalk from 'chalk';
import { ChainConfig } from '@nomicfoundation/hardhat-verify/types';
import { delay, encodeArguments, nextAttemptDelay, retrieveContractBytecode } from '../utils';
import { ZkSyncVerifyPluginError } from '../errors';
import { Bytecode } from '../solc/bytecode';
import {
COMPILER_VERSION_NOT_SUPPORTED,
CONST_ARGS_ARRAY_ERROR,
JSON_INPUT_CODE_FORMAT,
PENDING_CONTRACT_INFORMATION_MESSAGE,
TASK_COMPILE,
TASK_VERIFY_GET_COMPILER_VERSIONS,
TASK_VERIFY_GET_CONTRACT_INFORMATION,
} from '../constants';
import { ContractInformation } from '../solc/types';
import { getMinimalResolvedFiles, getSolidityStandardJsonInput } from '../plugin';
import { ZkSyncEtherscanExplorerVerifyRequest, ZkSyncExplorerVerifyRequest } from './verify-contract-request';
import { VerificationStatusResponse } from './verification-status-response';
import { PROVIDED_CHAIN_IS_NOT_SUPPORTED_FOR_VERIFICATION } from './constants';

export type VerificationServiceVerificationIdReturnType = string | number;
export type VerificationServiceVerifyRequest = ZkSyncExplorerVerifyRequest | ZkSyncEtherscanExplorerVerifyRequest;
export type VerificationServiceInitialVerifyRequest = ZkSyncExplorerVerifyRequest;
export type VerificationServiceVerificationStatus = VerificationStatusResponse;
export interface VerificationServiceVerifyResponse<
V extends VerificationServiceVerificationIdReturnType = VerificationServiceVerificationIdReturnType,
> {
verificationId: V;
contractVerifyDataInfo: ContractVerifyDataInfo;
}

export interface ContractVerifyDataInfo {
contractName: string;
contractAddress: string;
}

export abstract class VerificationService<
ReturnVerificationIdType extends
VerificationServiceVerificationIdReturnType = VerificationServiceVerificationIdReturnType,
ContractVerifyRequestType extends VerificationServiceVerifyRequest = VerificationServiceVerifyRequest,
VerificationStatusType extends VerificationServiceVerificationStatus = VerificationServiceVerificationStatus,
VerificationServiceVerifyResponseType = VerificationServiceVerifyResponse<ReturnVerificationIdType>,
> {
constructor(
protected hre: HardhatRuntimeEnvironment,
protected verifyUrl: string,
protected browserUrl?: string,
) {}
protected abstract generateRequest(
initialRequest: VerificationServiceInitialVerifyRequest,
): ContractVerifyRequestType;
protected abstract getVerificationId(
initialRequest: VerificationServiceInitialVerifyRequest,
): Promise<ReturnVerificationIdType>;
public abstract getVerificationStatus(
verificationId: ReturnVerificationIdType,
contractVerifyDataInfo: ContractVerifyDataInfo,
): Promise<VerificationStatusType>;
protected abstract getSupportedCompilerVersions(): Promise<string[]>;
protected abstract getSolcVersion(contractInformation: ContractInformation): Promise<string>;
protected abstract getContractBorwserUrl(address: string): string | undefined;

public static async getCurrentChainConfig(
ethereumProvider: EthereumProvider,
customChains: ChainConfig[],
builtinChains: ChainConfig[],
): Promise<ChainConfig> {
const currentChainId = parseInt(await ethereumProvider.send('eth_chainId'), 16);

const currentChainConfig = [...[...customChains].reverse(), ...builtinChains].find(
({ chainId }) => chainId === currentChainId,
);

if (currentChainConfig === undefined) {
throw new ZkSyncVerifyPluginError(PROVIDED_CHAIN_IS_NOT_SUPPORTED_FOR_VERIFICATION(currentChainId));
}

return currentChainConfig;
}

public async verify(
address: string,
contract: string,
constructorArguments: any,
libraries: any,
noCompile: boolean,
isWithFullContext: boolean = false,
): Promise<VerificationServiceVerifyResponseType> {
const { isAddress } = await import('@ethersproject/address');
if (!isAddress(address)) {
throw new ZkSyncVerifyPluginError(`${address} is an invalid address.`);
}

const deployedBytecodeHex = await retrieveContractBytecode(address, this.hre);
const deployedBytecode = new Bytecode(deployedBytecodeHex);

if (!noCompile) {
await this.hre.run(TASK_COMPILE, { quiet: true });
}

const compilerVersions: string[] = await this.hre.run(TASK_VERIFY_GET_COMPILER_VERSIONS);

const contractInformation: ContractInformation = await this.hre.run(TASK_VERIFY_GET_CONTRACT_INFORMATION, {
contract,
deployedBytecode,
matchingCompilerVersions: compilerVersions,
libraries,
});

const optimizationUsed = contractInformation.compilerInput.settings.optimizer.enabled ?? false;

let deployArgumentsEncoded;
if (!Array.isArray(constructorArguments)) {
if (constructorArguments.startsWith('0x')) {
deployArgumentsEncoded = constructorArguments;
} else {
throw new ZkSyncVerifyPluginError(chalk.red(CONST_ARGS_ARRAY_ERROR));
}
} else {
deployArgumentsEncoded = `0x${await encodeArguments(
contractInformation.contractOutput.abi,
constructorArguments,
)}`;
}

const compilerPossibleVersions = await this.getSupportedCompilerVersions();
const compilerVersion: string = contractInformation.solcVersion;
if (!compilerPossibleVersions.includes(compilerVersion)) {
throw new ZkSyncVerifyPluginError(COMPILER_VERSION_NOT_SUPPORTED);
}

const request: VerificationServiceInitialVerifyRequest = {
contractAddress: address,
sourceCode: getSolidityStandardJsonInput(
this.hre,
await getMinimalResolvedFiles(this.hre, contractInformation.sourceName),
contractInformation.compilerInput,
),
codeFormat: JSON_INPUT_CODE_FORMAT,
contractName: `${contractInformation.sourceName}:${contractInformation.contractName}`,
compilerSolcVersion: await this.getSolcVersion(contractInformation),
compilerZksolcVersion: `v${contractInformation.contractOutput.metadata.zk_version}`,
constructorArguments: deployArgumentsEncoded,
optimizationUsed,
};
if (isWithFullContext) {
request.sourceCode.sources = contractInformation.compilerInput.sources;
}

const verificationId = await this.getVerificationId(request);

console.info(chalk.cyan(`Your verification ID is: ${verificationId}`));

return {
contractVerifyDataInfo: {
contractName: request.contractName,
contractAddress: request.contractAddress,
},
verificationId,
} as VerificationServiceVerifyResponseType;
}

public async getVerificationStatusWithRetry(
verificationId: ReturnVerificationIdType,
contractVerifyDataInfo: ContractVerifyDataInfo,
maxRetries = 11,
baseRetries = 5,
baseDelayInMs = 2000,
): Promise<VerificationStatusType> {
let retries = 0;
let response: VerificationStatusType;

while (true) {
response = await this.getVerificationStatus(verificationId, contractVerifyDataInfo);

if (response.isPending()) {
retries += 1;
if (retries > maxRetries) {
throw new ZkSyncVerifyPluginError(PENDING_CONTRACT_INFORMATION_MESSAGE(this.browserUrl));
}

const delayInMs = nextAttemptDelay(retries, baseDelayInMs, baseRetries);
await delay(delayInMs);
} else {
break;
}
}

return response;
}
}

export interface VerificationServiceVerificationIdResponse {
isOk(): boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface VerificationStatusResponse {
isPending(): boolean;
isFailure(): boolean;
isSuccess(): boolean;
getError(): string | undefined;
errorExists(): boolean;
isOk(): boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CompilerInput } from 'hardhat/types';

export interface ZkSyncExplorerVerifyRequest {
contractAddress: string;
contractName: string;
sourceCode: CompilerInput;
codeFormat: string;
compilerSolcVersion: string;
compilerZksolcVersion: string;
optimizationUsed: boolean;
constructorArguments: string;
}

export interface ZkSyncEtherscanExplorerVerifyRequest {
action: 'verifysourcecode';
module: 'contract';
apikey: string;
compilermode: 'zksync';
zksolcVersion: string;
contractaddress: string;
contractname: string;
sourceCode: CompilerInput;
codeformat: string;
compilerversion: string;
optimizationUsed?: string;
constructorArguements: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ChainConfig } from '@nomicfoundation/hardhat-verify/types';

export const builtinChains: ChainConfig[] = [
{
network: 'zksyncmainnet',
chainId: 324,
urls: {
apiURL: 'https://zksync2-mainnet-explorer.zksync.io/contract_verification',
browserURL: 'https://explorer.zksync.io/',
},
},
{
network: 'zksyncsepolia',
chainId: 300,
urls: {
apiURL: 'https://explorer.sepolia.era.zksync.dev/contract_verification',
browserURL: 'https://sepolia.explorer.zksync.io/',
},
},
];
Loading
Loading