diff --git a/.github/workflows/finalize-round.yml b/.github/workflows/finalize-round.yml index adfcdfdfc..0109651d5 100644 --- a/.github/workflows/finalize-round.yml +++ b/.github/workflows/finalize-round.yml @@ -29,6 +29,8 @@ env: CIRCUIT_TYPE: micro ZKEYS_DOWNLOAD_SCRIPT: "download-6-9-2-3.sh" JSONRPC_HTTP_URL: ${{ github.event.inputs.jsonrpc_url }} + PINATA_API_KEY: ${{ secrets.PINATA_API_KEY }} + PINATA_SECRET_API_KEY: ${{ secrets.PINATA_SECRET_API_KEY }} jobs: finalize: @@ -84,10 +86,9 @@ jobs: mkdir -p proof_output yarn hardhat tally --clrfund "${CLRFUND_ADDRESS}" --network "${NETWORK}" \ --rapidsnark ${RAPID_SNARK} \ - --circuit-directory ${CIRCUIT_DIRECTORY} \ + --params-dir ${CIRCUIT_DIRECTORY} \ --blocks-per-batch ${BLOCKS_PER_BATCH} \ - --maci-tx-hash "${MACI_TX_HASH}" --output-dir "./proof_output" - curl --location --request POST 'https://api.pinata.cloud/pinning/pinFileToIPFS' \ - --header "Authorization: Bearer ${{ secrets.PINATA_JWT }}" \ - --form 'file=@"./proof_output/tally.json"' - yarn hardhat --network "${NETWORK}" finalize --clrfund "${CLRFUND_ADDRESS}" + --maci-tx-hash "${MACI_TX_HASH}" \ + --proof-dir "./proof_output" + yarn hardhat --network "${NETWORK}" finalize --clrfund "${CLRFUND_ADDRESS}" \ + --proof-dir "./proof_output" diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml index 5ebe7d346..05082aa5e 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -10,6 +10,8 @@ on: env: NODE_VERSION: 20.x ZKEYS_DOWNLOAD_SCRIPT: "download-6-9-2-3.sh" + PINATA_API_KEY: ${{ secrets.PINATA_API_KEY }} + PINATA_SECRET_API_KEY: ${{ secrets.PINATA_SECRET_API_KEY }} jobs: script-tests: diff --git a/contracts/.env.example b/contracts/.env.example index 5ace336a7..2db7006e4 100644 --- a/contracts/.env.example +++ b/contracts/.env.example @@ -6,13 +6,17 @@ JSONRPC_HTTP_URL=https://eth-goerli.alchemyapi.io/v2/ADD_API_KEY WALLET_MNEMONIC= WALLET_PRIVATE_KEY= -# The coordinator MACI private key, required by the tally script +# The coordinator MACI private key, required by the gen-proofs script COORDINATOR_MACISK= # API key used to verify contracts on arbitrum chain (including testnet) # Update the etherscan section in hardhat.config to add API key for other chains ARBISCAN_API_KEY= +# PINATE credentials to upload tally.json file to IPFS; used by the tally script +PINATA_API_KEY= +PINATA_SECRET_API_KEY= + # these are used in the e2e testing CIRCUIT_TYPE= CIRCUIT_DIRECTORY= diff --git a/contracts/e2e/index.ts b/contracts/e2e/index.ts index 5a22205dd..76e21525c 100644 --- a/contracts/e2e/index.ts +++ b/contracts/e2e/index.ts @@ -36,6 +36,7 @@ import path from 'path' import { FundingRound } from '../typechain-types' import { JSONFile } from '../utils/JSONFile' import { EContracts } from '../utils/types' +import { getTalyFilePath } from '../utils/misc' type VoteData = { recipientIndex: number; voiceCredits: bigint } type ClaimData = { [index: number]: bigint } @@ -359,6 +360,8 @@ describe('End-to-end Tests', function () { mkdirSync(outputDir, { recursive: true }) } + const tallyFile = getTalyFilePath(outputDir) + // past an end block that's later than the MACI start block const genProofArgs = getGenProofArgs({ maciAddress, @@ -368,6 +371,7 @@ describe('End-to-end Tests', function () { circuitType: circuit, circuitDirectory, outputDir, + tallyFile, blocksPerBatch: DEFAULT_GET_LOG_BATCH_SIZE, maciTxHash: maciTransactionHash, signer: coordinator, diff --git a/contracts/package.json b/contracts/package.json index 7bbbb8133..94bb91a88 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@clrfund/contracts", - "version": "5.1.0", + "version": "5.1.1", "license": "GPL-3.0", "scripts": { "hardhat": "hardhat", @@ -16,6 +16,7 @@ }, "dependencies": { "@openzeppelin/contracts": "4.9.0", + "@pinata/sdk": "^2.1.0", "dotenv": "^8.2.0", "maci-contracts": "^1.2.0", "solidity-rlp": "2.0.8" diff --git a/contracts/sh/runScriptTests.sh b/contracts/sh/runScriptTests.sh index f03a47bf9..11cde09a2 100755 --- a/contracts/sh/runScriptTests.sh +++ b/contracts/sh/runScriptTests.sh @@ -33,17 +33,17 @@ yarn hardhat contribute --network ${HARDHAT_NETWORK} yarn hardhat time-travel --seconds ${ROUND_DURATION} --network ${HARDHAT_NETWORK} -# run the tally script +# tally the votes NODE_OPTIONS="--max-old-space-size=4096" yarn hardhat tally \ --rapidsnark ${RAPID_SNARK} \ - --batch-size 8 \ - --output-dir ${OUTPUT_DIR} \ + --proof-dir ${OUTPUT_DIR} \ + --maci-start-block 0 \ --network "${HARDHAT_NETWORK}" # finalize the round -yarn hardhat finalize --tally-file ${TALLY_FILE} --network ${HARDHAT_NETWORK} +yarn hardhat finalize --proof-dir ${OUTPUT_DIR} --network ${HARDHAT_NETWORK} # claim funds -yarn hardhat claim --recipient 1 --tally-file ${TALLY_FILE} --network ${HARDHAT_NETWORK} -yarn hardhat claim --recipient 2 --tally-file ${TALLY_FILE} --network ${HARDHAT_NETWORK} +yarn hardhat claim --recipient 1 --proof-dir ${OUTPUT_DIR} --network ${HARDHAT_NETWORK} +yarn hardhat claim --recipient 2 --proof-dir ${OUTPUT_DIR} --network ${HARDHAT_NETWORK} 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..59f1c9699 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -23,3 +23,8 @@ import './runners/addRecipients' import './runners/findStorageSlot' import './runners/verifyTallyFile' import './runners/verifyAll' +import './runners/verifyDeployer' +import './runners/genProofs' +import './runners/proveOnChain' +import './runners/publishTallyResults' +import './runners/resetTally' diff --git a/contracts/tasks/runners/claim.ts b/contracts/tasks/runners/claim.ts index acce6d3a2..e5135aa24 100644 --- a/contracts/tasks/runners/claim.ts +++ b/contracts/tasks/runners/claim.ts @@ -2,75 +2,87 @@ * Claim funds. This script is mainly used by e2e testing * * Sample usage: - * yarn hardhat claim \ - * --tally-file \ - * --recipient \ - * --network + * yarn hardhat claim --recipient --network */ import { getEventArg } from '../../utils/contracts' import { getRecipientClaimData } from '@clrfund/common' import { JSONFile } from '../../utils/JSONFile' -import { isPathExist } from '../../utils/misc' +import { + getProofDirForRound, + getTalyFilePath, + isPathExist, +} from '../../utils/misc' import { getNumber } from 'ethers' import { task, types } from 'hardhat/config' import { EContracts } from '../../utils/types' import { ContractStorage } from '../helpers/ContractStorage' task('claim', 'Claim funnds for test recipients') + .addOptionalParam('roundAddress', 'Funding round contract address') .addParam( 'recipient', 'The recipient index in the tally file', undefined, types.int ) - .addParam('tallyFile', 'The tally file') - .setAction(async ({ tallyFile, recipient }, { ethers, network }) => { - if (!isPathExist(tallyFile)) { - throw new Error(`Path ${tallyFile} does not exist`) - } + .addParam('proofDir', 'The proof output directory', './proof_output') + .setAction( + async ({ proofDir, recipient, roundAddress }, { ethers, network }) => { + if (recipient <= 0) { + throw new Error('Recipient must be greater than 0') + } - if (recipient <= 0) { - throw new Error('Recipient must be greater than 0') - } + const storage = ContractStorage.getInstance() + const fundingRound = + roundAddress ?? + storage.mustGetAddress(EContracts.FundingRound, network.name) - const storage = ContractStorage.getInstance() - const fundingRound = storage.mustGetAddress( - EContracts.FundingRound, - network.name - ) + const proofDirForRound = getProofDirForRound( + proofDir, + network.name, + fundingRound + ) - const tally = JSONFile.read(tallyFile) + const tallyFile = getTalyFilePath(proofDirForRound) + if (!isPathExist(tallyFile)) { + throw new Error(`Path ${tallyFile} does not exist`) + } - const fundingRoundContract = await ethers.getContractAt( - EContracts.FundingRound, - fundingRound - ) + const tally = JSONFile.read(tallyFile) - const recipientStatus = await fundingRoundContract.recipients(recipient) - if (recipientStatus.fundsClaimed) { - throw new Error(`Recipient already claimed funds`) - } + const fundingRoundContract = await ethers.getContractAt( + EContracts.FundingRound, + fundingRound + ) - const pollAddress = await fundingRoundContract.poll() - console.log('pollAddress', pollAddress) + const recipientStatus = await fundingRoundContract.recipients(recipient) + if (recipientStatus.fundsClaimed) { + throw new Error(`Recipient already claimed funds`) + } - const poll = await ethers.getContractAt(EContracts.Poll, pollAddress) - const treeDepths = await poll.treeDepths() - const recipientTreeDepth = getNumber(treeDepths.voteOptionTreeDepth) + const pollAddress = await fundingRoundContract.poll() + console.log('pollAddress', pollAddress) - // Claim funds - const recipientClaimData = getRecipientClaimData( - recipient, - recipientTreeDepth, - tally - ) - const claimTx = await fundingRoundContract.claimFunds(...recipientClaimData) - const claimedAmount = await getEventArg( - claimTx, - fundingRoundContract, - 'FundsClaimed', - '_amount' - ) - console.log(`Recipient ${recipient} claimed ${claimedAmount} tokens.`) - }) + const poll = await ethers.getContractAt(EContracts.Poll, pollAddress) + const treeDepths = await poll.treeDepths() + const recipientTreeDepth = getNumber(treeDepths.voteOptionTreeDepth) + + // Claim funds + const recipientClaimData = getRecipientClaimData( + recipient, + recipientTreeDepth, + tally + ) + const claimTx = await fundingRoundContract.claimFunds( + ...recipientClaimData + ) + const claimedAmount = await getEventArg( + claimTx, + fundingRoundContract, + 'FundsClaimed', + '_amount' + ) + console.log(`Recipient ${recipient} claimed ${claimedAmount} tokens.`) + } + ) 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/finalize.ts b/contracts/tasks/runners/finalize.ts index 0240ba16b..e48a6c2b6 100644 --- a/contracts/tasks/runners/finalize.ts +++ b/contracts/tasks/runners/finalize.ts @@ -6,7 +6,7 @@ * - clrfund owner's wallet private key to interact with the contract * * Sample usage: - * yarn hardhat finalize --clrfund --tally-file --network + * yarn hardhat finalize --clrfund --network */ import { JSONFile } from '../../utils/JSONFile' @@ -16,20 +16,13 @@ import { task } from 'hardhat/config' import { EContracts } from '../../utils/types' import { ContractStorage } from '../helpers/ContractStorage' import { Subtask } from '../helpers/Subtask' +import { getProofDirForRound, getTalyFilePath } from '../../utils/misc' task('finalize', 'Finalize a funding round') .addOptionalParam('clrfund', 'The ClrFund contract address') - .addOptionalParam( - 'tallyFile', - 'The tally file path', - './proof_output/tally.json' - ) - .setAction(async ({ clrfund, tallyFile }, hre) => { + .addParam('proofDir', 'The proof output directory', './proof_output') + .setAction(async ({ clrfund, proofDir }, hre) => { const { ethers, network } = hre - const tally = JSONFile.read(tallyFile) - if (!tally.maci) { - throw Error('Bad tally file ' + tallyFile) - } const storage = ContractStorage.getInstance() const subtask = Subtask.getInstance(hre) @@ -63,6 +56,17 @@ task('finalize', 'Finalize a funding round') const treeDepths = await pollContract.treeDepths() console.log('voteOptionTreeDepth', treeDepths.voteOptionTreeDepth) + const currentRoundProofDir = getProofDirForRound( + proofDir, + network.name, + currentRoundAddress + ) + const tallyFile = getTalyFilePath(currentRoundProofDir) + const tally = JSONFile.read(tallyFile) + if (!tally.maci) { + throw Error('Bad tally file ' + tallyFile) + } + const totalSpent = tally.totalSpentVoiceCredits.spent const totalSpentSalt = tally.totalSpentVoiceCredits.salt diff --git a/contracts/tasks/runners/genProofs.ts b/contracts/tasks/runners/genProofs.ts new file mode 100644 index 000000000..ed3061edb --- /dev/null +++ b/contracts/tasks/runners/genProofs.ts @@ -0,0 +1,220 @@ +/** + * Script for generating MACI proofs + * + * Make sure to set the following environment variables in the .env file + * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC + * - coordinator's wallet private key to interact with contracts + * 2) COORDINATOR_MACISK - coordinator's MACI private key to decrypt messages + * + * Sample usage: + * + * yarn hardhat gen-proofs --clrfund --proof-dir \ + * --maci-tx-hash --network + * + */ +import { getNumber, NonceManager } from 'ethers' +import { task, types } from 'hardhat/config' + +import { + DEFAULT_GET_LOG_BATCH_SIZE, + DEFAULT_SR_QUEUE_OPS, +} from '../../utils/constants' +import { + getGenProofArgs, + genProofs, + genLocalState, + mergeMaciSubtrees, +} from '../../utils/maci' +import { + getMaciStateFilePath, + getTalyFilePath, + isPathExist, + makeDirectory, +} from '../../utils/misc' +import { EContracts } from '../../utils/types' +import { Subtask } from '../helpers/Subtask' +import { getCurrentFundingRoundContract } from '../../utils/contracts' +import { ContractStorage } from '../helpers/ContractStorage' +import { DEFAULT_CIRCUIT } from '../../utils/circuits' +import { JSONFile } from '../../utils/JSONFile' + +/** + * Check if the tally file with the maci contract address exists + * @param tallyFile The tally file path + * @param maciAddress The MACI contract address + * @returns true if the file exists and it contains the MACI contract address + */ +function tallyFileExists(tallyFile: string, maciAddress: string): boolean { + if (!isPathExist(tallyFile)) { + return false + } + try { + const tallyData = JSONFile.read(tallyFile) + return ( + tallyData.maci && + tallyData.maci.toLowerCase() === maciAddress.toLowerCase() + ) + } catch { + // in case the file does not have the expected format/field + return false + } +} + +task('gen-proofs', 'Generate MACI proofs offchain') + .addOptionalParam('clrfund', 'FundingRound contract address') + .addParam('proofDir', 'The proof output directory') + .addOptionalParam('maciTxHash', 'MACI creation transaction hash') + .addOptionalParam( + 'maciStartBlock', + 'MACI creation block', + undefined, + types.int + ) + .addFlag('manageNonce', 'Whether to manually manage transaction nonce') + .addOptionalParam('rapidsnark', 'The rapidsnark prover path') + .addParam('paramsDir', 'The circuit zkeys directory', './params') + .addOptionalParam( + 'blocksPerBatch', + 'The number of blocks per batch of logs to fetch on-chain', + DEFAULT_GET_LOG_BATCH_SIZE, + types.int + ) + .addOptionalParam( + 'numQueueOps', + 'The number of operations for MACI tree merging', + getNumber(DEFAULT_SR_QUEUE_OPS), + types.int + ) + .addOptionalParam('sleep', 'Number of seconds to sleep between log fetch') + .addOptionalParam( + 'quiet', + 'Whether to disable verbose logging', + false, + types.boolean + ) + .setAction( + async ( + { + clrfund, + maciStartBlock, + maciTxHash, + quiet, + proofDir, + paramsDir, + blocksPerBatch, + rapidsnark, + numQueueOps, + sleep, + manageNonce, + }, + hre + ) => { + console.log('Verbose logging enabled:', !quiet) + + const { ethers, network } = hre + const storage = ContractStorage.getInstance() + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) + + const [coordinatorSigner] = await ethers.getSigners() + if (!coordinatorSigner) { + throw new Error('Env. variable WALLET_PRIVATE_KEY not set') + } + const coordinator = manageNonce + ? new NonceManager(coordinatorSigner) + : coordinatorSigner + console.log('Coordinator address: ', await coordinator.getAddress()) + + const coordinatorMacisk = process.env.COORDINATOR_MACISK + if (!coordinatorMacisk) { + throw new Error('Env. variable COORDINATOR_MACISK not set') + } + + const circuit = + subtask.tryGetConfigField(EContracts.VkRegistry, 'circuit') || + DEFAULT_CIRCUIT + + const circuitDirectory = + subtask.tryGetConfigField( + EContracts.VkRegistry, + 'paramsDirectory' + ) || paramsDir + + await subtask.logStart() + + const clrfundContractAddress = + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) + const fundingRoundContract = await getCurrentFundingRoundContract( + clrfundContractAddress, + coordinator, + ethers + ) + console.log('Funding round contract', fundingRoundContract.target) + + const pollId = await fundingRoundContract.pollId() + console.log('PollId', pollId) + + const maciAddress = await fundingRoundContract.maci() + await mergeMaciSubtrees({ + maciAddress, + pollId, + numQueueOps, + signer: coordinator, + quiet, + }) + + if (!isPathExist(proofDir)) { + makeDirectory(proofDir) + } + + const tallyFile = getTalyFilePath(proofDir) + const maciStateFile = getMaciStateFilePath(proofDir) + const providerUrl = (network.config as any).url + + if (tallyFileExists(tallyFile, maciAddress)) { + console.log('The tally file has already been generated.') + return + } + + if (!isPathExist(maciStateFile)) { + if (!maciTxHash && maciStartBlock == null) { + throw new Error( + 'Please provide a value for --maci-tx-hash or --maci-start-block' + ) + } + + await genLocalState({ + quiet, + outputPath: maciStateFile, + pollId, + maciContractAddress: maciAddress, + coordinatorPrivateKey: coordinatorMacisk, + ethereumProvider: providerUrl, + transactionHash: maciTxHash, + startBlock: maciStartBlock, + blockPerBatch: blocksPerBatch, + signer: coordinator, + sleep, + }) + } + + const genProofArgs = getGenProofArgs({ + maciAddress, + pollId, + coordinatorMacisk, + rapidsnark, + circuitType: circuit, + circuitDirectory, + outputDir: proofDir, + blocksPerBatch: getNumber(blocksPerBatch), + maciStateFile, + tallyFile, + signer: coordinator, + quiet, + }) + await genProofs(genProofArgs) + + const success = true + await subtask.finish(success) + } + ) 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/proveOnChain.ts b/contracts/tasks/runners/proveOnChain.ts new file mode 100644 index 000000000..af777ed12 --- /dev/null +++ b/contracts/tasks/runners/proveOnChain.ts @@ -0,0 +1,103 @@ +/** + * Prove on chain the MACI proofs generated using genProofs + * + * Make sure to set the following environment variables in the .env file + * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC + * - coordinator's wallet private key to interact with contracts + * + * Sample usage: + * + * yarn hardhat prove-on-chain --clrfund --proof-dir --network + * + */ +import { BaseContract, NonceManager } from 'ethers' +import { task, types } from 'hardhat/config' + +import { proveOnChain } from '../../utils/maci' +import { Tally } from '../../typechain-types' +import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' +import { EContracts } from '../../utils/types' +import { Subtask } from '../helpers/Subtask' +import { getCurrentFundingRoundContract } from '../../utils/contracts' +import { ContractStorage } from '../helpers/ContractStorage' + +/** + * Get the message processor contract address from the tally contract + * @param tallyAddress Tally contract address + * @param ethers Hardhat ethers helper + * @returns Message processor contract address + */ +async function getMessageProcessorAddress( + tallyAddress: string, + ethers: HardhatEthersHelpers +): Promise { + const tallyContract = (await ethers.getContractAt( + EContracts.Tally, + tallyAddress + )) as BaseContract as Tally + + const messageProcessorAddress = await tallyContract.messageProcessor() + return messageProcessorAddress +} + +task('prove-on-chain', 'Prove on chain with the MACI proofs') + .addOptionalParam('clrfund', 'ClrFund contract address') + .addParam('proofDir', 'The proof output directory') + .addFlag('manageNonce', 'Whether to manually manage transaction nonce') + .addOptionalParam( + 'quiet', + 'Whether to disable verbose logging', + false, + types.boolean + ) + .setAction(async ({ clrfund, quiet, manageNonce, proofDir }, hre) => { + console.log('Verbose logging enabled:', !quiet) + + const { ethers, network } = hre + const storage = ContractStorage.getInstance() + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) + + const [coordinatorSigner] = await ethers.getSigners() + if (!coordinatorSigner) { + throw new Error('Env. variable WALLET_PRIVATE_KEY not set') + } + const coordinator = manageNonce + ? new NonceManager(coordinatorSigner) + : coordinatorSigner + console.log('Coordinator address: ', await coordinator.getAddress()) + + await subtask.logStart() + + const clrfundContractAddress = + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) + const fundingRoundContract = await getCurrentFundingRoundContract( + clrfundContractAddress, + coordinator, + ethers + ) + console.log('Funding round contract', fundingRoundContract.target) + + const pollId = await fundingRoundContract.pollId() + const maciAddress = await fundingRoundContract.maci() + const tallyAddress = await fundingRoundContract.tally() + const messageProcessorAddress = await getMessageProcessorAddress( + tallyAddress, + ethers + ) + + // proveOnChain if not already processed + await proveOnChain({ + pollId, + proofDir, + subsidyEnabled: false, + maciAddress, + messageProcessorAddress, + tallyAddress, + signer: coordinator, + quiet, + }) + + const success = true + await subtask.finish(success) + }) diff --git a/contracts/tasks/runners/publishTallyResults.ts b/contracts/tasks/runners/publishTallyResults.ts new file mode 100644 index 000000000..5651ac353 --- /dev/null +++ b/contracts/tasks/runners/publishTallyResults.ts @@ -0,0 +1,183 @@ +/** + * Script for tallying votes which involves fetching MACI logs, generating proofs, + * and proving on chain + * + * Make sure to set the following environment variables in the .env file + * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC + * - coordinator's wallet private key to interact with contracts + * 2) PINATA_API_KEY - The Pinata api key for pinning file to IPFS + * 3) PINATA_SECRET_API_KEY - The Pinata secret api key for pinning file to IPFS + * + * Sample usage: + * + * yarn hardhat publish-tally-results --clrfund + * --proof-dir --network + * + */ +import { BaseContract, getNumber, NonceManager } from 'ethers' +import { task, types } from 'hardhat/config' + +import { Ipfs } from '../../utils/ipfs' +import { JSONFile } from '../../utils/JSONFile' +import { addTallyResultsBatch, TallyData, verify } from '../../utils/maci' +import { FundingRound, Poll } from '../../typechain-types' +import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' +import { EContracts } from '../../utils/types' +import { Subtask } from '../helpers/Subtask' +import { getCurrentFundingRoundContract } from '../../utils/contracts' +import { getTalyFilePath } from '../../utils/misc' +import { ContractStorage } from '../helpers/ContractStorage' +import { PINATA_PINNING_URL } from '../../utils/constants' + +/** + * Publish the tally IPFS hash on chain if it's not already published + * @param fundingRoundContract Funding round contract + * @param tallyHash Tally hash + */ +async function publishTallyHash( + fundingRoundContract: FundingRound, + tallyHash: string +) { + console.log(`Tally hash is ${tallyHash}`) + + const tallyHashOnChain = await fundingRoundContract.tallyHash() + if (tallyHashOnChain !== tallyHash) { + const tx = await fundingRoundContract.publishTallyHash(tallyHash) + const receipt = await tx.wait() + if (receipt?.status !== 1) { + throw new Error('Failed to publish tally hash on chain') + } + + console.log('Published tally hash on chain') + } +} +/** + * Submit tally data to funding round contract + * @param fundingRoundContract Funding round contract + * @param batchSize Number of tally results per batch + * @param tallyData Tally file content + */ +async function submitTallyResults( + fundingRoundContract: FundingRound, + recipientTreeDepth: number, + tallyData: TallyData, + batchSize: number +) { + const startIndex = await fundingRoundContract.totalTallyResults() + const total = tallyData.results.tally.length + if (startIndex < total) { + console.log('Uploading tally results in batches of', batchSize) + } + const addTallyGas = await addTallyResultsBatch( + fundingRoundContract, + recipientTreeDepth, + tallyData, + getNumber(batchSize), + getNumber(startIndex), + (processed: number) => { + console.log(`Processed ${processed} / ${total}`) + } + ) + console.log('Tally results uploaded. Gas used:', addTallyGas.toString()) +} + +/** + * Get the recipient tree depth (aka vote option tree depth) + * @param fundingRoundContract Funding round conract + * @param ethers Hardhat Ethers Helper + * @returns Recipient tree depth + */ +async function getRecipientTreeDepth( + fundingRoundContract: FundingRound, + ethers: HardhatEthersHelpers +): Promise { + const pollAddress = await fundingRoundContract.poll() + const pollContract = await ethers.getContractAt(EContracts.Poll, pollAddress) + const treeDepths = await (pollContract as BaseContract as Poll).treeDepths() + const voteOptionTreeDepth = treeDepths.voteOptionTreeDepth + return getNumber(voteOptionTreeDepth) +} + +task('publish-tally-results', 'Publish tally results') + .addOptionalParam('clrfund', 'ClrFund contract address') + .addParam('proofDir', 'The proof output directory') + .addOptionalParam( + 'batchSize', + 'The batch size to upload tally result on-chain', + 8, + types.int + ) + .addFlag('manageNonce', 'Whether to manually manage transaction nonce') + .addFlag('quiet', 'Whether to log on the console') + .setAction( + async ({ clrfund, proofDir, batchSize, manageNonce, quiet }, hre) => { + const { ethers, network } = hre + const storage = ContractStorage.getInstance() + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) + + const [signer] = await ethers.getSigners() + if (!signer) { + throw new Error('Env. variable WALLET_PRIVATE_KEY not set') + } + const coordinator = manageNonce ? new NonceManager(signer) : signer + console.log('Coordinator address: ', await coordinator.getAddress()) + + const apiKey = process.env.PINATA_API_KEY + if (!apiKey) { + throw new Error('Env. variable PINATA_API_KEY not set') + } + + const secretApiKey = process.env.PINATA_SECRET_API_KEY + if (!secretApiKey) { + throw new Error('Env. variable PINATA_SECRET_API_KEY not set') + } + + await subtask.logStart() + + const clrfundContractAddress = + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) + const fundingRoundContract = await getCurrentFundingRoundContract( + clrfundContractAddress, + coordinator, + ethers + ) + console.log('Funding round contract', fundingRoundContract.target) + + const recipientTreeDepth = await getRecipientTreeDepth( + fundingRoundContract, + ethers + ) + + const tallyFile = getTalyFilePath(proofDir) + const tallyData = JSONFile.read(tallyFile) + const tallyAddress = await fundingRoundContract.tally() + + await verify({ + pollId: BigInt(tallyData.pollId), + subsidyEnabled: false, + tallyData, + maciAddress: tallyData.maci, + tallyAddress, + signer: coordinator, + quiet, + }) + + const tallyHash = await Ipfs.pinFile(tallyFile, apiKey, secretApiKey) + + // Publish tally hash if it is not already published + await publishTallyHash(fundingRoundContract, tallyHash) + + // Submit tally results to the funding round contract + // This function can be re-run from where it left off + await submitTallyResults( + fundingRoundContract, + recipientTreeDepth, + tallyData, + batchSize + ) + + const success = true + await subtask.finish(success) + } + ) diff --git a/contracts/tasks/runners/resetTally.ts b/contracts/tasks/runners/resetTally.ts new file mode 100644 index 000000000..adc9ebb4e --- /dev/null +++ b/contracts/tasks/runners/resetTally.ts @@ -0,0 +1,53 @@ +/** + * WARNING: + * This script will create a new instance of the tally contract in the funding round contract + * + * Usage: + * hardhat resetTally --funding-round --network + * + * Note: + * 1) This script needs to be run by the coordinator + * 2) It can only be run if the funding round hasn't been finalized + */ +import { task } from 'hardhat/config' +import { getCurrentFundingRoundContract } from '../../utils/contracts' +import { Subtask } from '../helpers/Subtask' + +task('reset-tally', 'Reset the tally contract') + .addParam('clrfund', 'The clrfund contract address') + .setAction(async ({ clrfund }, hre) => { + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) + + let success = false + try { + await subtask.logStart() + + const [coordinator] = await hre.ethers.getSigners() + console.log('Coordinator address: ', await coordinator.getAddress()) + + const fundingRoundContract = await getCurrentFundingRoundContract( + clrfund, + coordinator, + hre.ethers + ) + + const tx = await fundingRoundContract.resetTally() + const receipt = await tx.wait() + if (receipt?.status !== 1) { + throw new Error('Failed to reset the tally contract') + } + + subtask.logTransaction(tx) + success = true + } catch (err) { + console.error( + '\n=========================================================\nERROR:', + err, + '\n' + ) + success = false + } + + await subtask.finish(success) + }) 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/tally.ts b/contracts/tasks/runners/tally.ts index eb135b3e8..94482b679 100644 --- a/contracts/tasks/runners/tally.ts +++ b/contracts/tasks/runners/tally.ts @@ -1,177 +1,43 @@ /** * Script for tallying votes which involves fetching MACI logs, generating proofs, - * and proving on chain - * - * This script can be rerun by passing in --maci-state-file and --tally-file - * If the --maci-state-file is passed, it will skip MACI log fetching - * If the --tally-file is passed, it will skip MACI log fetching and proof generation - * - * Make sure to set the following environment variables in the .env file - * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC - * - coordinator's wallet private key to interact with contracts - * 2) COORDINATOR_MACISK - coordinator's MACI private key to decrypt messages + * proving on chain, and uploading tally results on chain * * Sample usage: - * * yarn hardhat tally --clrfund --maci-tx-hash --network * - * To rerun: - * - * yarn hardhat tally --clrfund --maci-state-file \ - * --tally-file --network + * This script can be re-run with the same input parameters */ -import { BaseContract, getNumber, Signer, NonceManager } from 'ethers' +import { getNumber } from 'ethers' import { task, types } from 'hardhat/config' +import { ClrFund } from '../../typechain-types' import { DEFAULT_SR_QUEUE_OPS, DEFAULT_GET_LOG_BATCH_SIZE, } from '../../utils/constants' -import { getIpfsHash } from '../../utils/ipfs' -import { JSONFile } from '../../utils/JSONFile' -import { - getGenProofArgs, - genProofs, - proveOnChain, - addTallyResultsBatch, - mergeMaciSubtrees, - genLocalState, - TallyData, -} from '../../utils/maci' -import { getMaciStateFilePath, getDirname } from '../../utils/misc' -import { FundingRound, Poll, Tally } from '../../typechain-types' -import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' +import { getProofDirForRound } from '../../utils/misc' import { EContracts } from '../../utils/types' import { ContractStorage } from '../helpers/ContractStorage' import { Subtask } from '../helpers/Subtask' -/** - * Publish the tally IPFS hash on chain if it's not already published - * @param fundingRoundContract Funding round contract - * @param tallyData Tally data - */ -async function publishTallyHash( - fundingRoundContract: FundingRound, - tallyData: TallyData -) { - const tallyHash = await getIpfsHash(tallyData) - console.log(`Tally hash is ${tallyHash}`) - - const tallyHashOnChain = await fundingRoundContract.tallyHash() - if (tallyHashOnChain !== tallyHash) { - const tx = await fundingRoundContract.publishTallyHash(tallyHash) - const receipt = await tx.wait() - if (receipt?.status !== 1) { - throw new Error('Failed to publish tally hash on chain') - } - - console.log('Published tally hash on chain') - } -} -/** - * Submit tally data to funding round contract - * @param fundingRoundContract Funding round contract - * @param batchSize Number of tally results per batch - * @param tallyData Tally file content - */ -async function submitTallyResults( - fundingRoundContract: FundingRound, - recipientTreeDepth: number, - tallyData: TallyData, - batchSize: number -) { - const startIndex = await fundingRoundContract.totalTallyResults() - const total = tallyData.results.tally.length - console.log('Uploading tally results in batches of', batchSize) - const addTallyGas = await addTallyResultsBatch( - fundingRoundContract, - recipientTreeDepth, - tallyData, - getNumber(batchSize), - getNumber(startIndex), - (processed: number) => { - console.log(`Processed ${processed} / ${total}`) - } - ) - console.log('Tally results uploaded. Gas used:', addTallyGas.toString()) -} - -/** - * Return the current funding round contract handle - * @param clrfund ClrFund contract address - * @param coordinator Signer who will interact with the funding round contract - * @param hre Hardhat runtime environment - */ -async function getFundingRound( - clrfund: string, - coordinator: Signer, - ethers: HardhatEthersHelpers -): Promise { - const clrfundContract = await ethers.getContractAt( - EContracts.ClrFund, - clrfund, - coordinator - ) - - const fundingRound = await clrfundContract.getCurrentRound() - const fundingRoundContract = await ethers.getContractAt( - EContracts.FundingRound, - fundingRound, - coordinator - ) - - return fundingRoundContract as BaseContract as FundingRound -} - -/** - * Get the recipient tree depth (aka vote option tree depth) - * @param fundingRoundContract Funding round conract - * @param ethers Hardhat Ethers Helper - * @returns Recipient tree depth - */ -async function getRecipientTreeDepth( - fundingRoundContract: FundingRound, - ethers: HardhatEthersHelpers -): Promise { - const pollAddress = await fundingRoundContract.poll() - const pollContract = await ethers.getContractAt(EContracts.Poll, pollAddress) - const treeDepths = await (pollContract as BaseContract as Poll).treeDepths() - const voteOptionTreeDepth = treeDepths.voteOptionTreeDepth - return getNumber(voteOptionTreeDepth) -} - -/** - * Get the message processor contract address from the tally contract - * @param tallyAddress Tally contract address - * @param ethers Hardhat ethers helper - * @returns Message processor contract address - */ -async function getMessageProcessorAddress( - tallyAddress: string, - ethers: HardhatEthersHelpers -): Promise { - const tallyContract = (await ethers.getContractAt( - EContracts.Tally, - tallyAddress - )) as BaseContract as Tally - - const messageProcessorAddress = await tallyContract.messageProcessor() - return messageProcessorAddress -} - task('tally', 'Tally votes') .addOptionalParam('clrfund', 'ClrFund contract address') .addOptionalParam('maciTxHash', 'MACI creation transaction hash') - .addOptionalParam('maciStateFile', 'MACI state file') + .addOptionalParam( + 'maciStartBlock', + 'MACI creation block', + undefined, + types.int + ) .addFlag('manageNonce', 'Whether to manually manage transaction nonce') - .addOptionalParam('tallyFile', 'The tally file path') .addOptionalParam( 'batchSize', 'The batch size to upload tally result on-chain', - 10, + 8, types.int ) - .addParam('outputDir', 'The proof output directory', './proof_output') + .addParam('proofDir', 'The proof output directory', './proof_output') + .addParam('paramsDir', 'The circuit zkeys directory', './params') .addOptionalParam('rapidsnark', 'The rapidsnark prover path') .addOptionalParam( 'numQueueOps', @@ -197,11 +63,11 @@ task('tally', 'Tally votes') { clrfund, maciTxHash, + maciStartBlock, quiet, - maciStateFile, - outputDir, + proofDir, + paramsDir, numQueueOps, - tallyFile, blocksPerBatch, rapidsnark, sleep, @@ -212,140 +78,70 @@ task('tally', 'Tally votes') ) => { console.log('Verbose logging enabled:', !quiet) - const { ethers, network } = hre - const storage = ContractStorage.getInstance() - const subtask = Subtask.getInstance(hre) - subtask.setHre(hre) - - const [coordinatorSigner] = await ethers.getSigners() - if (!coordinatorSigner) { - throw new Error('Env. variable WALLET_PRIVATE_KEY not set') + const apiKey = process.env.PINATA_API_KEY + if (!apiKey) { + throw new Error('Env. variable PINATA_API_KEY not set') } - const coordinator = manageNonce - ? new NonceManager(coordinatorSigner) - : coordinatorSigner - console.log('Coordinator address: ', await coordinator.getAddress()) - const coordinatorMacisk = process.env.COORDINATOR_MACISK - if (!coordinatorMacisk) { - throw new Error('Env. variable COORDINATOR_MACISK not set') + const secretApiKey = process.env.PINATA_SECRET_API_KEY + if (!secretApiKey) { + throw new Error('Env. variable PINATA_SECRET_API_KEY not set') } - const circuit = subtask.getConfigField( - EContracts.VkRegistry, - 'circuit' - ) - const circuitDirectory = subtask.getConfigField( - EContracts.VkRegistry, - 'paramsDirectory' - ) + const storage = ContractStorage.getInstance() + const subtask = Subtask.getInstance(hre) + subtask.setHre(hre) await subtask.logStart() const clrfundContractAddress = - clrfund ?? storage.mustGetAddress(EContracts.ClrFund, network.name) - const fundingRoundContract = await getFundingRound( - clrfundContractAddress, - coordinator, - ethers - ) - console.log('Funding round contract', fundingRoundContract.target) - - const recipientTreeDepth = await getRecipientTreeDepth( - fundingRoundContract, - ethers - ) + clrfund ?? storage.mustGetAddress(EContracts.ClrFund, hre.network.name) - const pollId = await fundingRoundContract.pollId() - console.log('PollId', pollId) + const clrfundContract = subtask.getContract({ + name: EContracts.ClrFund, + address: clrfundContractAddress, + }) - const maciAddress = await fundingRoundContract.maci() - const maciTransactionHash = - maciTxHash ?? storage.getTxHash(maciAddress, network.name) - console.log('MACI address', maciAddress) + const fundingRoundContractAddress = await ( + await clrfundContract + ).getCurrentRound() - const tallyAddress = await fundingRoundContract.tally() - const messageProcessorAddress = await getMessageProcessorAddress( - tallyAddress, - ethers + const outputDir = getProofDirForRound( + proofDir, + hre.network.name, + fundingRoundContractAddress ) - const providerUrl = (network.config as any).url - - const outputPath = maciStateFile - ? maciStateFile - : getMaciStateFilePath(outputDir) - - await mergeMaciSubtrees({ - maciAddress, - pollId, + await hre.run('gen-proofs', { + clrfund: clrfundContractAddress, + maciStartBlock, + maciTxHash, numQueueOps, - signer: coordinator, + blocksPerBatch, + rapidsnark, + sleep, + proofDir: outputDir, + paramsDir, + manageNonce, quiet, }) - let tallyFilePath: string = tallyFile || '' - if (!tallyFile) { - if (!maciStateFile) { - await genLocalState({ - quiet, - outputPath, - pollId, - maciContractAddress: maciAddress, - coordinatorPrivateKey: coordinatorMacisk, - ethereumProvider: providerUrl, - transactionHash: maciTransactionHash, - blockPerBatch: blocksPerBatch, - signer: coordinator, - sleep, - }) - } - - const genProofArgs = getGenProofArgs({ - maciAddress, - pollId, - coordinatorMacisk, - rapidsnark, - circuitType: circuit, - circuitDirectory, - outputDir, - blocksPerBatch: getNumber(blocksPerBatch), - maciTxHash: maciTransactionHash, - maciStateFile: outputPath, - signer: coordinator, - quiet, - }) - await genProofs(genProofArgs) - tallyFilePath = genProofArgs.tallyFile - } - - const tally = JSONFile.read(tallyFilePath) as TallyData - const proofDir = getDirname(tallyFilePath) - console.log('Proof directory', proofDir) - // proveOnChain if not already processed - await proveOnChain({ - pollId, - proofDir, - subsidyEnabled: false, - maciAddress, - messageProcessorAddress, - tallyAddress, - signer: coordinator, + await hre.run('prove-on-chain', { + clrfund: clrfundContractAddress, + proofDir: outputDir, + manageNonce, quiet, }) // Publish tally hash if it is not already published - await publishTallyHash(fundingRoundContract, tally) - - // Submit tally results to the funding round contract - // This function can be re-run from where it left off - await submitTallyResults( - fundingRoundContract, - recipientTreeDepth, - tally, - batchSize - ) + await hre.run('publish-tally-results', { + clrfund: clrfundContractAddress, + proofDir: outputDir, + batchSize, + manageNonce, + quiet, + }) const success = true await subtask.finish(success) diff --git a/contracts/tasks/runners/verifyAll.ts b/contracts/tasks/runners/verifyAll.ts index ab4647563..c010df8ff 100644 --- a/contracts/tasks/runners/verifyAll.ts +++ b/contracts/tasks/runners/verifyAll.ts @@ -1,108 +1,333 @@ /* 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 + * @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..284689063 --- /dev/null +++ b/contracts/tasks/runners/verifyDeployer.ts @@ -0,0 +1,33 @@ +import { task } from 'hardhat/config' +import { EContracts } from '../../utils/types' +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/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/contracts.ts b/contracts/utils/contracts.ts index e60d11a76..45250e3fb 100644 --- a/contracts/utils/contracts.ts +++ b/contracts/utils/contracts.ts @@ -2,6 +2,7 @@ import { BaseContract, ContractTransactionResponse, TransactionResponse, + Signer, } from 'ethers' import { getEventArg } from '@clrfund/common' import { EContracts } from './types' @@ -9,7 +10,7 @@ import { DeployContractOptions, HardhatEthersHelpers, } from '@nomicfoundation/hardhat-ethers/types' -import { VkRegistry } from '../typechain-types' +import { VkRegistry, FundingRound } from '../typechain-types' import { MaciParameters } from './maciParameters' import { IVerifyingKeyStruct } from 'maci-contracts' @@ -89,4 +90,30 @@ export async function getTxFee( return receipt ? BigInt(receipt.gasUsed) * BigInt(receipt.gasPrice) : 0n } +/** + * Return the current funding round contract handle + * @param clrfund ClrFund contract address + * @param signer Signer who will interact with the funding round contract + * @param hre Hardhat runtime environment + */ +export async function getCurrentFundingRoundContract( + clrfund: string, + signer: Signer, + ethers: HardhatEthersHelpers +): Promise { + const clrfundContract = await ethers.getContractAt( + EContracts.ClrFund, + clrfund, + signer + ) + + const fundingRound = await clrfundContract.getCurrentRound() + const fundingRoundContract = await ethers.getContractAt( + EContracts.FundingRound, + fundingRound, + signer + ) + + return fundingRoundContract as BaseContract as FundingRound +} export { getEventArg } diff --git a/contracts/utils/ipfs.ts b/contracts/utils/ipfs.ts index 8fc5de275..092d9a876 100644 --- a/contracts/utils/ipfs.ts +++ b/contracts/utils/ipfs.ts @@ -2,7 +2,9 @@ const Hash = require('ipfs-only-hash') import { FetchRequest } from 'ethers' import { DEFAULT_IPFS_GATEWAY } from './constants' - +import fs from 'fs' +import path from 'path' +import pinataSDK from '@pinata/sdk' /** * Get the ipfs hash for the input object * @param object a json object to get the ipfs hash for @@ -26,4 +28,28 @@ export class Ipfs { const resp = await req.send() return resp.bodyJson } + + /** + * Pin a file to IPFS + * @param file The file path to be uploaded to IPFS + * @param apiKey Pinata api key + * @param secretApiKey Pinata secret api key + * @returns IPFS hash + */ + static async pinFile( + file: string, + apiKey: string, + secretApiKey: string + ): Promise { + const pinata = new pinataSDK(apiKey, secretApiKey) + const data = fs.createReadStream(file) + const name = path.basename(file) + const options = { + pinataMetadata: { + name, + }, + } + const res = await pinata.pinFileToIPFS(data, options) + return res.IpfsHash + } } diff --git a/contracts/utils/maci.ts b/contracts/utils/maci.ts index 6065f383d..bb3e3bb0b 100644 --- a/contracts/utils/maci.ts +++ b/contracts/utils/maci.ts @@ -183,6 +183,8 @@ type getGenProofArgsInput = { endBlock?: number // MACI state file maciStateFile?: string + // Tally output file + tallyFile: string // transaction signer signer: Signer // flag to turn on verbose logging in MACI cli @@ -206,12 +208,11 @@ export function getGenProofArgs(args: getGenProofArgsInput): GenProofsArgs { startBlock, endBlock, maciStateFile, + tallyFile, signer, quiet, } = args - const tallyFile = getTalyFilePath(outputDir) - const { processZkFile, tallyZkFile, diff --git a/contracts/utils/misc.ts b/contracts/utils/misc.ts index 34bb53482..9f7a34297 100644 --- a/contracts/utils/misc.ts +++ b/contracts/utils/misc.ts @@ -19,6 +19,29 @@ export function getMaciStateFilePath(directory: string) { return path.join(directory, 'maci-state.json') } +/** + * Return the proof output directory + * @param directory The root directory + * @param network The network + * @param roundAddress The funding round contract address + * @returns The proofs output directory + */ +export function getProofDirForRound( + directory: string, + network: string, + roundAddress: string +) { + try { + return path.join( + directory, + network.toLowerCase(), + roundAddress.toLowerCase() + ) + } catch { + return directory + } +} + /** * Check if the path exist * @param path The path to check for existence @@ -29,10 +52,9 @@ export function isPathExist(path: string): boolean { } /** - * Returns the directory of the path - * @param file The file path - * @returns The directory of the file + * Create a directory + * @param directory The directory to create */ -export function getDirname(file: string): string { - return path.dirname(file) +export function makeDirectory(directory: string): void { + fs.mkdirSync(directory, { recursive: true }) } diff --git a/contracts/utils/parsers/RequestResolvedParser.ts b/contracts/utils/parsers/RequestResolvedParser.ts index 271cb563e..7b55c97a4 100644 --- a/contracts/utils/parsers/RequestResolvedParser.ts +++ b/contracts/utils/parsers/RequestResolvedParser.ts @@ -15,7 +15,7 @@ export class RequestResolvedParser extends BaseParser { const timestamp = toDate(args._timestamp) let state = - args._type === 1 ? RecipientState.Removed : RecipientState.Accepted + args._type === 1n ? RecipientState.Removed : RecipientState.Accepted if (args._rejected) { state = RecipientState.Rejected 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) } } 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', diff --git a/docs/tally-verify.md b/docs/tally-verify.md index b2edd2a50..9ce6bc7dd 100644 --- a/docs/tally-verify.md +++ b/docs/tally-verify.md @@ -18,34 +18,23 @@ COORDINATOR_MACISK= # private key for interacting with contracts WALLET_MNEMONIC= WALLET_PRIVATE_KEY -``` - -Decrypt messages and tally the votes: +# credential to upload tally result to IPFS +PINATA_API_KEY= +PINATA_SECRET_API_KEY= ``` -yarn hardhat tally --rapidsnark {RAPID_SNARK} --output-dir {OUTPUT_DIR} --network {network} -``` - -You only need to provide `--rapidsnark` if you are running the `tally` command on an intel chip. -If there's error and the tally task was stopped prematurely, it can be resumed by passing 2 additional parameters, '--tally-file' and/or '--maci-state-file', if the files were generated. +Decrypt messages, tally the votes: ``` -# for rerun -yarn hardhat tally --maci-state-file {maci-state.json} --tally-file {tally.json} --output-dir {OUTPUT_DIR} --network {network} +yarn hardhat tally --clrfund {CLRFUND_CONTRACT_ADDRESS} --maci-tx-hash {MACI_CREATION_TRANSACTION_HASH} --proof-dir {OUTPUT_DIR} --rapidsnark {RAPID_SNARK} --network {network} ``` -Result will be saved to `tally.json` file, which must then be published via IPFS. - -**Using [command line](https://docs.ipfs.tech/reference/kubo/cli/#ipfs)** - +You only need to provide `--rapidsnark` if you are running the `tally` command on an intel chip. +If the `tally` script failed, you can rerun the command with the same parameters. ``` -# start ipfs daemon in one terminal -ipfs daemon -# in a diff terminal, go to `/contracts` (or where you have the file) and publish the file -ipfs add tally.json -``` +Result will be saved to `{OUTPUT_DIR}/{network}-{fundingRoundAddress}/tally.json` file, which is also available on IPFS at `https://{ipfs-gateway-host}/ipfs/{tally-hash}`. ### Finalize round @@ -60,7 +49,7 @@ WALLET_PRIVATE_KEY= Once you have the `tally.json` from the tally script, run: ``` -yarn hardhat finalize --tally-file {tally.json} --network {network} +yarn hardhat finalize --clrfund {CLRFUND_CONTRACT_ADDRESS} --proof-dir {OUTPUT_DIR} --network {network} ``` # How to verify the tally results diff --git a/subgraph/package.json b/subgraph/package.json index 3b9809da7..e764fefc9 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -1,6 +1,6 @@ { "name": "@clrfund/subgraph", - "version": "5.1.0", + "version": "5.1.1", "repository": "https://github.com/clrfund/monorepo/subgraph", "keywords": [ "clr.fund", diff --git a/vue-app/package.json b/vue-app/package.json index e5317799f..1c7267fbd 100644 --- a/vue-app/package.json +++ b/vue-app/package.json @@ -1,6 +1,6 @@ { "name": "@clrfund/vue-app", - "version": "5.1.0", + "version": "5.1.1", "private": true, "license": "GPL-3.0", "type": "module", diff --git a/vue-app/src/App.vue b/vue-app/src/App.vue index 1d0d676b7..3c7ad9b98 100644 --- a/vue-app/src/App.vue +++ b/vue-app/src/App.vue @@ -369,6 +369,7 @@ summary:focus { padding: $modal-space; text-align: center; box-shadow: var(--box-shadow); + border: 2px solid rgba(115, 117, 166, 0.3); width: 400px; .loader { margin: $modal-space auto; diff --git a/vue-app/src/components.d.ts b/vue-app/src/components.d.ts index cd20f3947..5f037b3de 100644 --- a/vue-app/src/components.d.ts +++ b/vue-app/src/components.d.ts @@ -12,6 +12,7 @@ declare module '@vue/runtime-core' { AddToCartButton: typeof import('./components/AddToCartButton.vue')['default'] BackLink: typeof import('./components/BackLink.vue')['default'] BalanceItem: typeof import('./components/BalanceItem.vue')['default'] + BaseModal: typeof import('./components/BaseModal.vue')['default'] Breadcrumbs: typeof import('./components/Breadcrumbs.vue')['default'] BrightIdWidget: typeof import('./components/BrightIdWidget.vue')['default'] CallToActionCard: typeof import('./components/CallToActionCard.vue')['default'] diff --git a/vue-app/src/components/BaseModal.vue b/vue-app/src/components/BaseModal.vue new file mode 100644 index 000000000..fef021f8b --- /dev/null +++ b/vue-app/src/components/BaseModal.vue @@ -0,0 +1,9 @@ + + + diff --git a/vue-app/src/components/ClaimModal.vue b/vue-app/src/components/ClaimModal.vue index d6c2cd29c..9f0b3f2e3 100644 --- a/vue-app/src/components/ClaimModal.vue +++ b/vue-app/src/components/ClaimModal.vue @@ -1,5 +1,5 @@