Skip to content

Commit

Permalink
chore: add contract verification script
Browse files Browse the repository at this point in the history
  • Loading branch information
tamtamchik committed Sep 24, 2024
1 parent ff6a009 commit 168c959
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,6 @@ DEPLOYER=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
GENESIS_TIME=1639659600
GAS_PRIORITY_FEE=1
GAS_MAX_FEE=100

# Etherscan API key for verifying contracts
ETHERSCAN_API_KEY=
2 changes: 1 addition & 1 deletion .github/workflows/release-abis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
run: yarn compile

- name: Extract ABIs
run: yarn extract-abis
run: yarn abis:extract

- name: Create ABIs archive
run: zip -j ABIs.zip lib/abi/*.json
Expand Down
3 changes: 3 additions & 0 deletions globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,8 @@ declare namespace NodeJS {
MAINNET_VALIDATORS_EXIT_BUS_ORACLE_ADDRESS?: string;
MAINNET_WITHDRAWAL_QUEUE_ADDRESS?: string;
MAINNET_WITHDRAWAL_VAULT_ADDRESS?: string;

/* for contract verification */
ETHERSCAN_API_KEY?: string;
}
}
9 changes: 8 additions & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import "hardhat-ignore-warnings";
import "hardhat-contract-sizer";
import { globSync } from "glob";
import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from "hardhat/builtin-tasks/task-names";
import { HardhatUserConfig, subtask } from "hardhat/config";
import { HardhatUserConfig, subtask, task } from "hardhat/config";

import { mochaRootHooks } from "test/hooks";

import { verifyDeployedContracts } from "./tasks";

const RPC_URL: string = process.env.RPC_URL || "";
const MAINNET_FORKING_URL = process.env.MAINNET_FORKING_URL || "";
const INTEGRATION_SCRATCH_DEPLOY = process.env.INTEGRATION_SCRATCH_DEPLOY || "off";
Expand Down Expand Up @@ -75,6 +77,9 @@ const config: HardhatUserConfig = {
accounts: loadAccounts("sepolia"),
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY || "",
},
solidity: {
compilers: [
{
Expand Down Expand Up @@ -189,4 +194,6 @@ subtask(TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS).setAction(async (_, hre, runSupe
return [...paths, ...otherPaths];
});

task("verify:deployed", "Verifies deployed contracts based on state file").setAction(verifyDeployedContracts);

export default config;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"test:integration:fork:mainnet": "hardhat test test/integration/**/*.ts --network mainnet-fork --bail",
"typecheck": "tsc --noEmit",
"prepare": "husky",
"extract-abis": "ts-node scripts/utils/extract-abi.ts"
"abis:extract": "ts-node scripts/utils/extract-abi.ts",
"verify:deployed": "hardhat verify:deployed"
},
"lint-staged": {
"./**/*.ts": [
Expand Down
1 change: 1 addition & 0 deletions tasks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./verify-contracts";
176 changes: 176 additions & 0 deletions tasks/verify-contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import fs from "node:fs/promises";
import path from "node:path";

import { HardhatRuntimeEnvironment } from "hardhat/types";

import { cy, log, yl } from "lib/log";

type DeployedContract = {
contract: string;
address: string;
constructorArgs: unknown[];
};

type ProxyContract = {
proxy: DeployedContract;
implementation: DeployedContract;
};

type Contract = DeployedContract | ProxyContract;

type NetworkState = {
deployer: string;
[key: string]: Contract | string | number;
};

const MAX_RETRY_ATTEMPTS = 5;
const RETRY_DELAY_MS = 1000;

export async function verifyDeployedContracts(_: unknown, hre: HardhatRuntimeEnvironment) {
try {
await verifyContracts(hre);
} catch (error) {
log.error("Error verifying deployed contracts:", error as Error);
throw error;
}
}

async function verifyContracts(hre: HardhatRuntimeEnvironment) {
const network = hre.network.name;
log("Verifying contracts for network:", network);

const deployerAddress = await getDeployerAddress(hre);
log("Deployer address:", deployerAddress);
log.emptyLine();

const networkState = await getNetworkState(network, deployerAddress);
const deployedContracts = getDeployedContracts(networkState);
await verifyContractList(deployedContracts, hre);
}

async function getDeployerAddress(hre: HardhatRuntimeEnvironment): Promise<string> {
const deployer = await hre.ethers.provider.getSigner();
if (!deployer) {
throw new Error("Deployer not found!");
}
return deployer.address;
}

async function getNetworkState(network: string, deployerAddress: string): Promise<NetworkState> {
const networkStateFile = `deployed-${network}.json`;
const networkStateFilePath = path.resolve("./", networkStateFile);
const data = await fs.readFile(networkStateFilePath, "utf8");
const networkState = JSON.parse(data) as NetworkState;

if (networkState.deployer !== deployerAddress) {
throw new Error(`Deployer address mismatch: ${networkState.deployer} != ${deployerAddress}`);
}

return networkState;
}

function getDeployedContracts(networkState: NetworkState): DeployedContract[] {
return Object.values(networkState)
.filter((contract): contract is Contract => typeof contract === "object")
.flatMap(getDeployedContract);
}

async function verifyContractList(contracts: DeployedContract[], hre: HardhatRuntimeEnvironment) {
for (const contract of contracts) {
await verifyContract(contract, hre);
}
}

async function verifyContract(contract: DeployedContract, hre: HardhatRuntimeEnvironment) {
try {
if (await isContractVerified(contract.address, hre)) {
log.success(`Contract ${yl(contract.contract)} at ${cy(contract.address)} is already verified!`);
return;
}

const verificationParams = buildVerificationParams(contract);
log.withArguments(
`Verifying contract: ${yl(contract.contract)} at ${cy(contract.address)} with constructor args `,
verificationParams.constructorArguments as string[],
);

await hre.run("verify:verify", verificationParams);
log.success(`Successfully verified ${yl(contract.contract)}!`);
} catch (error) {
log.error(`Failed to verify ${yl(contract.contract)}:`, error as Error);
}
log.emptyLine();
}

function buildVerificationParams(contract: DeployedContract) {
const contractName = contract.contract.split("/").pop()?.split(".")[0];
return {
address: contract.address,
constructorArguments: contract.constructorArgs,
contract: `${contract.contract}:${contractName}`,
};
}

function getDeployedContract(contract: Contract): DeployedContract[] {
if ("proxy" in contract && "implementation" in contract) {
return [contract.proxy, contract.implementation];
} else if ("contract" in contract && "address" in contract && "constructorArgs" in contract) {
return [contract];
}
return [];
}

async function isContractVerified(address: string, hre: HardhatRuntimeEnvironment): Promise<boolean> {
try {
const apiURL = getEtherscanApiUrl(hre.network.name);
const etherscanApiKey = getEtherscanApiKey(hre);
const params = buildApiParams(address, etherscanApiKey);
const result = await fetchContractSourceCode(apiURL, params);
return isSourceCodeVerified(result);
} catch (error) {
console.error(`Failed to check verification status for contract ${address}:`, error);
return false;
}
}

function getEtherscanApiUrl(network: string): string {
return `https://api${network !== "mainnet" ? `-${network}` : ""}.etherscan.io/api`;
}

function getEtherscanApiKey(hre: HardhatRuntimeEnvironment): string {
return hre.config.etherscan.apiKey as string;
}

function buildApiParams(address: string, apiKey: string): URLSearchParams {
return new URLSearchParams({
module: "contract",
action: "getsourcecode",
address,
apikey: apiKey,
});
}

async function fetchContractSourceCode(apiURL: string, params: URLSearchParams) {
return await retryFetch(`${apiURL}?${params}`, MAX_RETRY_ATTEMPTS, RETRY_DELAY_MS);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isSourceCodeVerified(result: any): boolean {
return result.status === "1" && result.result.length > 0 && result.result[0].SourceCode !== "";
}

async function retryFetch(url: string, maxRetries: number, retryDelay: number) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url);
const data = await response.json();

if (data.status === "1" && data.result.length > 0) return data;

if (data.message === "NOTOK" && data.result.includes("rate limit")) {
await new Promise((resolve) => setTimeout(resolve, retryDelay));
} else {
return data;
}
}
throw new Error(`Max retries reached. Unable to fetch.`);
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
"typechain-types": ["./typechain-types"]
}
},
"include": ["./test", "./typechain-types", "./lib", "./scripts", "./deployed-*.json"],
"include": ["./test", "./typechain-types", "./lib", "./scripts", "./tasks", "./deployed-*.json"],
"files": ["./hardhat.config.ts", "./commitlint.config.ts", "./globals.d.ts"]
}

0 comments on commit 168c959

Please sign in to comment.