From 00cb623dd17c29cb102080a3f5f34d5ea38e3a42 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Fri, 12 Apr 2024 17:33:45 -0400 Subject: [PATCH 1/2] verify ClrFund contract by address --- .../tasks/helpers/ConstructorArguments.ts | 339 ++++++++++++++++ contracts/tasks/helpers/ContractStorage.ts | 20 + contracts/tasks/helpers/ContractVerifier.ts | 6 +- contracts/tasks/index.ts | 1 + contracts/tasks/runners/newClrFund.ts | 12 +- contracts/tasks/runners/newDeployer.ts | 11 +- contracts/tasks/runners/setToken.ts | 8 +- contracts/tasks/runners/verifyAll.ts | 368 ++++++++++++++---- contracts/tasks/runners/verifyDeployer.ts | 34 ++ contracts/tests/maciFactory.ts | 2 +- contracts/utils/testutils.ts | 2 +- contracts/utils/types.ts | 2 + 12 files changed, 715 insertions(+), 90 deletions(-) create mode 100644 contracts/tasks/helpers/ConstructorArguments.ts create mode 100644 contracts/tasks/runners/verifyDeployer.ts diff --git a/contracts/tasks/helpers/ConstructorArguments.ts b/contracts/tasks/helpers/ConstructorArguments.ts new file mode 100644 index 000000000..6f9f3d5b4 --- /dev/null +++ b/contracts/tasks/helpers/ConstructorArguments.ts @@ -0,0 +1,339 @@ +import type { HardhatRuntimeEnvironment } from 'hardhat/types' +import { BaseContract, Interface } from 'ethers' +import { ContractStorage } from './ContractStorage' +import { EContracts } from './types' +import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' +import { + BrightIdUserRegistry, + ClrFundDeployer, + MACIFactory, + MessageProcessor, + OptimisticRecipientRegistry, + Poll, + Tally, +} from '../../typechain-types' + +/** A list of functions to get contract constructor arguments from the contract */ +const ConstructorArgumentsGetters: Record< + string, + (address: string, ethers: HardhatEthersHelpers) => Promise> +> = { + [EContracts.FundingRound]: getFundingRoundConstructorArguments, + [EContracts.MACI]: getMaciConstructorArguments, + [EContracts.Poll]: getPollConstructorArguments, + [EContracts.Tally]: getTallyConstructorArguments, + [EContracts.MessageProcessor]: getMessageProcessorConstructorArguments, + [EContracts.BrightIdUserRegistry]: + getBrightIdUserRegistryConstructorArguments, + [EContracts.OptimisticRecipientRegistry]: + getOptimisticRecipientRegistryConstructorArguments, + [EContracts.ClrFundDeployer]: getClrFundDeployerConstructorArguments, + [EContracts.MACIFactory]: getMACIFactoryConstructorArguments, +} + +/** + * Get the constructor arguments for FundingRound + * @param address The funding round contract address + * @param ethers The Hardhat Ethers helper + * @returns The funding round constructor arguments + */ +async function getFundingRoundConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const round = await ethers.getContractAt(EContracts.FundingRound, address) + + const args = await Promise.all([ + round.nativeToken(), + round.userRegistry(), + round.recipientRegistry(), + round.coordinator(), + ]) + + return args +} + +/** + * Get the constructor arguments for MACI + * @param address The MACI contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getMaciConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const maci = await ethers.getContractAt(EContracts.MACI, address) + + const args = await Promise.all([ + maci.pollFactory(), + maci.messageProcessorFactory(), + maci.tallyFactory(), + maci.subsidyFactory(), + maci.signUpGatekeeper(), + maci.initialVoiceCreditProxy(), + maci.topupCredit(), + maci.stateTreeDepth(), + ]) + + return args +} + +/** + * Get the constructor arguments for Poll + * @param address The Poll contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getPollConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const pollContract = (await ethers.getContractAt( + EContracts.Poll, + address + )) as BaseContract as Poll + + const [, duration] = await pollContract.getDeployTimeAndDuration() + const [maxValues, treeDepths, coordinatorPubKey, extContracts] = + await Promise.all([ + pollContract.maxValues(), + pollContract.treeDepths(), + pollContract.coordinatorPubKey(), + pollContract.extContracts(), + ]) + + const args = [ + duration, + { + maxMessages: maxValues.maxMessages, + maxVoteOptions: maxValues.maxVoteOptions, + }, + { + intStateTreeDepth: treeDepths.intStateTreeDepth, + messageTreeSubDepth: treeDepths.messageTreeSubDepth, + messageTreeDepth: treeDepths.messageTreeDepth, + voteOptionTreeDepth: treeDepths.voteOptionTreeDepth, + }, + { + x: coordinatorPubKey.x, + y: coordinatorPubKey.y, + }, + { + maci: extContracts.maci, + messageAq: extContracts.messageAq, + topupCredit: extContracts.topupCredit, + }, + ] + + return args +} + +/** + * Get the constructor arguments for Tally + * @param address The Tally contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getTallyConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const tallyContract = (await ethers.getContractAt( + EContracts.Tally, + address + )) as BaseContract as Tally + + const args = await Promise.all([ + tallyContract.verifier(), + tallyContract.vkRegistry(), + tallyContract.poll(), + tallyContract.messageProcessor(), + ]) + + return args +} + +/** + * Get the constructor arguments for MessageProcessor + * @param address The MessageProcessor contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getMessageProcessorConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const messageProcesor = (await ethers.getContractAt( + EContracts.MessageProcessor, + address + )) as BaseContract as MessageProcessor + + const args = await Promise.all([ + messageProcesor.verifier(), + messageProcesor.vkRegistry(), + messageProcesor.poll(), + ]) + + return args +} + +/** + * Get the constructor arguments for BrightIdUserRegistry + * @param address The BrightIdUserRegistry contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getBrightIdUserRegistryConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const registry = (await ethers.getContractAt( + EContracts.BrightIdUserRegistry, + address + )) as BaseContract as BrightIdUserRegistry + + const args = await Promise.all([ + registry.context(), + registry.verifier(), + registry.brightIdSponsor(), + ]) + + return args +} + +/** + * Get the constructor arguments for OptimisticRecipientRegistry + * @param address The OptimisticRecipientRegistry contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getOptimisticRecipientRegistryConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const registry = (await ethers.getContractAt( + EContracts.OptimisticRecipientRegistry, + address + )) as BaseContract as OptimisticRecipientRegistry + + const args = await Promise.all([ + registry.baseDeposit(), + registry.challengePeriodDuration(), + registry.controller(), + ]) + + return args +} + +/** + * Get the constructor arguments for ClrFundDeployer + * @param address The ClrFundDeployer contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getClrFundDeployerConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const registry = (await ethers.getContractAt( + EContracts.ClrFundDeployer, + address + )) as BaseContract as ClrFundDeployer + + const args = await Promise.all([ + registry.clrfundTemplate(), + registry.maciFactory(), + registry.roundFactory(), + ]) + + return args +} + +/** + * Get the constructor arguments for MACIFactory + * @param address The MACIFactory contract address + * @param ethers The Hardhat Ethers helper + * @returns The constructor arguments + */ +async function getMACIFactoryConstructorArguments( + address: string, + ethers: HardhatEthersHelpers +): Promise> { + const registry = (await ethers.getContractAt( + EContracts.MACIFactory, + address + )) as BaseContract as MACIFactory + + const args = await Promise.all([ + registry.vkRegistry(), + registry.factories(), + registry.verifier(), + ]) + + return args +} + +/** + * @notice A helper to retrieve contract constructor arguments + */ +export class ConstructorArguments { + /** + * Hardhat runtime environment + */ + private hre: HardhatRuntimeEnvironment + + /** + * Local contract deployment information + */ + private storage: ContractStorage + + /** + * Initialize class properties + * + * @param hre - Hardhat runtime environment + */ + constructor(hre: HardhatRuntimeEnvironment) { + this.hre = hre + this.storage = ContractStorage.getInstance() + } + + /** + * Get the contract constructor arguments + * @param name - contract name + * @param address - contract address + * @param ethers = Hardhat Ethers helper + * @returns - stringified constructor arguments + */ + async get( + name: string, + address: string, + ethers: HardhatEthersHelpers + ): Promise> { + const contractArtifact = this.hre.artifacts.readArtifactSync(name) + const contractInterface = new Interface(contractArtifact.abi) + if (contractInterface.deploy.inputs.length === 0) { + // no argument + return [] + } + + // try to get arguments from deployed-contract.json file + const constructorArguments = this.storage.getConstructorArguments( + address, + this.hre.network.name + ) + if (constructorArguments) { + return constructorArguments + } + + // try to get custom constructor arguments from contract + let args: Array = [] + + const getConstructorArguments = ConstructorArgumentsGetters[name] + if (getConstructorArguments) { + args = await getConstructorArguments(address, ethers) + } + + return args + } +} diff --git a/contracts/tasks/helpers/ContractStorage.ts b/contracts/tasks/helpers/ContractStorage.ts index 9c092bd24..56db46e6e 100644 --- a/contracts/tasks/helpers/ContractStorage.ts +++ b/contracts/tasks/helpers/ContractStorage.ts @@ -204,6 +204,26 @@ export class ContractStorage { return instance?.txHash } + /** + * Get contract constructor argument by address from the json file + * + * @param address - contract address + * @param network - selected network + * @returns contract constructor arguments + */ + getConstructorArguments( + address: string, + network: string + ): Array | undefined { + if (!this.db[network]) { + return undefined + } + + const instance = this.db[network].instance?.[address] + const args = instance?.verify?.args + return args ? JSON.parse(args) : undefined + } + /** * Get contract address by name from the json file * diff --git a/contracts/tasks/helpers/ContractVerifier.ts b/contracts/tasks/helpers/ContractVerifier.ts index 6d71952bc..3a517373f 100644 --- a/contracts/tasks/helpers/ContractVerifier.ts +++ b/contracts/tasks/helpers/ContractVerifier.ts @@ -31,13 +31,13 @@ export class ContractVerifier { */ async verify( address: string, - constructorArguments: string, + constructorArguments: unknown[], libraries?: string, contract?: string ): Promise<[boolean, string]> { const params: IVerificationSubtaskArgs = { address, - constructorArguments: JSON.parse(constructorArguments) as unknown[], + constructorArguments, contract, } @@ -50,7 +50,7 @@ export class ContractVerifier { .run('verify:verify', params) .then(() => '') .catch((err: Error) => { - if (err.message === 'Contract source code already verified') { + if (err.message && err.message.match(/already verified/i)) { return '' } diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 5bcc7e4fc..297320679 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -23,3 +23,4 @@ import './runners/addRecipients' import './runners/findStorageSlot' import './runners/verifyTallyFile' import './runners/verifyAll' +import './runners/verifyDeployer' diff --git a/contracts/tasks/runners/newClrFund.ts b/contracts/tasks/runners/newClrFund.ts index 31ff081a0..9986ca223 100644 --- a/contracts/tasks/runners/newClrFund.ts +++ b/contracts/tasks/runners/newClrFund.ts @@ -13,9 +13,9 @@ * where `nonce too low` errors occur occasionally */ import { task, types } from 'hardhat/config' - +import { ContractStorage } from '../helpers/ContractStorage' import { Subtask } from '../helpers/Subtask' -import { type ISubtaskParams } from '../helpers/types' +import { EContracts, type ISubtaskParams } from '../helpers/types' task('new-clrfund', 'Deploy a new instance of ClrFund') .addFlag('incremental', 'Incremental deployment') @@ -26,6 +26,7 @@ task('new-clrfund', 'Deploy a new instance of ClrFund') .setAction(async (params: ISubtaskParams, hre) => { const { verify, manageNonce } = params const subtask = Subtask.getInstance(hre) + const storage = ContractStorage.getInstance() subtask.setHre(hre) const deployer = await subtask.getDeployer() @@ -62,7 +63,10 @@ task('new-clrfund', 'Deploy a new instance of ClrFund') await subtask.finish(success) if (verify) { - console.log('Verify all contracts') - await hre.run('verify-all') + const clrfund = storage.getAddress(EContracts.ClrFund, hre.network.name) + if (clrfund) { + console.log('Verify all contracts') + await hre.run('verify-all', { clrfund }) + } } }) diff --git a/contracts/tasks/runners/newDeployer.ts b/contracts/tasks/runners/newDeployer.ts index 14157d4f8..e0023e9b2 100644 --- a/contracts/tasks/runners/newDeployer.ts +++ b/contracts/tasks/runners/newDeployer.ts @@ -15,7 +15,8 @@ import { task, types } from 'hardhat/config' import { Subtask } from '../helpers/Subtask' -import { type ISubtaskParams } from '../helpers/types' +import { ContractStorage } from '../helpers/ContractStorage' +import { EContracts, type ISubtaskParams } from '../helpers/types' task('new-deployer', 'Deploy a new instance of ClrFund') .addFlag('incremental', 'Incremental deployment') @@ -26,6 +27,7 @@ task('new-deployer', 'Deploy a new instance of ClrFund') .setAction(async (params: ISubtaskParams, hre) => { const { verify, manageNonce } = params const subtask = Subtask.getInstance(hre) + const storage = ContractStorage.getInstance() subtask.setHre(hre) const deployer = await subtask.getDeployer() @@ -63,7 +65,10 @@ task('new-deployer', 'Deploy a new instance of ClrFund') await subtask.finish(success) if (verify) { - console.log('Verify all contracts') - await hre.run('verify-all') + const address = storage.mustGetAddress( + EContracts.ClrFundDeployer, + hre.network.name + ) + await hre.run('verify-deployer', { address }) } }) diff --git a/contracts/tasks/runners/setToken.ts b/contracts/tasks/runners/setToken.ts index f9fc9272a..348aac137 100644 --- a/contracts/tasks/runners/setToken.ts +++ b/contracts/tasks/runners/setToken.ts @@ -18,12 +18,11 @@ import { type ISubtaskParams } from '../helpers/types' task('set-token', 'Set the token in ClrFund') .addFlag('incremental', 'Incremental deployment') .addFlag('strict', 'Fail on warnings') - .addFlag('verify', 'Verify contracts at Etherscan') .addFlag('manageNonce', 'Manually increment nonce for each transaction') .addOptionalParam('clrfund', 'The ClrFund contract address') .addOptionalParam('skip', 'Skip steps with less or equal index', 0, types.int) .setAction(async (params: ISubtaskParams, hre) => { - const { verify, manageNonce } = params + const { manageNonce } = params const subtask = Subtask.getInstance(hre) subtask.setHre(hre) @@ -53,9 +52,4 @@ task('set-token', 'Set the token in ClrFund') } await subtask.finish(success) - - if (verify) { - console.log('Verify all contracts') - await hre.run('verify-all') - } }) diff --git a/contracts/tasks/runners/verifyAll.ts b/contracts/tasks/runners/verifyAll.ts index ab4647563..6425bf5eb 100644 --- a/contracts/tasks/runners/verifyAll.ts +++ b/contracts/tasks/runners/verifyAll.ts @@ -1,108 +1,334 @@ /* eslint-disable no-console */ import { task } from 'hardhat/config' -import type { IStorageInstanceEntry, IVerifyAllArgs } from '../helpers/types' +import { EContracts } from '../helpers/types' import { ContractStorage } from '../helpers/ContractStorage' import { ContractVerifier } from '../helpers/ContractVerifier' +import { + BrightIdUserRegistry, + ClrFund, + MerkleUserRegistry, + SemaphoreUserRegistry, + SnapshotUserRegistry, +} from '../../typechain-types' +import { BaseContract } from 'ethers' +import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' +import { ZERO_ADDRESS } from '../../utils/constants' +import { ConstructorArguments } from '../helpers/ConstructorArguments' + +type ContractInfo = { + name: string + address: string +} + +type VerificationSummary = { + contract: string + ok: boolean + err?: string +} /** - * Main verification task which runs hardhat-etherscan task for all the deployed contract. + * Get the recipient registry contract name + * @param registryAddress The recipient registry contract address + * @param ethers The Hardhat Ethers helper + * @returns The recipient registry contract name */ -task('verify-all', 'Verify contracts listed in storage') - .addFlag('force', 'Ignore verified status') - .setAction(async ({ force = false }: IVerifyAllArgs, hre) => { - const storage = ContractStorage.getInstance() - const verifier = new ContractVerifier(hre) - const addressList: string[] = [] - const entryList: IStorageInstanceEntry[] = [] - let index = 0 +async function getRecipientRegistryName( + registryAddress: string, + ethers: HardhatEthersHelpers +): Promise { + try { + const contract = await ethers.getContractAt( + EContracts.KlerosGTCRAdapter, + registryAddress + ) + const tcr = await contract.tcr() + if (tcr === ZERO_ADDRESS) { + throw new Error( + 'Unexpected zero tcr from a Kleros recipient registry: ' + + registryAddress + ) + } + return EContracts.KlerosGTCRAdapter + } catch { + // not a kleros registry + } - const addEntry = (address: string, entry: IStorageInstanceEntry) => { - if (!entry.verify) { - return - } + // try optimistic + const contract = await ethers.getContractAt( + EContracts.OptimisticRecipientRegistry, + registryAddress + ) - addressList.push(address) - entryList.push(entry) - index += 1 - } + try { + await contract.challengePeriodDuration() + return EContracts.OptimisticRecipientRegistry + } catch { + // not optimistic, use simple registry + return EContracts.SimpleRecipientRegistry + } +} + +/** + * Get the user registry contract name + * @param registryAddress The user registry contract address + * @param ethers The Hardhat Ethers helper + * @returns The user registry contract name + */ +async function getUserRegistryName( + registryAddress: string, + ethers: HardhatEthersHelpers +): Promise { + try { + const contract = (await ethers.getContractAt( + EContracts.BrightIdUserRegistry, + registryAddress + )) as BaseContract as BrightIdUserRegistry + await contract.context() + return EContracts.BrightIdUserRegistry + } catch { + // not a BrightId user registry + } - const instances = storage.getInstances(hre.network.name) + // try semaphore user registry + try { + const contract = (await ethers.getContractAt( + EContracts.SemaphoreUserRegistry, + registryAddress + )) as BaseContract as SemaphoreUserRegistry + await contract.isVerifiedSemaphoreId(1) + return EContracts.SemaphoreUserRegistry + } catch { + // not a semaphore user registry + } - instances.forEach(([key, entry]) => { - if (entry.id.includes('Poseidon')) { - return - } + // try snapshot user regitry + try { + const contract = (await ethers.getContractAt( + EContracts.SnapshotUserRegistry, + registryAddress + )) as BaseContract as SnapshotUserRegistry + await contract.storageRoot() + } catch { + // not snapshot user registry + } + + // try merkle user regitry + try { + const contract = (await ethers.getContractAt( + EContracts.MerkleUserRegistry, + registryAddress + )) as BaseContract as MerkleUserRegistry + await contract.merkleRoot() + } catch { + // not merkle user registry + } - addEntry(key, entry) + return EContracts.SimpleUserRegistry +} + +/** + * Get the list of contracts to verify + * @param clrfund The ClrFund contract address + * @param ethers The Hardhat Ethers helper + * @param etherscanProvider The Etherscan provider + * @returns The list of contracts to verify + */ +async function getContractList( + clrfund: string, + ethers: HardhatEthersHelpers +): Promise { + const contractList: ContractInfo[] = [ + { + name: EContracts.ClrFund, + address: clrfund, + }, + ] + + const clrfundContract = (await ethers.getContractAt( + EContracts.ClrFund, + clrfund + )) as BaseContract as ClrFund + + const fundingRoundFactoryAddress = await clrfundContract.roundFactory() + if (fundingRoundFactoryAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.FundingRoundFactory, + address: fundingRoundFactoryAddress, }) + } - console.log( - '======================================================================' - ) - console.log( - '======================================================================' - ) - console.log( - `Verification batch with ${addressList.length} entries of ${index} total.` + const maciFactoryAddress = await clrfundContract.maciFactory() + if (maciFactoryAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.MACIFactory, + address: maciFactoryAddress, + }) + + const maciFactory = await ethers.getContractAt( + EContracts.MACIFactory, + maciFactoryAddress ) - console.log( - '======================================================================' + const vkRegistryAddress = await maciFactory.vkRegistry() + contractList.push({ + name: EContracts.VkRegistry, + address: vkRegistryAddress, + }) + + const factories = await maciFactory.factories() + contractList.push({ + name: EContracts.PollFactory, + address: factories.pollFactory, + }) + + contractList.push({ + name: EContracts.TallyFactory, + address: factories.tallyFactory, + }) + + contractList.push({ + name: EContracts.MessageProcessorFactory, + address: factories.messageProcessorFactory, + }) + } + + const fundingRoundAddress = await clrfundContract.getCurrentRound() + if (fundingRoundAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.FundingRound, + address: fundingRoundAddress, + }) + + const fundingRound = await ethers.getContractAt( + EContracts.FundingRound, + fundingRoundAddress ) - const summary: string[] = [] - for (let i = 0; i < addressList.length; i += 1) { - const address = addressList[i] - const entry = entryList[i] + const maciAddress = await fundingRound.maci() + if (maciAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.MACI, + address: maciAddress, + }) + } - const params = entry.verify + // Poll + const pollAddress = await fundingRound.poll() + if (pollAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.Poll, + address: pollAddress, + }) + } - console.log( - '\n======================================================================' + // Tally + const tallyAddress = await fundingRound.tally() + if (tallyAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.Tally, + address: tallyAddress, + }) + + // Verifier + const tallyContract = await ethers.getContractAt( + EContracts.Tally, + tallyAddress ) - console.log( - `[${i}/${addressList.length}] Verify contract: ${entry.id} ${address}` + const verifierAddress = await tallyContract.verifier() + if (verifierAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.Verifier, + address: verifierAddress, + }) + } + + // MessageProcessor + const messageProcessorAddress = await tallyContract.messageProcessor() + if (messageProcessorAddress !== ZERO_ADDRESS) { + contractList.push({ + name: EContracts.MessageProcessor, + address: messageProcessorAddress, + }) + } + } + + // User Registry + const userRegistryAddress = await fundingRound.userRegistry() + if (userRegistryAddress !== ZERO_ADDRESS) { + const name = await getUserRegistryName(userRegistryAddress, ethers) + contractList.push({ + name, + address: userRegistryAddress, + }) + } + + // Recipient Registry + const recipientRegistryAddress = await fundingRound.recipientRegistry() + if (recipientRegistryAddress !== ZERO_ADDRESS) { + const name = await getRecipientRegistryName( + recipientRegistryAddress, + ethers ) - console.log('\tArgs:', params?.args) + contractList.push({ + name, + address: recipientRegistryAddress, + }) + } + } - const verifiedEntity = storage.getVerified(address, hre.network.name) + return contractList +} - if (!force && verifiedEntity) { - console.log('Already verified') - } else { +/** + * Main verification task which runs hardhat-etherscan task for all the deployed contract. + */ +task('verify-all', 'Verify contracts listed in storage') + .addOptionalParam('clrfund', 'The ClrFund contract address') + .addFlag('force', 'Ignore verified status') + .setAction(async ({ clrfund }, hre) => { + const { ethers, config, network } = hre + + const storage = ContractStorage.getInstance() + const clrfundContractAddress = + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) + + const contractList = await getContractList(clrfundContractAddress, ethers) + const constructorArguments = new ConstructorArguments(hre) + const verifier = new ContractVerifier(hre) + const summary: VerificationSummary[] = [] + + for (let i = 0; i < contractList.length; i += 1) { + const { name, address } = contractList[i] + + try { + const args = await constructorArguments.get(name, address, ethers) let contract: string | undefined let libraries: string | undefined - if (entry.id === 'AnyOldERC20Token') { - contract = 'contracts/AnyOldERC20Token.sol:AnyOldERC20Token' - } - // eslint-disable-next-line no-await-in-loop const [ok, err] = await verifier.verify( address, - params?.args ?? '', + args, libraries, contract ) - if (ok) { - storage.setVerified(address, hre.network.name, true) - } else { - summary.push(`${address} ${entry.id}: ${err}`) - } + summary.push({ contract: `${address} ${name}`, ok, err }) + } catch (e) { + // error getting the constructors, skipping + summary.push({ + contract: `${address} ${name}`, + ok: false, + err: 'Failed to get constructor. ' + (e as Error).message, + }) } } - console.log( - '\n======================================================================' - ) - console.log( - `Verification batch has finished with ${summary.length} issue(s).` - ) - console.log( - '======================================================================' - ) - console.log(summary.join('\n')) - console.log( - '======================================================================' - ) + summary.forEach(({ contract, ok, err }, i) => { + const color = ok ? '32' : '31' + console.log( + `${i + 1} ${contract}: \x1b[%sm%s\x1b[0m`, + color, + ok ? 'ok' : err + ) + }) }) diff --git a/contracts/tasks/runners/verifyDeployer.ts b/contracts/tasks/runners/verifyDeployer.ts new file mode 100644 index 000000000..a2fad0cf6 --- /dev/null +++ b/contracts/tasks/runners/verifyDeployer.ts @@ -0,0 +1,34 @@ +import { task } from 'hardhat/config' +import { EContracts } from '../../utils/types' +import { EtherscanProvider } from '../../utils/providers/EtherscanProvider' +import { ContractVerifier } from '../helpers/ContractVerifier' +import { ConstructorArguments } from '../helpers/ConstructorArguments' + +/** + * Verifies the ClrFundDeployer contract + * - it constructs the constructor arguments by querying the ClrFundDeployer contract + * - it calls the etherscan hardhat plugin to verify the contract + */ +task('verify-deployer', 'Verify a ClrFundDeployer contract') + .addParam('address', 'ClrFundDeployer contract address') + .setAction(async ({ address }, hre) => { + const contractVerifier = new ContractVerifier(hre) + const getter = new ConstructorArguments(hre) + + const name = EContracts.ClrFundDeployer + const constructorArgument = await getter.get( + EContracts.ClrFundDeployer, + address, + hre.ethers + ) + const [ok, err] = await contractVerifier.verify( + address, + constructorArgument + ) + + console.log( + `${address} ${name}: \x1b[%sm%s\x1b[0m`, + ok ? 32 : 31, + ok ? 'ok' : err + ) + }) diff --git a/contracts/tests/maciFactory.ts b/contracts/tests/maciFactory.ts index 44296bbf9..3f23d58ad 100644 --- a/contracts/tests/maciFactory.ts +++ b/contracts/tests/maciFactory.ts @@ -1,4 +1,4 @@ -import { artifacts, ethers, config } from 'hardhat' +import { artifacts, ethers } from 'hardhat' import { Contract, TransactionResponse } from 'ethers' import { expect } from 'chai' import { deployMockContract, MockContract } from '@clrfund/waffle-mock-contract' diff --git a/contracts/utils/testutils.ts b/contracts/utils/testutils.ts index db63401ec..e5ace7da7 100644 --- a/contracts/utils/testutils.ts +++ b/contracts/utils/testutils.ts @@ -1,6 +1,6 @@ import { Signer, Contract } from 'ethers' import { MockContract, deployMockContract } from '@clrfund/waffle-mock-contract' -import { artifacts, ethers, config } from 'hardhat' +import { artifacts, ethers } from 'hardhat' import { MaciParameters } from './maciParameters' import { PubKey } from '@clrfund/common' import { deployContract, getEventArg, setVerifyingKeys } from './contracts' diff --git a/contracts/utils/types.ts b/contracts/utils/types.ts index ba8f753f4..a389bec65 100644 --- a/contracts/utils/types.ts +++ b/contracts/utils/types.ts @@ -27,6 +27,8 @@ export enum EContracts { IUserRegistry = 'IUserRegistry', SimpleUserRegistry = 'SimpleUserRegistry', SemaphoreUserRegistry = 'SemaphoreUserRegistry', + SnapshotUserRegistry = 'SnapshotUserRegistry', + MerkleUserRegistry = 'MerkleUserRegistry', BrightIdUserRegistry = 'BrightIdUserRegistry', AnyOldERC20Token = 'AnyOldERC20Token', BrightIdSponsor = 'BrightIdSponsor', From e7ded06adb82128bf59f694d6f76442baf20c324 Mon Sep 17 00:00:00 2001 From: yuetloo Date: Tue, 16 Apr 2024 17:07:27 -0400 Subject: [PATCH 2/2] refactor etherscan provider to extract API key from custom config --- contracts/tasks/runners/exportRound.ts | 35 ++++-------- contracts/tasks/runners/verifyAll.ts | 1 - contracts/tasks/runners/verifyDeployer.ts | 1 - .../utils/RecipientRegistryLogProcessor.ts | 7 ++- contracts/utils/abi.ts | 11 ++++ .../utils/providers/EtherscanProvider.ts | 57 +++++++++++++++---- contracts/utils/providers/ProviderFactory.ts | 10 ++-- 7 files changed, 78 insertions(+), 44 deletions(-) diff --git a/contracts/tasks/runners/exportRound.ts b/contracts/tasks/runners/exportRound.ts index 4eadf9818..fa3e332b1 100644 --- a/contracts/tasks/runners/exportRound.ts +++ b/contracts/tasks/runners/exportRound.ts @@ -16,7 +16,7 @@ import { Contract, formatUnits, getNumber } from 'ethers' import { Ipfs } from '../../utils/ipfs' import { Project, Round, RoundFileContent } from '../../utils/types' import { RecipientRegistryLogProcessor } from '../../utils/RecipientRegistryLogProcessor' -import { getRecipientAddressAbi } from '../../utils/abi' +import { getRecipientAddressAbi, MaciV0Abi } from '../../utils/abi' import { JSONFile } from '../../utils/JSONFile' import path from 'path' import fs from 'fs' @@ -41,19 +41,6 @@ function roundListFileName(directory: string): string { return path.join(directory, 'rounds.json') } -function getEtherscanApiKey(config: any, network: string): string { - let etherscanApiKey = '' - if (config.etherscan?.apiKey) { - if (typeof config.etherscan.apiKey === 'string') { - etherscanApiKey = config.etherscan.apiKey - } else { - etherscanApiKey = config.etherscan.apiKey[network] - } - } - - return etherscanApiKey -} - function roundMapKey(round: RoundListEntry): string { return `${round.network}.${round.address}` } @@ -76,7 +63,10 @@ async function updateRoundList(filePath: string, round: RoundListEntry) { const rounds: RoundListEntry[] = Array.from(roundMap.values()) // sort in ascending start time order - rounds.sort((round1, round2) => round1.startTime - round2.startTime) + rounds.sort( + (round1, round2) => + getNumber(round1.startTime) - getNumber(round2.startTime) + ) JSONFile.write(filePath, rounds) console.log('Finished writing to', filePath) } @@ -137,7 +127,11 @@ async function mergeRecipientTally({ } const tallyResult = tally.results.tally[i] - const spentVoiceCredits = tally.perVOSpentVoiceCredits.tally[i] + + // In MACI V1, totalVoiceCreditsPerVoteOption is called perVOSpentVoiceCredits + const spentVoiceCredits = tally.perVOSpentVoiceCredits + ? tally.perVOSpentVoiceCredits.tally[i] + : tally.totalVoiceCreditsPerVoteOption.tally[i] const formattedDonationAmount = formatUnits( BigInt(spentVoiceCredits) * BigInt(voiceCreditFactor), nativeTokenDecimals @@ -237,7 +231,7 @@ async function getRoundInfo( maxMessages = maxValues.maxMessages maxRecipients = maxValues.maxVoteOptions } else { - const maci = await ethers.getContractAt('MACI', maciAddress) + const maci = await ethers.getContractAt(MaciV0Abi, maciAddress) startTime = await maci.signUpTimestamp().catch(toZero) signUpDuration = await maci.signUpDurationSeconds().catch(toZero) votingDuration = await maci.votingDurationSeconds().catch(toZero) @@ -352,11 +346,6 @@ task('export-round', 'Export round data for the leaderboard') console.log('Processing on ', network.name) console.log('Funding round address', roundAddress) - const etherscanApiKey = getEtherscanApiKey(config, network.name) - if (!etherscanApiKey) { - throw new Error('Etherscan API key not set') - } - const outputSubDir = path.join(outputDir, network.name) try { fs.statSync(outputSubDir) @@ -383,7 +372,7 @@ task('export-round', 'Export round data for the leaderboard') endBlock, blocksPerBatch, network: network.name, - etherscanApiKey, + config, }) console.log('Parsing logs...') diff --git a/contracts/tasks/runners/verifyAll.ts b/contracts/tasks/runners/verifyAll.ts index 6425bf5eb..c010df8ff 100644 --- a/contracts/tasks/runners/verifyAll.ts +++ b/contracts/tasks/runners/verifyAll.ts @@ -132,7 +132,6 @@ async function getUserRegistryName( * Get the list of contracts to verify * @param clrfund The ClrFund contract address * @param ethers The Hardhat Ethers helper - * @param etherscanProvider The Etherscan provider * @returns The list of contracts to verify */ async function getContractList( diff --git a/contracts/tasks/runners/verifyDeployer.ts b/contracts/tasks/runners/verifyDeployer.ts index a2fad0cf6..284689063 100644 --- a/contracts/tasks/runners/verifyDeployer.ts +++ b/contracts/tasks/runners/verifyDeployer.ts @@ -1,6 +1,5 @@ import { task } from 'hardhat/config' import { EContracts } from '../../utils/types' -import { EtherscanProvider } from '../../utils/providers/EtherscanProvider' import { ContractVerifier } from '../helpers/ContractVerifier' import { ConstructorArguments } from '../helpers/ConstructorArguments' diff --git a/contracts/utils/RecipientRegistryLogProcessor.ts b/contracts/utils/RecipientRegistryLogProcessor.ts index 615ab1cb0..6d74a4334 100644 --- a/contracts/utils/RecipientRegistryLogProcessor.ts +++ b/contracts/utils/RecipientRegistryLogProcessor.ts @@ -7,6 +7,7 @@ import { Log } from './providers/BaseProvider' import { toDate } from './date' import { EVENT_ABIS } from './abi' import { AbiInfo } from './types' +import { HardhatConfig } from 'hardhat/types' function getFilter(address: string, abiInfo: AbiInfo): EventFilter { const eventInterface = new Interface([abiInfo.abi]) @@ -41,14 +42,14 @@ export class RecipientRegistryLogProcessor { endBlock, startBlock, blocksPerBatch, - etherscanApiKey, + config, network, }: { recipientRegistry: Contract startBlock: number endBlock: number blocksPerBatch: number - etherscanApiKey: string + config: HardhatConfig network: string }): Promise { // fetch event logs containing project information @@ -64,7 +65,7 @@ export class RecipientRegistryLogProcessor { const logProvider = ProviderFactory.createProvider({ network, - etherscanApiKey, + config, }) let logs: Log[] = [] diff --git a/contracts/utils/abi.ts b/contracts/utils/abi.ts index 98101978a..dc3c0c004 100644 --- a/contracts/utils/abi.ts +++ b/contracts/utils/abi.ts @@ -6,6 +6,17 @@ type EventAbiEntry = { remove: AbiInfo } +/** + * MACI v0 ABI used in exportRound.ts + */ +export const MaciV0Abi = [ + 'function signUpTimestamp() view returns (uint256)', + 'function signUpDurationSeconds() view returns (uint256)', + 'function votingDurationSeconds() view returns (uint256)', + `function treeDepths() view returns ((uint8 stateTreeDepth, uint8 messageTreeDepth, uint8 voteOptionTreeDepth))`, + 'function numMessages() view returns (uint256)', +] + export const getRecipientAddressAbi = [ `function getRecipientAddress(uint256 _index, uint256 _startTime, uint256 _endTime)` + ` external view returns (address)`, diff --git a/contracts/utils/providers/EtherscanProvider.ts b/contracts/utils/providers/EtherscanProvider.ts index 5a35ac557..8534156ae 100644 --- a/contracts/utils/providers/EtherscanProvider.ts +++ b/contracts/utils/providers/EtherscanProvider.ts @@ -1,5 +1,6 @@ import { BaseProvider, FetchLogArgs, Log } from './BaseProvider' import { FetchRequest } from 'ethers' +import { HardhatConfig } from 'hardhat/types' const EtherscanApiUrl: Record = { xdai: 'https://api.gnosisscan.io', @@ -10,14 +11,57 @@ const EtherscanApiUrl: Record = { 'optimism-sepolia': 'https://api-sepolia-optimistic.etherscan.io', } +/** + * Mapping of the hardhat network name to the Etherscan network name in the hardhat.config + */ +const EtherscanNetworks: Record = { + arbitrum: 'arbitrumOne', + optimism: 'optimisticEthereum', +} + +/** + * The the Etherscan API key from the hardhat.config file + * @param config The Hardhat config object + * @param network The Hardhat network name + * @returns The Etherscan API key + */ +function getEtherscanApiKey(config: HardhatConfig, network: string): string { + let etherscanApiKey = '' + if (config.etherscan?.apiKey) { + if (typeof config.etherscan.apiKey === 'string') { + etherscanApiKey = config.etherscan.apiKey + } else { + const etherscanNetwork = EtherscanNetworks[network] ?? network + etherscanApiKey = config.etherscan.apiKey[etherscanNetwork] + } + } + + return etherscanApiKey +} + export class EtherscanProvider extends BaseProvider { apiKey: string network: string + baseUrl: string - constructor(apiKey: string, network: string) { + constructor(config: HardhatConfig, network: string) { super() - this.apiKey = apiKey + + const etherscanApiKey = getEtherscanApiKey(config, network) + if (!etherscanApiKey) { + throw new Error(`Etherscan API key is not found for ${network}`) + } + + const etherscanBaseUrl = EtherscanApiUrl[network] + if (!etherscanBaseUrl) { + throw new Error( + `Network ${network} is not supported in etherscan fetch log api` + ) + } + this.network = network + this.apiKey = etherscanApiKey + this.baseUrl = etherscanBaseUrl } async fetchLogs({ @@ -25,17 +69,10 @@ export class EtherscanProvider extends BaseProvider { startBlock, lastBlock, }: FetchLogArgs): Promise { - const baseUrl = EtherscanApiUrl[this.network] - if (!baseUrl) { - throw new Error( - `Network ${this.network} is not supported in etherscan fetch log api` - ) - } - const topic0 = filter.topics?.[0] || '' const toBlockQuery = lastBlock ? `&toBlock=${lastBlock}` : '' const url = - `${baseUrl}/api?module=logs&action=getLogs&address=${filter.address}` + + `${this.baseUrl}/api?module=logs&action=getLogs&address=${filter.address}` + `&topic0=${topic0}&fromBlock=${startBlock}${toBlockQuery}&apikey=${this.apiKey}` const req = new FetchRequest(url) diff --git a/contracts/utils/providers/ProviderFactory.ts b/contracts/utils/providers/ProviderFactory.ts index 6a79ad0ec..b7c5474d2 100644 --- a/contracts/utils/providers/ProviderFactory.ts +++ b/contracts/utils/providers/ProviderFactory.ts @@ -1,17 +1,15 @@ +import { HardhatConfig } from 'hardhat/types' import { BaseProvider } from './BaseProvider' import { EtherscanProvider } from './EtherscanProvider' export type CreateProviderArgs = { network: string - etherscanApiKey: string + config: HardhatConfig } export class ProviderFactory { - static createProvider({ - network, - etherscanApiKey, - }: CreateProviderArgs): BaseProvider { + static createProvider({ network, config }: CreateProviderArgs): BaseProvider { // use etherscan provider only as JsonRpcProvider is not reliable - return new EtherscanProvider(etherscanApiKey, network) + return new EtherscanProvider(config, network) } }