diff --git a/.github/workflows/cancel-round.yml b/.github/workflows/cancel-round.yml index f9ae75226..8f17f17c9 100644 --- a/.github/workflows/cancel-round.yml +++ b/.github/workflows/cancel-round.yml @@ -3,7 +3,7 @@ name: Cancel current round on: workflow_dispatch env: - NODE_VERSION: 16.x + NODE_VERSION: 18.x SUBGRPAH_URL: "https://api.thegraph.com/subgraphs/name/clrfund/clrfund-arbitrum-goerli" WALLET_PRIVATE_KEY: ${{ secrets.ARBITRUM_GOERLI_COORDINATOR_WALLET_PRIVATE_KEY }} diff --git a/.github/workflows/create-version.yml b/.github/workflows/create-version.yml index 4d6a0effb..b9a3841e2 100644 --- a/.github/workflows/create-version.yml +++ b/.github/workflows/create-version.yml @@ -12,7 +12,7 @@ on: - major env: - NODE_VERSION: 16.x + NODE_VERSION: 18.x VITE_CLRFUND_FACTORY_ADDRESS: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" VITE_ETHEREUM_API_CHAINID: 1 VITE_ETHEREUM_API_URL: "dummy" diff --git a/.github/workflows/finalize-round.yml b/.github/workflows/finalize-round.yml index e31894c37..06446df4f 100644 --- a/.github/workflows/finalize-round.yml +++ b/.github/workflows/finalize-round.yml @@ -13,13 +13,15 @@ on: default: 'yuetloo/clrfund-dev' env: - NODE_VERSION: 16.x + NODE_VERSION: 18.x NETWORK: "arbitrum-goerli" COORDINATOR_ETH_PK: ${{ secrets.ARBITRUM_GOERLI_COORDINATOR_WALLET_PRIVATE_KEY }} COORDINATOR_PK: ${{ secrets.ARBITRUM_GOERLI_COORDINATOR_MACI_PRIVATE_KEY }} JSONRPC_HTTP_URL: ${{ secrets.JSONRPC_HTTP_URL }} ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY_ARBITRUM }} ETHERSCAN_URL: "https://api-goerli.arbiscan.io" + CIRCUIT_TYPE: micro + ZKEYS_DOWNLOAD_SCRIPT: "download-6-8-2-3.sh" jobs: finalize: @@ -47,7 +49,7 @@ jobs: - name: Download batch 64 params run: | ls -la $GITHUB_WORKSPACE - $GITHUB_WORKSPACE/.github/scripts/download-batch64-params.sh + $GITHUB_WORKSPACE/monorepo/.github/scripts/${ZKEYS_DOWNLOAD_SCRIPT} - name: Build run: | # use https to avoid error: unable to connect to github.com diff --git a/.github/workflows/new-round.yml b/.github/workflows/new-round.yml index 25e00ae58..a12258667 100644 --- a/.github/workflows/new-round.yml +++ b/.github/workflows/new-round.yml @@ -3,7 +3,7 @@ name: Create new round on: workflow_dispatch env: - NODE_VERSION: 16.x + NODE_VERSION: 18.x SUBGRPAH_URL: "https://api.thegraph.com/subgraphs/name/clrfund/clrfund-arbitrum-goerli" WALLET_PRIVATE_KEY: ${{ secrets.ARBITRUM_GOERLI_COORDINATOR_WALLET_PRIVATE_KEY }} diff --git a/.github/workflows/test-contracts.yml b/.github/workflows/test-contracts.yml index 33e2ff21f..203a32d73 100644 --- a/.github/workflows/test-contracts.yml +++ b/.github/workflows/test-contracts.yml @@ -7,7 +7,7 @@ on: - 'contracts/**' env: - NODE_VERSION: 16.x + NODE_VERSION: 18.x jobs: test-contracts: @@ -22,7 +22,7 @@ jobs: - name: Install run: | git config --global url."https://".insteadOf git:// - yarn + yarn && yarn build - name: Run tests run: yarn test:contracts diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 9725af99a..2e4faa13c 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -7,7 +7,7 @@ on: - 'develop' env: - NODE_VERSION: 16.x + NODE_VERSION: 18.x CIRCUIT_TYPE: micro ZKEYS_DOWNLOAD_SCRIPT: "download-6-8-2-3.sh" @@ -23,20 +23,6 @@ jobs: run: | sudo apt update sudo apt-get install cmake build-essential libgmp-dev libsodium-dev nlohmann-json3-dev nasm g++ curl - - name: Install rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - name: Checkout circom - uses: actions/checkout@v3 - with: - repository: iden3/circom - path: circom - - name: Build circom - run: | - cd circom - cargo build --release - cargo install --path circom - name: Checkout rapidsnark source code uses: actions/checkout@v3 with: @@ -52,12 +38,6 @@ jobs: mkdir build_prover && cd build_prover cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../package make -j4 && make install - - name: Find rapidsnark - run: | - cd rapidsnark - pwd - echo "$GITHUB_WORKSPACE" - find . -name "prover" - name: Checkout Clrfund uses: actions/checkout@v3 with: diff --git a/.github/workflows/test-scripts.yml b/.github/workflows/test-scripts.yml index c115edf7b..72394b053 100644 --- a/.github/workflows/test-scripts.yml +++ b/.github/workflows/test-scripts.yml @@ -8,7 +8,8 @@ on: - '.github/workflows/test-scripts.yml' env: - NODE_VERSION: 16.x + NODE_VERSION: 18.x + ZKEYS_DOWNLOAD_SCRIPT: "download-6-8-2-3.sh" jobs: script-tests: @@ -22,13 +23,6 @@ jobs: run: | sudo apt update sudo apt-get install build-essential libgmp-dev libsodium-dev nlohmann-json3-dev nasm g++ curl - - name: Install rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - name: Install zkutil - run: | - cargo install zkutil --version 0.3.2 - name: Checkout rapidsnark source code uses: actions/checkout@v3 with: @@ -44,19 +38,13 @@ jobs: mkdir build_prover && cd build_prover cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../package make -j4 && make install - - name: Find rapidsnark - run: | - cd rapidsnark - pwd - echo "$GITHUB_WORKSPACE" - find . -name "prover" - name: Checkout source code uses: actions/checkout@v3 with: path: monorepo - name: Download batch 64 params run: | - $GITHUB_WORKSPACE/monorepo/.github/scripts/download-6-8-2-3.sh + $GITHUB_WORKSPACE/monorepo/.github/scripts/${ZKEYS_DOWNLOAD_SCRIPT} - name: Build CLR run: | cd monorepo diff --git a/.prettierignore b/.prettierignore index f470d60db..40bd8ca39 100644 --- a/.prettierignore +++ b/.prettierignore @@ -22,6 +22,8 @@ contracts/state.json contracts/tally.json contracts/proofs.json contracts/proof_output +contracts/typechain-types +contracts/local-state.json # local env files .env diff --git a/README.md b/README.md index 3d4b47ef1..f2a3cb231 100644 --- a/README.md +++ b/README.md @@ -62,17 +62,17 @@ In a future version, we plan to address this by routing ETH and token contributi ## Development -### Install Node v16 with nvm +### Install Node v18 with nvm ```sh -nvm install 16 -nvm use 16 +nvm install 18 +nvm use 18 ``` -### Install the dependencies +### Install the dependencies and build ```sh -yarn +yarn && yarn build # Along with the dependencies, git hooks are also installed. At the end of the installation, you will see the following line after a successful setup. husky - Git hooks installed @@ -102,9 +102,8 @@ In a 2nd terminal you will need to run your graph node (more on this [here](docs/subgraph.md)) ```sh -# go to the thegraph repo directory and init the node -cd graph-node/docker -docker-compose up +cd subgraph/graph-node +docker compose up -d ``` And finally, in a 3rd terminal diff --git a/common/.mocharc.json b/common/.mocharc.json new file mode 100644 index 000000000..3495e1e77 --- /dev/null +++ b/common/.mocharc.json @@ -0,0 +1,5 @@ +{ + "extension": ["ts"], + "spec": "src/**/*.spec.ts", + "require": "ts-node/register" +} diff --git a/common/package.json b/common/package.json index 02a5d6773..0adae6344 100644 --- a/common/package.json +++ b/common/package.json @@ -2,22 +2,29 @@ "name": "@clrfund/common", "version": "0.0.1", "description": "Common utility functions used by clrfund scripts and app", - "main": "src/index", + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/esm/index.d.ts", "scripts": { - "build": "tsc", + "build": "yarn build:esm && yarn build:cjs && yarn test", + "build:esm": "tsc -p tsconfig.json --outDir build/esm --module ES6", + "build:cjs": "tsc -p tsconfig.json --outDir build/cjs", + "test": "mocha", "lint": "eslint 'src/**/*.ts'", "clean": "rm -rf build" }, "license": "GPL-3.0", "devDependencies": { "eslint": "^8.31.0", + "mocha": "^10.2.0", + "ts-node": "^10.9.2", "typescript": "^4.9.3" }, "dependencies": { "@openzeppelin/merkle-tree": "^1.0.5", - "ethers": "^5.7.2", - "@clrfund/maci-crypto": "^1.1.7", - "@clrfund/maci-domainobjs": "^1.1.7" + "ethers": "^6.9.2", + "maci-crypto": "0.0.0-ci.45d1156", + "maci-domainobjs": "0.0.0-ci.45d1156" }, "repository": { "type": "git", diff --git a/common/src/__tests__/math.spec.ts b/common/src/__tests__/math.spec.ts new file mode 100644 index 000000000..b7071a51e --- /dev/null +++ b/common/src/__tests__/math.spec.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai' +import { bnSqrt } from '../index' +import { toBigInt } from 'ethers' + +const UNIT = BigInt(10) ** BigInt(18) +const cases = [ + [0n, 0n], + [1n, 1n], + [4n, 2n], + [100n, 10n], +] + +/** + * Get a random number in range + * @param min minimum number + * @param max maximum number + * @returns random number + */ +function getRandom(min: number, max: number) { + return Math.floor(Math.random() * (max - min) + min) +} + +describe('bnSqrt', function () { + it('should throw if value less than 0', function () { + expect(bnSqrt.bind(bnSqrt, -1n)).to.throw('Complex numbers not support') + }) + + for (const test of cases) { + const [val, expected] = test + it(`bnSqrt(${val}) === ${expected}`, function () { + expect(bnSqrt(val)).to.eq(expected) + }) + } + + it('Sqrt(8) throws as it took too long to calculate', function () { + expect(bnSqrt.bind(bnSqrt, 8n)).to.throw('Sqrt took too long to calculate') + }) + + it('testing random bigints', function () { + for (let i = 0; i < 100; i++) { + const rand = toBigInt(getRandom(5, 200)) * UNIT + const sqrtVal = bnSqrt(rand) + const val = sqrtVal * sqrtVal + expect(bnSqrt(val)).to.eq(sqrtVal) + } + }) +}) diff --git a/common/src/block.ts b/common/src/block.ts index fd00cf3f4..5ee2db782 100644 --- a/common/src/block.ts +++ b/common/src/block.ts @@ -1,4 +1,4 @@ -import { providers, utils } from 'ethers' +import { type JsonRpcProvider, toQuantity } from 'ethers' export interface Block { blockNumber: number @@ -6,14 +6,17 @@ export interface Block { stateRoot: string } -/* +/** * get the block stateRoot using eth_getBlockByHash + * @param blockNumber The block number + * @param provider the JSON rpc provider + * @returns the block information with block number, block hash and state root */ export async function getBlock( blockNumber: number, - provider: providers.JsonRpcProvider + provider: JsonRpcProvider ): Promise { - const blockNumberHex = utils.hexValue(blockNumber) + const blockNumberHex = toQuantity(blockNumber) const blockParams = [blockNumberHex, false] const rawBlock = await provider.send('eth_getBlockByNumber', blockParams) return { blockNumber, hash: rawBlock.hash, stateRoot: rawBlock.stateRoot } diff --git a/common/src/event.ts b/common/src/event.ts new file mode 100644 index 000000000..39a87ba46 --- /dev/null +++ b/common/src/event.ts @@ -0,0 +1,36 @@ +import { Contract, TransactionResponse } from 'ethers' + +/** + * Get event log argument value + * @param transactionReceipt transaction + * @param contract Contract handle + * @param eventName event name + * @param argumentName argument name + * @returns event argument value + */ +export async function getEventArg( + transaction: TransactionResponse, + contract: Contract, + eventName: string, + argumentName: string +): Promise { + const transactionReceipt = await transaction.wait() + const contractAddress = await contract.getAddress() + // eslint-disable-next-line + for (const log of transactionReceipt?.logs || []) { + if (log.address !== contractAddress) { + continue + } + const event = contract.interface.parseLog({ + data: log.data, + topics: [...log.topics], + }) + // eslint-disable-next-line + if (event && event.name === eventName) { + return event.args[argumentName] + } + } + throw new Error( + `Event ${eventName} from contract ${contractAddress} not found in transaction ${transaction.hash}` + ) +} diff --git a/common/src/index.ts b/common/src/index.ts index b9a1324c2..d55811b4a 100644 --- a/common/src/index.ts +++ b/common/src/index.ts @@ -5,3 +5,5 @@ export * from './ipfs' export * from './keypair' export * from './tally' export * from './utils' +export * from './event' +export * from './math' diff --git a/common/src/ipfs.ts b/common/src/ipfs.ts index 4254607e8..84e864c92 100644 --- a/common/src/ipfs.ts +++ b/common/src/ipfs.ts @@ -1,4 +1,4 @@ -import { utils } from 'ethers' +import { FetchRequest } from 'ethers' const IPFS_BASE_URL = 'https://ipfs.io' @@ -13,6 +13,6 @@ export async function getIpfsContent( gatewayUrl = IPFS_BASE_URL ): Promise { const url = `${gatewayUrl}/ipfs/${hash}` - const result = utils.fetchJson(url) - return result + const req = new FetchRequest(url) + return req.send() } diff --git a/common/src/keypair.ts b/common/src/keypair.ts index 5434b01e6..8e02ab52f 100644 --- a/common/src/keypair.ts +++ b/common/src/keypair.ts @@ -1,9 +1,5 @@ -import { utils, BigNumber } from 'ethers' -import { - Keypair as MaciKeypair, - PrivKey, - PubKey, -} from '@clrfund/maci-domainobjs' +import { keccak256, isBytesLike, concat, toBeArray } from 'ethers' +import { Keypair as MaciKeypair, PrivKey, PubKey } from 'maci-domainobjs' const SNARK_FIELD_SIZE = BigInt( '21888242871839275222246405745257275088548364400416034343698204186575808495617' @@ -12,7 +8,7 @@ const SNARK_FIELD_SIZE = BigInt( /** * Returns a BabyJub-compatible value. This function is modified from * the MACI's genRandomBabyJubValue(). Instead of returning random value - * for the private key, it derives the private key from the users', + * for the private key, it derives the private key from the users * signature hash * @param hash - user's signature hash */ @@ -20,28 +16,22 @@ function genPrivKey(hash: string): PrivKey { // Prevent modulo bias //const lim = BigInt('0x10000000000000000000000000000000000000000000000000000000000000000') //const min = (lim - SNARK_FIELD_SIZE) % SNARK_FIELD_SIZE - const min = BigNumber.from( + const min = BigInt( '6350874878119819312338956282401532410528162663560392320966563075034087161851' ) - if (!utils.isBytesLike(hash)) { + if (!isBytesLike(hash)) { throw new Error(`Hash must be a hex string: ${hash}`) } - let hashBN = BigNumber.from(hash) + let hashBN = BigInt(hash) // don't think we'll enter the for loop below, but, just in case - for (let counter = 1; min.gte(hashBN); counter++) { - const data = utils.concat([hashBN.toHexString(), utils.arrayify(counter)]) - hashBN = BigNumber.from(utils.keccak256(data)) - } - - const rawPrivKey = BigInt(hashBN.toString()) % SNARK_FIELD_SIZE - if (rawPrivKey >= SNARK_FIELD_SIZE) { - throw new Error( - `privKey ${rawPrivKey} must be less than SNARK_FIELD_SIZE ${SNARK_FIELD_SIZE}` - ) + for (let counter = 1; hashBN < min; counter++) { + const data = concat([toBeArray(hashBN), toBeArray(counter)]) + hashBN = BigInt(keccak256(data)) } + const rawPrivKey = hashBN % SNARK_FIELD_SIZE return new PrivKey(rawPrivKey) } diff --git a/common/src/math.ts b/common/src/math.ts new file mode 100644 index 000000000..61665d32d --- /dev/null +++ b/common/src/math.ts @@ -0,0 +1,30 @@ +/** + * Get the square root of a bigint + * @param val the value to apply square root on + */ +export function bnSqrt(val: bigint): bigint { + // Take square root from a bigint + // https://stackoverflow.com/a/52468569/1868395 + if (val < 0n) { + throw new Error('Complex numbers not support') + } + + if (val < 2n) { + return val + } + + let loop = 100 + let x: bigint + let x1 = val / 2n + do { + x = x1 + x1 = (x + val / x) / 2n + loop-- + } while (x !== x1 && loop) + + if (loop === 0 && x !== x1) { + throw new Error('Sqrt took too long to calculate') + } + + return x +} diff --git a/common/src/proof.ts b/common/src/proof.ts index 5cac6127f..415840377 100644 --- a/common/src/proof.ts +++ b/common/src/proof.ts @@ -1,4 +1,12 @@ -import { utils, providers } from 'ethers' +import { + keccak256, + concat, + encodeRlp, + decodeRlp, + zeroPadValue, + toBeHex, +} from 'ethers' +import type { JsonRpcProvider } from 'ethers' /** * RLP encode the proof returned from eth_getProof @@ -6,9 +14,9 @@ import { utils, providers } from 'ethers' * @returns */ export function rlpEncodeProof(proof: string[]) { - const decodedProof = proof.map((node: string) => utils.RLP.decode(node)) + const decodedProof = proof.map((node: string) => decodeRlp(node)) - return utils.RLP.encode(decodedProof) + return encodeRlp(decodedProof) } /** @@ -18,12 +26,7 @@ export function rlpEncodeProof(proof: string[]) { * @returns storage key used in the eth_getProof params */ export function getStorageKey(account: string, slotIndex: number) { - return utils.keccak256( - utils.concat([ - utils.hexZeroPad(account, 32), - utils.hexZeroPad(utils.hexValue(slotIndex), 32), - ]) - ) + return keccak256(concat([zeroPadValue(account, 32), toBeHex(slotIndex, 32)])) } /** @@ -33,7 +36,7 @@ export function getStorageKey(account: string, slotIndex: number) { */ async function getProof( params: Array, - provider: providers.JsonRpcProvider + provider: JsonRpcProvider ): Promise { try { const proof = await provider.send('eth_getProof', params) @@ -56,7 +59,7 @@ async function getProof( export async function getAccountProof( token: string, blockHash: string, - provider: providers.JsonRpcProvider + provider: JsonRpcProvider ): Promise { const params = [token, [], blockHash] return getProof(params, provider) @@ -76,7 +79,7 @@ export async function getStorageProof( blockHash: string, userAccount: string, storageSlotIndex: number, - provider: providers.JsonRpcProvider + provider: JsonRpcProvider ): Promise { const storageKey = getStorageKey(userAccount, storageSlotIndex) diff --git a/common/src/utils.ts b/common/src/utils.ts index 02c610f54..a0ec51db2 100644 --- a/common/src/utils.ts +++ b/common/src/utils.ts @@ -1,54 +1,27 @@ -import { BigNumber } from 'ethers' +import { id } from 'ethers' import { + genTreeCommitment as genTallyResultCommitment, genRandomSalt, IncrementalQuinTree, hashLeftRight, hash5, hash3, hash2, -} from '@clrfund/maci-crypto' -import { PubKey, PCommand, Message } from '@clrfund/maci-domainobjs' +} from 'maci-crypto' +import { PubKey, PCommand, Message } from 'maci-domainobjs' import { Keypair } from './keypair' -import { utils } from 'ethers' import { Tally } from './tally' +import { bnSqrt } from './math' const LEAVES_PER_NODE = 5 -declare type PathElements = bigint[][] -declare type Indices = number[] -declare type Leaf = bigint - -interface MerkleProof { - pathElements: PathElements - indices: Indices - depth: number - /* eslint-disable-next-line @typescript-eslint/ban-types */ - root: BigInt - leaf: Leaf -} - -export function bnSqrt(a: BigNumber): BigNumber { - // Take square root from a big number - // https://stackoverflow.com/a/52468569/1868395 - if (a.isZero()) { - return a - } - let x - let x1 = a.div(2) - do { - x = x1 - x1 = x.add(a.div(x)).div(2) - } while (!x.eq(x1)) - return x -} - export function createMessage( userStateIndex: number, userKeypair: Keypair, newUserKeypair: Keypair | null, coordinatorPubKey: PubKey, voteOptionIndex: number | null, - voiceCredits: BigNumber | null, + voiceCredits: bigint | null, nonce: number, pollId: bigint, salt?: bigint @@ -58,15 +31,13 @@ export function createMessage( salt = genRandomSalt() as bigint } - const quadraticVoteWeight = voiceCredits - ? bnSqrt(voiceCredits) - : BigNumber.from(0) + const quadraticVoteWeight = voiceCredits ? bnSqrt(voiceCredits) : 0n const command = new PCommand( BigInt(userStateIndex), encKeypair.pubKey, BigInt(voteOptionIndex || 0), - BigInt(quadraticVoteWeight.toString()), + quadraticVoteWeight, BigInt(nonce), pollId, salt @@ -96,7 +67,7 @@ export function getRecipientClaimData( for (const leaf of tally.perVOSpentVoiceCredits.tally) { spentTree.insert(BigInt(leaf)) } - const spentProof: MerkleProof = spentTree.genMerklePath(recipientIndex) + const spentProof = spentTree.genProof(recipientIndex) const resultsCommitment = genTallyResultCommitment( tally.results.tally.map((x) => BigInt(x)), @@ -119,41 +90,8 @@ export function getRecipientClaimData( ] } -/** - * get the id of the subgraph public key entity from the pubKey value - * @param pubKey MACI public key - * @returns the id for the subgraph public key entity - */ -export function getPubKeyId(pubKey: PubKey): string { - const pubKeyPair = pubKey.asContractParam() - const id = utils.id(pubKeyPair.x + '.' + pubKeyPair.y) - return id -} - -/* - * This function was copied from MACI to work around tree shaking not working - * https://github.com/privacy-scaling-explorations/maci/blob/master/core/ts/MaciState.ts#L1581 - * - * A helper function which hashes a list of results with a salt and returns the - * hash. - * - * @param results A list of vote weights - * @parm salt A random salt - * @return The hash of the results and the salt, with the salt last - */ -export function genTallyResultCommitment( - results: bigint[], - salt: bigint, - depth: number -): bigint { - const tree = new IncrementalQuinTree(depth, BigInt(0), 5, hash5) - for (const result of results) { - tree.insert(result) - } - return hashLeftRight(tree.root, salt) -} - export { + genTallyResultCommitment, Message, PCommand as Command, IncrementalQuinTree, diff --git a/common/tsconfig.json b/common/tsconfig.json index 957cdc1a2..ea1601133 100644 --- a/common/tsconfig.json +++ b/common/tsconfig.json @@ -12,11 +12,12 @@ "sourceMap": true, "strict": true, "outDir": "./build", - "target": "es2018", + "target": "es2020", "esModuleInterop": true, "module": "commonjs", + "moduleResolution": "node", "declaration": true }, "exclude": ["node_modules/**"], - "include": ["./src"] + "include": ["src/**/*"] } diff --git a/contracts/.env.example b/contracts/.env.example index c668e5a89..5ace336a7 100644 --- a/contracts/.env.example +++ b/contracts/.env.example @@ -1,60 +1,22 @@ -# Recipient registry type for local deployment: simple, optimistic -RECIPIENT_REGISTRY_TYPE=optimistic - -# Supported values: simple, brightid, snapshot, merkle -USER_REGISTRY_TYPE=simple -# clr.fund (prod) or CLRFundTest (testing) -BRIGHTID_CONTEXT=clr.fund -# BrightId node addr that signs verifications. Node One uses this one -#BRIGHTID_VERIFIER_ADDR=0xb1d71F62bEe34E9Fc349234C201090c33BCdF6DB -# brightid.clr.fund node uses this one, see the ethSigningAddress from https://brightid.clr.fund -BRIGHTID_VERIFIER_ADDR=0xdbf0b2ee9887fe11934789644096028ed3febe9c -# The contract that emits the sponsor events that the BrightId node is listening for -BRIGHTID_SPONSOR= # JSON-RPC endpoint to the selected network JSONRPC_HTTP_URL=https://eth-goerli.alchemyapi.io/v2/ADD_API_KEY -# One of the two options +# One of the two options to interact with contracts WALLET_MNEMONIC= WALLET_PRIVATE_KEY= -# Token address for funding round .setToken -NATIVE_TOKEN_ADDRESS= - -# Required to use in the tally and finalize scripts -COORDINATOR_PK= -COORDINATOR_ETH_PK= - -# Used in the tally script to add tally results to funding round by batch, default is 8 -TALLY_BATCH_SIZE= -# Number of blocks of the MACI event logs to fetch, default is 20,000 -NUM_BLOCKS_PER_REQUEST= - - -# Used to verify contracts -# Get the etherscan api keys for goerli, mainnet from https://etherscan.io -# Get the etherscan api keys for arbitrum and testnets from https://arbiscan.io/ -ETHERSCAN_API_KEY= +# The coordinator MACI private key, required by the tally 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= -# ZK proof circuit type, based on MACI v1 circuit type as defined in utils/deployment.ts -# e.g. micro -CIRCUIT_TYPE=micro - - -# The IPFS gateway url used by the prepare-results.ts script -IPFS_GATEWAY_URL= - -# Parameters used in the e2e testing +# these are used in the e2e testing CIRCUIT_TYPE= CIRCUIT_DIRECTORY= RAPID_SNARK= -# Used in MACI queue merging operation before genProofs, default is 4 -NUM_QUEUE_OPS= -# Used in e2e testing to store intermediate states -STATE_FILE= -# MACI creation transaction hash, used to find the start block of MACI logs -MACI_TRANSACTION_HASH= -# genProofs output directory PROOF_OUTPUT_DIR= +TALLY_BATCH_SIZE= + diff --git a/contracts/.gitignore b/contracts/.gitignore index 0f8b3e642..2094497b2 100644 --- a/contracts/.gitignore +++ b/contracts/.gitignore @@ -9,3 +9,6 @@ tally.json .DS_Store addresses.txt proof_output +typechain-types +params +local-state.json diff --git a/contracts/README.md b/contracts/README.md index 075351271..b3d1bca39 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -2,22 +2,15 @@ ## Working with ZK proofs -Install [zkutil](https://github.com/poma/zkutil) (see instructions in [MACI readme](https://github.com/appliedzkp/maci#get-started)). +Read the [deployment guide](../docs/deployment.md) on how to install MACI dependencies, which include installing rapidsnark (for ubuntu only), c++ libraries and downloading circuit parameter files. -Download [zkSNARK parameters](https://gateway.pinata.cloud/ipfs/Qmbi3nqjBwANPMk5BRyKjCJ4QSHK6WNp7v9NLLo4uwrG1f) for 'test' circuits to `snark-params` directory. Example: - -``` -ipfs get --output snark-params Qmbi3nqjBwANPMk5BRyKjCJ4QSHK6WNp7v9NLLo4uwrG1f -``` - -Set the path to downloaded parameter files and also the path to `zkutil` binary (if needed): +## End-to-end tests +Start the hardhat node: ``` -export NODE_CONFIG='{"snarkParamsPath": "../../../contracts/snark-params/", "zkutil_bin": "/usr/bin/zkutil"}' +yarn run node ``` -## End-to-end tests - Run the tests: ``` @@ -26,7 +19,7 @@ yarn e2e ## Scripts -### Deploy factory contract +### Deploy the ClrFund contract Deploy to local network: @@ -34,50 +27,26 @@ Deploy to local network: yarn deploy:local ``` -### Deploy test round - -This includes: - -- Deploying ERC20 token contract. -- Adding tokens to the matching pool. -- Adding recipients. -- Deploying funding round. -- Deploying MACI instance. - -``` -yarn deployTestRound:local -``` - ### Run test round -Set coordinator's private key (optional, by default the Ganache account #1 will be used): +Run the script, the github action test-script.yml uses this script. ``` -export COORDINATOR_ETH_PK='0x...' +sh/runScriptTests.sh ``` -Contribute funds, wait until sign-up period ends (10 minutes) and vote: - -``` -yarn contribute:local -sleep 300s && yarn vote:local -``` +The test includes setting coordinator keys, contribute funds, vote, tally, finalize and claim funds -Wait until voting period ends, process messages, tally votes and verify the results: +### Verify all clr.fund contracts +The following command will verify all clr.fund contracts. It will log a warning if contract already verified or missing. ``` -sleep 300s && yarn tally:local && yarn finalize:local +yarn hardhat verify-all --clrfund --network ``` -Claim funds: +### Generate coordinator key +If you want to genereate a single key to coordinate multiple rounds. ``` -yarn claim:local -``` - -### Verify all clr.fund contracts -The following command will verify all clr.fund contracts. It will log a warning if contract already verified or missing. - +yarn ts-node cli/newMaciKey.ts ``` -yarn hardhat verify-all --network -``` \ No newline at end of file diff --git a/contracts/cli/addContributors.ts b/contracts/cli/addContributors.ts index fe4678fdf..3df0a435b 100644 --- a/contracts/cli/addContributors.ts +++ b/contracts/cli/addContributors.ts @@ -32,7 +32,7 @@ async function main(args: any) { const users = [contributor1, contributor2] let addUserTx for (const account of users) { - addUserTx = await userRegistry.addUser(account.getAddress()) + addUserTx = await userRegistry.addUser(account.address) addUserTx.wait() } diff --git a/contracts/cli/addRecipients.ts b/contracts/cli/addRecipients.ts index 1afaf20ea..c9c229bd7 100644 --- a/contracts/cli/addRecipients.ts +++ b/contracts/cli/addRecipients.ts @@ -28,19 +28,28 @@ async function main(args: any) { signer ) + let numAdded = 0 for (let i = 6; i < 10; i++) { const recipient = recipients[i] + const recipientAddress = recipient?.address || signer.address const addRecipientTx = await recipientRegistry.addRecipient( - recipient.address, + recipientAddress, JSON.stringify({ name: `recipient ${i}`, description: `recipient ${i}`, + bannerImageHash: 'QmPAZJkV2TH2hmudpSwj8buxwiG1ckNa2ZbrPFop3Td5oD', + thumbnailImageHash: 'QmPAZJkV2TH2hmudpSwj8buxwiG1ckNa2ZbrPFop3Td5oD', }) ) - addRecipientTx.wait() + const receipt = await addRecipientTx.wait() + if (receipt.status === 1) { + numAdded++ + } else { + console.log('Failed to add recipient', i) + } } - console.log('Added test recipients') + console.log(`Added ${numAdded} test recipients`) } main(program.args) diff --git a/contracts/cli/claim.ts b/contracts/cli/claim.ts index 4c11dd8c3..768b4a569 100644 --- a/contracts/cli/claim.ts +++ b/contracts/cli/claim.ts @@ -11,6 +11,7 @@ import { JSONFile } from '../utils/JSONFile' import { ethers } from 'hardhat' import { program } from 'commander' import { isPathExist } from '../utils/misc' +import { Contract, getNumber } from 'ethers' program .description('Claim funnds for test recipients') @@ -39,7 +40,11 @@ async function main(args: any) { console.log('pollAddress', pollAddress) const poll = await ethers.getContractAt('Poll', pollAddress) - const recipientTreeDepth = (await poll.treeDepths()).voteOptionTreeDepth + const treeDepths = await poll.treeDepths() + const recipientTreeDepth = getNumber( + treeDepths.voteOptionTreeDepth, + 'voteOptionTreeDepth' + ) // Claim funds const recipients = [recipient0, recipient1, recipient2] @@ -51,7 +56,7 @@ async function main(args: any) { ) const fundingRoundAsRecipient = fundingRoundContract.connect( recipients[recipientIndex] - ) + ) as Contract const claimTx = await fundingRoundAsRecipient.claimFunds( ...recipientClaimData ) diff --git a/contracts/cli/contribute.ts b/contracts/cli/contribute.ts index 836ad7b6b..a4fead694 100644 --- a/contracts/cli/contribute.ts +++ b/contracts/cli/contribute.ts @@ -14,6 +14,7 @@ import { getEventArg } from '../utils/contracts' import { program } from 'commander' import { ethers } from 'hardhat' import { isPathExist } from '../utils/misc' +import type { FundingRound, ERC20 } from '../typechain-types' program .description('Contribute to a funding round') @@ -39,7 +40,7 @@ async function main(args: any) { const maciAddress = await fundingRound.maci() const maci = await ethers.getContractAt('MACI', maciAddress) - const contributionAmount = UNIT.mul(16).div(10) + const contributionAmount = (UNIT * BigInt(16)) / BigInt(10) state.contributors = {} for (const contributor of [contributor1, contributor2]) { @@ -49,10 +50,12 @@ async function main(args: any) { await token.transfer(contributorAddress, contributionAmount) const contributorKeypair = new Keypair() - const tokenAsContributor = token.connect(contributor) - await tokenAsContributor.approve(fundingRound.address, contributionAmount) + const tokenAsContributor = token.connect(contributor) as ERC20 + await tokenAsContributor.approve(fundingRound.target, contributionAmount) - const fundingRoundAsContributor = fundingRound.connect(contributor) + const fundingRoundAsContributor = fundingRound.connect( + contributor + ) as FundingRound const contributionTx = await fundingRoundAsContributor.contribute( contributorKeypair.pubKey.asContractParam(), contributionAmount diff --git a/contracts/cli/deploySponsor.ts b/contracts/cli/deploySponsor.ts index b5d3d04df..a294858bd 100644 --- a/contracts/cli/deploySponsor.ts +++ b/contracts/cli/deploySponsor.ts @@ -10,7 +10,9 @@ import { ethers } from 'hardhat' async function main() { const SponsorContract = await ethers.getContractFactory('BrightIdSponsor') const sponsor = await SponsorContract.deploy() - console.log('Deployed the sponsor contract at', sponsor.address) + // wait for the contract to be deployed to the network + await sponsor.waitForDeployment() + console.log('Deployed the sponsor contract at', sponsor.target) } main() diff --git a/contracts/cli/finalize.ts b/contracts/cli/finalize.ts index 7030dcf55..98bdbe6d9 100644 --- a/contracts/cli/finalize.ts +++ b/contracts/cli/finalize.ts @@ -1,9 +1,13 @@ /** * Finalize a funding round * + * Make sure to set the following environment variables in the .env file + * 1) WALLET_PRIVATE_KEY or WALLET_MNEMONIC + * - clrfund owner's wallet private key to interact with the contract + * * Sample usage: * HARDHAT_NETWORK=localhost yarn ts-node cli/finalize.ts \ - * --funding-round \ + * --clrfund \ * --tally-file */ @@ -11,11 +15,16 @@ import { ethers } from 'hardhat' import { JSONFile } from '../utils/JSONFile' import { genTallyResultCommitment } from '@clrfund/common' import { program } from 'commander' +import { getNumber } from 'ethers' program .description('Finalize a funding round') .requiredOption('-c --clrfund ', 'The ClrFund contract address') - .requiredOption('-t --tally-file ', 'The tally file path') + .option( + '-t --tally-file ', + 'The tally file path', + './proof_output/tally.json' + ) .parse() async function main(args: any) { @@ -33,7 +42,7 @@ async function main(args: any) { 'FundingRound', currentRoundAddress ) - console.log('Current round', fundingRound.address) + console.log('Current round', fundingRound.target) const pollAddress = await fundingRound.poll() const pollContract = await ethers.getContractAt('Poll', pollAddress) @@ -42,19 +51,23 @@ async function main(args: any) { const treeDepths = await pollContract.treeDepths() console.log('voteOptionTreeDepth', treeDepths.voteOptionTreeDepth) - const totalSpent = parseInt(tally.totalSpentVoiceCredits.spent) + const totalSpent = tally.totalSpentVoiceCredits.spent const totalSpentSalt = tally.totalSpentVoiceCredits.salt + const voteOptionTreeDepth = getNumber( + treeDepths.voteOptionTreeDepth, + 'voteOptionTreeDepth' + ) const resultsCommitment = genTallyResultCommitment( tally.results.tally.map((x: string) => BigInt(x)), - tally.results.salt, - treeDepths.voteOptionTreeDepth + BigInt(tally.results.salt), + voteOptionTreeDepth ) const perVOVoiceCreditCommitment = genTallyResultCommitment( tally.perVOSpentVoiceCredits.tally.map((x: string) => BigInt(x)), - tally.perVOSpentVoiceCredits.salt, - treeDepths.voteOptionTreeDepth + BigInt(tally.perVOSpentVoiceCredits.salt), + voteOptionTreeDepth ) const tx = await clrfundContract.transferMatchingFunds( diff --git a/contracts/cli/newClrFund.ts b/contracts/cli/newClrFund.ts index 22a29ec8a..9691d1216 100644 --- a/contracts/cli/newClrFund.ts +++ b/contracts/cli/newClrFund.ts @@ -2,16 +2,20 @@ * Create a new instance of the ClrFund contract. * * If the coordinator ETH address is not provided, use the signer address - * If the COORDINATOR_MACISK env. varibale not set, create a new random MACI key + * If the COORDINATOR_MACISK environment varibale not set in the .env file, + * this script will create a new random MACI key + * + * Display help: + * yarn ts-node cli/newClrFund.ts -h * * Sample usage: * * HARDHAT_NETWORK=localhost yarn ts-node cli/newClrFund.ts \ - * --deployer \ + * --directory \ * --token \ - * [--coordinator ] \ - * [--user-registry-type ] \ - * [--recipient-registry-type ] + * --coordinator \ + * --user-registry-type \ + * --recipient-registry-type * * * If user registry address and recipient registry address are not provided, @@ -20,31 +24,35 @@ * * If token is not provided, a new ERC20 token will be created */ -import { BigNumber } from 'ethers' -import { ethers, network } from 'hardhat' +import { parseUnits, Signer } from 'ethers' +import { ethers, network, config } from 'hardhat' import { getEventArg } from '../utils/contracts' import { newMaciPrivateKey } from '../utils/maci' +import { MaciParameters } from '../utils/maciParameters' import { challengePeriodSeconds, deployContract, deployUserRegistry, deployRecipientRegistry, setCoordinator, + deployMaciFactory, + deployPoseidonLibraries, } from '../utils/deployment' import { JSONFile } from '../utils/JSONFile' import { Option, program } from 'commander' import dotenv from 'dotenv' -import { UNIT } from '../utils/constants' +import { UNIT, BRIGHTID_VERIFIER_ADDR } from '../utils/constants' +import { DEFAULT_CIRCUIT } from '../utils/circuits' + dotenv.config() const DEFAULT_DEPOSIT_AMOUNT = '0.001' program .description('Deploy a new ClrFund instance') - .requiredOption( - '-d --deployer ', - 'The ClrFund deployer contract address' - ) + .option('-d --deployer ', 'The ClrFund deployer contract address') + .option('-q --circuit ', 'The circuit type', DEFAULT_CIRCUIT) + .option('-z --directory ', 'The circuit directory') .option('-c --coordinator ', 'The coordinator ETH address') .option('-t --token
', 'The native token address') .addOption( @@ -54,17 +62,16 @@ program ).default(1000) ) .addOption( - new Option( - '-u --user-registry-type ', - 'The user registry type' - ).choices(['simple', 'brightid', 'merkle', 'storage']) + new Option('-u --user-registry-type ', 'The user registry type') + .choices(['simple', 'brightid', 'merkle', 'storage']) + .default('simple') ) .option('-x --brightid-context ', 'The brightid context') .addOption( new Option( '-v --brightid-verifier ', 'The brightid verifier address' - ).default('0xdbf0b2ee9887fe11934789644096028ed3febe9c') + ).default(BRIGHTID_VERIFIER_ADDR) ) .option( '-o --brightid-sponsor ', @@ -72,7 +79,7 @@ program ) .addOption( new Option( - '-r --recipient-registry-type ', + '-r --recipient-registry-type ', 'The recipient registry type' ) .choices(['simple', 'optimistic']) @@ -95,23 +102,70 @@ program ) .parse() -async function main(args: any) { - const { deployer, coordinator, stateFile } = args - const [signer] = await ethers.getSigners() +/** + * Deploy ClrFund as a standalone instance + * @param artifactsPath The hardhat artifacts path + * @param circuit The circuit type + * @param directory The directory containing the zkeys files + * @returns ClrFund contract address + */ +async function deployStandaloneClrFund({ + artifactsPath, + circuit, + directory, + signer, +}: { + artifactsPath: string + circuit: string + directory: string + signer: Signer +}): Promise { + const libraries = await deployPoseidonLibraries({ + artifactsPath, + signer, + ethers, + }) + console.log('Deployed Poseidons', libraries) - console.log('Network: ', network.name) - console.log(`Deploying from address: ${signer.address}`) - console.log('args', args) + const maciParameters = await MaciParameters.fromConfig(circuit, directory) + const quiet = false + const maciFactory = await deployMaciFactory({ + libraries, + ethers, + maciParameters, + quiet, + }) - // If the maci secret key is not set in the env. variable, create a new key - const coordinatorMacisk = - process.env.COORDINATOR_MACISK ?? newMaciPrivateKey() + const fundingRoundFactory = await deployContract({ + name: 'FundingRoundFactory', + ethers, + }) + + const clrfund = await deployContract({ + name: 'ClrFund', + ethers, + signer, + }) + const clrfundAddress = await clrfund.getAddress() + console.log('Deployed ClrFund at', clrfundAddress) + + const initTx = await clrfund.init(maciFactory, fundingRoundFactory) + await initTx.wait() + return clrfundAddress +} + +/** + * Deploy the ClrFund contract using the deployer contract + * @param deployer ClrFund deployer contract + * @returns ClrFund contract address + */ +async function deployClrFundFromDeployer(deployer: string): Promise { const clrfundDeployer = await ethers.getContractAt( 'ClrFundDeployer', deployer ) - console.log('ClrFundDeployer:', clrfundDeployer.address) + console.log('ClrFundDeployer:', clrfundDeployer.target) const tx = await clrfundDeployer.deployClrFund() const receipt = await tx.wait() @@ -127,6 +181,34 @@ async function main(args: any) { ) } + return clrfund +} + +async function main(args: any) { + const { deployer, coordinator, stateFile, circuit, directory } = args + const [signer] = await ethers.getSigners() + + console.log('Network: ', network.name) + console.log(`Deploying from address: ${signer.address}`) + console.log('args', args) + + if (!directory && !deployer) { + throw new Error(`'-z --directory ' is required`) + } + + // If the maci secret key is not set in the env. variable, create a new key + const coordinatorMacisk = + process.env.COORDINATOR_MACISK ?? newMaciPrivateKey() + + const clrfund = deployer + ? await deployClrFundFromDeployer(deployer) + : await deployStandaloneClrFund({ + signer, + circuit, + directory, + artifactsPath: config.paths.artifacts, + }) + const clrfundContract = await ethers.getContractAt('ClrFund', clrfund, signer) // set coordinator, use the coordinator address if available, @@ -143,14 +225,14 @@ async function main(args: any) { // set token let tokenAddress = args.token if (!tokenAddress) { - const initialTokenSupply = UNIT.mul(args.initialTokenSupply) + const initialTokenSupply = UNIT * BigInt(args.initialTokenSupply) const tokenContract = await deployContract({ name: 'AnyOldERC20Token', contractArgs: [initialTokenSupply], ethers, signer, }) - tokenAddress = tokenContract.address + tokenAddress = await tokenContract.getAddress() } const setTokenTx = await clrfundContract.setToken(tokenAddress) await setTokenTx.wait() @@ -167,7 +249,7 @@ async function main(args: any) { brightidVerifier: args.brightidVerifier, brightidSponsor: args.brightidSponsor, }) - userRegistryAddress = userRegistryContract.address + userRegistryAddress = await userRegistryContract.getAddress() } const setUserRegistryTx = await clrfundContract.setUserRegistry(userRegistryAddress) @@ -188,7 +270,7 @@ async function main(args: any) { deposit, controller: clrfund, }) - recipientRegistryAddress = recipientRegistryContract.address + recipientRegistryAddress = await recipientRegistryContract.getAddress() } const setRecipientRegistryTx = await clrfundContract.setRecipientRegistry( @@ -201,13 +283,14 @@ async function main(args: any) { ) if (stateFile) { - JSONFile.update(stateFile, { clrfund }) + // save the test data for running the tally script later + JSONFile.update(stateFile, { clrfund, coordinatorMacisk }) } } -function parseDeposit(deposit: string): BigNumber { +function parseDeposit(deposit: string): bigint { try { - return ethers.utils.parseUnits(deposit) + return parseUnits(deposit) } catch (e) { throw new Error(`Error parsing deposit ${(e as Error).message}`) } diff --git a/contracts/cli/newDeployer.ts b/contracts/cli/newDeployer.ts index c3d87148e..78229b9eb 100644 --- a/contracts/cli/newDeployer.ts +++ b/contracts/cli/newDeployer.ts @@ -10,12 +10,12 @@ import { deployContract, deployPoseidonLibraries, deployMaciFactory, - setMaciParameters, } from '../utils/deployment' import { DEFAULT_CIRCUIT } from '../utils/circuits' import { JSONFile } from '../utils/JSONFile' import { ethers, config, network } from 'hardhat' import { program } from 'commander' +import { MaciParameters } from '../utils/maciParameters' program .description('Deploy a new ClrFund deployer') @@ -41,36 +41,43 @@ async function main(args: any) { }) console.log('Deployed Poseidons', libraries) - const maciFactory = await deployMaciFactory({ libraries, ethers }) - console.log('Deployed MaciFactory at', maciFactory.address) - await setMaciParameters(maciFactory, args.directory, args.circuit) + const maciParameters = await MaciParameters.fromConfig( + args.circuit, + args.directory + ) + const maciFactory = await deployMaciFactory({ + libraries, + ethers, + maciParameters, + }) + console.log('Deployed MaciFactory at', maciFactory.target) const clrfundTemplate = await deployContract({ name: 'ClrFund', ethers, }) - console.log('Deployed ClrFund Template at', clrfundTemplate.address) + console.log('Deployed ClrFund Template at', clrfundTemplate.target) const fundingRoundFactory = await deployContract({ name: 'FundingRoundFactory', - libraries, ethers, }) - console.log('Deployed FundingRoundFactory at', fundingRoundFactory.address) + console.log('Deployed FundingRoundFactory at', fundingRoundFactory.target) const clrfundDeployer = await deployContract({ name: 'ClrFundDeployer', ethers, contractArgs: [ - clrfundTemplate.address, - maciFactory.address, - fundingRoundFactory.address, + clrfundTemplate.target, + maciFactory.target, + fundingRoundFactory.target, ], }) - console.log('Deployed ClrfundDeployer at', clrfundDeployer.address) + console.log('Deployed ClrfundDeployer at', clrfundDeployer.target) if (args.stateFile) { - JSONFile.update(args.stateFile, { deployer: clrfundDeployer.address }) + const clrfundDeployerAddress = await clrfundDeployer.getAddress() + JSONFile.update(args.stateFile, { deployer: clrfundDeployerAddress }) } } diff --git a/contracts/cli/newRound.ts b/contracts/cli/newRound.ts index e13f5bdc4..72eb9c6b9 100644 --- a/contracts/cli/newRound.ts +++ b/contracts/cli/newRound.ts @@ -12,6 +12,7 @@ import { ethers } from 'hardhat' import { JSONFile } from '../utils/JSONFile' import { program } from 'commander' import { deployUserRegistry } from '../utils/deployment' +import { ZERO_ADDRESS } from '../utils/constants' program .description('Deploy a new funding round contract') @@ -62,7 +63,7 @@ async function main(args: any) { // check if the current round is finalized before starting a new round to avoid revert const currentRoundAddress = await clrfundContract.getCurrentRound() - if (currentRoundAddress !== ethers.constants.AddressZero) { + if (currentRoundAddress !== ZERO_ADDRESS) { const currentRound = await ethers.getContractAt( 'FundingRound', currentRoundAddress @@ -86,12 +87,12 @@ async function main(args: any) { }) const setUserRegistryTx = await clrfundContract.setUserRegistry( - userRegistryContract.address + userRegistryContract.target ) await setUserRegistryTx.wait() console.log( - `New ${args.userRegistryType} user registry: ${userRegistryContract.address}` + `New ${args.userRegistryType} user registry: ${userRegistryContract.target}` ) } diff --git a/contracts/cli/tally.ts b/contracts/cli/tally.ts index c14918929..6bc476431 100644 --- a/contracts/cli/tally.ts +++ b/contracts/cli/tally.ts @@ -1,64 +1,191 @@ /** * Script for tallying votes * - * This script can be rerun by passing in additional parameters: - * --maci-logs, --maci-state-file + * 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 - * if not running test using the localhost network - * 1) COORDINATOR_ETH_PK - coordinator's wallet private key to interact with contracts + * 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 ts-node cli/tally.ts --round-address
--start-block + * HARDHAT_NETWORK= yarn ts-node cli/tally.ts --clrfund --maci-tx-hash * * To rerun: * - * yarn ts-node cli/tally.ts --round-address
+ * yarn ts-node cli/tally.ts --clrfund --maci-state-file --tally-file */ -import { ethers, network, config } from 'hardhat' -import { Contract } from 'ethers' +import { ethers, network } from 'hardhat' +import { BaseContract, getNumber, Signer } from 'ethers' -import { DEFAULT_SR_QUEUE_OPS } from '../utils/constants' +import { + DEFAULT_SR_QUEUE_OPS, + DEFAULT_GET_LOG_BATCH_SIZE, +} from '../utils/constants' import { getIpfsHash } from '../utils/ipfs' import { JSONFile } from '../utils/JSONFile' -import { deployMessageProcesorAndTally } from '../utils/deployment' import { getGenProofArgs, genProofs, proveOnChain, addTallyResultsBatch, mergeMaciSubtrees, + genLocalState, + TallyData, } from '../utils/maci' -import { getTalyFilePath } from '../utils/misc' +import { getMaciStateFilePath, getDirname } from '../utils/misc' import { program } from 'commander' import { DEFAULT_CIRCUIT } from '../utils/circuits' +import { FundingRound, Poll, Tally } from '../typechain-types' program .description('Tally votes') .requiredOption('-c --clrfund ', 'ClrFund contract address') + .option('-s --maci-state-file ', 'MACI state file') + .option('-f --tally-file ', 'The tally file path') .option( '-b --batch-size ', 'The batch size to upload tally result on-chain', '10' ) - .requiredOption('-c --circuit ', 'The circuit type', DEFAULT_CIRCUIT) - .requiredOption('-d --circuit-directory ', 'The circuit directory') + .requiredOption('-u --circuit ', 'The circuit type', DEFAULT_CIRCUIT) + .requiredOption( + '-d --circuit-directory ', + 'The circuit directory', + './params' + ) .option( - '-s --state-file ', - 'File to store the ClrFundDeployer address for e2e testing' + '-o --output-dir ', + 'The proof output directory', + './proof_output' ) - .requiredOption('-o --output-dir ', 'The proof output directory') .option('-t --maci-tx-hash ', 'The MACI creation transaction hash') - .option('-r --rapid-snark ', 'The rapidsnark prover path') + .option('-r --rapidsnark ', 'The rapidsnark prover path') .option( '-n --num-queue-ops ', - 'The number of operation for tree merging', + 'The number of operations for MACI tree merging', DEFAULT_SR_QUEUE_OPS ) + .option( + '-k --blocks-per-batch ', + 'The number of blocks per batch of logs to fetch on-chain', + DEFAULT_GET_LOG_BATCH_SIZE.toString() + ) + .option('-z --sleep ', 'Sleep between log fetch') + .option('-q --quiet', 'Whether to disable verbose logging', false) .parse() +/** + * 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 + */ +async function getFundingRound( + clrfund: string, + coordinator: Signer +): Promise { + const clrfundContract = await ethers.getContractAt( + 'ClrFund', + clrfund, + coordinator + ) + + const fundingRound = await clrfundContract.getCurrentRound() + const fundingRoundContract = await ethers.getContractAt( + 'FundingRound', + fundingRound, + coordinator + ) + + return fundingRoundContract as BaseContract as FundingRound +} + +/** + * Get the recipient tree depth (aka vote option tree depth) + * @param fundingRoundContract Funding round conract + * @returns Recipient tree depth + */ +async function getRecipientTreeDepth( + fundingRoundContract: FundingRound +): Promise { + const pollAddress = await fundingRoundContract.poll() + const pollContract = await ethers.getContractAt('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 + * @returns Message processor contract address + */ +async function getMessageProcessorAddress( + tallyAddress: string +): Promise { + const tallyContract = (await ethers.getContractAt( + 'Tally', + tallyAddress + )) as BaseContract as Tally + + const messageProcessorAddress = await tallyContract.messageProcessor() + return messageProcessorAddress +} + /** * Main tally logic */ @@ -66,147 +193,115 @@ async function main(args: any) { const { clrfund, batchSize, - stateFile, + maciStateFile, + tallyFile, outputDir, circuit, circuitDirectory, - rapidSnark, + rapidsnark, maciTxHash, numQueueOps, + blocksPerBatch, + sleep, + quiet, } = args + console.log('Verbose logging disabled:', quiet) + const [coordinator] = await ethers.getSigners() console.log('Coordinator address: ', coordinator.address) - const providerUrl = (network.config as any).url - console.log('providerUrl', providerUrl) - - let clrfundContract: Contract - try { - clrfundContract = await ethers.getContractAt( - 'ClrFund', - clrfund, - coordinator - ) - } catch (e) { - console.error('Error accessing ClrFund Contract at', clrfund) - throw e + const coordinatorMacisk = process.env.COORDINATOR_MACISK + if (!coordinatorMacisk) { + throw Error('Env. variable COORDINATOR_MACISK not set') } - const fundingRound = await clrfundContract.getCurrentRound() - const fundingRoundContract = await ethers.getContractAt( - 'FundingRound', - fundingRound, - coordinator - ) - console.log('Funding round contract', fundingRoundContract.address) + const fundingRoundContract = await getFundingRound(clrfund, coordinator) + console.log('Funding round contract', fundingRoundContract.target) - const publishedTallyHash = await fundingRoundContract.tallyHash() - console.log('publishedTallyHash', publishedTallyHash) + const recipientTreeDepth = await getRecipientTreeDepth(fundingRoundContract) - let tally - if (!publishedTallyHash) { - const coordinatorMacisk = process.env.COORDINATOR_MACISK - if (!coordinatorMacisk) { - throw Error('Env. variable COORDINATOR_MACISK not set') - } + const pollId = await fundingRoundContract.pollId() + console.log('PollId', pollId) + + const maciAddress = await fundingRoundContract.maci() + console.log('MACI address', maciAddress) + + const tallyAddress = await fundingRoundContract.tally() + const messageProcessorAddress = await getMessageProcessorAddress(tallyAddress) - const pollIdBN = await fundingRoundContract.pollId() - const pollId = pollIdBN.toString() - console.log('PollId', pollId) + const providerUrl = (network.config as any).url + + const outputPath = maciStateFile + ? maciStateFile + : getMaciStateFilePath(outputDir) + + await mergeMaciSubtrees({ + maciAddress, + pollId, + numQueueOps, + quiet, + }) - const maciAddress = await fundingRoundContract.maci() - console.log('MACI address', maciAddress) + let tallyFilePath: string = tallyFile || '' + if (!tallyFile) { + if (!maciStateFile) { + await genLocalState({ + quiet, + outputPath, + pollId, + maciContractAddress: maciAddress, + coordinatorPrivateKey: coordinatorMacisk, + ethereumProvider: providerUrl, + transactionHash: maciTxHash, + blockPerBatch: blocksPerBatch, + sleep, + }) + } - // Generate proof and tally file const genProofArgs = getGenProofArgs({ maciAddress, - providerUrl, pollId, coordinatorMacisk, - maciTxHash, - rapidSnark, + rapidsnark, circuitType: circuit, circuitDirectory, outputDir, + blocksPerBatch: getNumber(blocksPerBatch), + maciTxHash, + maciStateFile: outputPath, + quiet, }) - console.log('genProofsArg', genProofArgs) - - await mergeMaciSubtrees(maciAddress, pollId, numQueueOps) - console.log('Completed tree merge') - await genProofs(genProofArgs) - console.log('Completed genProofs') + tallyFilePath = genProofArgs.tallyFile + } - tally = JSONFile.read(genProofArgs.tally_file) - if (stateFile) { - // Save tally file in the state - JSONFile.update(stateFile, { tallyFile: genProofArgs.tally_file }) - } + const tally = JSONFile.read(tallyFilePath) as TallyData + const proofDir = getDirname(tallyFilePath) + console.log('Proof directory', proofDir) - // deploy the MessageProcessor and Tally contracts used by proveOnChain - const { mpContract, tallyContract } = await deployMessageProcesorAndTally({ - artifactsPath: config.paths.artifacts, - ethers, - signer: coordinator, - }) - console.log('MessageProcessor', mpContract.address) - console.log('Tally Contract', tallyContract.address) - - try { - // Submit proofs to MACI contract - await proveOnChain({ - contract: maciAddress, - poll_id: pollId, - mp: mpContract.address, - tally: tallyContract.address, - //subsidy: tallyContractAddress, // TODO: make subsidy optional - proof_dir: outputDir, - }) - } catch (e) { - console.error('proveOnChain failed') - throw e - } + // proveOnChain if not already processed + await proveOnChain({ + pollId, + proofDir, + subsidyEnabled: false, + maciAddress, + messageProcessorAddress, + tallyAddress, + quiet, + }) - // set the Tally contract address for verifying tally result on chain - const setTallyTx = await fundingRoundContract.setTally( - tallyContract.address - ) - await setTallyTx.wait() - console.log('Tally contract set in funding round') - - // Publish tally hash - const tallyHash = await getIpfsHash(tally) - await fundingRoundContract.publishTallyHash(tallyHash) - console.log(`Tally hash is ${tallyHash}`) - } else { - // read the tally.json file - console.log(`Tally hash is ${publishedTallyHash}`) - try { - console.log(`Reading tally.json file...`) - const tallyFile = getTalyFilePath(outputDir) - tally = JSONFile.read(tallyFile) - } catch (err) { - console.log('Failed to get tally file', publishedTallyHash) - throw err - } - } + // Publish tally hash if it is not already published + await publishTallyHash(fundingRoundContract, tally) - // Submit results to the funding round contract - const startIndex = await fundingRoundContract.totalTallyResults() - const total = tally.results.tally.length - console.log('Uploading tally results in batches of', batchSize) - const addTallyGas = await addTallyResultsBatch( + // Submit tally results to the funding round contract + // This function can be re-run from where it left off + await submitTallyResults( fundingRoundContract, - 3, + recipientTreeDepth, tally, - Number(batchSize), - startIndex.toNumber(), - (processed: number) => { - console.log(`Processed ${processed} / ${total}`) - } + batchSize ) - console.log('Tally results uploaded. Gas used:', addTallyGas.toString()) } main(program.opts()) diff --git a/contracts/cli/timeTravel.ts b/contracts/cli/timeTravel.ts index b8f310f11..25a574eca 100644 --- a/contracts/cli/timeTravel.ts +++ b/contracts/cli/timeTravel.ts @@ -5,7 +5,7 @@ * HARDHAT_NETWORK=localhost yarn ts-node cli/timeTravel.ts */ -import { network } from 'hardhat' +import { time } from '@nomicfoundation/hardhat-network-helpers' import { program } from 'commander' program @@ -15,8 +15,7 @@ program async function main(args: any) { const seconds = Number(args[0]) - await network.provider.send('evm_increaseTime', [seconds]) - await network.provider.send('evm_mine') + await time.increase(seconds) } main(program.args) diff --git a/contracts/cli/verify.ts b/contracts/cli/verify.ts new file mode 100644 index 000000000..30d411c72 --- /dev/null +++ b/contracts/cli/verify.ts @@ -0,0 +1,44 @@ +/** + * Verify the tally file + * + * Sample usage: + * HARDHAT_NETWORK= yarn ts-node cli/verify.ts -f -t + */ +import { ethers, network } from 'hardhat' +import { verify } from '../utils/maci' +import { program } from 'commander' +import { JSONFile } from '../utils/JSONFile' + +program + .description('Verify the tally file') + .requiredOption('-f tally-file ', 'The tally file') + .requiredOption('-t tally-address
', 'The tally contract address') + .parse() + +async function main(args: any) { + const [deployer] = await ethers.getSigners() + console.log('deployer', deployer.address) + console.log('network', network.name) + console.log('args', args) + + const { tallyFile, tallyAddress } = args + const tallyData = JSONFile.read(tallyFile) + const pollId = tallyData.pollId + const maciAddress = tallyData.maci + + await verify({ + subsidyEnabled: false, + tallyFile, + pollId: BigInt(pollId), + maciAddress, + tallyAddress, + quiet: false, + }) +} + +main(program.opts()) + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/contracts/cli/vote.ts b/contracts/cli/vote.ts index 933f02ba2..3f03332e9 100644 --- a/contracts/cli/vote.ts +++ b/contracts/cli/vote.ts @@ -9,8 +9,13 @@ */ import { JSONFile } from '../utils/JSONFile' -import { PrivKey, Keypair, createMessage } from '@clrfund/common' -import { BigNumber } from 'ethers' +import { + PrivKey, + Keypair, + createMessage, + Message, + PubKey, +} from '@clrfund/common' import { ethers } from 'hardhat' import { program } from 'commander' import dotenv from 'dotenv' @@ -38,18 +43,18 @@ async function main(args: any) { await ethers.getSigners() const state = JSONFile.read(stateFile) - const coordinatorKeyPair = new Keypair(PrivKey.unserialize(coordinatorMacisk)) + const coordinatorKeyPair = new Keypair(PrivKey.deserialize(coordinatorMacisk)) const pollId = state.pollId for (const contributor of [contributor1, contributor2]) { const contributorAddress = await contributor.getAddress() const contributorData = state.contributors[contributorAddress] const contributorKeyPair = new Keypair( - PrivKey.unserialize(contributorData.privKey) + PrivKey.deserialize(contributorData.privKey) ) - const messages: { msgType: any; data: string[] }[] = [] - const encPubKeys: any[] = [] + const messages: Message[] = [] + const encPubKeys: PubKey[] = [] let nonce = 1 // Change key const newContributorKeypair = new Keypair() @@ -63,12 +68,12 @@ async function main(args: any) { nonce, pollId ) - messages.push(message.asContractParam()) - encPubKeys.push(encPubKey.asContractParam()) + messages.push(message) + encPubKeys.push(encPubKey) nonce += 1 // Vote for (const recipientIndex of [1, 2]) { - const votes = BigNumber.from(contributorData.voiceCredits).div(4) + const votes = BigInt(contributorData.voiceCredits) / BigInt(4) const [message, encPubKey] = createMessage( contributorData.stateIndex, newContributorKeypair, @@ -79,8 +84,8 @@ async function main(args: any) { nonce, pollId ) - messages.push(message.asContractParam()) - encPubKeys.push(encPubKey.asContractParam()) + messages.push(message) + encPubKeys.push(encPubKey) nonce += 1 } @@ -90,8 +95,8 @@ async function main(args: any) { contributor ) await fundingRoundAsContributor.submitMessageBatch( - messages.reverse(), - encPubKeys.reverse() + messages.reverse().map((msg) => msg.asContractParam()), + encPubKeys.reverse().map((key) => key.asContractParam()) ) console.log(`Contributor ${contributorAddress} voted.`) } diff --git a/contracts/contracts/ClrFund.sol b/contracts/contracts/ClrFund.sol index eaf988db7..f2939d132 100644 --- a/contracts/contracts/ClrFund.sol +++ b/contracts/contracts/ClrFund.sol @@ -6,20 +6,18 @@ import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; -import {SignUpGatekeeper} from "@clrfund/maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol"; -import {InitialVoiceCreditProxy} from "@clrfund/maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol"; -import {PollFactory} from '@clrfund/maci-contracts/contracts/Poll.sol'; -import {Params} from '@clrfund/maci-contracts/contracts/Params.sol'; +import {Params} from 'maci-contracts/contracts/utilities/Params.sol'; +import {DomainObjs} from 'maci-contracts/contracts/utilities/DomainObjs.sol'; -import './MACIFactory.sol'; +import {IMACIFactory} from './interfaces/IMACIFactory.sol'; import './userRegistry/IUserRegistry.sol'; import './recipientRegistry/IRecipientRegistry.sol'; -import {FundingRound} from './FundingRound.sol'; -import './OwnableUpgradeable.sol'; -import {FundingRoundFactory} from './FundingRoundFactory.sol'; +import {IFundingRound} from './interfaces/IFundingRound.sol'; +import {OwnableUpgradeable} from './OwnableUpgradeable.sol'; +import {IFundingRoundFactory} from './interfaces/IFundingRoundFactory.sol'; import {TopupToken} from './TopupToken.sol'; -contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { +contract ClrFund is OwnableUpgradeable, DomainObjs, Params { using EnumerableSet for EnumerableSet.AddressSet; using SafeERC20 for ERC20; @@ -27,15 +25,15 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { address public coordinator; ERC20 public nativeToken; - MACIFactory public maciFactory; + IMACIFactory public maciFactory; IUserRegistry public userRegistry; IRecipientRegistry public recipientRegistry; PubKey public coordinatorPubKey; EnumerableSet.AddressSet private fundingSources; - FundingRound[] private rounds; + IFundingRound[] private rounds; - FundingRoundFactory public roundFactory; + IFundingRoundFactory public roundFactory; // Events event FundingSourceAdded(address _source); @@ -45,9 +43,11 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { event TokenChanged(address _token); event CoordinatorChanged(address _coordinator); event Initialized(); - event UserRegistrySet(); - event RecipientRegistrySet(); - event FundingRoundTemplateChanged(); + event UserRegistryChanged(address _userRegistry); + event RecipientRegistryChanged(address _recipientRegistry); + event FundingRoundTemplateChanged(address _template); + event FundingRoundFactoryChanged(address _roundFactory); + event MaciFactoryChanged(address _maciFactory); // errors error FundingSourceAlreadyAdded(); @@ -56,16 +56,15 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { error NotFinalized(); error NotAuthorized(); error NoCurrentRound(); - error NoCoordinator(); - error NoToken(); - error NoRecipientRegistry(); - error NoUserRegistry(); error NotOwnerOfMaciFactory(); error InvalidFundingRoundFactory(); error InvalidMaciFactory(); + error RecipientRegistryNotSet(); + error NotInitialized(); + /** - * @dev Initialize clrfund instance with MACI factory and new round templates + * @dev Initialize clrfund instance with MACI factory and round factory */ function init( address _maciFactory, @@ -74,14 +73,58 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { external { __Ownable_init(); + _setMaciFactory(_maciFactory); + _setFundingRoundFactory(_roundFactory); + + emit Initialized(); + } + + /** + * @dev Set MACI factory. + * @param _maciFactory Address of a MACI factory. + */ + function _setMaciFactory(address _maciFactory) private + { + if (_maciFactory == address(0)) revert InvalidMaciFactory(); + + maciFactory = IMACIFactory(_maciFactory); + + emit MaciFactoryChanged(address(_maciFactory)); + } - if (address(_maciFactory) == address(0)) revert InvalidMaciFactory(); + /** + * @dev Set MACI factory. + * @param _maciFactory Address of a MACI factory. + */ + function setMaciFactory(address _maciFactory) + external + onlyOwner + { + _setMaciFactory(_maciFactory); + } + + /** + * @dev Set Funding found factory. + * @param _roundFactory Factory Address of a funding round factory. + */ + function _setFundingRoundFactory(address _roundFactory) private + { if (_roundFactory == address(0)) revert InvalidFundingRoundFactory(); - maciFactory = MACIFactory(_maciFactory); - roundFactory = FundingRoundFactory(_roundFactory); + roundFactory = IFundingRoundFactory(_roundFactory); - emit Initialized(); + emit FundingRoundFactoryChanged(address(roundFactory)); + } + + /** + * @dev Set Funding found factory. + * @param _roundFactory Factory Address of a funding round factory. + */ + function setFundingRoundFactory(address _roundFactory) + public + onlyOwner + { + _setFundingRoundFactory(_roundFactory); } /** @@ -94,7 +137,7 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { { userRegistry = _userRegistry; - emit UserRegistrySet(); + emit UserRegistryChanged(address(_userRegistry)); } /** @@ -106,10 +149,10 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { onlyOwner { recipientRegistry = _recipientRegistry; - (, uint256 maxVoteOptions) = maciFactory.maxValues(); - recipientRegistry.setMaxRecipients(maxVoteOptions); + MaxValues memory maxValues = maciFactory.maxValues(); + recipientRegistry.setMaxRecipients(maxValues.maxVoteOptions); - emit RecipientRegistrySet(); + emit RecipientRegistryChanged(address(_recipientRegistry)); } /** @@ -145,10 +188,10 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { function getCurrentRound() public view - returns (FundingRound _currentRound) + returns (IFundingRound _currentRound) { if (rounds.length == 0) { - return FundingRound(address(0)); + return IFundingRound(address(0)); } return rounds[rounds.length - 1]; } @@ -162,42 +205,21 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { ) external onlyOwner - requireToken - requireCoordinator - requireRecipientRegistry - requireUserRegistry { - FundingRound currentRound = getCurrentRound(); + IFundingRound currentRound = getCurrentRound(); if (address(currentRound) != address(0) && !currentRound.isFinalized()) { revert NotFinalized(); } + + if (address(recipientRegistry) == address(0)) revert RecipientRegistryNotSet(); + // Make sure that the max number of recipients is set correctly - (, uint256 maxVoteOptions) = maciFactory.maxValues(); - recipientRegistry.setMaxRecipients(maxVoteOptions); + MaxValues memory maxValues = maciFactory.maxValues(); + recipientRegistry.setMaxRecipients(maxValues.maxVoteOptions); + // Deploy funding round and MACI contracts - FundingRound newRound = roundFactory.deploy( - nativeToken, - userRegistry, - recipientRegistry, - coordinator, - address(this) - ); - rounds.push(newRound); - - TopupToken topupToken = newRound.topupToken(); - MACI maci = maciFactory.deployMaci( - SignUpGatekeeper(newRound), - InitialVoiceCreditProxy(newRound), - address(topupToken), - duration, - coordinator, - coordinatorPubKey - ); - - newRound.setMaci(maci); - - // since we just created a new MACI, the first poll id starts from 0 - newRound.setPoll(0); + address newRound = roundFactory.deploy(duration, address(this)); + rounds.push(IFundingRound(newRound)); emit RoundStarted(address(newRound)); } @@ -235,7 +257,7 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { external onlyOwner { - FundingRound currentRound = getCurrentRound(); + IFundingRound currentRound = getCurrentRound(); requireCurrentRound(currentRound); ERC20 roundToken = currentRound.nativeToken(); @@ -265,7 +287,7 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { external onlyOwner { - FundingRound currentRound = getCurrentRound(); + IFundingRound currentRound = getCurrentRound(); requireCurrentRound(currentRound); if (currentRound.isFinalized()) { @@ -313,7 +335,7 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { // the address being 0x0 coordinator = address(0); coordinatorPubKey = PubKey(0, 0); - FundingRound currentRound = getCurrentRound(); + IFundingRound currentRound = getCurrentRound(); if (address(currentRound) != address(0) && !currentRound.isFinalized()) { currentRound.cancel(); emit RoundFinalized(address(currentRound)); @@ -328,37 +350,9 @@ contract ClrFund is OwnableUpgradeable, IPubKey, SnarkCommon, Params { _; } - function requireCurrentRound(FundingRound currentRound) private pure { + function requireCurrentRound(IFundingRound currentRound) private pure { if (address(currentRound) == address(0)) { revert NoCurrentRound(); } } - - modifier requireToken() { - if (address(nativeToken) == address(0)) { - revert NoToken(); - } - _; - } - - modifier requireCoordinator() { - if (coordinator == address(0)) { - revert NoCoordinator(); - } - _; - } - - modifier requireUserRegistry() { - if (address(userRegistry) == address(0)) { - revert NoUserRegistry(); - } - _; - } - - modifier requireRecipientRegistry() { - if (address(recipientRegistry) == address(0)) { - revert NoRecipientRegistry(); - } - _; - } } diff --git a/contracts/contracts/ClrFundDeployer.sol b/contracts/contracts/ClrFundDeployer.sol index 8db5bc7fd..2487723ff 100644 --- a/contracts/contracts/ClrFundDeployer.sol +++ b/contracts/contracts/ClrFundDeployer.sol @@ -5,8 +5,8 @@ pragma solidity 0.8.10; import {MACIFactory} from './MACIFactory.sol'; import {ClrFund} from './ClrFund.sol'; import {CloneFactory} from './CloneFactory.sol'; -import {SignUpGatekeeper} from "@clrfund/maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol"; -import {InitialVoiceCreditProxy} from "@clrfund/maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol"; +import {SignUpGatekeeper} from "maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol"; +import {InitialVoiceCreditProxy} from "maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol"; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; contract ClrFundDeployer is CloneFactory, Ownable { @@ -61,26 +61,12 @@ contract ClrFundDeployer is CloneFactory, Ownable { function deployClrFund() public returns (address) { ClrFund clrfund = ClrFund(createClone(clrfundTemplate)); clrfund.init(maciFactory, roundFactory); - emit NewInstance(address(clrfund)); - return address(clrfund); - } + // clrfund.init() set the owner to us, now transfer to the caller + clrfund.transferOwnership(msg.sender); - /** - * @dev Register the clrfund instance of subgraph event processing - * @param _clrFundAddress ClrFund address - * @param _metadata Clrfund metadata - */ - function registerInstance( - address _clrFundAddress, - string memory _metadata - ) public returns (bool) { - - if (clrfunds[_clrFundAddress] == true) revert ClrFundAlreadyRegistered(); - - clrfunds[_clrFundAddress] = true; + emit NewInstance(address(clrfund)); - emit Register(_clrFundAddress, _metadata); - return true; + return address(clrfund); } } diff --git a/contracts/contracts/ExternalContacts.sol b/contracts/contracts/ExternalContacts.sol new file mode 100644 index 000000000..51706fb5f --- /dev/null +++ b/contracts/contracts/ExternalContacts.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.10; + +/* + * These imports are just for hardhat to find the contracts for deployment + * They are not used anywhere else + */ +import {Poll} from 'maci-contracts/contracts/Poll.sol'; +import {PollFactory} from 'maci-contracts/contracts/PollFactory.sol'; +import {TallyFactory} from 'maci-contracts/contracts/TallyFactory.sol'; +import {SubsidyFactory} from 'maci-contracts/contracts/SubsidyFactory.sol'; +import {MessageProcessorFactory} from 'maci-contracts/contracts/MessageProcessorFactory.sol'; diff --git a/contracts/contracts/FundingRound.sol b/contracts/contracts/FundingRound.sol index a4f465a39..9d2e67c1a 100644 --- a/contracts/contracts/FundingRound.sol +++ b/contracts/contracts/FundingRound.sol @@ -6,29 +6,42 @@ import '@openzeppelin/contracts/access/Ownable.sol'; import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; -import {DomainObjs} from '@clrfund/maci-contracts/contracts/DomainObjs.sol'; -import {MACI} from '@clrfund/maci-contracts/contracts/MACI.sol'; -import {Poll} from '@clrfund/maci-contracts/contracts/Poll.sol'; -import {Tally} from '@clrfund/maci-contracts/contracts/Tally.sol'; +import {DomainObjs} from 'maci-contracts/contracts/utilities/DomainObjs.sol'; +import {MACI} from 'maci-contracts/contracts/MACI.sol'; +import {Poll} from 'maci-contracts/contracts/Poll.sol'; +import {Tally} from 'maci-contracts/contracts/Tally.sol'; import {TopupToken} from './TopupToken.sol'; -import {SignUpGatekeeper} from "@clrfund/maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol"; -import {InitialVoiceCreditProxy} from "@clrfund/maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol"; +import {SignUpGatekeeper} from 'maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol'; +import {InitialVoiceCreditProxy} from 'maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol'; +import {CommonUtilities} from 'maci-contracts/contracts/utilities/CommonUtilities.sol'; +import {SnarkCommon} from 'maci-contracts/contracts/crypto/SnarkCommon.sol'; +import {ITallySubsidyFactory} from 'maci-contracts/contracts/interfaces/ITallySubsidyFactory.sol'; +import {IMessageProcessorFactory} from 'maci-contracts/contracts/interfaces/IMPFactory.sol'; +import {IClrFund} from './interfaces/IClrFund.sol'; +import {IMACIFactory} from './interfaces/IMACIFactory.sol'; +import {MACICommon} from './MACICommon.sol'; import './userRegistry/IUserRegistry.sol'; import './recipientRegistry/IRecipientRegistry.sol'; -contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, DomainObjs { +contract FundingRound is + Ownable, + SignUpGatekeeper, + InitialVoiceCreditProxy, + DomainObjs, + SnarkCommon, + CommonUtilities, + MACICommon +{ using SafeERC20 for ERC20; // Errors error OnlyMaciCanRegisterVoters(); error NotCoordinator(); - error PollNotSet(); - error TallyNotSet(); - error InvalidPollId(); + error InvalidPoll(); error InvalidTally(); + error InvalidMessageProcessor(); error MaciAlreadySet(); - error MaciNotSet(); error ContributionAmountIsZero(); error ContributionAmountTooLarge(); error AlreadyContributed(); @@ -49,12 +62,19 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom error IncorrectTallyResult(); error IncorrectSpentVoiceCredits(); error IncorrectPerVOSpentVoiceCredits(); - error VotingIsNotOver(); error FundsAlreadyClaimed(); error TallyHashNotPublished(); - error BudgetGreaterThanVotes(); - error IncompleteTallyResults(); + error IncompleteTallyResults(uint256 total, uint256 actual); error NoVotes(); + error MaciNotSet(); + error PollNotSet(); + error InvalidMaci(); + error InvalidNativeToken(); + error InvalidUserRegistry(); + error InvalidRecipientRegistry(); + error InvalidCoordinator(); + error UnexpectedPollAddress(address expected, address actual); + // Constants uint256 private constant MAX_VOICE_CREDITS = 10 ** 9; // MACI allows 2 ** 32 voice credits max @@ -88,7 +108,6 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom uint256 public pollId; Poll public poll; - Tally public tally; address public coordinator; @@ -115,7 +134,7 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom event TallyPublished(string _tallyHash); event Voted(address indexed _contributor); event TallyResultsAdded(uint256 indexed _voteOptionIndex, uint256 _tally); - event PollSet(uint256 indexed _pollId, address indexed _poll); + event PollSet(address indexed _poll); event TallySet(address indexed _tally); modifier onlyCoordinator() { @@ -127,10 +146,6 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom /** * @dev Set round parameters. - * @param _nativeToken Address of a token which will be accepted for contributions. - * @param _userRegistry Address of the registry of verified users. - * @param _recipientRegistry Address of the recipient registry. - * @param _coordinator Address of the coordinator. */ constructor( ERC20 _nativeToken, @@ -139,9 +154,15 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom address _coordinator ) { + if (isAddressZero(address(_nativeToken))) revert InvalidNativeToken(); + if (isAddressZero(address(_userRegistry))) revert InvalidUserRegistry(); + if (isAddressZero(address(_recipientRegistry))) revert InvalidRecipientRegistry(); + if (isAddressZero(_coordinator)) revert InvalidCoordinator(); + nativeToken = _nativeToken; voiceCreditFactor = (MAX_CONTRIBUTION_AMOUNT * uint256(10) ** nativeToken.decimals()) / MAX_VOICE_CREDITS; voiceCreditFactor = voiceCreditFactor > 0 ? voiceCreditFactor : 1; + userRegistry = _userRegistry; recipientRegistry = _recipientRegistry; coordinator = _coordinator; @@ -149,25 +170,16 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom } /** - * @dev Check if the voting period is over. + * @dev Is the given address a zero address */ - function isVotingOver() internal view returns (bool) { - if (address(poll) == address(0)) { - revert PollNotSet(); - } - (uint256 deployTime, uint256 duration) = poll.getDeployTimeAndDuration(); - uint256 secondsPassed = block.timestamp - deployTime; - return (secondsPassed >= duration); + function isAddressZero(address addressValue) public pure returns (bool) { + return (addressValue == address(0)); } /** * @dev Have the votes been tallied */ - function isTallied() internal view returns (bool) { - if (address(tally) == address(0)) { - revert TallyNotSet(); - } - + function isTallied() private view returns (bool) { (uint256 numSignUps, ) = poll.numSignUpsAndMessages(); (, uint256 tallyBatchSize, ) = poll.batchSizes(); uint256 tallyBatchNum = tally.tallyBatchNum(); @@ -177,53 +189,73 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom } /** - * @dev Set the MACI poll - * @param _pollId The poll id. - */ - function setPoll(uint256 _pollId) - external - onlyOwner + * @dev Set the tally contract + * @param _tally The tally contract address + */ + function _setTally(address _tally) private { - poll = maci.getPoll(_pollId); - if (address(poll) == address(0)) { - revert InvalidPollId(); + if (isAddressZero(_tally)) { + revert InvalidTally(); } - pollId = _pollId; - emit PollSet(pollId, address(poll)); + tally = Tally(_tally); + emit TallySet(address(tally)); } /** - * @dev Set the tally contract - * @param _tally The tally contract address + * @dev Reset tally results. This should only be used if the tally script + * failed to proveOnChain due to unexpected error processing MACI logs */ - function setTally(Tally _tally) + function resetTally() external onlyCoordinator { - if (address(_tally) == address(0)) { - revert InvalidTally(); + if (isAddressZero(address(maci))) revert MaciNotSet(); + + _votingPeriodOver(poll); + if (isFinalized) { + revert RoundAlreadyFinalized(); } - tally = _tally; + address verifier = address(tally.verifier()); + address vkRegistry = address(tally.vkRegistry()); - emit TallySet(address(tally)); + IMessageProcessorFactory messageProcessorFactory = maci.messageProcessorFactory(); + ITallySubsidyFactory tallyFactory = maci.tallyFactory(); + + address mp = messageProcessorFactory.deploy(verifier, vkRegistry, address(poll), coordinator); + address newTally = tallyFactory.deploy(verifier, vkRegistry, address(poll), mp, coordinator); + _setTally(newTally); } /** - * @dev Link MACI instance to this funding round. + * @dev Link MACI related contracts to this funding round. */ function setMaci( - MACI _maci + MACI _maci, + MACI.PollContracts memory _pollContracts ) external onlyOwner { - if (address(maci) != address(0)) { - revert MaciAlreadySet(); + if (!isAddressZero(address(maci))) revert MaciAlreadySet(); + + if (isAddressZero(address(_maci))) revert InvalidMaci(); + if (isAddressZero(_pollContracts.poll)) revert InvalidPoll(); + if (isAddressZero(_pollContracts.messageProcessor)) revert InvalidMessageProcessor(); + + // we only create 1 poll per maci, make sure MACI use pollId = 0 + // as the first poll index + pollId = 0; + + address expectedPoll = _maci.getPoll(pollId); + if( _pollContracts.poll != expectedPoll ) { + revert UnexpectedPollAddress(expectedPoll, _pollContracts.poll); } maci = _maci; + poll = Poll(_pollContracts.poll); + _setTally(_pollContracts.tally); } /** @@ -237,7 +269,7 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom ) external { - if (address(maci) == address(0)) revert MaciNotSet(); + if (isAddressZero(address(maci))) revert MaciNotSet(); if (isFinalized) revert RoundAlreadyFinalized(); if (amount == 0) revert ContributionAmountIsZero(); if (amount > MAX_VOICE_CREDITS * voiceCreditFactor) revert ContributionAmountTooLarge(); @@ -327,9 +359,7 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom ) external { - if (address(poll) == address(0)) { - revert PollNotSet(); - } + if (isAddressZero(address(poll))) revert PollNotSet(); uint256 batchSize = _messages.length; for (uint8 i = 0; i < batchSize; i++) { @@ -428,13 +458,8 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom revert NoProjectHasMoreThanOneVote(); } - uint256 quadraticVotes = voiceCreditFactor * _totalVotesSquares; - if (_budget < quadraticVotes) { - _alpha = (_budget - contributions) * ALPHA_PRECISION / (quadraticVotes - contributions); - } else { - // protect against overflow error in getAllocatedAmount() - _alpha = ALPHA_PRECISION; - } + return (_budget - contributions) * ALPHA_PRECISION / + (voiceCreditFactor * (_totalVotesSquares - _totalSpent)); } @@ -458,12 +483,11 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom if (isFinalized) { revert RoundAlreadyFinalized(); } - if (address(maci) == address(0)) { - revert MaciNotSet(); - } - if (!isVotingOver()) { - revert VotingIsNotOver(); - } + + if (isAddressZero(address(maci))) revert MaciNotSet(); + + _votingPeriodOver(poll); + if (!isTallied()) { revert VotesNotTallied(); } @@ -471,19 +495,14 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom revert TallyHashNotPublished(); } - // make sure we have received all the tally results (,,, uint8 voteOptionTreeDepth) = poll.treeDepths(); uint256 totalResults = uint256(LEAVES_PER_NODE) ** uint256(voteOptionTreeDepth); if ( totalTallyResults != totalResults ) { - revert IncompleteTallyResults(); + revert IncompleteTallyResults(totalResults, totalTallyResults); } -/* TODO how to check this in maci v1?? - totalVotes = maci.totalVotes(); // If nobody voted, the round should be cancelled to avoid locking of matching funds - require(totalVotes > 0, 'FundingRound: No votes'); -*/ if ( _totalSpent == 0) { revert NoVotes(); } @@ -535,8 +554,10 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom { // amount = ( alpha * (quadratic votes)^2 + (precision - alpha) * totalSpent ) / precision uint256 quadratic = alpha * voiceCreditFactor * _tallyResult * _tallyResult; - uint256 linear = (ALPHA_PRECISION - alpha) * voiceCreditFactor * _spent; - return (quadratic + linear) / ALPHA_PRECISION; + uint256 totalSpentCredits = voiceCreditFactor * _spent; + uint256 linearPrecision = ALPHA_PRECISION * totalSpentCredits; + uint256 linearAlpha = alpha * totalSpentCredits; + return ((quadratic + linearPrecision) - linearAlpha) / ALPHA_PRECISION; } /** @@ -616,7 +637,7 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom function _addTallyResult( uint256 _voteOptionIndex, uint256 _tallyResult, - uint256[][] calldata _tallyResultProof, + uint256[][] memory _tallyResultProof, uint256 _tallyResultSalt, uint256 _spentVoiceCreditsHash, uint256 _perVOSpentVoiceCreditsHash @@ -652,7 +673,6 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom /** * @dev Add and verify tally results by batch. - * @param _voteOptionTreeDepth Vote option tree depth. * @param _voteOptionIndices Vote option index. * @param _tallyResults The results of vote tally for the recipients. * @param _tallyResultProofs Proofs of correctness of the vote tally results. @@ -661,7 +681,6 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom * @param _perVOSpentVoiceCreditsHashes hashLeftRight(merkle root of the no spent voice credits per vote option, perVOSpentVoiceCredits salt) */ function addTallyResultsBatch( - uint8 _voteOptionTreeDepth, uint256[] calldata _voteOptionIndices, uint256[] calldata _tallyResults, uint256[][][] calldata _tallyResultProofs, @@ -672,6 +691,8 @@ contract FundingRound is Ownable, SignUpGatekeeper, InitialVoiceCreditProxy, Dom external onlyCoordinator { + if (isAddressZero(address(maci))) revert MaciNotSet(); + if (!isTallied()) { revert VotesNotTallied(); } diff --git a/contracts/contracts/FundingRoundFactory.sol b/contracts/contracts/FundingRoundFactory.sol index 6d4d586de..9a35c1077 100644 --- a/contracts/contracts/FundingRoundFactory.sol +++ b/contracts/contracts/FundingRoundFactory.sol @@ -3,28 +3,54 @@ pragma solidity ^0.8.10; import {FundingRound} from './FundingRound.sol'; -import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; -import {IUserRegistry} from './userRegistry/IUserRegistry.sol'; -import {IRecipientRegistry} from './recipientRegistry/IRecipientRegistry.sol'; +import {IClrFund} from './interfaces/IClrFund.sol'; +import {IMACIFactory} from './interfaces/IMACIFactory.sol'; +import {MACICommon} from './MACICommon.sol'; +import {MACI} from 'maci-contracts/contracts/MACI.sol'; +import {SignUpGatekeeper} from 'maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol'; +import {InitialVoiceCreditProxy} from 'maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol'; -contract FundingRoundFactory { +/** +* @dev A factory to deploy the funding round contract +*/ +contract FundingRoundFactory is MACICommon { + /** + * @dev Deploy the funding round contract + * @param _duration the funding round duration + * @param _clrfund the clrfund contract containing information used to + * deploy a funding round, e.g. nativeToken, coordinator address + * coordinator public key, etc. + */ function deploy( - ERC20 _nativeToken, - IUserRegistry _userRegistry, - IRecipientRegistry _recipientRegistry, - address _coordinator, - address _owner + uint256 _duration, + address _clrfund ) external - returns (FundingRound newRound) + returns (address) { - newRound = new FundingRound( - _nativeToken, - _userRegistry, - _recipientRegistry, - _coordinator + IClrFund clrfund = IClrFund(_clrfund); + FundingRound newRound = new FundingRound( + clrfund.nativeToken(), + clrfund.userRegistry(), + clrfund.recipientRegistry(), + clrfund.coordinator() ); - newRound.transferOwnership(_owner); + IMACIFactory maciFactory = clrfund.maciFactory(); + (MACI maci, MACI.PollContracts memory pollContracts) = maciFactory.deployMaci( + SignUpGatekeeper(newRound), + InitialVoiceCreditProxy(newRound), + address(newRound.topupToken()), + _duration, + newRound.coordinator(), + clrfund.coordinatorPubKey(), + address(this) + ); + + // link funding round with maci related contracts + newRound.setMaci(maci, pollContracts); + newRound.transferOwnership(_clrfund); + maci.transferOwnership(address(newRound)); + return address(newRound); } } diff --git a/contracts/contracts/MACICommon.sol b/contracts/contracts/MACICommon.sol new file mode 100644 index 000000000..0872ac85e --- /dev/null +++ b/contracts/contracts/MACICommon.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.10; + +/** + * @dev a contract that holds common MACI structures + */ +contract MACICommon { + /** + * @dev These are contract factories used to deploy MACI poll processing contracts + * when creating a new ClrFund funding round. + */ + struct Factories { + address pollFactory; + address tallyFactory; + // subsidyFactory is not currently used, it's just a place holder here + address subsidyFactory; + address messageProcessorFactory; + } + +} \ No newline at end of file diff --git a/contracts/contracts/MACIFactory.sol b/contracts/contracts/MACIFactory.sol index e633fead4..f0a94c230 100644 --- a/contracts/contracts/MACIFactory.sol +++ b/contracts/contracts/MACIFactory.sol @@ -2,24 +2,31 @@ pragma solidity ^0.8.10; -import {MACI} from '@clrfund/maci-contracts/contracts/MACI.sol'; -import {Poll, PollFactory} from '@clrfund/maci-contracts/contracts/Poll.sol'; -import {SignUpGatekeeper} from '@clrfund/maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol'; -import {InitialVoiceCreditProxy} from '@clrfund/maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol'; -import {TopupCredit} from '@clrfund/maci-contracts/contracts/TopupCredit.sol'; -import {VkRegistry} from '@clrfund/maci-contracts/contracts/VkRegistry.sol'; -import {SnarkCommon} from '@clrfund/maci-contracts/contracts/crypto/SnarkCommon.sol'; +import {MACI} from 'maci-contracts/contracts/MACI.sol'; +import {IPollFactory} from 'maci-contracts/contracts/interfaces/IPollFactory.sol'; +import {ITallySubsidyFactory} from 'maci-contracts/contracts/interfaces/ITallySubsidyFactory.sol'; +import {IMessageProcessorFactory} from 'maci-contracts/contracts/interfaces/IMPFactory.sol'; +import {SignUpGatekeeper} from 'maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol'; +import {InitialVoiceCreditProxy} from 'maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol'; +import {TopupCredit} from 'maci-contracts/contracts/TopupCredit.sol'; +import {VkRegistry} from 'maci-contracts/contracts/VkRegistry.sol'; +import {Verifier} from 'maci-contracts/contracts/crypto/Verifier.sol'; +import {SnarkCommon} from 'maci-contracts/contracts/crypto/SnarkCommon.sol'; import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol'; -import {Params} from '@clrfund/maci-contracts/contracts/Params.sol'; -import {IPubKey} from '@clrfund/maci-contracts/contracts/DomainObjs.sol'; +import {Params} from 'maci-contracts/contracts/utilities/Params.sol'; +import {DomainObjs} from 'maci-contracts/contracts/utilities/DomainObjs.sol'; +import {MACICommon} from './MACICommon.sol'; -contract MACIFactory is Ownable, Params, SnarkCommon, IPubKey { - // Constants - uint8 private constant VOTE_OPTION_TREE_BASE = 5; +contract MACIFactory is Ownable, Params, SnarkCommon, DomainObjs, MACICommon { - // State + // Verifying Key Registry containing circuit parameters VkRegistry public vkRegistry; - PollFactory public pollFactory; + // All the factory contracts used to deploy Poll, Tally, MessageProcessor, Subsidy + Factories public factories; + // verifier is used when creating Tally, MessageProcessor, Subsidy + Verifier public verifier; + + // circuit parameters uint8 public stateTreeDepth; TreeDepths public treeDepths; MaxValues public maxValues; @@ -35,13 +42,25 @@ contract MACIFactory is Ownable, Params, SnarkCommon, IPubKey { error TallyVkNotSet(); error InvalidVkRegistry(); error InvalidPollFactory(); - - constructor(address _vkRegistry, address _pollFactory) { + error InvalidTallyFactory(); + error InvalidSubsidyFactory(); + error InvalidMessageProcessorFactory(); + error InvalidVerifier(); + + constructor( + address _vkRegistry, + Factories memory _factories, + address _verifier + ) { if (_vkRegistry == address(0)) revert InvalidVkRegistry(); - if (_pollFactory == address(0)) revert InvalidPollFactory(); + if (_factories.pollFactory == address(0)) revert InvalidPollFactory(); + if (_factories.tallyFactory == address(0)) revert InvalidTallyFactory(); + if (_factories.messageProcessorFactory == address(0)) revert InvalidMessageProcessorFactory(); + if (_verifier == address(0)) revert InvalidVerifier(); vkRegistry = VkRegistry(_vkRegistry); - pollFactory = PollFactory(_pollFactory); + factories = _factories; + verifier = Verifier(_verifier); } /** @@ -60,7 +79,37 @@ contract MACIFactory is Ownable, Params, SnarkCommon, IPubKey { function setPollFactory(address _pollFactory) public onlyOwner { if (_pollFactory == address(0)) revert InvalidPollFactory(); - pollFactory = PollFactory(_pollFactory); + factories.pollFactory = _pollFactory; + } + + /** + * @dev set tally factory in MACI factory + * @param _tallyFactory tally factory + */ + function setTallyFactory(address _tallyFactory) public onlyOwner { + if (_tallyFactory == address(0)) revert InvalidTallyFactory(); + + factories.tallyFactory = _tallyFactory; + } + + /** + * @dev set message processor factory in MACI factory + * @param _messageProcessorFactory message processor factory + */ + function setMessageProcessorFactory(address _messageProcessorFactory) public onlyOwner { + if (_messageProcessorFactory == address(0)) revert InvalidMessageProcessorFactory(); + + factories.messageProcessorFactory = _messageProcessorFactory; + } + + /** + * @dev set verifier in MACI factory + * @param _verifier verifier contract + */ + function setVerifier(address _verifier) public onlyOwner { + if (_verifier == address(0)) revert InvalidVerifier(); + + verifier = Verifier(_verifier); } /** @@ -70,38 +119,32 @@ contract MACIFactory is Ownable, Params, SnarkCommon, IPubKey { uint8 _stateTreeDepth, TreeDepths calldata _treeDepths, MaxValues calldata _maxValues, - uint256 _messageBatchSize, - VerifyingKey calldata _processVk, - VerifyingKey calldata _tallyVk + uint256 _messageBatchSize ) public onlyOwner { + if (!vkRegistry.hasProcessVk( - _stateTreeDepth, - _treeDepths.messageTreeDepth, - _treeDepths.voteOptionTreeDepth, - _messageBatchSize) || - !vkRegistry.hasTallyVk( - _stateTreeDepth, - _treeDepths.intStateTreeDepth, - _treeDepths.voteOptionTreeDepth - ) + _stateTreeDepth, + _treeDepths.messageTreeDepth, + _treeDepths.voteOptionTreeDepth, + _messageBatchSize) ) { - vkRegistry.setVerifyingKeys( - _stateTreeDepth, - _treeDepths.intStateTreeDepth, - _treeDepths.messageTreeDepth, - _treeDepths.voteOptionTreeDepth, - _messageBatchSize, - _processVk, - _tallyVk - ); + revert ProcessVkNotSet(); + } + + if (!vkRegistry.hasTallyVk( + _stateTreeDepth, + _treeDepths.intStateTreeDepth, + _treeDepths.voteOptionTreeDepth) + ) { + revert TallyVkNotSet(); } stateTreeDepth = _stateTreeDepth; - maxValues = _maxValues; treeDepths = _treeDepths; + maxValues = _maxValues; messageBatchSize = _messageBatchSize; emit MaciParametersChanged(); @@ -116,10 +159,11 @@ contract MACIFactory is Ownable, Params, SnarkCommon, IPubKey { address topupCredit, uint256 duration, address coordinator, - PubKey calldata coordinatorPubKey + PubKey calldata coordinatorPubKey, + address maciOwner ) external - returns (MACI _maci) + returns (MACI _maci, MACI.PollContracts memory _pollContracts) { if (!vkRegistry.hasProcessVk( stateTreeDepth, @@ -139,14 +183,33 @@ contract MACIFactory is Ownable, Params, SnarkCommon, IPubKey { } _maci = new MACI( - pollFactory, + IPollFactory(factories.pollFactory), + IMessageProcessorFactory(factories.messageProcessorFactory), + ITallySubsidyFactory(factories.tallyFactory), + ITallySubsidyFactory(factories.subsidyFactory), signUpGatekeeper, - initialVoiceCreditProxy + initialVoiceCreditProxy, + TopupCredit(topupCredit), + stateTreeDepth ); - _maci.init(vkRegistry, TopupCredit(topupCredit)); - address poll = _maci.deployPoll(duration, maxValues, treeDepths, coordinatorPubKey); - Poll(poll).transferOwnership(coordinator); + _pollContracts = _maci.deployPoll( + duration, + maxValues, + treeDepths, + coordinatorPubKey, + address(verifier), + address(vkRegistry), + // pass false to not deploy the subsidy contract + false + ); + + // transfer ownership to coordinator to run the tally scripts + Ownable(_pollContracts.poll).transferOwnership(coordinator); + Ownable(_pollContracts.messageProcessor).transferOwnership(coordinator); + Ownable(_pollContracts.tally).transferOwnership(coordinator); + + _maci.transferOwnership(maciOwner); emit MaciDeployed(address(_maci)); } diff --git a/contracts/contracts/OwnableUpgradeable.sol b/contracts/contracts/OwnableUpgradeable.sol index 63d587c3a..3826e125a 100644 --- a/contracts/contracts/OwnableUpgradeable.sol +++ b/contracts/contracts/OwnableUpgradeable.sol @@ -65,7 +65,7 @@ abstract contract ContextUpgradeable is Initializable { function __Context_init_unchained() internal initializer { } function _msgSender() internal view virtual returns (address) { - return tx.origin; + return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { diff --git a/contracts/contracts/interfaces/IClrFund.sol b/contracts/contracts/interfaces/IClrFund.sol new file mode 100644 index 000000000..5da1b8b45 --- /dev/null +++ b/contracts/contracts/interfaces/IClrFund.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.10; + +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +import {IUserRegistry} from '../userRegistry/IUserRegistry.sol'; +import {IRecipientRegistry} from '../recipientRegistry/IRecipientRegistry.sol'; +import {DomainObjs} from 'maci-contracts/contracts/utilities/DomainObjs.sol'; +import {IMACIFactory} from './IMACIFactory.sol'; + +/** + * @dev ClrFund interface + */ +interface IClrFund { + function nativeToken() external view returns (ERC20); + function maciFactory() external view returns (IMACIFactory); + function userRegistry() external view returns (IUserRegistry); + function recipientRegistry() external view returns (IRecipientRegistry); + function coordinatorPubKey() external view returns (DomainObjs.PubKey memory); + function coordinator() external view returns (address); +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/IFundingRound.sol b/contracts/contracts/interfaces/IFundingRound.sol new file mode 100644 index 000000000..026f40c0a --- /dev/null +++ b/contracts/contracts/interfaces/IFundingRound.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.10; + +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +/** + * @dev FundingRound interface used by the ClrFund contract + */ +interface IFundingRound { + function nativeToken() external view returns (ERC20); + function isFinalized() external view returns (bool); + function isCancelled() external view returns (bool); + function cancel() external; + function finalize( + uint256 _totalSpent, + uint256 _totalSpentSalt, + uint256 _newResultCommitment, + uint256 _perVOSpentVoiceCreditsHash + ) external; +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/IFundingRoundFactory.sol b/contracts/contracts/interfaces/IFundingRoundFactory.sol new file mode 100644 index 000000000..9ca5806f1 --- /dev/null +++ b/contracts/contracts/interfaces/IFundingRoundFactory.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.10; + +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +/** + * @dev FundingRoundFactory interface used by the ClrFund contract + */ +interface IFundingRoundFactory { + function deploy(uint256 _duration, address _clrfund) external returns (address); +} \ No newline at end of file diff --git a/contracts/contracts/interfaces/IMACIFactory.sol b/contracts/contracts/interfaces/IMACIFactory.sol new file mode 100644 index 000000000..3ed50f8e1 --- /dev/null +++ b/contracts/contracts/interfaces/IMACIFactory.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.10; + +import {IVkRegistry} from 'maci-contracts/contracts/interfaces/IVkRegistry.sol'; +import {IVerifier} from 'maci-contracts/contracts/interfaces/IVerifier.sol'; +import {MACI} from 'maci-contracts/contracts/MACI.sol'; +import {Params} from 'maci-contracts/contracts/utilities/Params.sol'; +import {DomainObjs} from 'maci-contracts/contracts/utilities/DomainObjs.sol'; +import {SignUpGatekeeper} from 'maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol'; +import {InitialVoiceCreditProxy} from 'maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol'; +import {MACICommon} from '../MACICommon.sol'; + +/** + * @dev MACIFactory interface + */ +interface IMACIFactory { + // Verifying Key Registry containing zk circuit parameters + function vkRegistry() external view returns (IVkRegistry); + + // All the factory contracts used to deploy Poll, Tally, MessageProcessor, Subsidy + function factories() external view returns (MACICommon.Factories memory); + + // verifier is used when creating Tally, MessageProcessor, Subsidy + function verifier() external view returns (IVerifier); + + // poll parameters + function stateTreeDepth() external view returns (uint8); + function treeDepths() external view returns (Params.TreeDepths memory); + function maxValues() external view returns (Params.MaxValues memory); + function messageBatchSize() external view returns (uint256); + + function deployMaci( + SignUpGatekeeper signUpGatekeeper, + InitialVoiceCreditProxy initialVoiceCreditProxy, + address topupCredit, + uint256 duration, + address coordinator, + DomainObjs.PubKey calldata coordinatorPubKey, + address maciOwner + ) external returns (MACI _maci, MACI.PollContracts memory _pollContracts); +} diff --git a/contracts/e2e/index.ts b/contracts/e2e/index.ts index 6e6983b17..04e1e7768 100644 --- a/contracts/e2e/index.ts +++ b/contracts/e2e/index.ts @@ -1,18 +1,27 @@ -/* eslint-disable @typescript-eslint/camelcase */ -import { ethers, waffle, config } from 'hardhat' -import { use, expect } from 'chai' -import { solidity } from 'ethereum-waffle' -import { BigNumber, Contract, Signer, Wallet } from 'ethers' -import { Keypair, createMessage, Message, PubKey } from '@clrfund/common' -import { genTallyResultCommitment } from '@clrfund/common' - -import { UNIT } from '../utils/constants' +import { ethers, config } from 'hardhat' +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' +import { time, mine } from '@nomicfoundation/hardhat-network-helpers' +import { expect } from 'chai' +import { Contract, toNumber } from 'ethers' +import { + Keypair, + createMessage, + Message, + PubKey, + genTallyResultCommitment, +} from '@clrfund/common' + +import { + UNIT, + ALPHA_PRECISION, + DEFAULT_GET_LOG_BATCH_SIZE, + DEFAULT_SR_QUEUE_OPS, +} from '../utils/constants' import { getEventArg } from '../utils/contracts' import { deployContract, deployPoseidonLibraries, deployMaciFactory, - deployMessageProcesorAndTally, } from '../utils/deployment' import { getIpfsHash } from '../utils/ipfs' import { @@ -29,33 +38,28 @@ import { MaciParameters } from '../utils/maciParameters' import { readFileSync, existsSync, mkdirSync } from 'fs' import path from 'path' -use(solidity) +type VoteData = { recipientIndex: number; voiceCredits: bigint } -const ZERO = BigNumber.from(0) -const DEFAULT_SR_QUEUE_OPS = 4 +const ZERO = BigInt(0) +// funding round duration const roundDuration = 7 * 86400 +// log output for debugging +const quiet = !(process.env.DEBUG || false) +console.log('quiet', quiet) +function debugLog(message?: any, ...optionalParams: any[]) { + if (!quiet) console.log(message, ...optionalParams) +} + // MACI zkFiles const circuit = process.env.CIRCUIT_TYPE || DEFAULT_CIRCUIT -const circuitDirectory = process.env.CIRCUIT_DIRECTORY || '../../params' -const rapidSnark = process.env.RAPID_SNARK || '~/rapidsnark/package/bin/prover' +const circuitDirectory = process.env.CIRCUIT_DIRECTORY || './params' +const rapidsnark = process.env.RAPID_SNARK || '~/rapidsnark/package/bin/prover' const proofOutputDirectory = process.env.PROOF_OUTPUT_DIR || './proof_output' const tallyBatchSize = Number(process.env.TALLY_BATCH_SIZE || 8) -let maciTransactionHash: string -const ALPHA_PRECISION = BigNumber.from(10).pow(18) - -const currentBlockTimestamp = async (provider: any): Promise => { - const blockNum = await provider.getBlockNumber() - const block = await provider.getBlock(blockNum) - return Number(block.timestamp) -} - -function sumVoiceCredits(voiceCredits: BigNumber[]): string { - const total = voiceCredits.reduce( - (sum, credits) => sum.add(credits), - BigNumber.from(0) - ) +function sumVoiceCredits(voiceCredits: bigint[]): string { + const total = voiceCredits.reduce((sum, credits) => sum + credits, BigInt(0)) return total.toString() } @@ -64,11 +68,11 @@ function sumVoiceCredits(voiceCredits: BigNumber[]): string { * recipientsVotes[i][j] is the jth vote received by recipient i * returns the tally result for each recipient */ -function tallyVotes(recipientsVotes: BigNumber[][]): BigNumber[] { +function tallyVotes(recipientsVotes: bigint[][]): bigint[] { const result = recipientsVotes.map((votes) => { return votes.reduce( - (sum, voiceCredits) => sum.add(bnSqrt(voiceCredits)), - BigNumber.from(0) + (sum, voiceCredits) => sum + bnSqrt(voiceCredits), + BigInt(0) ) }) return result @@ -81,35 +85,43 @@ function tallyVotes(recipientsVotes: BigNumber[][]): BigNumber[] { */ async function calculateClaims( fundingRound: Contract, - votes: BigNumber[][] -): Promise { + votes: bigint[][] +): Promise { const alpha = await fundingRound.alpha() const factor = await fundingRound.voiceCreditFactor() const tallyResult = tallyVotes(votes) return tallyResult.map((quadraticVotes, i) => { const spent = sumVoiceCredits(votes[i]) - const quadratic = quadraticVotes.mul(quadraticVotes).mul(factor).mul(alpha) - const linear = BigNumber.from(spent) - .mul(factor) - .mul(ALPHA_PRECISION.sub(alpha)) - return quadratic.add(linear).div(ALPHA_PRECISION) + const quadratic = quadraticVotes * quadraticVotes * factor * alpha + const linear = BigInt(spent) * factor * (ALPHA_PRECISION - alpha) + return (quadratic + linear) / ALPHA_PRECISION }) } +/** + * Make a vote with recipient and voice credits information + * @param recipientIndex recipient index + * @param voiceCredits voice credits in the vote + * @returns a Vote + */ +function makeVote(recipientIndex: number, voiceCredits: bigint): VoteData { + return { recipientIndex, voiceCredits } +} + describe('End-to-end Tests', function () { this.timeout(60 * 60 * 1000) this.bail(true) - const provider = waffle.provider + let maciTransactionHash: string - let deployer: Signer - let coordinator: Wallet - let poolContributor1: Signer - let poolContributor2: Signer - let recipient1: Signer - let recipient2: Signer - let recipient3: Signer - let contributors: Signer[] + let deployer: HardhatEthersSigner + let coordinator: HardhatEthersSigner + let poolContributor1: HardhatEthersSigner + let poolContributor2: HardhatEthersSigner + let recipient1: HardhatEthersSigner + let recipient2: HardhatEthersSigner + let recipient3: HardhatEthersSigner + let contributors: HardhatEthersSigner[] let poseidonLibraries: { [key: string]: string } let userRegistry: Contract @@ -144,15 +156,13 @@ describe('End-to-end Tests', function () { ] = await ethers.getSigners() // Deploy funding round factory + debugLog('Deploying MACI factory') const maciFactory = await deployMaciFactory({ libraries: poseidonLibraries, signer: deployer, ethers, + maciParameters: params, }) - const setMaciTx = await maciFactory.setMaciParameters( - ...params.asContractParam() - ) - await setMaciTx.wait() clrfund = await deployContract({ name: 'ClrFund', @@ -162,44 +172,39 @@ describe('End-to-end Tests', function () { const roundFactory = await deployContract({ name: 'FundingRoundFactory', - libraries: poseidonLibraries, signer: deployer, ethers, }) const initClrfundTx = await clrfund.init( - maciFactory.address, - roundFactory.address + maciFactory.target, + roundFactory.target ) await initClrfundTx.wait() - const transferTx = await maciFactory.transferOwnership(clrfund.address) + const transferTx = await maciFactory.transferOwnership(clrfund.target) await transferTx.wait() - const SimpleUserRegistry = await ethers.getContractFactory( - 'SimpleUserRegistry', - deployer - ) - userRegistry = await SimpleUserRegistry.deploy() - await clrfund.setUserRegistry(userRegistry.address) + userRegistry = await ethers.deployContract('SimpleUserRegistry', deployer) + await clrfund.setUserRegistry(userRegistry.target) const SimpleRecipientRegistry = await ethers.getContractFactory( 'SimpleRecipientRegistry', deployer ) - recipientRegistry = await SimpleRecipientRegistry.deploy(clrfund.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) + recipientRegistry = await SimpleRecipientRegistry.deploy(clrfund.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) // Deploy ERC20 token contract const Token = await ethers.getContractFactory('AnyOldERC20Token', deployer) - const tokenInitialSupply = UNIT.mul(10000) + const tokenInitialSupply = UNIT * BigInt(10000) token = await Token.deploy(tokenInitialSupply) - await token.transfer(await poolContributor1.getAddress(), UNIT.mul(50)) - await token.transfer(await poolContributor2.getAddress(), UNIT.mul(50)) + await token.transfer(await poolContributor1.getAddress(), UNIT * BigInt(50)) + await token.transfer(await poolContributor2.getAddress(), UNIT * BigInt(50)) for (const contributor of contributors) { - await token.transfer(await contributor.getAddress(), UNIT.mul(100)) + await token.transfer(await contributor.getAddress(), UNIT * BigInt(100)) } // Configure factory - await clrfund.setToken(token.address) + await clrfund.setToken(token.target) coordinatorKeypair = new Keypair() await clrfund.setCoordinator( coordinator.address, @@ -207,16 +212,18 @@ describe('End-to-end Tests', function () { ) // Add funds to matching pool - const poolContributionAmount = UNIT.mul(5) - await token - .connect(poolContributor1) - .transfer(clrfund.address, poolContributionAmount) + const poolContributionAmount = UNIT * BigInt(5) + await (token.connect(poolContributor1) as Contract).transfer( + clrfund.target, + poolContributionAmount + ) // Add additional funding source await clrfund.addFundingSource(await poolContributor2.getAddress()) - await token - .connect(poolContributor2) - .approve(clrfund.address, poolContributionAmount) + await (token.connect(poolContributor2) as Contract).approve( + clrfund.target, + poolContributionAmount + ) // Add recipients await recipientRegistry.addRecipient( @@ -247,6 +254,7 @@ describe('End-to-end Tests', function () { // Deploy new funding round and MACI const newRoundTx = await clrfund.deployNewRound(roundDuration) maciTransactionHash = newRoundTx.hash + const fundingRoundAddress = await clrfund.getCurrentRound() fundingRound = await ethers.getContractAt( 'FundingRound', @@ -255,16 +263,12 @@ describe('End-to-end Tests', function () { const maciAddress = await fundingRound.maci() maci = await ethers.getContractAt('MACI', maciAddress) - pollId = BigInt(await fundingRound.pollId()) - }) + pollId = await fundingRound.pollId() - async function timeTravel(duration: number) { - const currentTime = await currentBlockTimestamp(provider) - await provider.send('evm_increaseTime', [duration + currentTime]) - await provider.send('evm_mine') - } + await mine() + }) - async function makeContributions(amounts: BigNumber[]) { + async function makeContributions(amounts: bigint[]) { const contributions: { [key: string]: any }[] = [] for (let index = 0; index < contributors.length; index++) { const contributionAmount = amounts[index] @@ -276,17 +280,18 @@ describe('End-to-end Tests', function () { const contributorAddress = await contributor.getAddress() await userRegistry.addUser(contributorAddress) // Approve transfer - await token - .connect(contributor) - .approve(fundingRound.address, contributionAmount) + await (token.connect(contributor) as Contract).approve( + fundingRound.target, + contributionAmount + ) // Contribute const contributorKeypair = new Keypair() - const contributionTx = await fundingRound - .connect(contributor) - .contribute( - contributorKeypair.pubKey.asContractParam(), - contributionAmount - ) + const contributionTx = await ( + fundingRound.connect(contributor) as Contract + ).contribute( + contributorKeypair.pubKey.asContractParam(), + contributionAmount + ) const stateIndex = await getEventArg( contributionTx, maci, @@ -302,70 +307,112 @@ describe('End-to-end Tests', function () { contributions.push({ signer: contributor, keypair: contributorKeypair, - stateIndex: parseInt(stateIndex), + stateIndex: toNumber(stateIndex), contribution: contributionAmount, - voiceCredits: voiceCredits, + voiceCredits: BigInt(voiceCredits), }) } return contributions } - async function finalizeRound(): Promise { - const providerUrl = (provider as any)._hardhatNetwork.config.url + /** + * Get the tally and message processor contract address from the funding round + * @returns tally and message processor contract addresses + */ + async function getTallyAndMessageProcessor(): Promise<{ + tallyAddress: string + messageProcessorAddress: string + }> { + const tallyAddress = await fundingRound.tally() + const tallyContact = await ethers.getContractAt('Tally', tallyAddress) + const messageProcessorAddress = await tallyContact.messageProcessor() + + return { tallyAddress, messageProcessorAddress } + } + async function finalizeRound(testResetTally = false): Promise { + debugLog('Finalizing round') // Process messages and tally votes - await mergeMaciSubtrees( - maci.address, - pollId.toString(), - DEFAULT_SR_QUEUE_OPS - ) + const maciAddress = await maci.getAddress() + await mergeMaciSubtrees({ + maciAddress, + pollId, + numQueueOps: DEFAULT_SR_QUEUE_OPS, + }) + debugLog('Merged MACI trees') const random = Math.floor(Math.random() * 10 ** 8) const outputDir = path.join(proofOutputDirectory, `${random}`) if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }) } + + // past an end block that's later than the MACI start block const genProofArgs = getGenProofArgs({ - maciAddress: maci.address, - providerUrl, - pollId: pollId.toString(), + maciAddress, + pollId, coordinatorMacisk: coordinatorKeypair.privKey.serialize(), - maciTxHash: maciTransactionHash, - rapidSnark, + rapidsnark, circuitType: circuit, circuitDirectory, outputDir, + blocksPerBatch: DEFAULT_GET_LOG_BATCH_SIZE, + maciTxHash: maciTransactionHash, + quiet, }) + + debugLog('Generating proof') await genProofs(genProofArgs) + debugLog('Generated proof') - const { mpContract, tallyContract } = await deployMessageProcesorAndTally({ - libraries: poseidonLibraries, - ethers, - signer: coordinator, - }) + const { tallyAddress, messageProcessorAddress } = + await getTallyAndMessageProcessor() + debugLog('Proving on chain') // Submit proofs to MACI contract await proveOnChain({ - contract: maci.address, - poll_id: pollId.toString(), - mp: mpContract.address, - tally: tallyContract.address, - //subsidy: tallyContract.address, // TODO: make subsidy optional - proof_dir: genProofArgs.output, + pollId, + proofDir: genProofArgs.outputDir, + subsidyEnabled: false, + maciAddress, + messageProcessorAddress, + tallyAddress, + quiet, }) + + if (testResetTally) { + console.log('resetting tally and message processor contracts') + const resetTx = await fundingRound.resetTally() + await resetTx.wait() + + const { tallyAddress, messageProcessorAddress } = + await getTallyAndMessageProcessor() + console.log('redoing proveOnChain with new tallyAddress', tallyAddress) + + await proveOnChain({ + pollId, + proofDir: genProofArgs.outputDir, + subsidyEnabled: false, + maciAddress, + messageProcessorAddress, + tallyAddress, + quiet, + }) + } console.log('finished proveOnChain') - await fundingRound.connect(coordinator).setTally(tallyContract.address) - const tally = JSON.parse(readFileSync(genProofArgs.tally_file).toString()) + const tally = JSON.parse(readFileSync(genProofArgs.tallyFile).toString()) const tallyHash = await getIpfsHash(tally) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) + await (fundingRound.connect(coordinator) as Contract).publishTallyHash( + tallyHash + ) console.log('Tally hash', tallyHash) // add tally results to funding round - const recipientTreeDepth = params.voteOptionTreeDepth + const recipientTreeDepth = params.treeDepths.voteOptionTreeDepth console.log('Adding tally result on chain in batches of', tallyBatchSize) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRound.connect(coordinator) as Contract, recipientTreeDepth, tally, tallyBatchSize @@ -373,18 +420,19 @@ describe('End-to-end Tests', function () { console.log('Finished adding tally results') const newResultCommitment = genTallyResultCommitment( - tally.results.tally.map((x) => BigInt(x)), + tally.results.tally.map((x: string) => BigInt(x)), tally.results.salt, recipientTreeDepth ) const perVOSpentVoiceCreditsCommitment = genTallyResultCommitment( - tally.perVOSpentVoiceCredits.tally.map((x) => BigInt(x)), + tally.perVOSpentVoiceCredits.tally.map((x: string) => BigInt(x)), tally.perVOSpentVoiceCredits.salt, recipientTreeDepth ) // Finalize round + debugLog('Transfering matching funds') await clrfund.transferMatchingFunds( tally.totalSpentVoiceCredits.spent, tally.totalSpentVoiceCredits.salt, @@ -393,7 +441,7 @@ describe('End-to-end Tests', function () { ) // Claim funds - const claims: { [index: number]: BigNumber } = {} + const claims: { [index: number]: bigint } = {} for (const recipientIndex of [1, 2]) { const recipient = recipientIndex === 1 ? recipient1 : recipient2 @@ -402,9 +450,9 @@ describe('End-to-end Tests', function () { recipientTreeDepth, tally ) - const claimTx = await fundingRound - .connect(recipient) - .claimFunds(...claimData) + const claimTx = await ( + fundingRound.connect(recipient) as Contract + ).claimFunds(...claimData) const claimedAmount = await getEventArg( claimTx, fundingRound, @@ -418,8 +466,8 @@ describe('End-to-end Tests', function () { it('should allocate funds correctly when users change keys', async () => { const contributions = await makeContributions([ - UNIT.mul(50).div(10), - UNIT.mul(50).div(10), + (UNIT * BigInt(50)) / BigInt(10), + (UNIT * BigInt(50)) / BigInt(10), ]) // Submit messages for (const contribution of contributions) { @@ -446,7 +494,7 @@ describe('End-to-end Tests', function () { // Spend voice credits on both recipients for (const recipientIndex of [1, 2]) { - const voiceCredits = contribution.voiceCredits.div(2) + const voiceCredits = BigInt(contribution.voiceCredits) / BigInt(2) const [message, encPubKey] = createMessage( contribution.stateIndex, newContributorKeypair, @@ -462,13 +510,13 @@ describe('End-to-end Tests', function () { nonce += 1 } - await fundingRound.connect(contributor).submitMessageBatch( + await (fundingRound.connect(contributor) as Contract).submitMessageBatch( messages.reverse().map((msg) => msg.asContractParam()), encPubKeys.reverse().map((key) => key.asContractParam()) ) } - await timeTravel(roundDuration) + await time.increase(roundDuration) const { tally, claims } = await finalizeRound() const expectedTotalVoiceCredits = sumVoiceCredits( contributions.map((x) => x.voiceCredits) @@ -478,22 +526,25 @@ describe('End-to-end Tests', function () { ) const expectedClaims = await calculateClaims( fundingRound, - new Array(2).fill(contributions.map((x) => x.voiceCredits.div(2))) + new Array(2).fill( + contributions.map((x) => BigInt(x.voiceCredits) / BigInt(2)) + ) ) - expect(claims[1]).to.equal(expectedClaims[0]) - expect(claims[2]).to.equal(expectedClaims[1]) + console.log('expected claim', claims[1], expectedClaims[0]) + expect(BigInt(claims[1])).to.equal(expectedClaims[0]) + expect(BigInt(claims[2])).to.equal(expectedClaims[1]) }) it('should allocate funds correctly if not all voice credits are spent', async () => { const contributions = await makeContributions([ - UNIT.mul(36).div(10), - UNIT.mul(36).div(10), + (UNIT * BigInt(36)) / BigInt(10), + (UNIT * BigInt(36)) / BigInt(10), ]) // 2 contirbutors, divide their contributions into 4 parts, only contribute 2 parts to 2 projects for (const contribution of contributions) { const contributor = contribution.signer - const voiceCredits = contribution.voiceCredits.div(4) + const voiceCredits = BigInt(contribution.voiceCredits) / BigInt(4) let nonce = 1 const messages: Message[] = [] const encPubKeys: PubKey[] = [] @@ -513,16 +564,16 @@ describe('End-to-end Tests', function () { encPubKeys.push(encPubKey) nonce += 1 } - await fundingRound.connect(contributor).submitMessageBatch( + await (fundingRound.connect(contributor) as Contract).submitMessageBatch( messages.reverse().map((msg) => msg.asContractParam()), encPubKeys.reverse().map((key) => key.asContractParam()) ) } - await timeTravel(roundDuration) + await time.increase(roundDuration) const { tally, claims } = await finalizeRound() const expectedTotalVoiceCredits = sumVoiceCredits( - contributions.map((x) => x.voiceCredits.div(2)) + contributions.map((x) => BigInt(x.voiceCredits) / BigInt(2)) ) expect(tally.totalSpentVoiceCredits.spent).to.equal( expectedTotalVoiceCredits @@ -530,7 +581,9 @@ describe('End-to-end Tests', function () { const expectedClaims = await calculateClaims( fundingRound, - new Array(2).fill(contributions.map((x) => x.voiceCredits.div(4))) + new Array(2).fill( + contributions.map((x) => BigInt(x.voiceCredits) / BigInt(4)) + ) ) expect(claims[1]).to.equal(expectedClaims[0]) expect(claims[2]).to.equal(expectedClaims[1]) @@ -538,19 +591,19 @@ describe('End-to-end Tests', function () { it('should overwrite votes 1', async () => { const [contribution, contribution2] = await makeContributions([ - UNIT.mul(5), - UNIT.mul(90), + UNIT * BigInt(5), + UNIT * BigInt(90), ]) const contributor = contribution.signer - const votes = [ - [1, contribution.voiceCredits.div(6)], - [2, contribution.voiceCredits.div(2)], - [1, contribution.voiceCredits.div(2)], + const votes: VoteData[] = [ + makeVote(1, BigInt(contribution.voiceCredits) / BigInt(6)), + makeVote(2, BigInt(contribution.voiceCredits) / BigInt(2)), + makeVote(1, BigInt(contribution.voiceCredits) / BigInt(2)), ] const messages: Message[] = [] const encPubKeys: PubKey[] = [] let nonce = 1 - for (const [recipientIndex, voiceCredits] of votes) { + for (const { recipientIndex, voiceCredits } of votes) { const [message, encPubKey] = createMessage( contribution.stateIndex, contribution.keypair, @@ -565,7 +618,7 @@ describe('End-to-end Tests', function () { messages.push(message) encPubKeys.push(encPubKey) } - await fundingRound.connect(contributor).submitMessageBatch( + await (fundingRound.connect(contributor) as Contract).submitMessageBatch( messages.reverse().map((msg) => msg.asContractParam()), encPubKeys.reverse().map((key) => key.asContractParam()) ) @@ -579,14 +632,14 @@ describe('End-to-end Tests', function () { 1, pollId ) - await fundingRound - .connect(contribution2.signer) - .submitMessageBatch( - [message.asContractParam()], - [encPubKey.asContractParam()] - ) + await ( + fundingRound.connect(contribution2.signer) as Contract + ).submitMessageBatch( + [message.asContractParam()], + [encPubKey.asContractParam()] + ) - await timeTravel(roundDuration) + await time.increase(roundDuration) const { tally, claims } = await finalizeRound() const expectedTotalVoiceCredits = sumVoiceCredits([ contribution.voiceCredits, @@ -596,7 +649,7 @@ describe('End-to-end Tests', function () { expectedTotalVoiceCredits ) - const voiceCredits1 = contribution.voiceCredits.div(2) + const voiceCredits1 = BigInt(contribution.voiceCredits) / BigInt(2) const submittedVoiceCredits = [ [voiceCredits1], [voiceCredits1, contribution2.voiceCredits], @@ -614,20 +667,20 @@ describe('End-to-end Tests', function () { it('should overwrite votes 2', async () => { const [contribution, contribution2] = await makeContributions([ - UNIT.mul(90), - UNIT.mul(40), + UNIT * BigInt(90), + UNIT * BigInt(40), ]) const contributor = contribution.signer - const votes = [ - [1, contribution.voiceCredits.div(2)], - [2, contribution.voiceCredits.div(2)], - [1, ZERO], - [2, contribution.voiceCredits], + const votes: VoteData[] = [ + makeVote(1, BigInt(contribution.voiceCredits) / BigInt(2)), + makeVote(2, BigInt(contribution.voiceCredits) / BigInt(2)), + makeVote(1, ZERO), + makeVote(2, BigInt(contribution.voiceCredits)), ] const messages: Message[] = [] const encPubKeys: PubKey[] = [] let nonce = 1 - for (const [recipientIndex, voiceCredits] of votes) { + for (const { recipientIndex, voiceCredits } of votes) { const [message, encPubKey] = createMessage( contribution.stateIndex, contribution.keypair, @@ -642,7 +695,7 @@ describe('End-to-end Tests', function () { messages.push(message) encPubKeys.push(encPubKey) } - await fundingRound.connect(contributor).submitMessageBatch( + await (fundingRound.connect(contributor) as Contract).submitMessageBatch( messages.reverse().map((msg) => msg.asContractParam()), encPubKeys.reverse().map((key) => key.asContractParam()) ) @@ -658,14 +711,14 @@ describe('End-to-end Tests', function () { 1, pollId ) - await fundingRound - .connect(contribution2.signer) - .submitMessageBatch( - [message.asContractParam()], - [encPubKey.asContractParam()] - ) + await ( + fundingRound.connect(contribution2.signer) as Contract + ).submitMessageBatch( + [message.asContractParam()], + [encPubKey.asContractParam()] + ) - await timeTravel(roundDuration) + await time.increase(roundDuration) const { tally, claims } = await finalizeRound() const expectedTotalVoiceCredits = sumVoiceCredits([ contribution.voiceCredits, @@ -691,27 +744,27 @@ describe('End-to-end Tests', function () { it('should overwrite previous batch of votes', async () => { const [contribution, contribution2] = await makeContributions([ - UNIT.mul(5), - UNIT.mul(40), + UNIT * BigInt(5), + UNIT * BigInt(40), ]) const contributor = contribution.signer - const votes = [ + const votes: VoteData[][] = [ [ - [1, contribution.voiceCredits.div(3)], - [2, contribution.voiceCredits.div(3)], - [3, contribution.voiceCredits.div(3)], + makeVote(1, BigInt(contribution.voiceCredits) / BigInt(3)), + makeVote(2, BigInt(contribution.voiceCredits) / BigInt(3)), + makeVote(3, BigInt(contribution.voiceCredits) / BigInt(3)), ], [ - [1, contribution.voiceCredits.div(2)], - [2, contribution.voiceCredits.div(2)], - [3, ZERO], + makeVote(1, BigInt(contribution.voiceCredits) / BigInt(2)), + makeVote(2, BigInt(contribution.voiceCredits) / BigInt(2)), + makeVote(3, ZERO), ], ] for (const batch of votes) { const messages: Message[] = [] const encPubKeys: PubKey[] = [] let nonce = 1 - for (const [recipientIndex, voiceCredits] of batch) { + for (const { recipientIndex, voiceCredits } of batch) { const [message, encPubKey] = createMessage( contribution.stateIndex, contribution.keypair, @@ -726,7 +779,7 @@ describe('End-to-end Tests', function () { messages.push(message) encPubKeys.push(encPubKey) } - await fundingRound.connect(contributor).submitMessageBatch( + await (fundingRound.connect(contributor) as Contract).submitMessageBatch( messages.reverse().map((msg) => msg.asContractParam()), encPubKeys.reverse().map((key) => key.asContractParam()) ) @@ -743,14 +796,14 @@ describe('End-to-end Tests', function () { 1, pollId ) - await fundingRound - .connect(contribution2.signer) - .submitMessageBatch( - [message.asContractParam()], - [encPubKey.asContractParam()] - ) + await ( + fundingRound.connect(contribution2.signer) as Contract + ).submitMessageBatch( + [message.asContractParam()], + [encPubKey.asContractParam()] + ) - await timeTravel(roundDuration) + await time.increase(roundDuration) const { tally, claims } = await finalizeRound() const expectedTotalVoiceCredits = sumVoiceCredits([ contribution.voiceCredits, @@ -760,7 +813,7 @@ describe('End-to-end Tests', function () { expectedTotalVoiceCredits ) - const voiceCredits1 = contribution.voiceCredits.div(2) + const voiceCredits1 = BigInt(contribution.voiceCredits) / BigInt(2) const submittedVoiceCredits = [ [voiceCredits1], [voiceCredits1, contribution2.voiceCredits], @@ -781,8 +834,8 @@ describe('End-to-end Tests', function () { it('should invalidate votes in case of bribe', async () => { const [contribution, contribution2] = await makeContributions([ - UNIT.mul(90), - UNIT.mul(40), + UNIT * BigInt(90), + UNIT * BigInt(40), ]) const contributor = contribution.signer let message @@ -816,7 +869,7 @@ describe('End-to-end Tests', function () { ) messageBatch1.push(message) encPubKeyBatch1.push(encPubKey) - await fundingRound.connect(contributor).submitMessageBatch( + await (fundingRound.connect(contributor) as Contract).submitMessageBatch( messageBatch1.reverse().map((msg) => msg.asContractParam()), encPubKeyBatch1.reverse().map((key) => key.asContractParam()) ) @@ -845,7 +898,7 @@ describe('End-to-end Tests', function () { null, coordinatorKeypair.pubKey, 1, - BigNumber.from(0), + BigInt(0), 2, pollId ) @@ -864,7 +917,7 @@ describe('End-to-end Tests', function () { ) messageBatch2.push(message) encPubKeyBatch2.push(encPubKey) - await fundingRound.connect(contributor).submitMessageBatch( + await (fundingRound.connect(contributor) as Contract).submitMessageBatch( messageBatch2.reverse().map((msg) => msg.asContractParam()), encPubKeyBatch2.reverse().map((key) => key.asContractParam()) ) @@ -880,14 +933,14 @@ describe('End-to-end Tests', function () { 1, pollId ) - await fundingRound - .connect(contribution2.signer) - .submitMessageBatch( - [message.asContractParam()], - [encPubKey.asContractParam()] - ) + await ( + fundingRound.connect(contribution2.signer) as Contract + ).submitMessageBatch( + [message.asContractParam()], + [encPubKey.asContractParam()] + ) - await timeTravel(roundDuration) + await time.increase(roundDuration) const { tally, claims } = await finalizeRound() const expectedTotalVoiceCredits = sumVoiceCredits([ contribution.voiceCredits, @@ -896,11 +949,67 @@ describe('End-to-end Tests', function () { expect(tally.totalSpentVoiceCredits.spent).to.equal( expectedTotalVoiceCredits ) - expect(claims[1]).to.equal(BigNumber.from(0)) + expect(claims[1]).to.equal(BigInt(0)) const expectedClaims = await calculateClaims(fundingRound, [ [contribution.voiceCredits, contribution2.voiceCredits], ]) expect(claims[2]).to.equal(expectedClaims[0]) }) + + it('should allow reset and re-run tally', async () => { + const contributions = await makeContributions([ + (UNIT * BigInt(50)) / BigInt(10), + (UNIT * BigInt(50)) / BigInt(10), + ]) + // Submit messages + for (const contribution of contributions) { + const contributor = contribution.signer + const messages: Message[] = [] + const encPubKeys: PubKey[] = [] + let nonce = 1 + + // Spend voice credits on both recipients + for (const recipientIndex of [1, 2]) { + const voiceCredits = BigInt(contribution.voiceCredits) / BigInt(2) + const [message, encPubKey] = createMessage( + contribution.stateIndex, + contribution.keypair, + null, + coordinatorKeypair.pubKey, + recipientIndex, + voiceCredits, + nonce, + pollId + ) + messages.push(message) + encPubKeys.push(encPubKey) + nonce += 1 + } + + await (fundingRound.connect(contributor) as Contract).submitMessageBatch( + messages.reverse().map((msg) => msg.asContractParam()), + encPubKeys.reverse().map((key) => key.asContractParam()) + ) + } + + await time.increase(roundDuration) + const testResetTally = true + const { tally, claims } = await finalizeRound(testResetTally) + const expectedTotalVoiceCredits = sumVoiceCredits( + contributions.map((x) => x.voiceCredits) + ) + expect(tally.totalSpentVoiceCredits.spent).to.equal( + expectedTotalVoiceCredits + ) + const expectedClaims = await calculateClaims( + fundingRound, + new Array(2).fill( + contributions.map((x) => BigInt(x.voiceCredits) / BigInt(2)) + ) + ) + console.log('expected claim', claims[1], expectedClaims[0]) + expect(BigInt(claims[1])).to.equal(expectedClaims[0]) + expect(BigInt(claims[2])).to.equal(expectedClaims[1]) + }) }) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 8a7d3a827..4b30f5d7b 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -4,10 +4,9 @@ import dotenv from 'dotenv' import { TASK_COMPILE_SOLIDITY_GET_SOURCE_PATHS } from 'hardhat/builtin-tasks/task-names' import { subtask, task } from 'hardhat/config' -import '@nomiclabs/hardhat-waffle' +import '@nomicfoundation/hardhat-toolbox' import '@nomiclabs/hardhat-ganache' import 'hardhat-contract-sizer' -import '@nomiclabs/hardhat-etherscan' import './tasks' dotenv.config() @@ -55,12 +54,18 @@ export default { process.env.JSONRPC_HTTP_URL || 'https://goerli-rollup.arbitrum.io/rpc', accounts, }, - 'mantle-testnet': { - url: process.env.JSONRPC_HTTP_URL || 'https://rpc.testnet.mantle.xyz', + 'arbitrum-sepolia': { + url: + process.env.JSONRPC_HTTP_URL || + 'https://sepolia-rollup.arbitrum.io/rpc', + accounts, + }, + sepolia: { + url: process.env.JSONRPC_HTTP_URL || 'http://127.0.0.1:8545', accounts, }, - rinkarby: { - url: process.env.JSONRPC_HTTP_URL || 'https://rinkeby.arbitrum.io/rpc', + 'mantle-testnet': { + url: process.env.JSONRPC_HTTP_URL || 'https://rpc.testnet.mantle.xyz', accounts, }, arbitrum: { @@ -69,7 +74,24 @@ export default { }, }, etherscan: { - apiKey: process.env.ETHERSCAN_API_KEY || 'YOUR_ETHERSCAN_API_KEY', + apiKey: { + arbitrum: process.env.ARBISCAN_API_KEY || 'YOUR_ARBISCAN_API_KEY', + 'arbitrum-sepolia': + process.env.ARBISCAN_API_KEY || 'YOUR_ARBISCAN_API_KEY', + }, + customChains: [ + { + network: 'arbitrum-sepolia', + chainId: 421614, + urls: { + apiURL: 'https://api-sepolia.arbiscan.io/api', + browserURL: 'https://sepolia.arbiscan.io', + }, + }, + ], + }, + sourcify: { + enabled: false, }, paths: { artifacts: 'build/contracts', @@ -181,7 +203,7 @@ task( const artifact = JSON.parse( fs .readFileSync( - `./node_modules/@clrfund/maci-contracts/artifacts/contracts/crypto/Hasher.sol/${contractName}.json` + `./node_modules/maci-contracts/build/artifacts/contracts/crypto/${contractName}.sol/${contractName}.json` ) .toString() ) diff --git a/contracts/maci.d.ts b/contracts/maci.d.ts deleted file mode 100644 index 730c26386..000000000 --- a/contracts/maci.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '@clrfund/maci-contracts' diff --git a/contracts/package.json b/contracts/package.json index 9504ec3eb..ad1f5f516 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -6,9 +6,8 @@ "hardhat": "hardhat", "build": "hardhat compile", "node": "hardhat node --port 18545 --hostname 0.0.0.0", - "deployer:arbitrum": "yarn clean && yarn build && HARDHAT_NETWORK=arbitrum ts-node cli/newDeployer.ts -d ~/zkeys", - "deployer:local": "yarn clean && yarn build && HARDHAT_NETWORK=local ts-node cli/newDeployer.ts -d ~/zkeys", "test": "hardhat test", + "deploy-local": "./sh/deployLocal.sh", "e2e": "NODE_OPTIONS=--max-old-space-size=4096 hardhat test --network localhost e2e/index.ts", "lint:js": "eslint '{tests,e2e,scripts}/**/*.ts'", "lint:solidity": "solhint 'contracts/**/*.sol'", @@ -16,38 +15,37 @@ "clean": "rm -rf cache && rm -rf build" }, "dependencies": { - "@clrfund/maci-contracts": "^1.1.9", "@openzeppelin/contracts": "4.9.0", "commander": "^11.1.0", "dotenv": "^8.2.0", + "maci-contracts": "0.0.0-ci.45d1156", "solidity-rlp": "2.0.8" }, "devDependencies": { "@clrfund/common": "^0.0.1", - "@clrfund/maci-circuits": "^1.1.10", - "@clrfund/maci-cli": "^1.1.10", - "@ethereum-waffle/mock-contract": "^3.4.4", + "@clrfund/waffle-mock-contract": "^0.0.4", "@kleros/gtcr-encoder": "^1.4.0", - "@nomiclabs/hardhat-ethers": "^2.2.3", - "@nomiclabs/hardhat-etherscan": "^3.1.4", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.3", + "@nomicfoundation/hardhat-ethers": "^3.0.5", + "@nomicfoundation/hardhat-network-helpers": "^1.0.10", + "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.3", "@nomiclabs/hardhat-ganache": "^2.0.1", - "@nomiclabs/hardhat-waffle": "^2.0.3", - "@types/chai": "^4.2.11", - "@types/mocha": "^7.0.2", - "@types/node": "^13.9.2", - "@typescript-eslint/eslint-plugin": "^5.44.0", - "@typescript-eslint/parser": "^5.44.0", - "chai": "^4.2.0", - "eslint": "^8.28.0", - "eslint-config-prettier": "^8.5.0", - "ethereum-waffle": "^3.4.4", - "ethers": "^5.7.2", - "hardhat": "^2.19.1", - "hardhat-contract-sizer": "^2.6.1", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/mocha": "^10.0.6", + "ethers": "^6.10.0", + "hardhat": "^2.19.4", + "hardhat-contract-sizer": "^2.10.0", + "hardhat-gas-reporter": "^1.0.8", "ipfs-only-hash": "^2.0.1", - "solhint": "^3.3.2", - "ts-generator": "^0.0.8", - "ts-node": "^10.9.1", + "maci-circuits": "0.0.0-ci.45d1156", + "maci-cli": "0.0.0-ci.45d1156", + "maci-domainobjs": "0.0.0-ci.45d1156", + "mocha": "^10.2.0", + "solidity-coverage": "^0.8.1", + "ts-node": "^10.9.2", + "typechain": "^8.3.2", "typescript": "^4.9.3" } } diff --git a/contracts/sh/deployLocal.sh b/contracts/sh/deployLocal.sh new file mode 100755 index 000000000..2b770eff8 --- /dev/null +++ b/contracts/sh/deployLocal.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -e + +# +# Run the hardhat scripts/tasks to simulate e2e testing +# + +# Local settings +export CLRFUND_ROOT=$(cd $(dirname $0) && cd ../.. && pwd) +export CONTRACTS_DIRECTORY=${CLRFUND_ROOT}/contracts +export CIRCUIT=micro +export NETWORK=localhost +export CIRCUIT_DIRECTORY=${CONTRACTS_DIRECTORY}/params +export HARDHAT_NETWORK=localhost +export STATE_FILE=${CONTRACTS_DIRECTORY}/local-state.json + +# download the circuit params if not exists +if ! [ -f "${CIRCUIT_DIRECTORY}/processMessages_6-8-2-3_test" ]; then + ${CLRFUND_ROOT}/.github/scripts/download-6-8-2-3.sh +fi + +# 20 mins +ROUND_DURATION=1800 + +# A helper to extract field value from the JSON state file +# The pattern "field": "value" must be on 1 line +# Usage: extract 'clrfund' +function extract() { + val=$(cat "${STATE_FILE}" | grep -o "${1}\": *\"[^\"]*" | grep -o "[^\"]*$") + echo ${val} +} + +# create a new maci key for the coordinator +MACI_KEYPAIR=$(yarn ts-node cli/newMaciKey.ts) +export COORDINATOR_MACISK=$(echo "${MACI_KEYPAIR}" | grep -o "macisk.*$") + +# create a new instance of ClrFund +DEPLOYER=$(extract 'deployer') +yarn ts-node cli/newClrFund.ts \ + --circuit "${CIRCUIT}" \ + --directory "${CIRCUIT_DIRECTORY}" \ + --user-registry-type simple \ + --recipient-registry-type simple \ + --state-file ${STATE_FILE} + +# deploy a new funding round +CLRFUND=$(extract 'clrfund') +yarn ts-node cli/newRound.ts \ + --clrfund "${CLRFUND}" \ + --duration "${ROUND_DURATION}" \ + --state-file ${STATE_FILE} diff --git a/contracts/sh/runScriptTests.sh b/contracts/sh/runScriptTests.sh index 6e7b218a2..e1425113e 100755 --- a/contracts/sh/runScriptTests.sh +++ b/contracts/sh/runScriptTests.sh @@ -9,9 +9,9 @@ set -e NOW=$(date +%s) export OUTPUT_DIR="./proof_output/${NOW}" export CIRCUIT=micro -export NETWORK=localhost -export CIRCUIT_DIRECTORY=${CIRCUIT_DIRECTORY:-"./snark-params"} +export CIRCUIT_DIRECTORY=${CIRCUIT_DIRECTORY:-"./params"} export STATE_FILE=${OUTPUT_DIR}/state.json +export TALLY_FILE=${OUTPUT_DIR}/tally.json export HARDHAT_NETWORK=localhost export RAPID_SNARK=${RAPID_SNARK:-~/rapidsnark/package/bin/prover} @@ -28,20 +28,14 @@ function extract() { echo ${val} } -# create a ClrFund deployer -yarn ts-node cli/newDeployer.ts \ - --directory "${CIRCUIT_DIRECTORY}" \ - --state-file "${STATE_FILE}" \ - --circuit "${CIRCUIT}" - # create a new maci key for the coordinator MACI_KEYPAIR=$(yarn ts-node cli/newMaciKey.ts) export COORDINATOR_MACISK=$(echo "${MACI_KEYPAIR}" | grep -o "macisk.*$") # create a new instance of ClrFund -DEPLOYER=$(extract 'deployer') yarn ts-node cli/newClrFund.ts \ - --deployer "${DEPLOYER}" \ + --circuit "${CIRCUIT}" \ + --directory "${CIRCUIT_DIRECTORY}" \ --user-registry-type simple \ --recipient-registry-type simple \ --state-file ${STATE_FILE} @@ -68,14 +62,12 @@ yarn ts-node cli/tally.ts \ --clrfund ${CLRFUND} \ --circuit-directory ${CIRCUIT_DIRECTORY} \ --circuit "${CIRCUIT}" \ - --rapid-snark ${RAPID_SNARK} \ + --rapidsnark ${RAPID_SNARK} \ --batch-size 8 \ --output-dir ${OUTPUT_DIR} \ - --maci-tx-hash "${MACI_TRANSACTION_HASH}" \ - --state-file ${STATE_FILE} + --maci-tx-hash "${MACI_TRANSACTION_HASH}" -# # finalize the round -TALLY_FILE=$(extract 'tallyFile') +# finalize the round yarn ts-node cli/finalize.ts --clrfund "${CLRFUND}" --tally-file ${TALLY_FILE} # claim funds diff --git a/contracts/tasks/auditTally.ts b/contracts/tasks/auditTally.ts index 03c7c294b..a99e19bdf 100644 --- a/contracts/tasks/auditTally.ts +++ b/contracts/tasks/auditTally.ts @@ -6,8 +6,7 @@ */ import { task, types } from 'hardhat/config' -import { utils, providers, Contract, BigNumber } from 'ethers' -import { EventFilter, Log } from '@ethersproject/abstract-provider' +import { Contract, EventFilter, type Log, type Provider } from 'ethers' import { Ipfs } from '../utils/ipfs' import fs from 'fs' @@ -36,7 +35,7 @@ async function fetchLogs({ lastBlock, blocksPerBatch, }: { - provider: providers.Provider + provider: Provider filter: EventFilter startBlock: number lastBlock: number diff --git a/contracts/tasks/exportRound.ts b/contracts/tasks/exportRound.ts index 83304385b..7f3f30337 100644 --- a/contracts/tasks/exportRound.ts +++ b/contracts/tasks/exportRound.ts @@ -3,19 +3,20 @@ * * Sample usage: * - * yarn hardhat export-round --round-address
--out-dir ../vue-app/src/rounds --operator --ipfs --start-block --network + * yarn hardhat export-round --round-address
--output-dir ../vue-app/src/rounds \ + * --operator --ipfs \ + * --start-block --network * * To generate the leaderboard view, deploy the clrfund website with the generated round data in the vue-app/src/rounds folder */ import { task, types } from 'hardhat/config' -import { HardhatConfig } from 'hardhat/types' -import { utils, Contract, BigNumber } from 'ethers' +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 { writeToFile } from '../utils/file' +import { JSONFile } from '../utils/JSONFile' import path from 'path' import fs from 'fs' @@ -28,8 +29,8 @@ type RoundListEntry = { } const toUndefined = () => undefined -const toString = (val: BigNumber) => BigNumber.from(val).toString() -const toZero = () => BigNumber.from(0) +const toString = (val: bigint) => BigInt(val).toString() +const toZero = () => BigInt(0) function roundFileName(directory: string, address: string): string { return path.join(directory, `${address}.json`) @@ -39,9 +40,9 @@ function roundListFileName(directory: string): string { return path.join(directory, 'rounds.json') } -function getEtherscanApiKey(config: HardhatConfig, network: string): string { +function getEtherscanApiKey(config: any, network: string): string { let etherscanApiKey = '' - if (config.etherscan.apiKey) { + if (config.etherscan?.apiKey) { if (typeof config.etherscan.apiKey === 'string') { etherscanApiKey = config.etherscan.apiKey } else { @@ -75,7 +76,8 @@ async function updateRoundList(filePath: string, round: RoundListEntry) { // sort in ascending start time order rounds.sort((round1, round2) => round1.startTime - round2.startTime) - writeToFile(filePath, rounds) + JSONFile.write(filePath, rounds) + console.log('Finished writing to', filePath) } async function mergeRecipientTally({ @@ -127,15 +129,16 @@ async function mergeRecipientTally({ startTime, endTime ) - } catch { + } catch (err) { + console.log('err', err) // some older recipient registry contract does not have // the getRecipientAddress function, ignore error } const tallyResult = tally.results.tally[i] - const spentVoiceCredits = tally.totalVoiceCreditsPerVoteOption.tally[i] - const formattedDonationAmount = utils.formatUnits( - BigNumber.from(spentVoiceCredits).mul(voiceCreditFactor), + const spentVoiceCredits = tally.perVOSpentVoiceCredits.tally[i] + const formattedDonationAmount = formatUnits( + BigInt(spentVoiceCredits) * BigInt(voiceCreditFactor), nativeTokenDecimals ) @@ -163,10 +166,14 @@ async function getRoundInfo( ethers: any, operator: string ): Promise { - console.log('Fetching round data for', roundContract.address) - const round: any = { address: roundContract.address } + console.log('Fetching round data for', roundContract.target) + const address = await roundContract.getAddress() + + let nativeTokenAddress: string + let nativeTokenDecimals = BigInt(0) + let nativeTokenSymbol = '' try { - round.nativeTokenAddress = await roundContract.nativeToken() + nativeTokenAddress = await roundContract.nativeToken() } catch (err) { const errorMessage = `Failed to get nativeToken. Make sure the environment variable JSONRPC_HTTP_URL is set properly: ${ (err as Error).message @@ -175,14 +182,14 @@ async function getRoundInfo( } try { - const token = await ethers.getContractAt('ERC20', round.nativeTokenAddress) - round.nativeTokenDecimals = await token.decimals().catch(toUndefined) - round.nativeTokenSymbol = await token.symbol().catch(toUndefined) + const token = await ethers.getContractAt('ERC20', nativeTokenAddress) + nativeTokenDecimals = await token.decimals().catch(toUndefined) + nativeTokenSymbol = await token.symbol().catch(toUndefined) console.log( 'Fetched token data', - round.nativeTokenAddress, - round.nativeTokenSymbol, - round.nativeTokenDecimals + nativeTokenAddress, + nativeTokenSymbol, + nativeTokenDecimals ) } catch (err) { const errorMessage = err instanceof Error ? err.message : '' @@ -190,67 +197,114 @@ async function getRoundInfo( } const contributorCount = await roundContract.contributorCount().catch(toZero) - round.contributorCount = contributorCount.toNumber() - const matchingPoolSize = await roundContract.matchingPoolSize().catch(toZero) - round.matchingPoolSize = matchingPoolSize.toString() - round.totalSpent = await roundContract + const totalSpent = await roundContract .totalSpent() .then(toString) .catch(toUndefined) const voiceCreditFactor = await roundContract.voiceCreditFactor() - round.voiceCreditFactor = voiceCreditFactor.toString() - - round.isFinalized = await roundContract.isFinalized() - round.isCancelled = await roundContract.isCancelled() - round.tallyHash = await roundContract.tallyHash() + const isFinalized = await roundContract.isFinalized() + const isCancelled = await roundContract.isCancelled() + const tallyHash = await roundContract.tallyHash() + const maciAddress = await roundContract.maci().catch(toUndefined) + const pollAddress = await roundContract.poll().catch(toUndefined) + let startTime = 0 + let endTime = 0 + let pollId: bigint | undefined + let messages: bigint + let maxMessages: bigint + let maxRecipients: bigint + let signUpDuration = BigInt(0) + let votingDuration = BigInt(0) try { - round.maciAddress = await roundContract.maci().catch(toUndefined) - const maci = await ethers.getContractAt('MACI', round.maciAddress) - const startTime = await maci.signUpTimestamp().catch(toZero) - round.startTime = startTime.toNumber() - const signUpDuration = await maci.signUpDurationSeconds().catch(toZero) - const votingDuration = await maci.votingDurationSeconds().catch(toZero) - const endTime = startTime.add(signUpDuration).add(votingDuration) - round.endTime = endTime.toNumber() - round.signUpDuration = signUpDuration.toNumber() - round.votingDuration = votingDuration.toNumber() - - const maciTreeDepths = await maci.treeDepths() - const messages = await maci.numMessages() - - round.messages = messages.toNumber() - round.maxMessages = 2 ** maciTreeDepths.messageTreeDepth - 1 - round.maxRecipients = 5 ** maciTreeDepths.voteOptionTreeDepth - 1 + if (pollAddress) { + const pollContract = await ethers.getContractAt('Poll', pollAddress) + const [roundStartTime, roundDuration] = + await pollContract.getDeployTimeAndDuration() + startTime = getNumber(roundStartTime) + signUpDuration = roundDuration + votingDuration = roundDuration + endTime = startTime + getNumber(roundDuration) + + pollId = await roundContract.pollId() + + messages = await pollContract.numMessages() + const maxValues = await pollContract.maxValues() + maxMessages = maxValues.maxMessages + maxRecipients = maxValues.maxVoteOptions + } else { + const maci = await ethers.getContractAt('MACI', maciAddress) + startTime = await maci.signUpTimestamp().catch(toZero) + signUpDuration = await maci.signUpDurationSeconds().catch(toZero) + votingDuration = await maci.votingDurationSeconds().catch(toZero) + endTime = + getNumber(startTime) + + getNumber(signUpDuration) + + getNumber(votingDuration) + + const treeDepths = await maci.treeDepths() + messages = await maci.numMessages() + maxMessages = BigInt(2) ** BigInt(treeDepths.messageTreeDepth) - BigInt(1) + maxRecipients = + BigInt(5) ** BigInt(treeDepths.voteOptionTreeDepth) - BigInt(1) + } } catch (err) { const errorMessage = err instanceof Error ? err.message : '' - throw new Error(`Failed to get MACI data ${errorMessage}`) + throw new Error(`Failed to get round duration ${errorMessage}`) } - round.userRegistryAddress = await roundContract + const userRegistryAddress = await roundContract .userRegistry() .catch(toUndefined) - round.recipientRegistryAddress = await roundContract.recipientRegistry() + const recipientRegistryAddress = await roundContract.recipientRegistry() + let recipientDepositAmount = '0' try { const recipientRegistry = await ethers.getContractAt( 'OptimisticRecipientRegistry', - round.recipientRegistryAddress + recipientRegistryAddress ) - round.recipientDepositAmount = await recipientRegistry + recipientDepositAmount = await recipientRegistry .baseDeposit() .then(toString) } catch { // ignore error - non optimistic recipient registry does not have deposit } - round.operator = operator const providerNetwork = await ethers.provider.getNetwork() - round.chainId = providerNetwork.chainId - + const chainId = getNumber(providerNetwork.chainId) + + const round: Round = { + chainId, + operator, + address, + userRegistryAddress, + recipientRegistryAddress, + recipientDepositAmount, + maciAddress, + pollAddress, + pollId, + contributorCount, + totalSpent: totalSpent || '', + matchingPoolSize, + voiceCreditFactor, + isFinalized, + isCancelled, + tallyHash, + nativeTokenAddress, + nativeTokenSymbol, + nativeTokenDecimals: getNumber(nativeTokenDecimals), + startTime, + endTime, + signUpDuration: getNumber(signUpDuration), + votingDuration: getNumber(votingDuration), + messages, + maxMessages, + maxRecipients, + } console.log('Round', round) return round } @@ -371,7 +425,8 @@ task('export-round', 'Export round data for the leaderboard') projects, tally, } - writeToFile(filename, roundData) + JSONFile.write(filename, roundData) + console.log('Finished writing to', filename) // update round list const listFilename = roundListFileName(outputDir) diff --git a/contracts/tasks/index.ts b/contracts/tasks/index.ts index 16be9d08d..3d1dc3f85 100644 --- a/contracts/tasks/index.ts +++ b/contracts/tasks/index.ts @@ -4,3 +4,10 @@ import './verifyRound' import './verifyMaci' import './verifyRecipientRegistry' import './verifyUserRegistry' +import './verifyPoll' +import './pubkey' +import './loadUsers' +import './exportRound' +import './setRecipientRegistry' +import './setFundingRoundFactory' +import './setToken' diff --git a/contracts/tasks/loadUsers.ts b/contracts/tasks/loadUsers.ts index 3a0515d95..af77e7d32 100644 --- a/contracts/tasks/loadUsers.ts +++ b/contracts/tasks/loadUsers.ts @@ -1,5 +1,5 @@ import { task } from 'hardhat/config' -import { Contract, utils, ContractReceipt } from 'ethers' +import { Contract, ContractTransactionReceipt, isAddress } from 'ethers' import fs from 'fs' /* @@ -22,7 +22,7 @@ import fs from 'fs' async function addUser( registry: Contract, address: string -): Promise { +): Promise { const tx = await registry.addUser(address) const receipt = await tx.wait() return receipt @@ -50,14 +50,14 @@ async function loadFile(registry: Contract, filePath: string) { for (let i = 0; i < addresses.length; i++) { const address = addresses[i] - const isValidAddress = Boolean(address) && utils.isAddress(address) + const isValidAddress = Boolean(address) && isAddress(address) if (isValidAddress) { console.log('Adding address', address) try { const result = await addUser(registry, address) if (result.status !== 1) { throw new Error( - `Transaction ${result.transactionHash} failed with status ${result.status}` + `Transaction ${result.hash} failed with status ${result.status}` ) } } catch (err: any) { @@ -67,7 +67,7 @@ async function loadFile(registry: Contract, filePath: string) { console.error('Failed to add address', address, err) } } - } else { + } else if (address) { console.warn('Skipping invalid address', address) } } diff --git a/contracts/tasks/mergeAllocations.ts b/contracts/tasks/mergeAllocations.ts index d6beab4fb..100361019 100644 --- a/contracts/tasks/mergeAllocations.ts +++ b/contracts/tasks/mergeAllocations.ts @@ -8,10 +8,10 @@ */ import { task, types } from 'hardhat/config' -import { utils, BigNumber } from 'ethers' +import { formatUnits, parseUnits } from 'ethers' import fs from 'fs' import { Project, RoundFileContent, Tally } from '../utils/types' -import { writeToFile } from '../utils/file' +import { JSONFile } from '../utils/JSONFile' const COLUMN_PROJECT_NAME = 0 const COLUMN_RECIPIENT_ADDRESS = 1 @@ -153,7 +153,7 @@ task('merge-allocations', 'Merge the allocations data into the round JSON file') totalVoiceCreditsPerVoteOption: { tally: [] }, } - let totalPayout = BigNumber.from(0) + let totalPayout = BigInt(0) for (let index = 0; index < allocations.length; index++) { const { recipientAddress, projectName, payoutAmount, votes } = allocations[index] @@ -167,13 +167,13 @@ task('merge-allocations', 'Merge the allocations data into the round JSON file') continue } - const allocatedAmountBN = utils.parseUnits(payoutAmount, decimals) + const allocatedAmountBN = parseUnits(payoutAmount, decimals) const allocatedAmount = allocatedAmountBN.toString() const projectKey = makeProjectKey(recipientAddress) if (projects[projectKey]) { projects[projectKey].allocatedAmount = allocatedAmount console.log(index, projectName, '-', payoutAmount) - totalPayout = totalPayout.add(allocatedAmount) + totalPayout = totalPayout + BigInt(allocatedAmount) const { recipientIndex } = projects[projectKey] if (recipientIndex) { @@ -186,12 +186,17 @@ task('merge-allocations', 'Merge the allocations data into the round JSON file') } } - if (roundData && Object.keys(projects).length > 0 && totalPayout.gt(0)) { + if ( + roundData && + Object.keys(projects).length > 0 && + totalPayout > BigInt(0) + ) { roundData.projects = Object.values(projects) - console.log('totalPayout ', utils.formatUnits(totalPayout, decimals)) + console.log('totalPayout ', formatUnits(totalPayout, decimals)) roundData.round.matchingPoolSize = totalPayout.toString() roundData.tally = sanitizeTally(tally) - writeToFile(roundFile, roundData) + JSONFile.write(roundFile, roundData) + console.log('Finished writing to', roundFile) } }) diff --git a/contracts/tasks/newClrFund.ts b/contracts/tasks/newClrFund.ts deleted file mode 100644 index 54a5c18ca..000000000 --- a/contracts/tasks/newClrFund.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Create a new instance of the ClrFund contract. - * If the coordinator ETH address is not provided, use the signer address - * If the coordinator MACI secret key is not provided, create a random one - * - * Sample usage: - * - * yarn hardhat new-clrfund --network \ - * --deployer \ - * --token \ - * [--coordinator ] \ - * [--coordinator-macisk ] \ - * [--user-type ] \ - * [--recipient-type ] - * - * - * If user registry address and recipient registry address are not provided, - * the registry types become mandatory as well as the other parameters needed - * to deploy the registries - * - * If token is not provided, a new ERC20 token will be created - */ - -import { task } from 'hardhat/config' -import { getEventArg } from '../utils/contracts' -import { challengePeriodSeconds } from '../utils/deployment' -import { JSONFile } from '../utils/JSONFile' - -task('new-clrfund', 'Deploy a new ClrFund instance') - .addParam('deployer', 'ClrFund deployer contract address') - .addOptionalParam('token', 'The token address') - .addOptionalParam('coordinator', 'The coordinator ETH address') - .addOptionalParam( - 'coordinatorMacisk', - 'The coordinator MACI serialized secret key' - ) - .addOptionalParam( - 'userType', - 'The user registry type, e.g brightid, simple, merkle, snapshot' - ) - .addOptionalParam('userRegistry', 'The user registry contract address') - .addOptionalParam('context', 'The BrightId context') - .addOptionalParam('verifier', 'The BrightId verifier address') - .addOptionalParam('sponsor', 'The BrightId sponsor contract address') - .addOptionalParam('recipientType', 'The recipient registry type') - .addOptionalParam('recipientRegistry', 'The recipient registry address') - .addOptionalParam( - 'deposit', - 'The deposit for optimistic recipient registry', - '0.01' - ) - .addOptionalParam( - 'challengePeriod', - 'The challenge period for optimistic recipient registry', - challengePeriodSeconds - ) - .addOptionalParam('stateFile', 'The state file to save the clrfund address') - .setAction( - async ( - { - deployer, - token, - coordinator, - coordinatorMacisk, - userType, - userRegistry, - context, - verifier, - sponsor, - recipientType, - recipientRegistry, - deposit, - challengePeriod, - stateFile, - }, - { run, ethers } - ) => { - const [signer] = await ethers.getSigners() - console.log(`Deploying from address: ${signer.address}`) - - const clrfundDeployer = await ethers.getContractAt( - 'ClrFundDeployer', - deployer - ) - console.log('ClrFundDeployer:', clrfundDeployer.address) - - const tx = await clrfundDeployer.deployClrFund() - const receipt = await tx.wait() - - let clrfund: string - try { - clrfund = await getEventArg( - tx, - clrfundDeployer, - 'NewInstance', - 'clrfund' - ) - console.log('ClrFund: ', clrfund) - } catch (e) { - console.log('receipt', receipt) - throw new Error( - 'Unable to get clrfund address after deployment. ' + - (e as Error).message - ) - } - - // set coordinator, use the coordinator address if available, - // otherwise use the signer address - // If the maci secret key is not provided, it will create a new key - const coordinatorAddress = coordinator ?? signer.address - await run('set-coordinator', { - clrfund, - coordinator: coordinatorAddress, - coordinatorMacisk, - stateFile, - }) - - // set token - await run('set-token', { clrfund, token }) - - // set user registry - await run('set-user-registry', { - clrfund, - type: userType, - registry: userRegistry, - context, - verifier, - sponsor, - }) - - // set recipient registry - await run('set-recipient-registry', { - clrfund, - type: recipientType, - registry: recipientRegistry, - deposit, - challengePeriod, - }) - - if (stateFile) { - JSONFile.update(stateFile, { clrfund }) - } - } - ) diff --git a/contracts/tasks/newDeployer.ts b/contracts/tasks/newDeployer.ts deleted file mode 100644 index d14f499e2..000000000 --- a/contracts/tasks/newDeployer.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Create a new instance of the ClrFundDeployer - * - * Sample usage: - * - * yarn hardhat new-deployer --network - * - */ - -import { task } from 'hardhat/config' -import { - deployContract, - deployPoseidonLibraries, - deployMaciFactory, -} from '../utils/deployment' -import { DEFAULT_CIRCUIT } from '../utils/circuits' -import { JSONFile } from '../utils/JSONFile' - -task('new-deployer', 'Create the ClrFund deployer and its dependent contracts') - .addParam('circuit', 'The circuit type', DEFAULT_CIRCUIT) - .addParam('directory', 'The zkeys directory') - .addParam('stateFile', 'The file to save the deployer contract address') - .setAction( - async ({ circuit, directory, stateFile }, { ethers, config, run }) => { - const [signer] = await ethers.getSigners() - console.log(`Deploying from address: ${signer.address}`) - - const libraries = await deployPoseidonLibraries({ - artifactsPath: config.paths.artifacts, - signer, - ethers, - }) - console.log('Deployed Poseidons', libraries) - - const maciFactory = await deployMaciFactory({ libraries, ethers }) - console.log('Deployed MaciFactory at', maciFactory.address) - - await run('set-maci-parameters', { - maciFactory: maciFactory.address, - circuit, - directory, - }) - - const clrfundTemplate = await deployContract({ - name: 'ClrFund', - ethers, - }) - console.log('Deployed clrfundTemplate at', clrfundTemplate.address) - - const fundingRoundFactory = await deployContract({ - name: 'FundingRoundFactory', - libraries, - ethers, - }) - console.log( - 'Deployed FundingRoundFactory at', - fundingRoundFactory.address - ) - - const clrfundDeployer = await deployContract({ - name: 'ClrFundDeployer', - ethers, - contractArgs: [ - clrfundTemplate.address, - maciFactory.address, - fundingRoundFactory.address, - ], - }) - console.log('Deployed ClrfundDeployer at', clrfundDeployer.address) - - if (stateFile) { - JSONFile.update(stateFile, { deployer: clrfundDeployer.address }) - } - } - ) diff --git a/contracts/tasks/pubkey.ts b/contracts/tasks/pubkey.ts index 592bfd0f4..b53550feb 100644 --- a/contracts/tasks/pubkey.ts +++ b/contracts/tasks/pubkey.ts @@ -4,9 +4,9 @@ * * Usage: hardhat pubkey --macisk */ -import { utils } from 'ethers' +import { id } from 'ethers' import { task } from 'hardhat/config' -import { PubKey, PrivKey, Keypair } from '@clrfund/maci-domainobjs' +import { PubKey, PrivKey, Keypair } from '@clrfund/common' task('pubkey', 'Get the serialized MACI public key') .addOptionalParam('x', 'MACI public key x') @@ -14,7 +14,7 @@ task('pubkey', 'Get the serialized MACI public key') .addOptionalParam('macisk', 'MACI secret key') .setAction(async ({ x, y, macisk }) => { if (macisk) { - const keypair = new Keypair(PrivKey.unserialize(macisk)) + const keypair = new Keypair(PrivKey.deserialize(macisk)) console.log(`Public Key: ${keypair.pubKey.serialize()}`) } else { if (!x || !y) { @@ -24,7 +24,7 @@ task('pubkey', 'Get the serialized MACI public key') const pubKey = new PubKey([BigInt(x), BigInt(y)]) console.log(`Public Key: ${pubKey.serialize()}`) - const id = utils.id(x + '.' + y) - console.log(`Subgraph id: ${id}`) + const subgraphId = id(x + '.' + y) + console.log(`Subgraph id: ${subgraphId}`) } }) diff --git a/contracts/tasks/setFundingRoundFactory.ts b/contracts/tasks/setFundingRoundFactory.ts new file mode 100644 index 000000000..a402566b9 --- /dev/null +++ b/contracts/tasks/setFundingRoundFactory.ts @@ -0,0 +1,46 @@ +/** + * Set the funding round factory in clrfund + * Usage: + * hardhat set-round-factory \ + * --clrfund \ + * [--round-factory ] \ + * --network + */ +import { BaseContract } from 'ethers' +import { task } from 'hardhat/config' +import { ClrFund } from '../typechain-types' +import { deployContract } from '../utils/deployment' + +task( + 'set-round-factory', + 'Set (create if non-existent) the funding round factory address in the ClrFund contract' +) + .addParam('clrfund', 'The ClrFund contract address') + .addOptionalParam( + 'roundFactory', + 'The funding round factory contract address' + ) + .setAction(async ({ clrfund, roundFactory }, { ethers }) => { + const clrfundContract = (await ethers.getContractAt( + 'ClrFund', + clrfund + )) as BaseContract as ClrFund + + let roundFactoryAddress = roundFactory + if (!roundFactoryAddress) { + const roundFactoryContract = await deployContract({ + name: 'FundingRoundFactory', + ethers, + }) + roundFactoryAddress = roundFactoryContract.target + console.log('Deployed funding round factory at', roundFactoryAddress) + } + + const tx = await clrfundContract.setFundingRoundFactory(roundFactoryAddress) + const receipt = await tx.wait() + if (receipt?.status !== 1) { + throw new Error('Failed to set funding round factory') + } + + console.log('Set funding round factory at tx', tx.hash) + }) diff --git a/contracts/tasks/setRecipientRegistry.ts b/contracts/tasks/setRecipientRegistry.ts index e92553933..4cd3d7331 100644 --- a/contracts/tasks/setRecipientRegistry.ts +++ b/contracts/tasks/setRecipientRegistry.ts @@ -3,111 +3,26 @@ * * Sample usage: * - * yarn hardhat set-recipient-registry --network \ - * --clrfund \ - * [--type ] \ - * [--registry ] \ - * [--context ] \ - * [--verifier ] \ - * [--sponsor ] + * yarn hardhat set-recipient-registry --clrfund --type optimistic --network * - * Valid user registry types are simple, brightid, merkle, storage - * - * Verifier is the brightid node verifier address. - * Clrfund's brightId node is in the ethSigningAddress field from https://brightid.clr.fund + * Valid recipient registry types are simple, brightid, merkle, storage * */ import { task } from 'hardhat/config' -import { BigNumber, Contract, utils } from 'ethers' -import { HardhatEthersHelpers } from '@nomiclabs/hardhat-ethers/types' +import { parseUnits } from 'ethers' import { deployRecipientRegistry, challengePeriodSeconds, } from '../utils/deployment' -async function getDepositInUnits( - clrfundContract: Contract, - ethers: HardhatEthersHelpers, - deposit: string -): Promise { - let depositInUnits = BigNumber.from(0) - try { - const token = await clrfundContract.nativeToken() - const tokenContract = await ethers.getContractAt('ERC20', token) - const decimals = await tokenContract.decimals() - depositInUnits = utils.parseUnits(deposit, decimals) - } catch (e) { - console.log('Error formatting deposit amount ' + (e as Error).message) - console.log('Set deposit to 0') - } - - return depositInUnits -} - -/** - * Set the token address in the ClrFund contract - * - * @param clrfundContract ClrFund contract - * @param registryType The user registry type, e.g brightid, simple, merkle, snapshot - * @param registryAddress The user registry address to set in ClrFund - * @param ethers the hardhat ethers handle - */ -async function setRecipientRegistry({ - clrfundContract, - registryType, - registryAddress, - deposit, - challengePeriod, - ethers, -}: { - clrfundContract: Contract - registryType?: string - registryAddress?: string - deposit: string - challengePeriod: string - ethers: HardhatEthersHelpers -}) { - let recipientRegistryAddress = registryAddress - if (!recipientRegistryAddress) { - const recipientRegistryType = registryType || '' - const [signer] = await ethers.getSigners() - console.log(`Deploying recipient registry by: ${signer.address}`) - - const controller = clrfundContract.address - const depositInUnits = await getDepositInUnits( - clrfundContract, - ethers, - deposit - ) - const registry = await deployRecipientRegistry({ - type: recipientRegistryType, - controller, - deposit: depositInUnits, - challengePeriod, - ethers, - }) - recipientRegistryAddress = registry.address - } - - const tx = await clrfundContract.setRecipientRegistry( - recipientRegistryAddress - ) - await tx.wait() - - console.log( - `Recipient registry (${registryType}): ${recipientRegistryAddress}` - ) - console.log(`Recipient registry set at tx: ${tx.hash}`) -} - task('set-recipient-registry', 'Set the recipient registry in ClrFund') .addParam('clrfund', 'The ClrFund contract address') .addOptionalParam( 'type', 'The recipient registry type, e.g simple, optimistic' ) - .addOptionalParam('registry', 'The user registry contract address') + .addOptionalParam('registry', 'The recipient registry to set to') .addOptionalParam( 'deposit', 'The base deposit for the optimistic registry', @@ -125,13 +40,36 @@ task('set-recipient-registry', 'Set the recipient registry in ClrFund') ) => { const clrfundContract = await ethers.getContractAt('ClrFund', clrfund) - await setRecipientRegistry({ - clrfundContract: clrfundContract, - registryType: type, - registryAddress: registry, - deposit, - challengePeriod, - ethers, - }) + const recipientRegistryType = type || '' + let recipientRegistryAddress = registry + if (!recipientRegistryAddress) { + const [signer] = await ethers.getSigners() + console.log(`Deploying recipient registry by: ${signer.address}`) + + const token = await clrfundContract.nativeToken() + const tokenContract = await ethers.getContractAt('ERC20', token) + const decimals = await tokenContract.decimals() + const depositInUnits = parseUnits(deposit, decimals) + + const controller = await clrfundContract.getAddress() + const registry = await deployRecipientRegistry({ + type: recipientRegistryType, + controller, + deposit: depositInUnits, + challengePeriod, + ethers, + }) + recipientRegistryAddress = await registry.getAddress() + } + + const tx = await clrfundContract.setRecipientRegistry( + recipientRegistryAddress + ) + await tx.wait() + + console.log( + `Recipient registry (${recipientRegistryType}): ${recipientRegistryAddress}` + ) + console.log(`Recipient registry set at tx: ${tx.hash}`) } ) diff --git a/contracts/tasks/setToken.ts b/contracts/tasks/setToken.ts index f4096d175..adc43ab2e 100644 --- a/contracts/tasks/setToken.ts +++ b/contracts/tasks/setToken.ts @@ -2,11 +2,11 @@ * Set the native token in the ClrFund contract * Sample usage: * - * yarn hardhat set-token --token --clrfund --network arbitrum-goerli + * yarn hardhat set-token --token --clrfund --network */ import { task } from 'hardhat/config' -import { Contract, BigNumber } from 'ethers' +import { Contract } from 'ethers' import { deployContract } from '../utils/deployment' import { UNIT } from '../utils/constants' @@ -38,13 +38,13 @@ task('set-token', 'Set the token in ClrFund') let tokenAddress: string = token || '' if (!tokenAddress) { - const initialTokenSupply = BigNumber.from(tokenAmount).mul(UNIT) + const initialTokenSupply = BigInt(tokenAmount) * UNIT const tokenContract = await deployContract({ name: 'AnyOldERC20Token', contractArgs: [initialTokenSupply], ethers, }) - tokenAddress = tokenContract.address + tokenAddress = await tokenContract.getAddress() console.log('New token address', tokenAddress) } await setToken(clrfundContract, tokenAddress) diff --git a/contracts/tasks/verifyAll.ts b/contracts/tasks/verifyAll.ts index a7f94d291..5ba717a7e 100644 --- a/contracts/tasks/verifyAll.ts +++ b/contracts/tasks/verifyAll.ts @@ -1,5 +1,6 @@ import { Contract } from 'ethers' import { task } from 'hardhat/config' +import { ZERO_ADDRESS } from '../utils/constants' const SUCCESS = 'success' @@ -10,27 +11,28 @@ type Result = { async function verifyDeployer(deployer: Contract, run: any): Promise { try { - const { address } = deployer const constructorArguments = await Promise.all([ deployer.clrfundTemplate(), deployer.maciFactory(), deployer.roundFactory(), ]) - await run('verify:verify', { address, constructorArguments }) + await run('verify:verify', { + address: deployer.target, + constructorArguments, + }) return SUCCESS } catch (error) { return (error as Error).message } } -async function verifyMaciFactory( - deployer: Contract, - run: any -): Promise { +async function verifyMaciFactory(clrfund: Contract, run: any): Promise { try { - const address = await deployer.maciFactory() - await run('verify-maci-factory', { address }) + const address = await clrfund.maciFactory() + if (address !== ZERO_ADDRESS) { + await run('verify-maci-factory', { address }) + } return SUCCESS } catch (error) { return (error as Error).message @@ -39,7 +41,22 @@ async function verifyMaciFactory( async function verifyClrFund(clrfund: Contract, run: any): Promise { try { - await run('verify', { address: clrfund.address }) + await run('verify', { address: clrfund.target }) + return SUCCESS + } catch (error) { + return (error as Error).message + } +} + +async function verifyFundingRoundFactory( + clrfund: Contract, + run: any +): Promise { + try { + const address = await clrfund.roundFactory() + if (address !== ZERO_ADDRESS) { + await run('verify:verify', { address }) + } return SUCCESS } catch (error) { return (error as Error).message @@ -47,12 +64,14 @@ async function verifyClrFund(clrfund: Contract, run: any): Promise { } async function verifyRecipientRegistry( - factory: Contract, + clrfund: Contract, run: any ): Promise { try { - const address = await factory.recipientRegistry() - await run('verify-recipient-registry', { address }) + const address = await clrfund.recipientRegistry() + if (address !== ZERO_ADDRESS) { + await run('verify-recipient-registry', { address }) + } return SUCCESS } catch (error) { return (error as Error).message @@ -60,12 +79,14 @@ async function verifyRecipientRegistry( } async function verifyUserRegistry( - factory: Contract, + clrfund: Contract, run: any ): Promise { try { - const address = await factory.userRegistry() - await run('verify-user-registry', { address }) + const address = await clrfund.userRegistry() + if (address !== ZERO_ADDRESS) { + await run('verify-user-registry', { address }) + } return SUCCESS } catch (error) { return (error as Error).message @@ -92,27 +113,42 @@ async function verifyMaci(maciAddress: string, run: any): Promise { async function verifyTally(tally: Contract, run: any): Promise { try { - const constructorArguments = await Promise.all([tally.verifier()]) - await run('verify:verify', { address: tally.address, constructorArguments }) + const constructorArguments = await Promise.all([ + tally.verifier(), + tally.vkRegistry(), + tally.poll(), + tally.messageProcessor(), + ]) + await run('verify:verify', { address: tally.target, constructorArguments }) return SUCCESS } catch (error) { return (error as Error).message } } -async function verifyPoll(pollContract: Contract, run: any): Promise { +async function verifyMessageProcessor( + messageProcesor: Contract, + run: any +): Promise { try { - const [, duration] = await pollContract.getDeployTimeAndDuration() const constructorArguments = await Promise.all([ - Promise.resolve(duration), - pollContract.maxValues(), - pollContract.treeDepths(), - pollContract.batchSizes(), - pollContract.coordinatorPubKey(), - pollContract.extContracts(), + messageProcesor.verifier(), + messageProcesor.vkRegistry(), + messageProcesor.poll(), ]) - const { address } = pollContract - await run('verify:verify', { address, constructorArguments }) + await run('verify:verify', { + address: messageProcesor.target, + constructorArguments, + }) + return SUCCESS + } catch (error) { + return (error as Error).message + } +} + +async function verifyPoll(address: string, run: any): Promise { + try { + await run('verify-poll', { address }) return SUCCESS } catch (error) { return (error as Error).message @@ -155,89 +191,84 @@ async function getBrightIdSponsor( * Verifies all the contracts created for clrfund app */ task('verify-all', 'Verify all clrfund contracts') - .addParam('deployer', 'ClrFundDeployer contract address') - .addOptionalParam('clrfund', 'ClrFund contract address') - .setAction(async ({ deployer, clrfund }, { run, ethers }) => { - const deployerContract = await ethers.getContractAt( - 'ClrFundDeployer', - deployer - ) - const maciFactoryAddress = await deployerContract.maciFactory() + .addParam('clrfund', 'ClrFund contract address') + .setAction(async ({ clrfund }, { run, ethers }) => { + const clrfundContract = await ethers.getContractAt('ClrFund', clrfund) + const maciFactoryAddress = await clrfundContract.maciFactory() const maciFactory = await ethers.getContractAt( 'MACIFactory', maciFactoryAddress ) const results: Result[] = [] - let status = await verifyDeployer(deployerContract, run) - results.push({ name: 'ClrFund Deployer', status }) - status = await verifyMaciFactory(deployerContract, run) - results.push({ name: 'Maci facotry', status }) - - if (clrfund) { - const clrfundContract = await ethers.getContractAt('ClrFund', clrfund) - status = await verifyClrFund(clrfundContract, run) - results.push({ name: 'ClrFund', status }) - status = await verifyRecipientRegistry(clrfundContract, run) - results.push({ name: 'Recipient registry', status }) - status = await verifyUserRegistry(clrfundContract, run) - results.push({ name: 'User factory', status }) - const sponsor = await getBrightIdSponsor(clrfundContract, ethers) - if (sponsor) { - await verifyContract('Sponsor', sponsor, run, results) - } - - const roundAddress = await clrfundContract.getCurrentRound() - if (roundAddress !== ethers.constants.AddressZero) { - const round = await ethers.getContractAt('FundingRound', roundAddress) - const maciAddress = await round.maci() - status = await verifyRound(roundAddress, run) - results.push({ name: 'Funding round', status }) - status = await verifyMaci(maciAddress, run) - results.push({ name: 'MACI', status }) - - const poll = await round.poll() - if (poll !== ethers.constants.AddressZero) { - const pollContract = await ethers.getContractAt('Poll', poll) - status = await verifyPoll(pollContract, run) - results.push({ name: 'Poll', status }) - } + let status = await verifyMaciFactory(clrfundContract, run) + results.push({ name: 'Maci factory', status }) - const tally = await round.tally() - if (tally !== ethers.constants.AddressZero) { - const tallyContract = await ethers.getContractAt('Tally', tally) - status = await verifyTally(tallyContract, run) - results.push({ name: 'Tally', status }) - } - - await verifyContract( - 'TopupToken', - await round.topupToken(), - run, - results - ) - } - } - - await verifyContract( - 'clrfundTemplate', - await deployerContract.clrfundTemplate(), - run, - results - ) await verifyContract( 'VkRegistry', await maciFactory.vkRegistry(), run, results ) + + const factories = await maciFactory.factories() + await verifyContract('PollFactory', factories.pollFactory, run, results) + await verifyContract('TallyFactory', factories.tallyFactory, run, results) await verifyContract( - 'PollFactory', - await maciFactory.pollFactory(), + 'MessageProcessorFactory', + factories.messageProcessorFactory, run, results ) + status = await verifyClrFund(clrfundContract, run) + results.push({ name: 'ClrFund', status }) + status = await verifyRecipientRegistry(clrfundContract, run) + results.push({ name: 'Recipient registry', status }) + status = await verifyUserRegistry(clrfundContract, run) + results.push({ name: 'User registry', status }) + const sponsor = await getBrightIdSponsor(clrfundContract, ethers) + if (sponsor) { + await verifyContract('Sponsor', sponsor, run, results) + } + status = await verifyFundingRoundFactory(clrfundContract, run) + results.push({ name: 'Funding Round Factory', status }) + + const roundAddress = await clrfundContract.getCurrentRound() + if (roundAddress !== ZERO_ADDRESS) { + const round = await ethers.getContractAt('FundingRound', roundAddress) + const maciAddress = await round.maci() + status = await verifyRound(roundAddress, run) + results.push({ name: 'Funding round', status }) + status = await verifyMaci(maciAddress, run) + results.push({ name: 'MACI', status }) + + const poll = await round.poll() + if (poll !== ZERO_ADDRESS) { + status = await verifyPoll(poll, run) + results.push({ name: 'Poll', status }) + } + + const tally = await round.tally() + if (tally !== ZERO_ADDRESS) { + const tallyContract = await ethers.getContractAt('Tally', tally) + status = await verifyTally(tallyContract, run) + results.push({ name: 'Tally', status }) + + const messageProcessorAddress = await tallyContract.messageProcessor() + if (messageProcessorAddress !== ZERO_ADDRESS) { + const mpContract = await ethers.getContractAt( + 'MessageProcessor', + messageProcessorAddress + ) + status = await verifyMessageProcessor(mpContract, run) + results.push({ name: 'MessageProcessor', status }) + } + } + + await verifyContract('TopupToken', await round.topupToken(), run, results) + } + results.forEach(({ name, status }, i) => { const color = status === SUCCESS ? '32' : '31' console.log(`${i} ${name}: \x1b[%sm%s\x1b[0m`, color, status) diff --git a/contracts/tasks/verifyMaci.ts b/contracts/tasks/verifyMaci.ts index 560c1c921..5a109bcbd 100644 --- a/contracts/tasks/verifyMaci.ts +++ b/contracts/tasks/verifyMaci.ts @@ -14,8 +14,13 @@ task('verify-maci', 'Verify a MACI contract') const maci = await ethers.getContractAt('MACI', maciAddress) const constructorArguments = await Promise.all([ maci.pollFactory(), + maci.messageProcessorFactory(), + maci.tallyFactory(), + maci.subsidyFactory(), maci.signUpGatekeeper(), maci.initialVoiceCreditProxy(), + maci.topupCredit(), + maci.stateTreeDepth(), ]) console.log('Verifying the MACI contract', maciAddress) diff --git a/contracts/tasks/verifyMaciFactory.ts b/contracts/tasks/verifyMaciFactory.ts index 54fdcf86f..911fecfd3 100644 --- a/contracts/tasks/verifyMaciFactory.ts +++ b/contracts/tasks/verifyMaciFactory.ts @@ -4,7 +4,8 @@ import { Contract } from 'ethers' async function getConstructorArguments(maciFactory: Contract): Promise { const result = await Promise.all([ maciFactory.vkRegistry(), - maciFactory.pollFactory(), + maciFactory.factories(), + maciFactory.verifier(), ]) return result } diff --git a/contracts/tasks/verifyPoll.ts b/contracts/tasks/verifyPoll.ts new file mode 100644 index 000000000..f1a6ee3ef --- /dev/null +++ b/contracts/tasks/verifyPoll.ts @@ -0,0 +1,62 @@ +import { task } from 'hardhat/config' +import { Contract } from 'ethers' + +async function getConstructorArguments(pollContract: Contract): Promise { + const [, duration] = await pollContract.getDeployTimeAndDuration() + const [maxValues, treeDepths, batchSizes, coordinatorPubKey, extContracts] = + await Promise.all([ + pollContract.maxValues(), + pollContract.treeDepths(), + pollContract.batchSizes(), + pollContract.coordinatorPubKey(), + pollContract.extContracts(), + ]) + + const result = [ + duration, + { + maxMessages: maxValues.maxMessages, + maxVoteOptions: maxValues.maxVoteOptions, + }, + { + intStateTreeDepth: treeDepths.intStateTreeDepth, + messageTreeSubDepth: treeDepths.messageTreeSubDepth, + messageTreeDepth: treeDepths.messageTreeDepth, + voteOptionTreeDepth: treeDepths.voteOptionTreeDepth, + }, + { + messageBatchSize: batchSizes.messageBatchSize, + tallyBatchSize: batchSizes.tallyBatchSize, + subsidyBatchSize: batchSizes.subsidyBatchSize, + }, + { + x: coordinatorPubKey.x, + y: coordinatorPubKey.y, + }, + { + maci: extContracts.maci, + messageAq: extContracts.messageAq, + topupCredit: extContracts.topupCredit, + }, + ] + return result +} + +/** + * Verifies the Poll contract + * - it constructs the constructor arguments by querying the Poll contract + * - it calls the etherscan hardhat plugin to verify the contract + */ +task('verify-poll', 'Verify a Poll contract') + .addParam('address', 'Poll contract address') + .setAction(async ({ address }, { run, ethers }) => { + const poll = await ethers.getContractAt('Poll', address) + + const constructorArguments = await getConstructorArguments(poll) + console.log('Constructor arguments', constructorArguments) + + await run('verify:verify', { + address, + constructorArguments, + }) + }) diff --git a/contracts/tests/deployer.ts b/contracts/tests/deployer.ts index 9ed53383a..c1f175d5e 100644 --- a/contracts/tests/deployer.ts +++ b/contracts/tests/deployer.ts @@ -1,8 +1,8 @@ -import { ethers, waffle, config } from 'hardhat' -import { use, expect } from 'chai' -import { solidity } from 'ethereum-waffle' -import { Signer, Contract, ContractTransaction, constants } from 'ethers' -import { genRandomSalt } from '@clrfund/maci-crypto' +import { ethers, config, artifacts } from 'hardhat' +import { time } from '@nomicfoundation/hardhat-network-helpers' +import { expect } from 'chai' +import { Contract } from 'ethers' +import { genRandomSalt } from 'maci-crypto' import { Keypair } from '@clrfund/common' import { ZERO_ADDRESS, UNIT } from '../utils/constants' @@ -13,46 +13,14 @@ import { deployMaciFactory, } from '../utils/deployment' import { MaciParameters } from '../utils/maciParameters' -import { DEFAULT_CIRCUIT } from '../utils/circuits' - -use(solidity) +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' const roundDuration = 10000 -const circuit = DEFAULT_CIRCUIT - -async function setRoundTally( - clrfund: Contract, - coordinator: Signer -): Promise { - const libraries = await deployPoseidonLibraries({ - artifactsPath: config.paths.artifacts, - ethers, - }) - const verifier = await deployContract({ - name: 'MockVerifier', - ethers, - signer: coordinator, - }) - const tally = await deployContract({ - name: 'Tally', - libraries, - contractArgs: [verifier.address], - signer: coordinator, - ethers, - }) - const roundAddress = await clrfund.getCurrentRound() - const round = await ethers.getContractAt( - 'FundingRound', - roundAddress, - coordinator - ) - return round.setTally(tally.address) -} - -describe('Clr fund deployer', () => { - const provider = waffle.provider - const [, deployer, coordinator, contributor] = provider.getWallets() +describe('Clr fund deployer', async () => { + let deployer: HardhatEthersSigner + let coordinator: HardhatEthersSigner + let contributor: HardhatEthersSigner let maciFactory: Contract let userRegistry: Contract let recipientRegistry: Contract @@ -62,6 +30,17 @@ describe('Clr fund deployer', () => { let token: Contract const coordinatorPubKey = new Keypair().pubKey.asContractParam() let poseidonContracts: { [name: string]: string } + let roundInterface: Contract + + before(async () => { + ;[, deployer, coordinator, contributor] = await ethers.getSigners() + + // this is just a dummy funding round contract to be passed as the + // contract argument to the revertedByCustomError() as a way to + // pass the Abi. + const FundingRoundArtifacts = await artifacts.readArtifact('FundingRound') + roundInterface = new Contract(ZERO_ADDRESS, FundingRoundArtifacts.abi) + }) beforeEach(async () => { if (!poseidonContracts) { @@ -71,13 +50,13 @@ describe('Clr fund deployer', () => { }) } + const params = MaciParameters.mock() maciFactory = await deployMaciFactory({ libraries: poseidonContracts, signer: deployer, ethers, + maciParameters: params, }) - const params = MaciParameters.mock(circuit) - await maciFactory.setMaciParameters(...params.asContractParam()) factoryTemplate = await deployContract({ name: 'ClrFund', @@ -85,33 +64,43 @@ describe('Clr fund deployer', () => { signer: deployer, }) - expect(factoryTemplate.address).to.properAddress - expect(await getGasUsage(factoryTemplate.deployTransaction)).lessThan( - 5400000 - ) + expect(await factoryTemplate.getAddress()).to.be.properAddress + const tx = factoryTemplate.deploymentTransaction() + if (tx) { + expect(await getGasUsage(tx)).lessThan(5400000) + } else { + expect(tx).to.not.be.null + } const roundFactory = await deployContract({ name: 'FundingRoundFactory', - libraries: poseidonContracts, ethers, }) - expect(await getGasUsage(roundFactory.deployTransaction)).lessThan(4000000) + const roundFactoryTx = roundFactory.deploymentTransaction() + if (roundFactoryTx) { + expect(await getGasUsage(roundFactoryTx)).lessThan(4600000) + } else { + expect(roundFactoryTx).to.not.be.null + } clrFundDeployer = await deployContract({ name: 'ClrFundDeployer', contractArgs: [ - factoryTemplate.address, - maciFactory.address, - roundFactory.address, + factoryTemplate.target, + maciFactory.target, + roundFactory.target, ], ethers, signer: deployer, }) - expect(clrFundDeployer.address).to.properAddress - expect(await getGasUsage(clrFundDeployer.deployTransaction)).lessThan( - 5400000 - ) + expect(clrFundDeployer.target).to.be.properAddress + const deployerTx = clrFundDeployer.deploymentTransaction() + if (deployerTx) { + expect(await getGasUsage(deployerTx)).lessThan(5400000) + } else { + expect(deployerTx).to.not.be.null + } const newInstanceTx = await clrFundDeployer.deployClrFund() const instanceAddress = await getEventArg( @@ -132,56 +121,28 @@ describe('Clr fund deployer', () => { 'SimpleRecipientRegistry', deployer ) - recipientRegistry = await SimpleRecipientRegistry.deploy(clrfund.address) + recipientRegistry = await SimpleRecipientRegistry.deploy(clrfund.target) // Deploy token contract and transfer tokens to contributor - const tokenInitialSupply = UNIT.mul(1000) + const tokenInitialSupply = UNIT * 1000n const Token = await ethers.getContractFactory('AnyOldERC20Token', deployer) token = await Token.deploy(tokenInitialSupply) - expect(token.address).to.properAddress + expect(token.target).to.properAddress await token.transfer(contributor.address, tokenInitialSupply) }) it('can only be initialized once', async () => { - const dummyRoundFactory = constants.AddressZero + const dummyRoundFactory = ZERO_ADDRESS await expect( - clrfund.init(maciFactory.address, dummyRoundFactory) + clrfund.init(maciFactory.target, dummyRoundFactory) ).to.be.revertedWith('Initializable: contract is already initialized') }) - it('can register with the subgraph', async () => { - await expect( - clrFundDeployer.registerInstance( - clrfund.address, - '{name:dead,title:beef}' - ) - ) - .to.emit(clrFundDeployer, 'Register') - .withArgs(clrfund.address, '{name:dead,title:beef}') - }) - - it('cannot register with the subgraph twice', async () => { - await expect( - clrFundDeployer.registerInstance( - clrfund.address, - '{name:dead,title:beef}' - ) - ) - .to.emit(clrFundDeployer, 'Register') - .withArgs(clrfund.address, '{name:dead,title:beef}') - await expect( - clrFundDeployer.registerInstance( - clrfund.address, - '{name:dead,title:beef}' - ) - ).to.be.revertedWith('ClrFundAlreadyRegistered') - }) - it('initializes clrfund', async () => { expect(await clrfund.coordinator()).to.equal(ZERO_ADDRESS) expect(await clrfund.nativeToken()).to.equal(ZERO_ADDRESS) - expect(await clrfund.maciFactory()).to.equal(maciFactory.address) + expect(await clrfund.maciFactory()).to.equal(maciFactory.target) expect(await clrfund.userRegistry()).to.equal(ZERO_ADDRESS) expect(await clrfund.recipientRegistry()).to.equal(ZERO_ADDRESS) }) @@ -194,62 +155,64 @@ describe('Clr fund deployer', () => { describe('changing user registry', () => { it('allows owner to set user registry', async () => { - await clrfund.setUserRegistry(userRegistry.address) - expect(await clrfund.userRegistry()).to.equal(userRegistry.address) + await clrfund.setUserRegistry(userRegistry.target) + expect(await clrfund.userRegistry()).to.equal(userRegistry.target) }) it('allows only owner to set user registry', async () => { await expect( - clrfund.connect(contributor).setUserRegistry(userRegistry.address) + (clrfund.connect(contributor) as Contract).setUserRegistry( + userRegistry.target + ) ).to.be.revertedWith('Ownable: caller is not the owner') }) it('allows owner to change recipient registry', async () => { - await clrfund.setRecipientRegistry(recipientRegistry.address) + await clrfund.setRecipientRegistry(recipientRegistry.target) const SimpleUserRegistry = await ethers.getContractFactory( 'SimpleUserRegistry', deployer ) const anotherUserRegistry = await SimpleUserRegistry.deploy() - await clrfund.setUserRegistry(anotherUserRegistry.address) - expect(await clrfund.userRegistry()).to.equal(anotherUserRegistry.address) + await clrfund.setUserRegistry(anotherUserRegistry.target) + expect(await clrfund.userRegistry()).to.equal(anotherUserRegistry.target) }) }) describe('changing recipient registry', () => { it('allows owner to set recipient registry', async () => { await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) - await clrfund.setRecipientRegistry(recipientRegistry.address) + await clrfund.setRecipientRegistry(recipientRegistry.target) expect(await clrfund.recipientRegistry()).to.equal( - recipientRegistry.address + recipientRegistry.target ) - expect(await recipientRegistry.controller()).to.equal(clrfund.address) - const params = MaciParameters.mock(circuit) + expect(await recipientRegistry.controller()).to.equal(clrfund.target) + const params = MaciParameters.mock() expect(await recipientRegistry.maxRecipients()).to.equal( - 5 ** params.voteOptionTreeDepth + BigInt(5) ** BigInt(params.treeDepths.voteOptionTreeDepth) ) }) it('allows only owner to set recipient registry', async () => { await expect( - clrfund - .connect(contributor) - .setRecipientRegistry(recipientRegistry.address) + (clrfund.connect(contributor) as Contract).setRecipientRegistry( + recipientRegistry.target + ) ).to.be.revertedWith('Ownable: caller is not the owner') }) it('allows owner to change recipient registry', async () => { - await clrfund.setRecipientRegistry(recipientRegistry.address) + await clrfund.setRecipientRegistry(recipientRegistry.target) const SimpleRecipientRegistry = await ethers.getContractFactory( 'SimpleRecipientRegistry', deployer ) const anotherRecipientRegistry = await SimpleRecipientRegistry.deploy( - clrfund.address + clrfund.target ) - await clrfund.setRecipientRegistry(anotherRecipientRegistry.address) + await clrfund.setRecipientRegistry(anotherRecipientRegistry.target) expect(await clrfund.recipientRegistry()).to.equal( - anotherRecipientRegistry.address + anotherRecipientRegistry.target ) }) }) @@ -263,7 +226,9 @@ describe('Clr fund deployer', () => { it('allows only owner to add funding source', async () => { await expect( - clrfund.connect(contributor).addFundingSource(contributor.address) + (clrfund.connect(contributor) as Contract).addFundingSource( + contributor.address + ) ).to.be.revertedWith('Ownable: caller is not the owner') }) @@ -271,7 +236,7 @@ describe('Clr fund deployer', () => { await clrfund.addFundingSource(contributor.address) await expect( clrfund.addFundingSource(contributor.address) - ).to.be.revertedWith('FundingSourceAlreadyAdded') + ).to.be.revertedWithCustomError(clrfund, 'FundingSourceAlreadyAdded') }) it('allows owner to remove funding source', async () => { @@ -284,7 +249,9 @@ describe('Clr fund deployer', () => { it('allows only owner to remove funding source', async () => { await clrfund.addFundingSource(contributor.address) await expect( - clrfund.connect(contributor).removeFundingSource(contributor.address) + (clrfund.connect(contributor) as Contract).removeFundingSource( + contributor.address + ) ).to.be.revertedWith('Ownable: caller is not the owner') }) @@ -293,26 +260,29 @@ describe('Clr fund deployer', () => { await clrfund.removeFundingSource(contributor.address) await expect( clrfund.removeFundingSource(contributor.address) - ).to.be.revertedWith('FundingSourceNotFound') + ).to.be.revertedWithCustomError(clrfund, 'FundingSourceNotFound') }) }) it('allows direct contributions to the matching pool', async () => { - const contributionAmount = UNIT.mul(10) - await clrfund.setToken(token.address) + const contributionAmount = UNIT * 10n + await clrfund.setToken(token.target) await expect( - token.connect(contributor).transfer(clrfund.address, contributionAmount) + (token.connect(contributor) as Contract).transfer( + clrfund.target, + contributionAmount + ) ) .to.emit(token, 'Transfer') - .withArgs(contributor.address, clrfund.address, contributionAmount) - expect(await token.balanceOf(clrfund.address)).to.equal(contributionAmount) + .withArgs(contributor.address, clrfund.target, contributionAmount) + expect(await token.balanceOf(clrfund.target)).to.equal(contributionAmount) }) describe('deploying funding round', () => { it('deploys funding round', async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) - await clrfund.setToken(token.address) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) + await clrfund.setToken(token.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) const deployed = clrfund.deployNewRound(roundDuration) await expect(deployed).to.emit(clrfund, 'RoundStarted') @@ -328,8 +298,8 @@ describe('Clr fund deployer', () => { 'FundingRound', fundingRoundAddress ) - expect(await fundingRound.owner()).to.equal(clrfund.address) - expect(await fundingRound.nativeToken()).to.equal(token.address) + expect(await fundingRound.owner()).to.equal(clrfund.target) + expect(await fundingRound.nativeToken()).to.equal(token.target) const maciAddress = await getEventArg( deployTx, @@ -344,69 +314,71 @@ describe('Clr fund deployer', () => { deployTx, maci, 'DeployPoll', - '_pollAddr' + 'pollAddr' ) - const poll = await ethers.getContractAt('Poll', pollAddress) + + const poll = await ethers.getContractAt('Poll', pollAddress.poll) const roundCoordinatorPubKey = await poll.coordinatorPubKey() expect(roundCoordinatorPubKey.x).to.equal(coordinatorPubKey.x) expect(roundCoordinatorPubKey.y).to.equal(coordinatorPubKey.y) }) it('reverts if user registry is not set', async () => { - await clrfund.setRecipientRegistry(recipientRegistry.address) - await clrfund.setToken(token.address) + await clrfund.setRecipientRegistry(recipientRegistry.target) + await clrfund.setToken(token.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) - await expect(clrfund.deployNewRound(roundDuration)).to.be.revertedWith( - 'NoUserRegistry' - ) + await expect( + clrfund.deployNewRound(roundDuration) + ).to.be.revertedWithCustomError(roundInterface, 'InvalidUserRegistry') }) + // TODO investigate why this fails it('reverts if recipient registry is not set', async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setToken(token.address) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setToken(token.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) - await expect(clrfund.deployNewRound(roundDuration)).to.be.revertedWith( - 'NoRecipientRegistry' - ) + await expect( + clrfund.deployNewRound(roundDuration) + ).to.be.revertedWithCustomError(clrfund, 'RecipientRegistryNotSet') }) it('reverts if native token is not set', async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) - await expect(clrfund.deployNewRound(roundDuration)).to.be.revertedWith( - 'NoToken' - ) + await expect( + clrfund.deployNewRound(roundDuration) + ).to.be.revertedWithCustomError(roundInterface, 'InvalidNativeToken') }) it('reverts if coordinator is not set', async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) - await clrfund.setToken(token.address) - await expect(clrfund.deployNewRound(roundDuration)).to.be.revertedWith( - 'NoCoordinator' - ) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) + await clrfund.setToken(token.target) + await expect( + clrfund.deployNewRound(roundDuration) + ).to.be.revertedWithCustomError(roundInterface, 'InvalidCoordinator') }) it('reverts if current round is not finalized', async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) - await clrfund.setToken(token.address) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) + await clrfund.setToken(token.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) await clrfund.deployNewRound(roundDuration) - await expect(clrfund.deployNewRound(roundDuration)).to.be.revertedWith( - 'NotFinalized' - ) + await expect( + clrfund.deployNewRound(roundDuration) + ).to.be.revertedWithCustomError(clrfund, 'NotFinalized') }) it('deploys new funding round after previous round has been finalized', async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) - await clrfund.setToken(token.address) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) + await clrfund.setToken(token.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) await clrfund.deployNewRound(roundDuration) @@ -418,12 +390,12 @@ describe('Clr fund deployer', () => { }) it('only owner can deploy funding round', async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) - await clrfund.setToken(token.address) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) + await clrfund.setToken(token.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) - const clrfundAsContributor = clrfund.connect(contributor) + const clrfundAsContributor = clrfund.connect(contributor) as Contract await expect( clrfundAsContributor.deployNewRound(roundDuration) ).to.be.revertedWith('Ownable: caller is not the owner') @@ -431,16 +403,16 @@ describe('Clr fund deployer', () => { }) describe('transferring matching funds', () => { - const contributionAmount = UNIT.mul(10) - const totalSpent = UNIT.mul(100) + const contributionAmount = UNIT * 10n + const totalSpent = UNIT * 100n const totalSpentSalt = genRandomSalt().toString() const resultsCommitment = genRandomSalt().toString() const perVOVoiceCreditCommitment = genRandomSalt().toString() beforeEach(async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) - await clrfund.setToken(token.address) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) + await clrfund.setToken(token.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) }) @@ -448,29 +420,29 @@ describe('Clr fund deployer', () => { await clrfund.addFundingSource(deployer.address) await clrfund.addFundingSource(contributor.address) // Allowance is more than available balance - await token.connect(deployer).approve(clrfund.address, contributionAmount) + await (token.connect(deployer) as Contract).approve( + clrfund.target, + contributionAmount + ) // Allowance is less than available balance - await token - .connect(contributor) - .approve(clrfund.address, contributionAmount) + const tokenAsContributor = token.connect(contributor) as Contract + await tokenAsContributor.approve(clrfund.target, contributionAmount) // Direct contribution - await token - .connect(contributor) - .transfer(clrfund.address, contributionAmount) + await tokenAsContributor.transfer(clrfund.target, contributionAmount) await clrfund.deployNewRound(roundDuration) - expect(await clrfund.getMatchingFunds(token.address)).to.equal( - contributionAmount.mul(2) + expect(await clrfund.getMatchingFunds(token.target)).to.equal( + contributionAmount * 2n ) }) it('allows owner to finalize round', async () => { - await token - .connect(contributor) - .transfer(clrfund.address, contributionAmount) + await (token.connect(contributor) as Contract).transfer( + clrfund.target, + contributionAmount + ) await clrfund.deployNewRound(roundDuration) - await provider.send('evm_increaseTime', [roundDuration]) - await setRoundTally(clrfund, coordinator) + await time.increase(roundDuration) await expect( clrfund.transferMatchingFunds( totalSpent, @@ -478,13 +450,12 @@ describe('Clr fund deployer', () => { resultsCommitment, perVOVoiceCreditCommitment ) - ).to.be.revertedWith('VotesNotTallied') + ).to.be.revertedWithCustomError(roundInterface, 'VotesNotTallied') }) it('allows owner to finalize round even without matching funds', async () => { await clrfund.deployNewRound(roundDuration) - await provider.send('evm_increaseTime', [roundDuration]) - await setRoundTally(clrfund, coordinator) + await time.increase(roundDuration) await expect( clrfund.transferMatchingFunds( totalSpent, @@ -492,16 +463,18 @@ describe('Clr fund deployer', () => { resultsCommitment, perVOVoiceCreditCommitment ) - ).to.be.revertedWith('VotesNotTallied') + ).to.be.revertedWithCustomError(roundInterface, 'VotesNotTallied') }) it('pulls funds from funding source', async () => { await clrfund.addFundingSource(contributor.address) - token.connect(contributor).approve(clrfund.address, contributionAmount) + await (token.connect(contributor) as Contract).approve( + clrfund.target, + contributionAmount + ) await clrfund.addFundingSource(deployer.address) // Doesn't have tokens await clrfund.deployNewRound(roundDuration) - await provider.send('evm_increaseTime', [roundDuration]) - await setRoundTally(clrfund, coordinator) + await time.increase(roundDuration) await expect( clrfund.transferMatchingFunds( totalSpent, @@ -509,17 +482,17 @@ describe('Clr fund deployer', () => { resultsCommitment, perVOVoiceCreditCommitment ) - ).to.be.revertedWith('VotesNotTallied') + ).to.be.revertedWithCustomError(roundInterface, 'VotesNotTallied') }) it('pulls funds from funding source if allowance is greater than balance', async () => { await clrfund.addFundingSource(contributor.address) - token - .connect(contributor) - .approve(clrfund.address, contributionAmount.mul(2)) + await (token.connect(contributor) as Contract).approve( + clrfund.target, + contributionAmount * 2n + ) await clrfund.deployNewRound(roundDuration) - await provider.send('evm_increaseTime', [roundDuration]) - await setRoundTally(clrfund, coordinator) + await time.increase(roundDuration) await expect( clrfund.transferMatchingFunds( totalSpent, @@ -527,22 +500,19 @@ describe('Clr fund deployer', () => { resultsCommitment, perVOVoiceCreditCommitment ) - ).to.be.revertedWith('VotesNotTallied') + ).to.be.revertedWithCustomError(roundInterface, 'VotesNotTallied') }) it('allows only owner to finalize round', async () => { await clrfund.deployNewRound(roundDuration) - await provider.send('evm_increaseTime', [roundDuration]) - await setRoundTally(clrfund, coordinator) + await time.increase(roundDuration) await expect( - clrfund - .connect(contributor) - .transferMatchingFunds( - totalSpent, - totalSpentSalt, - resultsCommitment, - perVOVoiceCreditCommitment - ) + (clrfund.connect(contributor) as Contract).transferMatchingFunds( + totalSpent, + totalSpentSalt, + resultsCommitment, + perVOVoiceCreditCommitment + ) ).to.be.revertedWith('Ownable: caller is not the owner') }) @@ -554,15 +524,15 @@ describe('Clr fund deployer', () => { resultsCommitment, perVOVoiceCreditCommitment ) - ).to.be.revertedWith('NoCurrentRound') + ).to.be.revertedWithCustomError(clrfund, 'NoCurrentRound') }) }) describe('cancelling round', () => { beforeEach(async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) - await clrfund.setToken(token.address) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) + await clrfund.setToken(token.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) }) @@ -582,12 +552,13 @@ describe('Clr fund deployer', () => { it('allows only owner to cancel round', async () => { await clrfund.deployNewRound(roundDuration) await expect( - clrfund.connect(contributor).cancelCurrentRound() + (clrfund.connect(contributor) as Contract).cancelCurrentRound() ).to.be.revertedWith('Ownable: caller is not the owner') }) it('reverts if round has not been deployed', async () => { - await expect(clrfund.cancelCurrentRound()).to.be.revertedWith( + await expect(clrfund.cancelCurrentRound()).to.be.revertedWithCustomError( + clrfund, 'NoCurrentRound' ) }) @@ -595,23 +566,24 @@ describe('Clr fund deployer', () => { it('reverts if round is finalized', async () => { await clrfund.deployNewRound(roundDuration) await clrfund.cancelCurrentRound() - await expect(clrfund.cancelCurrentRound()).to.be.revertedWith( + await expect(clrfund.cancelCurrentRound()).to.be.revertedWithCustomError( + clrfund, 'AlreadyFinalized' ) }) }) it('allows owner to set native token', async () => { - await expect(clrfund.setToken(token.address)) + await expect(clrfund.setToken(token.target)) .to.emit(clrfund, 'TokenChanged') - .withArgs(token.address) - expect(await clrfund.nativeToken()).to.equal(token.address) + .withArgs(token.target) + expect(await clrfund.nativeToken()).to.equal(token.target) }) it('only owner can set native token', async () => { - const clrfundAsContributor = clrfund.connect(contributor) + const clrfundAsContributor = clrfund.connect(contributor) as Contract await expect( - clrfundAsContributor.setToken(token.address) + clrfundAsContributor.setToken(token.target) ).to.be.revertedWith('Ownable: caller is not the owner') }) @@ -623,7 +595,7 @@ describe('Clr fund deployer', () => { }) it('allows only the owner to set a new coordinator', async () => { - const clrfundAsContributor = clrfund.connect(contributor) + const clrfundAsContributor = clrfund.connect(contributor) as Contract await expect( clrfundAsContributor.setCoordinator( coordinator.address, @@ -634,7 +606,7 @@ describe('Clr fund deployer', () => { it('allows coordinator to call coordinatorQuit and sets coordinator to null', async () => { await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) - const clrfundAsCoordinator = clrfund.connect(coordinator) + const clrfundAsCoordinator = clrfund.connect(coordinator) as Contract await expect(clrfundAsCoordinator.coordinatorQuit()) .to.emit(clrfund, 'CoordinatorChanged') .withArgs(ZERO_ADDRESS) @@ -643,13 +615,16 @@ describe('Clr fund deployer', () => { it('only coordinator can call coordinatorQuit', async () => { await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) - await expect(clrfund.coordinatorQuit()).to.be.revertedWith('NotAuthorized') + await expect(clrfund.coordinatorQuit()).to.be.revertedWithCustomError( + clrfund, + 'NotAuthorized' + ) }) it('should cancel current round when coordinator quits', async () => { - await clrfund.setUserRegistry(userRegistry.address) - await clrfund.setRecipientRegistry(recipientRegistry.address) - await clrfund.setToken(token.address) + await clrfund.setUserRegistry(userRegistry.target) + await clrfund.setRecipientRegistry(recipientRegistry.target) + await clrfund.setToken(token.target) await clrfund.setCoordinator(coordinator.address, coordinatorPubKey) await clrfund.deployNewRound(roundDuration) const fundingRoundAddress = await clrfund.getCurrentRound() @@ -658,7 +633,7 @@ describe('Clr fund deployer', () => { fundingRoundAddress ) - const clrfundAsCoordinator = clrfund.connect(coordinator) + const clrfundAsCoordinator = clrfund.connect(coordinator) as Contract await expect(clrfundAsCoordinator.coordinatorQuit()) .to.emit(clrfund, 'RoundFinalized') .withArgs(fundingRoundAddress) diff --git a/contracts/tests/maciFactory.ts b/contracts/tests/maciFactory.ts index f3baa4c2d..a70e78400 100644 --- a/contracts/tests/maciFactory.ts +++ b/contracts/tests/maciFactory.ts @@ -1,30 +1,31 @@ -import { waffle, artifacts, ethers, config } from 'hardhat' -import { Contract } from 'ethers' -import { use, expect } from 'chai' -import { solidity } from 'ethereum-waffle' -import { deployMockContract } from '@ethereum-waffle/mock-contract' -import { Keypair } from '@clrfund/common' +import { artifacts, ethers, config } from 'hardhat' +import { Contract, TransactionResponse } from 'ethers' +import { expect } from 'chai' +import { deployMockContract, MockContract } from '@clrfund/waffle-mock-contract' import { getEventArg, getGasUsage } from '../utils/contracts' import { deployMaciFactory, deployPoseidonLibraries } from '../utils/deployment' import { MaciParameters } from '../utils/maciParameters' -import { DEFAULT_CIRCUIT } from '../utils/circuits' - -use(solidity) +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' +import { Keypair } from '@clrfund/common' describe('MACI factory', () => { - const provider = waffle.provider - const [, deployer, coordinator] = provider.getWallets() + let deployer: HardhatEthersSigner + let coordinator: HardhatEthersSigner const duration = 100 let maciFactory: Contract - let signUpGatekeeper: Contract - let initialVoiceCreditProxy: Contract - let topupContract: Contract + let signUpGatekeeper: MockContract + let initialVoiceCreditProxy: MockContract + let topupContract: MockContract let maciParameters: MaciParameters let poseidonContracts: { [name: string]: string } const coordinatorPubKey = new Keypair().pubKey.asContractParam() + before(async () => { + ;[, deployer, coordinator] = await ethers.getSigners() + }) + beforeEach(async () => { if (!poseidonContracts) { poseidonContracts = await deployPoseidonLibraries({ @@ -33,14 +34,18 @@ describe('MACI factory', () => { signer: deployer, }) } + maciParameters = MaciParameters.mock() maciFactory = await deployMaciFactory({ ethers, signer: deployer, libraries: poseidonContracts, + maciParameters, }) - expect(await getGasUsage(maciFactory.deployTransaction)).lessThan(5600000) - - maciParameters = MaciParameters.mock(DEFAULT_CIRCUIT) + const transaction = await maciFactory.deploymentTransaction() + expect(transaction).to.be.not.null + expect(await getGasUsage(transaction as TransactionResponse)).lessThan( + 5600000 + ) const SignUpGatekeeperArtifact = await artifacts.readArtifact('SignUpGatekeeper') @@ -61,8 +66,10 @@ describe('MACI factory', () => { it('sets default MACI parameters', async () => { const { maxMessages, maxVoteOptions } = await maciFactory.maxValues() - expect(maxMessages).to.equal(0) - expect(maxVoteOptions).to.equal(0) + expect(maxMessages).to.be.greaterThan(BigInt(0)) + expect(maxVoteOptions).to.be.greaterThan(BigInt(0)) + expect(maxMessages).to.equal(maciParameters.maxValues.maxMessages) + expect(maxVoteOptions).to.equal(maciParameters.maxValues.maxVoteOptions) }) it('sets MACI parameters', async () => { @@ -72,13 +79,15 @@ describe('MACI factory', () => { const { messageTreeDepth } = await maciFactory.treeDepths() const { maxMessages, maxVoteOptions } = await maciFactory.maxValues() - expect(maxMessages).to.equal(maciParameters.maxMessages) - expect(maxVoteOptions).to.equal(maciParameters.maxVoteOptions) - expect(messageTreeDepth).to.equal(maciParameters.messageTreeDepth) + expect(maxMessages).to.equal(maciParameters.maxValues.maxMessages) + expect(maxVoteOptions).to.equal(maciParameters.maxValues.maxVoteOptions) + expect(messageTreeDepth).to.equal( + maciParameters.treeDepths.messageTreeDepth + ) }) it('allows only owner to set MACI parameters', async () => { - const coordinatorMaciFactory = maciFactory.connect(coordinator) + const coordinatorMaciFactory = maciFactory.connect(coordinator) as Contract await expect( coordinatorMaciFactory.setMaciParameters( ...maciParameters.asContractParam() @@ -92,12 +101,13 @@ describe('MACI factory', () => { ) await setParamTx.wait() const maciDeployed = maciFactory.deployMaci( - signUpGatekeeper.address, - initialVoiceCreditProxy.address, - topupContract.address, + signUpGatekeeper.target, + initialVoiceCreditProxy.target, + topupContract.target, duration, coordinator.address, - coordinatorPubKey + coordinatorPubKey, + deployer.address ) await expect(maciDeployed).to.emit(maciFactory, 'MaciDeployed') @@ -111,15 +121,16 @@ describe('MACI factory', () => { ...maciParameters.asContractParam() ) await setParamTx.wait() - const coordinatorMaciFactory = maciFactory.connect(coordinator) + const coordinatorMaciFactory = maciFactory.connect(coordinator) as Contract const deployTx = await coordinatorMaciFactory.deployMaci( - signUpGatekeeper.address, - initialVoiceCreditProxy.address, - topupContract.address, + signUpGatekeeper.target, + initialVoiceCreditProxy.target, + topupContract.target, duration, coordinator.address, - coordinatorPubKey + coordinatorPubKey, + coordinator.address ) await expect(deployTx).to.emit(maciFactory, 'MaciDeployed') }) @@ -130,12 +141,13 @@ describe('MACI factory', () => { ) await setParamTx.wait() const deployTx = await maciFactory.deployMaci( - signUpGatekeeper.address, - initialVoiceCreditProxy.address, - topupContract.address, + signUpGatekeeper.target, + initialVoiceCreditProxy.target, + topupContract.target, duration, coordinator.address, - coordinatorPubKey + coordinatorPubKey, + deployer.address ) const maciAddress = await getEventArg( @@ -152,17 +164,14 @@ describe('MACI factory', () => { }) it('links with PoseidonT6 correctly', async () => { - const setParamTx = await maciFactory.setMaciParameters( - ...maciParameters.asContractParam() - ) - await setParamTx.wait() const deployTx = await maciFactory.deployMaci( - signUpGatekeeper.address, - initialVoiceCreditProxy.address, - topupContract.address, + signUpGatekeeper.target, + initialVoiceCreditProxy.target, + topupContract.target, duration, coordinator.address, - coordinatorPubKey + coordinatorPubKey, + deployer.address ) const maciAddress = await getEventArg( diff --git a/contracts/tests/recipientRegistry.ts b/contracts/tests/recipientRegistry.ts index 398d747c1..70d0c5d32 100644 --- a/contracts/tests/recipientRegistry.ts +++ b/contracts/tests/recipientRegistry.ts @@ -1,21 +1,18 @@ -import { ethers, waffle } from 'hardhat' -import { use, expect } from 'chai' -import { solidity } from 'ethereum-waffle' -import { BigNumber, Contract } from 'ethers' -import { keccak256 } from '@ethersproject/solidity' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { Contract, solidityPackedKeccak256 } from 'ethers' import { gtcrEncode } from '@kleros/gtcr-encoder' +import { time } from '@nomicfoundation/hardhat-network-helpers' import { UNIT, ZERO_ADDRESS } from '../utils/constants' import { getTxFee, getEventArg } from '../utils/contracts' import { deployContract } from '../utils/deployment' +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -use(solidity) - -const { provider } = waffle const MAX_RECIPIENTS = 15 async function getCurrentTime(): Promise { - return (await provider.getBlock('latest')).timestamp + return await time.latest() } function getRecipientId( @@ -23,16 +20,22 @@ function getRecipientId( address: string, metadata: string ): string { - return keccak256( + return solidityPackedKeccak256( ['address', 'address', 'string'], [registryAddress, address, metadata] ) } -describe('Simple Recipient Registry', () => { - const [, deployer, controller, recipient] = provider.getWallets() - +describe('Simple Recipient Registry', async () => { let registry: Contract + let registryAddress: string + let deployer: HardhatEthersSigner + let controller: HardhatEthersSigner + let recipient: HardhatEthersSigner + + before(async () => { + ;[, deployer, controller, recipient] = await ethers.getSigners() + }) beforeEach(async () => { const SimpleRecipientRegistry = await ethers.getContractFactory( @@ -40,6 +43,7 @@ describe('Simple Recipient Registry', () => { deployer ) registry = await SimpleRecipientRegistry.deploy(controller.address) + registryAddress = await registry.getAddress() }) describe('initializing and configuring', () => { @@ -49,14 +53,18 @@ describe('Simple Recipient Registry', () => { }) it('sets max number of recipients', async () => { - await registry.connect(controller).setMaxRecipients(MAX_RECIPIENTS) + await (registry.connect(controller) as Contract).setMaxRecipients( + MAX_RECIPIENTS + ) expect(await registry.maxRecipients()).to.equal(MAX_RECIPIENTS) }) it('reverts if given number is less than current limit', async () => { - await registry.connect(controller).setMaxRecipients(MAX_RECIPIENTS) + await (registry.connect(controller) as Contract).setMaxRecipients( + MAX_RECIPIENTS + ) await expect( - registry.connect(controller).setMaxRecipients(1) + (registry.connect(controller) as Contract).setMaxRecipients(1) ).to.be.revertedWith( 'RecipientRegistry: Max number of recipients can not be decreased' ) @@ -81,14 +89,17 @@ describe('Simple Recipient Registry', () => { let recipientId: string beforeEach(async () => { - await registry.connect(controller).setMaxRecipients(MAX_RECIPIENTS) + await (registry.connect(controller) as Contract).setMaxRecipients( + MAX_RECIPIENTS + ) recipientAddress = recipient.address metadata = JSON.stringify({ name: 'Recipient', description: 'Description', imageHash: 'Ipfs imageHash', }) - recipientId = getRecipientId(registry.address, recipientAddress, metadata) + const registryAddress = await registry.getAddress() + recipientId = getRecipientId(registryAddress, recipientAddress, metadata) }) it('allows owner to add recipient', async () => { @@ -117,7 +128,7 @@ describe('Simple Recipient Registry', () => { const anotherRecipientAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' const anotherRecipientId = getRecipientId( - registry.address, + registryAddress, anotherRecipientAddress, metadata ) @@ -139,7 +150,7 @@ describe('Simple Recipient Registry', () => { }) it('rejects attempts to add recipient from anyone except owner', async () => { - const registryAsRecipient = registry.connect(recipient) + const registryAsRecipient = registry.connect(recipient) as Contract await expect( registryAsRecipient.addRecipient(recipientAddress, metadata) ).to.be.revertedWith('Ownable: caller is not the owner') @@ -203,7 +214,7 @@ describe('Simple Recipient Registry', () => { }) it('rejects attempts to remove recipient from anyone except owner', async () => { - const registryAsRecipient = registry.connect(recipient) + const registryAsRecipient = registry.connect(recipient) as Contract await expect( registryAsRecipient.removeRecipient(recipientId) ).to.be.revertedWith('Ownable: caller is not the owner') @@ -226,7 +237,7 @@ describe('Simple Recipient Registry', () => { it('should not return recipient address for recipient that has been added after the end of round', async () => { const startTime = await getCurrentTime() - await provider.send('evm_increaseTime', [1000]) + await time.increase(1000) const endTime = await getCurrentTime() await registry.addRecipient(recipientAddress, metadata) expect( @@ -238,7 +249,7 @@ describe('Simple Recipient Registry', () => { await registry.addRecipient(recipientAddress, metadata) const startTime = await getCurrentTime() await registry.removeRecipient(recipientId) - await provider.send('evm_increaseTime', [1000]) + await time.increase(1000) const endTime = await getCurrentTime() expect( await registry.getRecipientAddress(recipientIndex, startTime, endTime) @@ -265,13 +276,13 @@ describe('Simple Recipient Registry', () => { // Replace recipients const removedRecipient1 = '0x0000000000000000000000000000000000000001' const removedRecipient1Id = getRecipientId( - registry.address, + registryAddress, removedRecipient1, metadata ) const removedRecipient2 = '0x0000000000000000000000000000000000000002' const removedRecipient2Id = getRecipientId( - registry.address, + registryAddress, removedRecipient2, metadata ) @@ -295,7 +306,7 @@ describe('Simple Recipient Registry', () => { removedRecipient2 ) - await provider.send('evm_increaseTime', [1000]) + await time.increase(1000) const time3 = await getCurrentTime() // Recipients removed before the beginning of the round should be replaced expect(await registry.getRecipientAddress(1, time2, time3)).to.equal( @@ -325,7 +336,13 @@ describe('Simple Recipient Registry', () => { }) describe('Kleros GTCR adapter', () => { - const [, deployer, controller, recipient] = provider.getWallets() + let tcr: Contract + let registry: Contract + let deployer: HardhatEthersSigner + let controller: HardhatEthersSigner + let recipient: HardhatEthersSigner + let tcrAddress: string + const gtcrColumns = [ { label: 'Name', @@ -346,12 +363,13 @@ describe('Kleros GTCR adapter', () => { columns: gtcrColumns, values: { Name: `test-${address}`, Address: address }, }) - const recipientId = keccak256(['bytes'], [recipientData]) + const recipientId = solidityPackedKeccak256(['bytes'], [recipientData]) return [recipientId, recipientData] } - let tcr: Contract - let registry: Contract + before(async () => { + ;[, deployer, controller, recipient] = await ethers.getSigners() + }) beforeEach(async () => { const KlerosGTCRMock = await ethers.getContractFactory( @@ -359,32 +377,40 @@ describe('Kleros GTCR adapter', () => { deployer ) tcr = await KlerosGTCRMock.deploy('/ipfs/0', '/ipfs/1') + tcrAddress = await tcr.getAddress() const KlerosGTCRAdapter = await ethers.getContractFactory( 'KlerosGTCRAdapter', deployer ) - registry = await KlerosGTCRAdapter.deploy(tcr.address, controller.address) + registry = await KlerosGTCRAdapter.deploy(tcrAddress, controller.address) }) it('initializes correctly', async () => { - expect(await registry.tcr()).to.equal(tcr.address) + expect(await registry.tcr()).to.equal(tcrAddress) expect(await registry.controller()).to.equal(controller.address) expect(await registry.maxRecipients()).to.equal(0) }) describe('managing recipients', () => { const recipientIndex = 1 - const [recipientId, recipientData] = encodeRecipient(recipient.address) + let recipientId: string + let recipientData: string + + before(async () => { + ;[recipientId, recipientData] = encodeRecipient(recipient.address) + }) beforeEach(async () => { - await registry.connect(controller).setMaxRecipients(MAX_RECIPIENTS) + await (registry.connect(controller) as Contract).setMaxRecipients( + MAX_RECIPIENTS + ) }) it('allows anyone to add recipient', async () => { await tcr.addItem(recipientData) - const recipientAdded = await registry - .connect(recipient) - .addRecipient(recipientId) + const recipientAdded = await ( + registry.connect(recipient) as Contract + ).addRecipient(recipientId) let currentTime = await getCurrentTime() expect(recipientAdded) .to.emit(registry, 'RecipientAdded') @@ -403,9 +429,9 @@ describe('Kleros GTCR adapter', () => { anotherRecipientAddress ) await tcr.addItem(anotherRecipientData) - const anotherRecipientAdded = await registry - .connect(recipient) - .addRecipient(anotherRecipientId) + const anotherRecipientAdded = await ( + registry.connect(recipient) as Contract + ).addRecipient(anotherRecipientId) currentTime = await getCurrentTime() // Should increase recipient index for every new recipient expect(anotherRecipientAdded) @@ -439,11 +465,11 @@ describe('Kleros GTCR adapter', () => { it('allows anyone to remove recipient', async () => { await tcr.addItem(recipientData) - await registry.connect(recipient).addRecipient(recipientId) + await (registry.connect(recipient) as Contract).addRecipient(recipientId) await tcr.removeItem(recipientId) - const recipientRemoved = await registry - .connect(recipient) - .removeRecipient(recipientId) + const recipientRemoved = await ( + registry.connect(recipient) as Contract + ).removeRecipient(recipientId) const currentTime = await getCurrentTime() expect(recipientRemoved) .to.emit(registry, 'RecipientRemoved') @@ -490,7 +516,9 @@ describe('Kleros GTCR adapter', () => { } beforeEach(async () => { - await registry.connect(controller).setMaxRecipients(MAX_RECIPIENTS) + await (registry.connect(controller) as Contract).setMaxRecipients( + MAX_RECIPIENTS + ) }) it('allows to re-use index of removed recipient', async () => { @@ -525,7 +553,7 @@ describe('Kleros GTCR adapter', () => { removedRecipient2 ) - await provider.send('evm_increaseTime', [1000]) + time.increase(1000) const time3 = await getCurrentTime() // Recipients removed before the beginning of the round should be replaced expect(await registry.getRecipientAddress(1, time2, time3)).to.equal( @@ -539,17 +567,25 @@ describe('Kleros GTCR adapter', () => { }) describe('Optimistic recipient registry', () => { - const [, deployer, controller, recipient, requester] = provider.getWallets() let registry: Contract + let registryAddress: string - const baseDeposit = UNIT.div(10) // 0.1 ETH - const challengePeriodDuration = BigNumber.from(86400) // Seconds + let deployer: HardhatEthersSigner + let controller: HardhatEthersSigner + let recipient: HardhatEthersSigner + let requester: HardhatEthersSigner + + const baseDeposit = UNIT / 10n // 0.1 ETH + const challengePeriodDuration = 86400 // Seconds enum RequestType { Registration = 0, Removal = 1, } + before(async () => { + ;[, deployer, controller, recipient, requester] = await ethers.getSigners() + }) beforeEach(async () => { registry = await deployContract({ name: 'OptimisticRecipientRegistry', @@ -557,6 +593,7 @@ describe('Optimistic recipient registry', () => { ethers, signer: deployer, }) + registryAddress = await registry.getAddress() }) it('initializes correctly', async () => { @@ -569,13 +606,13 @@ describe('Optimistic recipient registry', () => { }) it('changes base deposit', async () => { - const newBaseDeposit = baseDeposit.mul(2) + const newBaseDeposit = baseDeposit * 2n await registry.setBaseDeposit(newBaseDeposit) expect(await registry.baseDeposit()).to.equal(newBaseDeposit) }) it('changes challenge period duration', async () => { - const newChallengePeriodDuration = challengePeriodDuration.mul(2) + const newChallengePeriodDuration = challengePeriodDuration * 2 await registry.setChallengePeriodDuration(newChallengePeriodDuration) expect(await registry.challengePeriodDuration()).to.equal( newChallengePeriodDuration @@ -589,20 +626,22 @@ describe('Optimistic recipient registry', () => { let recipientId: string beforeEach(async () => { - await registry.connect(controller).setMaxRecipients(MAX_RECIPIENTS) + await (registry.connect(controller) as Contract).setMaxRecipients( + MAX_RECIPIENTS + ) recipientAddress = recipient.address metadata = JSON.stringify({ name: 'Recipient', description: 'Description', imageHash: 'Ipfs imageHash', }) - recipientId = getRecipientId(registry.address, recipientAddress, metadata) + recipientId = getRecipientId(registryAddress, recipientAddress, metadata) }) it('allows anyone to submit registration request', async () => { - const requestSubmitted = await registry - .connect(requester) - .addRecipient(recipientAddress, metadata, { value: baseDeposit }) + const requestSubmitted = await ( + registry.connect(requester) as Contract + ).addRecipient(recipientAddress, metadata, { value: baseDeposit }) const currentTime = await getCurrentTime() expect(requestSubmitted) .to.emit(registry, 'RequestSubmitted') @@ -613,7 +652,9 @@ describe('Optimistic recipient registry', () => { metadata, currentTime ) - expect(await provider.getBalance(registry.address)).to.equal(baseDeposit) + expect(await ethers.provider.getBalance(registryAddress)).to.equal( + baseDeposit + ) }) it('should not accept zero-address as recipient address', async () => { @@ -638,7 +679,7 @@ describe('Optimistic recipient registry', () => { await registry.addRecipient(recipientAddress, metadata, { value: baseDeposit, }) - await provider.send('evm_increaseTime', [86400]) + await time.increase(86400) await registry.executeRequest(recipientId) await expect( registry.addRecipient(recipientAddress, metadata, { @@ -661,16 +702,18 @@ describe('Optimistic recipient registry', () => { it('should not accept registration request with incorrect deposit size', async () => { await expect( registry.addRecipient(recipientAddress, metadata, { - value: baseDeposit.div(2), + value: baseDeposit / 2n, }) ).to.be.revertedWith('RecipientRegistry: Incorrect deposit amount') }) it('allows owner to challenge registration request', async () => { - await registry - .connect(requester) - .addRecipient(recipientAddress, metadata, { value: baseDeposit }) - const requesterBalanceBefore = await provider.getBalance( + await (registry.connect(requester) as Contract).addRecipient( + recipientAddress, + metadata, + { value: baseDeposit } + ) + const requesterBalanceBefore = await ethers.provider.getBalance( requester.address ) const requestRejected = await registry.challengeRequest( @@ -681,8 +724,10 @@ describe('Optimistic recipient registry', () => { expect(requestRejected) .to.emit(registry, 'RequestResolved') .withArgs(recipientId, RequestType.Registration, true, 0, currentTime) - const requesterBalanceAfter = await provider.getBalance(requester.address) - expect(requesterBalanceBefore.add(baseDeposit)).to.equal( + const requesterBalanceAfter = await ethers.provider.getBalance( + requester.address + ) + expect(requesterBalanceBefore + baseDeposit).to.equal( requesterBalanceAfter ) }) @@ -691,33 +736,38 @@ describe('Optimistic recipient registry', () => { await registry.addRecipient(recipientAddress, metadata, { value: baseDeposit, }) - const controllerBalanceBefore = await provider.getBalance( + const controllerBalanceBefore = await ethers.provider.getBalance( controller.address ) await registry.challengeRequest(recipientId, controller.address) - const controllerBalanceAfter = await provider.getBalance( + const controllerBalanceAfter = await ethers.provider.getBalance( controller.address ) - expect(controllerBalanceBefore.add(baseDeposit)).to.equal( + expect(controllerBalanceBefore + baseDeposit).to.equal( controllerBalanceAfter ) }) it('allows only owner to challenge requests', async () => { - await registry - .connect(requester) - .addRecipient(recipientAddress, metadata, { value: baseDeposit }) + await (registry.connect(requester) as Contract).addRecipient( + recipientAddress, + metadata, + { value: baseDeposit } + ) await expect( - registry - .connect(requester) - .challengeRequest(recipientId, requester.address) + (registry.connect(requester) as Contract).challengeRequest( + recipientId, + requester.address + ) ).to.be.revertedWith('Ownable: caller is not the owner') }) it('should not allow to challenge resolved request', async () => { - await registry - .connect(requester) - .addRecipient(recipientAddress, metadata, { value: baseDeposit }) + await (registry.connect(requester) as Contract).addRecipient( + recipientAddress, + metadata, + { value: baseDeposit } + ) await registry.challengeRequest(recipientId, requester.address) await expect( registry.challengeRequest(recipientId, requester.address) @@ -725,17 +775,19 @@ describe('Optimistic recipient registry', () => { }) it('allows anyone to execute unchallenged registration request', async () => { - await registry - .connect(requester) - .addRecipient(recipientAddress, metadata, { value: baseDeposit }) - await provider.send('evm_increaseTime', [86400]) + await (registry.connect(requester) as Contract).addRecipient( + recipientAddress, + metadata, + { value: baseDeposit } + ) + await time.increase(86400) - const requesterBalanceBefore = await provider.getBalance( + const requesterBalanceBefore = await ethers.provider.getBalance( requester.address ) - const requestExecuted = await registry - .connect(requester) - .executeRequest(recipientId) + const requestExecuted = await ( + registry.connect(requester) as Contract + ).executeRequest(recipientId) const currentTime = await getCurrentTime() expect(requestExecuted) .to.emit(registry, 'RequestResolved') @@ -747,8 +799,10 @@ describe('Optimistic recipient registry', () => { currentTime ) const txFee = await getTxFee(requestExecuted) - const requesterBalanceAfter = await provider.getBalance(requester.address) - expect(requesterBalanceBefore.sub(txFee).add(baseDeposit)).to.equal( + const requesterBalanceAfter = await ethers.provider.getBalance( + requester.address + ) + expect(requesterBalanceBefore - txFee + baseDeposit).to.equal( requesterBalanceAfter ) @@ -773,7 +827,7 @@ describe('Optimistic recipient registry', () => { }) await expect( - registry.connect(requester).executeRequest(recipientId) + (registry.connect(requester) as Contract).executeRequest(recipientId) ).to.be.revertedWith('RecipientRegistry: Challenge period is not over') }) @@ -783,30 +837,34 @@ describe('Optimistic recipient registry', () => { }) let recipientCount = await registry.getRecipientCount() - expect(recipientCount.toNumber()).to.equal(0) + expect(Number(recipientCount)).to.equal(0) await registry.executeRequest(recipientId) recipientCount = await registry.getRecipientCount() - expect(recipientCount.toNumber()).to.equal(1) + expect(Number(recipientCount)).to.equal(1) }) it('should remember initial deposit amount during registration', async () => { - await registry - .connect(requester) - .addRecipient(recipientAddress, metadata, { value: baseDeposit }) - await registry.setBaseDeposit(baseDeposit.mul(2)) - await provider.send('evm_increaseTime', [86400]) + await (registry.connect(requester) as Contract).addRecipient( + recipientAddress, + metadata, + { value: baseDeposit } + ) + await registry.setBaseDeposit(baseDeposit * 2n) + await time.increase(86400) - const requesterBalanceBefore = await provider.getBalance( + const requesterBalanceBefore = await ethers.provider.getBalance( requester.address ) - const requestExecuted = await registry - .connect(requester) - .executeRequest(recipientId) + const requestExecuted = await ( + registry.connect(requester) as Contract + ).executeRequest(recipientId) const txFee = await getTxFee(requestExecuted) - const requesterBalanceAfter = await provider.getBalance(requester.address) - expect(requesterBalanceBefore.sub(txFee).add(baseDeposit)).to.equal( + const requesterBalanceAfter = await ethers.provider.getBalance( + requester.address + ) + expect(requesterBalanceBefore - txFee + baseDeposit).to.equal( requesterBalanceAfter ) }) @@ -822,7 +880,7 @@ describe('Optimistic recipient registry', () => { }) recipientAddress = `0x000000000000000000000000000000000000${recipientName}` recipientId = getRecipientId( - registry.address, + registryAddress, recipientAddress, metadata ) @@ -830,13 +888,13 @@ describe('Optimistic recipient registry', () => { await registry.addRecipient(recipientAddress, metadata, { value: baseDeposit, }) - await provider.send('evm_increaseTime', [86400]) + await time.increase(86400) await registry.executeRequest(recipientId) } else { await registry.addRecipient(recipientAddress, metadata, { value: baseDeposit, }) - await provider.send('evm_increaseTime', [86400]) + await time.increase(86400) await expect(registry.executeRequest(recipientId)).to.be.revertedWith( 'RecipientRegistry: Recipient limit reached' ) @@ -848,7 +906,7 @@ describe('Optimistic recipient registry', () => { await registry.addRecipient(recipientAddress, metadata, { value: baseDeposit, }) - await provider.send('evm_increaseTime', [86400]) + await time.increase(86400) await registry.executeRequest(recipientId) const requestSubmitted = await registry.removeRecipient(recipientId, { @@ -872,7 +930,7 @@ describe('Optimistic recipient registry', () => { }) await registry.executeRequest(recipientId) - const registryAsRequester = registry.connect(requester) + const registryAsRequester = registry.connect(requester) as Contract await registryAsRequester.removeRecipient(recipientId, { value: baseDeposit, }) @@ -892,12 +950,14 @@ describe('Optimistic recipient registry', () => { await registry.addRecipient(recipientAddress, metadata, { value: baseDeposit, }) - await provider.send('evm_increaseTime', [86400]) + await time.increase(86400) await registry.executeRequest(recipientId) await registry.removeRecipient(recipientId, { value: baseDeposit }) - await provider.send('evm_increaseTime', [86400]) - await registry.connect(requester).executeRequest(recipientId) + await time.increase(86400) + await (registry.connect(requester) as Contract).executeRequest( + recipientId + ) await expect(registry.removeRecipient(recipientId)).to.be.revertedWith( 'RecipientRegistry: Recipient already removed' @@ -908,7 +968,7 @@ describe('Optimistic recipient registry', () => { await registry.addRecipient(recipientAddress, metadata, { value: baseDeposit, }) - await provider.send('evm_increaseTime', [86400]) + await time.increase(86400) await registry.executeRequest(recipientId) await registry.removeRecipient(recipientId, { value: baseDeposit }) @@ -921,7 +981,7 @@ describe('Optimistic recipient registry', () => { await registry.addRecipient(recipientAddress, metadata, { value: baseDeposit, }) - await provider.send('evm_increaseTime', [86400]) + await time.increase(86400) await registry.executeRequest(recipientId) await registry.removeRecipient(recipientId, { value: baseDeposit }) @@ -948,15 +1008,15 @@ describe('Optimistic recipient registry', () => { await registry.addRecipient(recipientAddress, metadata, { value: baseDeposit, }) - await provider.send('evm_increaseTime', [86400]) + await time.increase(86400) await registry.executeRequest(recipientId) await registry.removeRecipient(recipientId, { value: baseDeposit }) - await provider.send('evm_increaseTime', [86400]) + await time.increase(86400) - const requestExecuted = await registry - .connect(requester) - .executeRequest(recipientId) + const requestExecuted = await ( + registry.connect(requester) as Contract + ).executeRequest(recipientId) const currentTime = await getCurrentTime() expect(requestExecuted) .to.emit(registry, 'RequestResolved') diff --git a/contracts/tests/round.ts b/contracts/tests/round.ts index 1628aee99..08176f998 100644 --- a/contracts/tests/round.ts +++ b/contracts/tests/round.ts @@ -1,11 +1,19 @@ -import { ethers, waffle, artifacts, config } from 'hardhat' -import { use, expect } from 'chai' -import { solidity } from 'ethereum-waffle' -import { deployMockContract } from '@ethereum-waffle/mock-contract' -import { Contract, BigNumber, ContractTransaction } from 'ethers' -import { defaultAbiCoder } from '@ethersproject/abi' -import { genRandomSalt } from '@clrfund/maci-crypto' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' +import { MockContract } from '@clrfund/waffle-mock-contract' +import { + Contract, + AbiCoder, + parseEther, + sha256, + randomBytes, + hexlify, + toNumber, +} from 'ethers' +import { genRandomSalt } from 'maci-crypto' import { Keypair } from '@clrfund/common' +import { time } from '@nomicfoundation/hardhat-network-helpers' import { ZERO_ADDRESS, @@ -14,235 +22,160 @@ import { ALPHA_PRECISION, } from '../utils/constants' import { getEventArg, getGasUsage } from '../utils/contracts' -import { - deployContract, - deployPoseidonLibraries, - deployMaciFactory, -} from '../utils/deployment' import { bnSqrt, createMessage, addTallyResultsBatch, getRecipientClaimData, - getRecipientTallyResultsBatch, } from '../utils/maci' -import { sha256 } from 'ethers/lib/utils' -import { MaciParameters } from '../utils/maciParameters' -import { DEFAULT_CIRCUIT } from '../utils/circuits' - -use(solidity) +import { deployTestFundingRound } from '../utils/testutils' // ethStaker test vectors for Quadratic Funding with alpha import smallTallyTestData from './data/testTallySmall.json' -const totalSpent = BigNumber.from( - smallTallyTestData.totalSpentVoiceCredits.spent -) -const budget = BigNumber.from(totalSpent).mul(VOICE_CREDIT_FACTOR).mul(2) +import { FundingRound } from '../typechain-types' + +const newResultCommitment = hexlify(randomBytes(32)) +const perVOSpentVoiceCreditsHash = hexlify(randomBytes(32)) +const totalSpent = BigInt(smallTallyTestData.totalSpentVoiceCredits.spent) +const budget = BigInt(totalSpent) * VOICE_CREDIT_FACTOR * 2n const totalQuadraticVotes = smallTallyTestData.results.tally.reduce( (total, tally) => { - return BigNumber.from(tally).pow(2).add(total) + return BigInt(tally) ** 2n + total }, - BigNumber.from(0) + BigInt(0) ) -const matchingPoolSize = budget.sub(totalSpent.mul(VOICE_CREDIT_FACTOR)) - -const expectedAlpha = matchingPoolSize - .mul(ALPHA_PRECISION) - .div(totalQuadraticVotes.sub(totalSpent)) - .div(VOICE_CREDIT_FACTOR) - -function calcAllocationAmount(tally: string, voiceCredit: string): BigNumber { - const quadratic = expectedAlpha - .mul(VOICE_CREDIT_FACTOR) - .mul(BigNumber.from(tally).pow(2)) - const linear = ALPHA_PRECISION.sub(expectedAlpha).mul( - VOICE_CREDIT_FACTOR.mul(voiceCredit) - ) - const allocation = quadratic.add(linear) - return allocation.div(ALPHA_PRECISION) -} +const matchingPoolSize = budget - totalSpent * VOICE_CREDIT_FACTOR + +const expectedAlpha = + (matchingPoolSize * ALPHA_PRECISION) / + (totalQuadraticVotes - totalSpent) / + VOICE_CREDIT_FACTOR -async function finalizeRound( - fundingRound: Contract, - totalSpent: string | BigNumber, - totalSpentSalt: string -): Promise { - // generate random 32 bytes for test only - const newResultCommitment = genRandomSalt().toString() - const perVOVoiceCreditsCommitment = genRandomSalt().toString() - return fundingRound.finalize( - totalSpent, - totalSpentSalt, - newResultCommitment, - perVOVoiceCreditsCommitment - ) +const abiCoder = new AbiCoder() + +function calcAllocationAmount(tally: string, voiceCredit: string): bigint { + const quadratic = expectedAlpha * VOICE_CREDIT_FACTOR * BigInt(tally) ** 2n + + const linear = + (ALPHA_PRECISION - expectedAlpha) * + (VOICE_CREDIT_FACTOR * BigInt(voiceCredit)) + + const allocation = quadratic + linear + return allocation / ALPHA_PRECISION } describe('Funding Round', () => { - const provider = waffle.provider - const [, deployer, coordinator, contributor, anotherContributor, recipient] = - provider.getWallets() - const coordinatorPubKey = new Keypair().pubKey + const roundDuration = 86400 * 7 const userKeypair = new Keypair() - const contributionAmount = UNIT.mul(10) + const userPubKey = userKeypair.pubKey.asContractParam() + const contributionAmount = UNIT * BigInt(10) const tallyHash = 'test' - const tallyTreeDepth = 2 - const pollDuration = 86400 * 7 - const halfPollDuration = Math.floor(pollDuration / 2) - const numSignUps = 4 - const tallyBatchSize = 2 - const tallyBatchNum = 2 + let tallyTreeDepth: number let token: Contract - let userRegistry: Contract - let recipientRegistry: Contract + let tokenAsContributor: Contract + let userRegistry: MockContract + let recipientRegistry: MockContract + let tally: MockContract let fundingRound: Contract + let fundingRoundAsCoordinator: FundingRound + let fundingRoundAsContributor: FundingRound let maci: Contract - let pollId: bigint let poll: Contract - let tally: Contract + let pollId: bigint - async function deployMaciMock(): Promise { - const MACIArtifact = await artifacts.readArtifact('MACI') - const maci = await deployMockContract(deployer, MACIArtifact.abi) - await maci.mock.signUp.returns() - return maci - } + let deployer: HardhatEthersSigner + let coordinator: HardhatEthersSigner + let contributor: HardhatEthersSigner + let anotherContributor: HardhatEthersSigner + let recipient: HardhatEthersSigner - async function deployMockContractByName(name: string): Promise { - const artifact = await artifacts.readArtifact(name) - const contract = await deployMockContract(deployer, artifact.abi) - return contract - } + before(async () => { + ;[, deployer, coordinator, contributor, anotherContributor, recipient] = + await ethers.getSigners() + }) beforeEach(async () => { - const tokenInitialSupply = UNIT.mul(1000000) - const Token = await ethers.getContractFactory('AnyOldERC20Token', deployer) - token = await Token.deploy(tokenInitialSupply.add(budget)) - await token.transfer(contributor.address, tokenInitialSupply.div(4)) - await token.transfer(anotherContributor.address, tokenInitialSupply.div(4)) - await token.transfer(coordinator.address, tokenInitialSupply.div(4)) - - const IUserRegistryArtifact = await artifacts.readArtifact('IUserRegistry') - userRegistry = await deployMockContract(deployer, IUserRegistryArtifact.abi) - await userRegistry.mock.isVerifiedUser.returns(true) - - const IRecipientRegistryArtifact = - await artifacts.readArtifact('IRecipientRegistry') - recipientRegistry = await deployMockContract( - deployer, - IRecipientRegistryArtifact.abi - ) - - const libraries = await deployPoseidonLibraries({ - artifactsPath: config.paths.artifacts, - ethers, - signer: deployer, - }) - fundingRound = await deployContract({ - name: 'FundingRound', - libraries, - contractArgs: [ - token.address, - userRegistry.address, - recipientRegistry.address, - coordinator.address, - ], - ethers, - signer: deployer, - }) - const maciFactory = await deployMaciFactory({ - ethers, - signer: deployer, - libraries, - }) - - const maciParams = MaciParameters.mock(DEFAULT_CIRCUIT) - await maciFactory.setMaciParameters(...maciParams.asContractParam()) - - const maciDeployed = await maciFactory.deployMaci( - fundingRound.address, - fundingRound.address, - token.address, - pollDuration, + const tokenInitialSupply = UNIT * BigInt(1000000) + const deployed = await deployTestFundingRound( + tokenInitialSupply + budget, coordinator.address, - coordinatorPubKey.asContractParam() - ) - - const maciAddress = await getEventArg( - maciDeployed, - maciFactory, - 'MaciDeployed', - '_maci' + coordinatorPubKey, + roundDuration, + deployer ) - + token = deployed.token + fundingRound = deployed.fundingRound + userRegistry = deployed.mockUserRegistry + recipientRegistry = deployed.mockRecipientRegistry + tally = deployed.mockTally + const mockVerifier = deployed.mockVerifier + + // make the verifier to alwasy returns true + await mockVerifier.mock.verify.returns(true) + await userRegistry.mock.isVerifiedUser.returns(true) + await tally.mock.tallyBatchNum.returns(1) + await tally.mock.verifyTallyResult.returns(true) + await tally.mock.verifySpentVoiceCredits.returns(true) + + tokenAsContributor = token.connect(contributor) as Contract + fundingRoundAsCoordinator = fundingRound.connect( + coordinator + ) as FundingRound + fundingRoundAsContributor = fundingRound.connect( + contributor + ) as FundingRound + + await token.transfer(contributor.address, tokenInitialSupply / 4n) + await token.transfer(anotherContributor.address, tokenInitialSupply / 4n) + await token.transfer(coordinator.address, tokenInitialSupply / 4n) + + const maciAddress = await fundingRound.maci() maci = await ethers.getContractAt('MACI', maciAddress) - - pollId = await getEventArg(maciDeployed, maci, 'DeployPoll', '_pollId') - const pollAddress = await getEventArg( - maciDeployed, - maci, - 'DeployPoll', - '_pollAddr' - ) + const pollAddress = await fundingRound.poll() poll = await ethers.getContractAt('Poll', pollAddress) + pollId = await fundingRound.pollId() + + const treeDepths = await poll.treeDepths() + tallyTreeDepth = toNumber(treeDepths.voteOptionTreeDepth) }) it('initializes funding round correctly', async () => { expect(await fundingRound.owner()).to.equal(deployer.address) - expect(await fundingRound.nativeToken()).to.equal(token.address) + expect(await fundingRound.nativeToken()).to.equal(token.target) expect(await fundingRound.voiceCreditFactor()).to.equal(VOICE_CREDIT_FACTOR) expect(await fundingRound.matchingPoolSize()).to.equal(0) expect(await fundingRound.totalSpent()).to.equal(0) - expect(await fundingRound.totalVotes()).to.equal(0) - expect(await fundingRound.userRegistry()).to.equal(userRegistry.address) + expect(await fundingRound.userRegistry()).to.equal(userRegistry.target) expect(await fundingRound.recipientRegistry()).to.equal( - recipientRegistry.address + recipientRegistry.target ) expect(await fundingRound.isFinalized()).to.equal(false) expect(await fundingRound.isCancelled()).to.equal(false) expect(await fundingRound.coordinator()).to.equal(coordinator.address) - expect(await fundingRound.maci()).to.equal(ZERO_ADDRESS) - }) - - it('allows owner to set MACI address', async () => { - await fundingRound.setMaci(maci.address) - expect(await fundingRound.maci()).to.equal(maci.address) - }) - - it('allows to set MACI address only once', async () => { - await fundingRound.setMaci(maci.address) - await expect(fundingRound.setMaci(maci.address)).to.be.revertedWith( - 'MaciAlreadySet' - ) - }) - - it('allows only owner to set MACI address', async () => { - const fundingRoundAsCoordinator = fundingRound.connect(coordinator) - await expect( - fundingRoundAsCoordinator.setMaci(maci.address) - ).to.be.revertedWith('Ownable: caller is not the owner') + expect(await fundingRound.maci()).to.be.properAddress }) describe('accepting contributions', () => { const userPubKey = userKeypair.pubKey.asContractParam() - const encodedContributorAddress = defaultAbiCoder.encode( - ['address'], - [contributor.address] - ) + let encodedContributorAddress: string + let tokenAsContributor: Contract let fundingRoundAsContributor: Contract beforeEach(async () => { - tokenAsContributor = token.connect(contributor) - fundingRoundAsContributor = fundingRound.connect(contributor) + tokenAsContributor = token.connect(contributor) as Contract + fundingRoundAsContributor = fundingRound.connect(contributor) as Contract + encodedContributorAddress = abiCoder.encode( + ['address'], + [contributor.address] + ) }) it('accepts contributions from everyone', async () => { - await fundingRound.setMaci(maci.address) - await tokenAsContributor.approve(fundingRound.address, contributionAmount) - const expectedVoiceCredits = contributionAmount.div(VOICE_CREDIT_FACTOR) + await tokenAsContributor.approve(fundingRound.target, contributionAmount) + const expectedVoiceCredits = contributionAmount / VOICE_CREDIT_FACTOR await expect( fundingRoundAsContributor.contribute(userPubKey, contributionAmount) ) @@ -251,131 +184,113 @@ describe('Funding Round', () => { .to.emit(maci, 'SignUp') // We use [] to skip argument matching, otherwise it will fail // Possibly related: https://github.com/EthWorks/Waffle/issues/245 - //.withArgs([], 1, expectedVoiceCredits, []) - expect(await token.balanceOf(fundingRound.address)).to.equal( + //.withArgs([], 1, expectedVoiceCredits) + + expect(await token.balanceOf(fundingRound.target)).to.equal( contributionAmount ) expect(await fundingRound.contributorCount()).to.equal(1) expect( await fundingRound.getVoiceCredits( - fundingRound.address, + fundingRound.target, encodedContributorAddress ) ).to.equal(expectedVoiceCredits) }) - it('rejects contributions if MACI has not been linked to a round', async () => { - await tokenAsContributor.approve(fundingRound.address, contributionAmount) - await expect( - fundingRoundAsContributor.contribute(userPubKey, contributionAmount) - ).to.be.revertedWith('MaciNotSet') - }) - it('limits the number of contributors', async () => { // TODO: add test later }) it('rejects contributions if funding round has been finalized', async () => { - await fundingRound.setMaci(maci.address) await fundingRound.cancel() - await tokenAsContributor.approve(fundingRound.address, contributionAmount) + await tokenAsContributor.approve(fundingRound.target, contributionAmount) await expect( fundingRoundAsContributor.contribute(userPubKey, contributionAmount) - ).to.be.revertedWith('AlreadyFinalized') + ).to.be.revertedWithCustomError(fundingRound, 'RoundAlreadyFinalized') }) it('rejects contributions with zero amount', async () => { - await fundingRound.setMaci(maci.address) - await tokenAsContributor.approve(fundingRound.address, contributionAmount) + await tokenAsContributor.approve(fundingRound.target, contributionAmount) await expect( fundingRoundAsContributor.contribute(userPubKey, 0) - ).to.be.revertedWith('ContributionAmountIsZero') + ).to.be.revertedWithCustomError(fundingRound, 'ContributionAmountIsZero') }) it('rejects contributions that are too large', async () => { - await fundingRound.setMaci(maci.address) - const contributionAmount = UNIT.mul(10001) - await tokenAsContributor.approve(fundingRound.address, contributionAmount) + const contributionAmount = UNIT * BigInt(10001) + await tokenAsContributor.approve(fundingRound.target, contributionAmount) await expect( fundingRoundAsContributor.contribute(userPubKey, contributionAmount) - ).to.be.revertedWith('ContributionAmountTooLarge') + ).to.be.revertedWithCustomError( + fundingRound, + 'ContributionAmountTooLarge' + ) }) it('allows to contribute only once per round', async () => { - await fundingRound.setMaci(maci.address) await tokenAsContributor.approve( - fundingRound.address, - contributionAmount.mul(2) + fundingRound.target, + contributionAmount * BigInt(2) ) await fundingRoundAsContributor.contribute(userPubKey, contributionAmount) await expect( fundingRoundAsContributor.contribute(userPubKey, contributionAmount) - ).to.be.revertedWith('AlreadyContributed') + ).to.be.revertedWithCustomError(fundingRound, 'AlreadyContributed') }) it('requires approval', async () => { - await fundingRound.setMaci(maci.address) await expect( fundingRoundAsContributor.contribute(userPubKey, contributionAmount) ).to.be.revertedWith('ERC20: insufficient allowance') }) it('rejects contributions from unverified users', async () => { - await fundingRound.setMaci(maci.address) - await tokenAsContributor.approve(fundingRound.address, contributionAmount) + await tokenAsContributor.approve(fundingRound.target, contributionAmount) await userRegistry.mock.isVerifiedUser.returns(false) await expect( fundingRoundAsContributor.contribute(userPubKey, contributionAmount) - ).to.be.revertedWith('UserNotVerified') + ).to.be.revertedWithCustomError(fundingRound, 'UserNotVerified') }) it('should not allow users who have not contributed to sign up directly in MACI', async () => { - await fundingRound.setMaci(maci.address) - const signUpData = defaultAbiCoder.encode( - ['address'], - [contributor.address] - ) + const signUpData = abiCoder.encode(['address'], [contributor.address]) await expect( maci.signUp(userPubKey, signUpData, encodedContributorAddress) - ).to.be.revertedWith('UserHasNotContributed') + ).to.be.revertedWithCustomError(fundingRound, 'UserHasNotContributed') }) it('should not allow users who have already signed up to sign up directly in MACI', async () => { - await fundingRound.setMaci(maci.address) - await tokenAsContributor.approve(fundingRound.address, contributionAmount) + await tokenAsContributor.approve(fundingRound.target, contributionAmount) await fundingRoundAsContributor.contribute(userPubKey, contributionAmount) - const signUpData = defaultAbiCoder.encode( - ['address'], - [contributor.address] - ) + const signUpData = abiCoder.encode(['address'], [contributor.address]) await expect( maci.signUp(userPubKey, signUpData, encodedContributorAddress) - ).to.be.revertedWith('UserAlreadyRegistered') + ).to.be.revertedWithCustomError(fundingRound, 'UserAlreadyRegistered') }) it('should not return the amount of voice credits for user who has not contributed', async () => { await expect( fundingRound.getVoiceCredits( - fundingRound.address, + fundingRound.target, encodedContributorAddress ) - ).to.be.revertedWith('NoVoiceCredits') + ).to.be.revertedWithCustomError(fundingRound, 'NoVoiceCredits') }) }) describe('voting', () => { - const singleVote = UNIT.mul(4) + const singleVote = UNIT * BigInt(4) let fundingRoundAsContributor: Contract let userStateIndex: number let recipientIndex = 1 let nonce = 1 beforeEach(async () => { - await fundingRound.setMaci(maci.address) - const tokenAsContributor = token.connect(contributor) - await tokenAsContributor.approve(fundingRound.address, contributionAmount) - fundingRoundAsContributor = fundingRound.connect(contributor) + const tokenAsContributor = token.connect(contributor) as Contract + await tokenAsContributor.approve(fundingRound.target, contributionAmount) + fundingRoundAsContributor = fundingRound.connect(contributor) as Contract const contributionTx = await fundingRoundAsContributor.contribute( userKeypair.pubKey.asContractParam(), contributionAmount @@ -399,7 +314,6 @@ describe('Funding Round', () => { nonce, pollId ) - const messagePublished = poll.publishMessage( message.asContractParam(), encPubKey.asContractParam() @@ -421,14 +335,13 @@ describe('Funding Round', () => { nonce, pollId ) - const messagePublished = await poll.publishMessage( + await poll.publishMessage( message.asContractParam(), encPubKey.asContractParam() ) - await expect(messagePublished).to.emit(poll, 'PublishMessage') }) - it('use a seed to generate new key and submit change message', async () => { + it('use a seed to generate new key and submit change change message', async () => { const signature = await contributor.signMessage('hello world') const hash = sha256(signature) const newUserKeypair = Keypair.createFromSeed(hash) @@ -442,11 +355,10 @@ describe('Funding Round', () => { nonce, pollId ) - const messagePublished = await poll.publishMessage( + await poll.publishMessage( message.asContractParam(), encPubKey.asContractParam() ) - await expect(messagePublished).to.emit(poll, 'PublishMessage') }) it('submits an invalid vote', async () => { @@ -461,11 +373,10 @@ describe('Funding Round', () => { nonce, pollId ) - const publishTx1 = await poll.publishMessage( + await poll.publishMessage( message1.asContractParam(), encPubKey1.asContractParam() ) - await expect(publishTx1).to.emit(poll, 'PublishMessage') const [message2, encPubKey2] = createMessage( userStateIndex, userKeypair, @@ -476,11 +387,10 @@ describe('Funding Round', () => { nonce + 1, pollId ) - const publishTx2 = await poll.publishMessage( + await poll.publishMessage( message2.asContractParam(), encPubKey2.asContractParam() ) - await expect(publishTx2).to.emit(poll, 'PublishMessage') }) it('submits a vote for invalid vote option', async () => { @@ -495,16 +405,13 @@ describe('Funding Round', () => { nonce, pollId ) - const messagePublished = await poll.publishMessage( + await poll.publishMessage( message.asContractParam(), encPubKey.asContractParam() ) - await expect(messagePublished).to.emit(poll, 'PublishMessage') }) it('submits a batch of messages', async () => { - await fundingRound.setPoll(pollId) - const messages = [] const encPubKeys = [] const numMessages = 3 @@ -537,94 +444,75 @@ describe('Funding Round', () => { describe('publishing tally hash', () => { it('allows coordinator to publish vote tally hash', async () => { - await expect( - fundingRound.connect(coordinator).publishTallyHash(tallyHash) - ) + await expect(fundingRoundAsCoordinator.publishTallyHash(tallyHash)) .to.emit(fundingRound, 'TallyPublished') .withArgs(tallyHash) expect(await fundingRound.tallyHash()).to.equal(tallyHash) // Should be possible to re-publish - await expect( - fundingRound.connect(coordinator).publishTallyHash('fixed') - ).to.emit(fundingRound, 'TallyPublished') + await expect(fundingRoundAsCoordinator.publishTallyHash('fixed')).to.emit( + fundingRound, + 'TallyPublished' + ) }) it('allows only coordinator to publish tally hash', async () => { - await expect(fundingRound.publishTallyHash(tallyHash)).to.be.revertedWith( - 'NotCoordinator' - ) + await expect( + fundingRound.publishTallyHash(tallyHash) + ).to.be.revertedWithCustomError(fundingRound, 'NotCoordinator') }) it('reverts if round has been finalized', async () => { await fundingRound.cancel() await expect( - fundingRound.connect(coordinator).publishTallyHash(tallyHash) - ).to.be.revertedWith('RoundAlreadyFinalized') + fundingRoundAsCoordinator.publishTallyHash(tallyHash) + ).to.be.revertedWithCustomError(fundingRound, 'RoundAlreadyFinalized') }) it('rejects empty string', async () => { await expect( - fundingRound.connect(coordinator).publishTallyHash('') - ).to.be.revertedWith('EmptyTallyHash') + fundingRoundAsCoordinator.publishTallyHash('') + ).to.be.revertedWithCustomError(fundingRound, 'EmptyTallyHash') }) }) describe('finalizing round', () => { - const matchingPoolSize = UNIT.mul(10000) - const totalContributions = UNIT.mul(1000) - const totalSpent = totalContributions.div(VOICE_CREDIT_FACTOR) + const matchingPoolSize = UNIT * BigInt(10000) + const totalContributions = UNIT * BigInt(1000) + const totalSpent = totalContributions / VOICE_CREDIT_FACTOR const totalSpentSalt = genRandomSalt().toString() const totalVotes = bnSqrt(totalSpent) - const tallyTreeDepth = 2 - - expect(totalVotes.toNumber()).to.equal(10000) + expect(totalVotes).to.equal(BigInt(10000)) beforeEach(async () => { - maci = await deployMaciMock() - poll = await deployMockContractByName('Poll') - tally = await deployMockContractByName('Tally') - pollId = BigInt(0) - await poll.mock.treeDepths.returns(1, 1, 1, tallyTreeDepth) - await maci.mock.getPoll.returns(poll.address) - - // round.isTallied() = tallyBatchSize * tallyBatchNum >= numSignups - await poll.mock.numSignUpsAndMessages.returns(numSignUps, 1) - await poll.mock.batchSizes.returns(1, tallyBatchSize, 1) - await tally.mock.tallyBatchNum.returns(tallyBatchNum) - await tally.mock.verifyTallyResult.returns(true) - await tally.mock.verifySpentVoiceCredits.returns(true) - - // round.isVotingOver() - const deployTime = (await provider.getBlock('latest')).timestamp - await poll.mock.getDeployTimeAndDuration.returns(deployTime, pollDuration) - - await token - .connect(contributor) - .approve(fundingRound.address, totalContributions) + await (token.connect(contributor) as Contract).approve( + fundingRound.target, + totalContributions + ) }) it('allows owner to finalize round', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) - expect(await fundingRound.tallyHash()).to.equal(tallyHash) - await token.transfer(fundingRound.address, matchingPoolSize) + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration) + + await fundingRoundAsCoordinator.publishTallyHash(tallyHash) + await token.transfer(fundingRound.target, matchingPoolSize) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) - - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) expect(await fundingRound.isFinalized()).to.equal(true) expect(await fundingRound.isCancelled()).to.equal(false) expect(await fundingRound.totalSpent()).to.equal(totalSpent) @@ -632,198 +520,221 @@ describe('Funding Round', () => { }) it('allows owner to finalize round when matching pool is empty', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration) + await fundingRoundAsCoordinator.publishTallyHash(tallyHash) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) expect(await fundingRound.totalSpent()).to.equal(totalSpent) - // TODO: how to get totalVotes from maci v1? - //expect(await fundingRound.totalVotes()).to.equal(totalVotes) expect(await fundingRound.matchingPoolSize()).to.equal(0) }) it('counts direct token transfers to funding round as matching pool contributions', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) - await token.transfer(fundingRound.address, matchingPoolSize) - await token - .connect(contributor) - .transfer(fundingRound.address, contributionAmount) + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration) + await fundingRoundAsCoordinator.publishTallyHash(tallyHash) + await token.transfer(fundingRound.target, matchingPoolSize) + await (token.connect(contributor) as Contract).transfer( + fundingRound.target, + contributionAmount + ) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) expect(await fundingRound.matchingPoolSize()).to.equal( - matchingPoolSize.add(contributionAmount) + matchingPoolSize + contributionAmount ) }) it('reverts if round has been finalized already', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) - await token.transfer(fundingRound.address, matchingPoolSize) + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration) + + await fundingRoundAsCoordinator.publishTallyHash(tallyHash) + await token.transfer(fundingRound.target, matchingPoolSize) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) - await expect( - finalizeRound(fundingRound, totalSpent, totalSpentSalt) - ).to.be.revertedWith('RoundAlreadyFinalized') - }) - - it('reverts MACI has not been deployed', async () => { - await provider.send('evm_increaseTime', [pollDuration]) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) await expect( - finalizeRound(fundingRound, totalSpent, totalSpentSalt) - ).to.be.revertedWith('MaciNotSet') + fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) + ).to.be.revertedWithCustomError(fundingRound, 'RoundAlreadyFinalized') }) it('reverts if voting is still in progress', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [halfPollDuration]) + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration / 2) await expect( - finalizeRound(fundingRound, totalSpent, totalSpentSalt) - ).to.be.revertedWith('VotingIsNotOver') + fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) + ).to.be.revertedWithCustomError(fundingRound, 'VotingPeriodNotPassed') }) it('reverts if votes has not been tallied', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) - await poll.mock.numSignUpsAndMessages.returns(numSignUps * 2, 1) + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration) + await tally.mock.tallyBatchNum.returns(0) await expect( - finalizeRound(fundingRound, totalSpent, totalSpentSalt) - ).to.be.revertedWith('VotesNotTallied') + fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) + ).to.be.revertedWithCustomError(fundingRound, 'VotesNotTallied') }) it('reverts if tally hash has not been published', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration) await expect( - finalizeRound(fundingRound, totalSpent, totalSpentSalt) - ).to.be.revertedWith('TallyHashNotPublished') + fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) + ).to.be.revertedWithCustomError(fundingRound, 'TallyHashNotPublished') }) - // TODO: get total votes in maci v1 - it.skip('reverts if total votes is zero', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) - await token.transfer(fundingRound.address, matchingPoolSize) - await maci.mock.totalVotes.returns(0) + it('reverts if total votes (== totalSpent) is zero', async () => { + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration) + await fundingRoundAsCoordinator.publishTallyHash(tallyHash) + await token.transfer(fundingRound.target, matchingPoolSize) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) await expect( - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) - ).to.be.revertedWith('FundingRound: No votes') + fundingRound.finalize( + 0, // totalSpent + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) + ).to.be.revertedWithCustomError(fundingRound, 'NoVotes') }) - it.skip('reverts if total amount of spent voice credits is incorrect', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) + it('reverts if total amount of spent voice credits is incorrect', async () => { + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + + await time.increase(roundDuration) - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) - await token.transfer(fundingRound.address, matchingPoolSize) - await poll.mock.verifySpentVoiceCredits.returns(false) + await fundingRoundAsCoordinator.publishTallyHash(tallyHash) + await token.transfer(fundingRound.target, matchingPoolSize) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) + await tally.mock.verifySpentVoiceCredits.returns(false) await expect( - finalizeRound(fundingRound, totalSpent, totalSpentSalt) - ).to.be.revertedWith('IncorrectSpentVoiceCredits') + fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) + ).to.be.revertedWithCustomError( + fundingRound, + 'IncorrectSpentVoiceCredits' + ) }) it('allows only owner to finalize round', async () => { - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) - await token.transfer(fundingRound.address, matchingPoolSize) - - const fundingRoundAsCoordinator = fundingRound.connect(coordinator) + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration) + + const fundingRoundAsCoordinator = fundingRound.connect( + coordinator + ) as Contract + + await fundingRoundAsCoordinator.publishTallyHash(tallyHash) + await token.transfer(fundingRound.target, matchingPoolSize) + await expect( - finalizeRound(fundingRoundAsCoordinator, totalSpent, totalSpentSalt) + fundingRoundAsCoordinator.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) ).to.be.revertedWith('Ownable: caller is not the owner') }) }) @@ -836,63 +747,54 @@ describe('Funding Round', () => { }) it('reverts if round has been finalized already', async () => { - const matchingPoolSize = UNIT.mul(10000) - const totalContributions = UNIT.mul(1000) - const totalSpent = totalContributions.div(VOICE_CREDIT_FACTOR) + const matchingPoolSize = UNIT * BigInt(10000) + const totalContributions = UNIT * BigInt(1000) + const totalSpent = totalContributions / VOICE_CREDIT_FACTOR const totalSpentSalt = genRandomSalt().toString() - maci = await deployMaciMock() - poll = await deployMockContractByName('Poll') - tally = await deployMockContractByName('Tally') - pollId = BigInt(0) - await tally.mock.verifyTallyResult.returns(true) - await tally.mock.verifySpentVoiceCredits.returns(true) - await poll.mock.treeDepths.returns(1, 2, 3, tallyTreeDepth) - await maci.mock.getPoll.returns(poll.address) - - // round.isTallied() = tallyBatchSize * tallyBatchNum >= numSignups - await poll.mock.numSignUpsAndMessages.returns(numSignUps, 1) - await poll.mock.batchSizes.returns(1, tallyBatchSize, 1) - await tally.mock.tallyBatchNum.returns(tallyBatchNum) - - // round.isVotingOver() - const deployTime = (await provider.getBlock('latest')).timestamp - await poll.mock.getDeployTimeAndDuration.returns(deployTime, pollDuration) - - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - await token - .connect(contributor) - .approve(fundingRound.address, totalContributions) - await fundingRound - .connect(contributor) - .contribute(userKeypair.pubKey.asContractParam(), totalContributions) - await provider.send('evm_increaseTime', [pollDuration]) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) - await token.transfer(fundingRound.address, matchingPoolSize) + await (token.connect(contributor) as Contract).approve( + fundingRound.target, + totalContributions + ) + await (fundingRound.connect(contributor) as Contract).contribute( + userKeypair.pubKey.asContractParam(), + totalContributions + ) + await time.increase(roundDuration) + + await fundingRoundAsCoordinator.publishTallyHash(tallyHash) + await token.transfer(fundingRound.target, matchingPoolSize) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 3 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) - await expect(fundingRound.cancel()).to.be.revertedWith( + await expect(fundingRound.cancel()).to.be.revertedWithCustomError( + fundingRound, 'RoundAlreadyFinalized' ) }) it('reverts if round has been cancelled already', async () => { await fundingRound.cancel() - await expect(fundingRound.cancel()).to.be.revertedWith( + await expect(fundingRound.cancel()).to.be.revertedWithCustomError( + fundingRound, 'RoundAlreadyFinalized' ) }) it('allows only owner to cancel round', async () => { - const fundingRoundAsCoordinator = fundingRound.connect(coordinator) + const fundingRoundAsCoordinator = fundingRound.connect( + coordinator + ) as Contract await expect(fundingRoundAsCoordinator.cancel()).to.be.revertedWith( 'Ownable: caller is not the owner' ) @@ -902,103 +804,104 @@ describe('Funding Round', () => { describe('withdrawing funds', () => { const userPubKey = userKeypair.pubKey.asContractParam() const anotherUserPubKey = userKeypair.pubKey.asContractParam() - const contributionAmount = UNIT.mul(10) + const contributionAmount = UNIT * BigInt(10) let fundingRoundAsContributor: Contract beforeEach(async () => { - fundingRoundAsContributor = fundingRound.connect(contributor) - await fundingRound.setMaci(maci.address) - await token - .connect(contributor) - .approve(fundingRound.address, contributionAmount) - await token - .connect(anotherContributor) - .approve(fundingRound.address, contributionAmount) + fundingRoundAsContributor = fundingRound.connect(contributor) as Contract + await (token.connect(contributor) as Contract).approve( + fundingRound.target, + contributionAmount + ) + await (token.connect(anotherContributor) as Contract).approve( + fundingRound.target, + contributionAmount + ) }) it('allows contributors to withdraw funds', async () => { await fundingRoundAsContributor.contribute(userPubKey, contributionAmount) - await fundingRound - .connect(anotherContributor) - .contribute(anotherUserPubKey, contributionAmount) + await (fundingRound.connect(anotherContributor) as Contract).contribute( + anotherUserPubKey, + contributionAmount + ) await fundingRound.cancel() await expect(fundingRoundAsContributor.withdrawContribution()) .to.emit(fundingRound, 'ContributionWithdrawn') .withArgs(contributor.address) - await fundingRound.connect(anotherContributor).withdrawContribution() - expect(await token.balanceOf(fundingRound.address)).to.equal(0) + await ( + fundingRound.connect(anotherContributor) as Contract + ).withdrawContribution() + expect(await token.balanceOf(fundingRound.target)).to.equal(0) }) it('disallows withdrawal if round is not cancelled', async () => { await fundingRoundAsContributor.contribute(userPubKey, contributionAmount) await expect( fundingRoundAsContributor.withdrawContribution() - ).to.be.revertedWith('RoundNotCancelled') + ).to.be.revertedWithCustomError(fundingRound, 'RoundNotCancelled') }) it('reverts if user did not contribute to the round', async () => { await fundingRound.cancel() await expect( fundingRoundAsContributor.withdrawContribution() - ).to.be.revertedWith('NothingToWithdraw') + ).to.be.revertedWithCustomError(fundingRound, 'NothingToWithdraw') }) it('reverts if funds are already withdrawn', async () => { - await fundingRound - .connect(contributor) - .contribute(userPubKey, contributionAmount) - await fundingRound - .connect(anotherContributor) - .contribute(anotherUserPubKey, contributionAmount) + const fundingRoundAsContributor = fundingRound.connect( + contributor + ) as Contract + await fundingRoundAsContributor.contribute(userPubKey, contributionAmount) + await (fundingRound.connect(anotherContributor) as Contract).contribute( + anotherUserPubKey, + contributionAmount + ) await fundingRound.cancel() - await fundingRound.connect(contributor).withdrawContribution() + await fundingRoundAsContributor.withdrawContribution() await expect( - fundingRound.connect(contributor).withdrawContribution() - ).to.be.revertedWith('NothingToWithdraw') + fundingRoundAsContributor.withdrawContribution() + ).to.be.revertedWithCustomError(fundingRound, 'NothingToWithdraw') }) it('allows anyone to withdraw multiple contributions', async () => { - await fundingRound - .connect(contributor) - .contribute(userPubKey, contributionAmount) - await fundingRound - .connect(anotherContributor) - .contribute(anotherUserPubKey, contributionAmount) + await (fundingRound.connect(contributor) as Contract).contribute( + userPubKey, + contributionAmount + ) + await (fundingRound.connect(anotherContributor) as Contract).contribute( + anotherUserPubKey, + contributionAmount + ) await fundingRound.cancel() - const tx = await fundingRound - .connect(coordinator) - .withdrawContributions([ - contributor.address, - anotherContributor.address, - ]) + const tx = await ( + fundingRound.connect(coordinator) as Contract + ).withdrawContributions([contributor.address, anotherContributor.address]) await tx.wait() - expect(await token.balanceOf(fundingRound.address)).to.equal(0) + expect(await token.balanceOf(fundingRound.target)).to.equal(0) }) it('allows transaction to complete even if some contributions fail to withdraw', async () => { - await fundingRound - .connect(contributor) - .contribute(userPubKey, contributionAmount) + await (fundingRound.connect(contributor) as Contract).contribute( + userPubKey, + contributionAmount + ) await fundingRound.cancel() - const tx = await fundingRound - .connect(coordinator) - .withdrawContributions([ - contributor.address, - anotherContributor.address, - ]) + const tx = await ( + fundingRound.connect(coordinator) as Contract + ).withdrawContributions([contributor.address, anotherContributor.address]) await tx.wait() - expect(await token.balanceOf(fundingRound.address)).to.equal(0) + expect(await token.balanceOf(fundingRound.target)).to.equal(0) }) }) describe('claiming funds', () => { - const totalVotes = totalQuadraticVotes const recipientIndex = 3 - const { spent: totalSpent, salt: totalSpentSalt } = smallTallyTestData.totalSpentVoiceCredits const contributions = @@ -1007,54 +910,41 @@ describe('Funding Round', () => { const expectedAllocatedAmount = calcAllocationAmount( smallTallyTestData.results.tally[recipientIndex], smallTallyTestData.perVOSpentVoiceCredits.tally[recipientIndex] - ).toString() + ) let fundingRoundAsRecipient: Contract let fundingRoundAsContributor: Contract beforeEach(async () => { - maci = await deployMaciMock() - poll = await deployMockContractByName('Poll') - tally = await deployMockContractByName('Tally') - pollId = BigInt(0) - await poll.mock.treeDepths.returns(1, 1, 1, tallyTreeDepth) - await maci.mock.getPoll.returns(poll.address) - - // round.isTallied() = tallyBatchSize * tallyBatchNum >= numSignups - await poll.mock.numSignUpsAndMessages.returns(numSignUps, 1) - await poll.mock.batchSizes.returns(1, tallyBatchSize, 1) - await tally.mock.verifyPerVOSpentVoiceCredits.returns(true) - await tally.mock.verifyTallyResult.returns(true) - await tally.mock.verifySpentVoiceCredits.returns(true) - await tally.mock.tallyBatchNum.returns(tallyBatchNum) - - // round.isVotingOver() - const deployTime = (await provider.getBlock('latest')).timestamp - await poll.mock.getDeployTimeAndDuration.returns(deployTime, pollDuration) await recipientRegistry.mock.getRecipientAddress.returns( recipient.address ) - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) - const tokenAsContributor = token.connect(contributor) - await tokenAsContributor.approve(fundingRound.address, contributions) - fundingRoundAsContributor = fundingRound.connect(contributor) + const tokenAsContributor = token.connect(contributor) as Contract + await tokenAsContributor.approve(fundingRound.target, contributions) + fundingRoundAsContributor = fundingRound.connect(contributor) as Contract - await provider.send('evm_increaseTime', [pollDuration]) - await fundingRound.connect(coordinator).publishTallyHash(tallyHash) - fundingRoundAsRecipient = fundingRound.connect(recipient) + await time.increase(roundDuration) + await (fundingRound.connect(coordinator) as Contract).publishTallyHash( + tallyHash + ) + fundingRoundAsRecipient = fundingRound.connect(recipient) as Contract + await tally.mock.verifyPerVOSpentVoiceCredits.returns(true) }) it('allows recipient to claim allocated funds', async () => { - await token.transfer(fundingRound.address, budget) + await token.transfer(fundingRound.target, budget) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 3 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) const { results, perVOSpentVoiceCredits } = smallTallyTestData expect( @@ -1069,11 +959,9 @@ describe('Funding Round', () => { tallyTreeDepth, smallTallyTestData ) - await expect(fundingRoundAsRecipient.claimFunds(...claimData)) .to.emit(fundingRound, 'FundsClaimed') .withArgs(recipientIndex, recipient.address, expectedAllocatedAmount) - expect(await token.balanceOf(recipient.address)).to.equal( expectedAllocatedAmount, 'mismatch token balance' @@ -1081,14 +969,19 @@ describe('Funding Round', () => { }) it('allows address different than recipient to claim allocated funds', async () => { - await token.transfer(fundingRound.address, budget) + await token.transfer(fundingRound.target, budget) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 3 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) const claimData = getRecipientClaimData( recipientIndex, @@ -1105,14 +998,19 @@ describe('Funding Round', () => { }) it('allows recipient to claim zero amount', async () => { - await token.transfer(fundingRound.address, budget) + await token.transfer(fundingRound.target, budget) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 3 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) const recipientWithZeroFunds = 2 const claimData = getRecipientClaimData( @@ -1127,20 +1025,23 @@ describe('Funding Round', () => { }) it('allows recipient to claim if the matching pool is empty', async () => { - const totalContributions = - ethers.BigNumber.from(totalSpent).mul(VOICE_CREDIT_FACTOR) - await token.transfer(fundingRound.address, totalContributions) + const totalContributions = BigInt(totalSpent) * VOICE_CREDIT_FACTOR + await token.transfer(fundingRound.target, totalContributions) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 3 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) - const expectedWithoutMatching = ethers.BigNumber.from(contributions) - .mul(VOICE_CREDIT_FACTOR) - .toString() + const expectedWithoutMatching = + BigInt(contributions) * VOICE_CREDIT_FACTOR const claimData = getRecipientClaimData( recipientIndex, @@ -1153,7 +1054,7 @@ describe('Funding Round', () => { }) it('should not allow recipient to claim funds if round has not been finalized', async () => { - await token.transfer(fundingRound.address, budget) + await token.transfer(fundingRound.target, budget) const claimData = getRecipientClaimData( recipientIndex, @@ -1162,11 +1063,11 @@ describe('Funding Round', () => { ) await expect( fundingRoundAsRecipient.claimFunds(...claimData) - ).to.be.revertedWith('RoundNotFinalized') + ).to.be.revertedWithCustomError(fundingRound, 'RoundNotFinalized') }) it('should not allow recipient to claim funds if round has been cancelled', async () => { - await token.transfer(fundingRound.address, budget) + await token.transfer(fundingRound.target, budget) await fundingRound.cancel() const claimData = getRecipientClaimData( @@ -1176,18 +1077,23 @@ describe('Funding Round', () => { ) await expect( fundingRoundAsRecipient.claimFunds(...claimData) - ).to.be.revertedWith('RoundCancelled') + ).to.be.revertedWithCustomError(fundingRound, 'RoundCancelled') }) it('sends funds allocated to unverified recipients back to matching pool', async () => { - await token.transfer(fundingRound.address, budget) + await token.transfer(fundingRound.target, budget) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 3 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) await recipientRegistry.mock.getRecipientAddress.returns(ZERO_ADDRESS) const claimData = getRecipientClaimData( @@ -1200,19 +1106,24 @@ describe('Funding Round', () => { .to.emit(fundingRound, 'FundsClaimed') .withArgs(recipientIndex, deployer.address, expectedAllocatedAmount) expect(await token.balanceOf(deployer.address)).to.equal( - initialDeployerBalance.add(expectedAllocatedAmount) + BigInt(initialDeployerBalance) + expectedAllocatedAmount ) }) it('allows recipient to claim allocated funds only once', async () => { - await token.transfer(fundingRound.address, budget) + await token.transfer(fundingRound.target, budget) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 3 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) const claimData = getRecipientClaimData( recipientIndex, @@ -1222,100 +1133,83 @@ describe('Funding Round', () => { await fundingRoundAsRecipient.claimFunds(...claimData) await expect( fundingRoundAsRecipient.claimFunds(...claimData) - ).to.be.revertedWith('FundsAlreadyClaimed') + ).to.be.revertedWithCustomError(fundingRound, 'FundsAlreadyClaimed') }) it('should verify that tally result is correct', async () => { - await token.transfer(fundingRound.address, budget) - + await token.transfer(fundingRound.target, budget) await tally.mock.verifyTallyResult.returns(false) await expect( addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 3 ) - ).to.be.revertedWith('IncorrectTallyResult') + ).to.be.revertedWithCustomError(fundingRound, 'IncorrectTallyResult') }) - it.skip('should verify that amount of spent voice credits is correct', async () => { - await token.transfer(fundingRound.address, budget) - await tally.mock.verifyPerVOSpentVoiceCredits.returns(false) + it('should verify that amount of spent voice credits is correct', async () => { + await token.transfer(fundingRound.target, budget) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 3 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + await fundingRound.finalize( + totalSpent, + totalSpentSalt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) const claimData = getRecipientClaimData( recipientIndex, tallyTreeDepth, smallTallyTestData ) + + await tally.mock.verifyPerVOSpentVoiceCredits.returns(false) await expect( fundingRoundAsRecipient.claimFunds(...claimData) - ).to.be.revertedWith('IncorrectSpentVoiceCredits') + ).to.be.revertedWithCustomError( + fundingRound, + 'IncorrectPerVOSpentVoiceCredits' + ) }) }) describe('finalizing with alpha', function () { this.timeout(2 * 60 * 1000) const treeDepth = 2 - const totalSpentSalt = genRandomSalt().toString() - beforeEach(async () => { - maci = await deployMaciMock() - poll = await deployMockContractByName('Poll') - tally = await deployMockContractByName('Tally') - pollId = BigInt(0) - await tally.mock.verifyTallyResult.returns(true) - await tally.mock.verifySpentVoiceCredits.returns(true) - await poll.mock.treeDepths.returns(1, 1, 1, treeDepth) - await maci.mock.getPoll.returns(poll.address) - - // round.isTallied() = tallyBatchSize * tallyBatchNum >= numSignups - await poll.mock.numSignUpsAndMessages.returns(numSignUps, 1) - await poll.mock.batchSizes.returns(1, tallyBatchSize, 1) - await tally.mock.verifyPerVOSpentVoiceCredits.returns(true) - await tally.mock.tallyBatchNum.returns(tallyBatchNum) - - // round.isVotingOver() - const deployTime = (await provider.getBlock('latest')).timestamp - await poll.mock.getDeployTimeAndDuration.returns(deployTime, pollDuration) - await recipientRegistry.mock.getRecipientAddress.returns( - recipient.address - ) - - await fundingRound.setMaci(maci.address) - await fundingRound.setPoll(pollId) - await fundingRound.connect(coordinator).setTally(tally.address) await recipientRegistry.mock.getRecipientAddress.returns( recipient.address ) - await token.transfer(fundingRound.address, budget) + await token.transfer(fundingRound.target, budget) - const fundingRoundAsCoordinator = fundingRound.connect(coordinator) + const fundingRoundAsCoordinator = fundingRound.connect( + coordinator + ) as Contract await fundingRoundAsCoordinator.publishTallyHash(tallyHash) - await provider.send('evm_increaseTime', [pollDuration]) + await time.increase(roundDuration) }) it('adds and verifies tally results', async function () { this.timeout(2 * 60 * 1000) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, treeDepth, smallTallyTestData, 5 ) const totalResults = await fundingRound.totalTallyResults() - expect(totalResults.toNumber()).to.eq(25, 'total verified mismatch') + expect(toNumber(totalResults)).to.eq(25, 'total verified mismatch') const totalSquares = await fundingRound.totalVotesSquares() expect(totalSquares.toString()).to.eq( @@ -1327,7 +1221,7 @@ describe('Funding Round', () => { it('calculates alpha correctly', async function () { this.timeout(2 * 60 * 1000) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, treeDepth, smallTallyTestData, 5 @@ -1345,12 +1239,18 @@ describe('Funding Round', () => { it('finalizes successfully', async function () { await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, treeDepth, smallTallyTestData, 3 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + const { spent, salt } = smallTallyTestData.totalSpentVoiceCredits + await fundingRound.finalize( + spent, + salt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) const alpha = await fundingRound.alpha() expect(alpha.toString()).to.eq( @@ -1359,16 +1259,17 @@ describe('Funding Round', () => { ) }) - it('fails to finalize if no project has more than 1 vote', async function () { - const tallyTreeDepth = 1 - await poll.mock.treeDepths.returns(1, 1, 1, tallyTreeDepth) + it('fails to finalize if all projects only have 1 contributor', async function () { const tallyWith1Contributor = { newTallyCommitment: - '0x2a7a1fe8c2773fdba262033741655070ba52fea7c1333049ec87c2c248e600bb', + '0xae3fc926f8347c17f9787eded70bc60e32a175cb46c58c03ffe2f4372cd736', results: { commitment: '0x2f44c97ce649078012fd686eaf996fc6b8d817e11ab574f0d0a0d750ee1ec101', - tally: [0, 200, 200, 0, 0], + tally: [ + 0, 200, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, + ], salt: '0xa1f71f9e48a5f2ec55020051a190f079ca43d66457879972554c3c2e8a07ea0', }, totalSpentVoiceCredits: { @@ -1380,38 +1281,78 @@ describe('Funding Round', () => { perVOSpentVoiceCredits: { commitment: '0x26e6ae35c82006eff6408b713d477307b2da16c7a1ff15fb46c0762ee308e88a', - tally: ['0', '40000', '40000', '0', '0'], - salt: '0x2013aa4e350542684f78adbf3e716c3bcf96e12c64b8e8ef3d962e3568132778', + tally: [ + '0', + '40000', + '40000', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + ], + salt: '0x63c80f2b0319790c19b3b17ecd7b00fc1dc7398198601d0dfb30253306ecb34', }, - salt: '0x63c80f2b0319790c19b3b17ecd7b00fc1dc7398198601d0dfb30253306ecb34', } const batchSize = 3 await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, tallyWith1Contributor, batchSize ) - + const { spent, salt } = smallTallyTestData.totalSpentVoiceCredits await expect( - finalizeRound(fundingRound, totalSpent, totalSpentSalt) - ).to.be.revertedWith('NoProjectHasMoreThanOneVote') + fundingRound.finalize( + spent, + salt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) + ).to.be.revertedWithCustomError( + fundingRound, + 'NoProjectHasMoreThanOneVote' + ) }) it('calculates claim funds correctly', async function () { await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, treeDepth, smallTallyTestData, 20 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + const { spent, salt } = smallTallyTestData.totalSpentVoiceCredits + await fundingRound.finalize( + spent, + salt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) + await tally.mock.verifyPerVOSpentVoiceCredits.returns(true) - const { tally } = smallTallyTestData.results + const { tally: tallyResults } = smallTallyTestData.results const { tally: spents } = smallTallyTestData.perVOSpentVoiceCredits - for (let i = 0; i < tally.length; i++) { - const tallyResult = tally[i] + for (let i = 0; i < tallyResults.length; i++) { + const tallyResult = tallyResults[i] if (tallyResult !== '0') { const amount = await fundingRound.getAllocatedAmount( tallyResult, @@ -1436,23 +1377,19 @@ describe('Funding Round', () => { } }) - it.skip('prevents finalize if tally results not completely received', async function () { - // increase the number of signup to simulate incomplete tallying - await poll.mock.numSignUpsAndMessages.returns(numSignUps * 2, 1) - await addTallyResultsBatch( - fundingRound.connect(coordinator), - tallyTreeDepth, - smallTallyTestData, - tallyBatchSize - ) - + it('prevents finalize if tally results not completely received', async function () { + const { spent, salt } = smallTallyTestData.totalSpentVoiceCredits await expect( - finalizeRound(fundingRound, totalSpent, totalSpentSalt) - ).to.be.revertedWith('FundingRound: Incomplete tally results') + fundingRound.finalize( + spent, + salt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) + ).to.be.revertedWithCustomError(fundingRound, 'IncompleteTallyResults') }) it('allows only coordinator to add tally results', async function () { - const fundingRoundAsContributor = fundingRound.connect(contributor) await expect( addTallyResultsBatch( fundingRoundAsContributor, @@ -1460,11 +1397,10 @@ describe('Funding Round', () => { smallTallyTestData, 5 ) - ).to.be.revertedWith('NotCoordinator') + ).to.be.revertedWithCustomError(fundingRound, 'NotCoordinator') }) it('allows only coordinator to add tally results in batches', async function () { - const fundingRoundAsContributor = fundingRound.connect(contributor) await expect( addTallyResultsBatch( fundingRoundAsContributor, @@ -1472,73 +1408,73 @@ describe('Funding Round', () => { smallTallyTestData, 5 ) - ).to.be.revertedWith('NotCoordinator') + ).to.be.revertedWithCustomError(fundingRound, 'NotCoordinator') }) it('prevents adding tally results if maci has not completed tallying', async function () { - // increase the number of signup to simulate incomplete tallying - await poll.mock.numSignUpsAndMessages.returns(numSignUps * 2, 1) - + await tally.mock.tallyBatchNum.returns(0) await expect( addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) - ).to.be.revertedWith('VotesNotTallied') + ).to.be.revertedWithCustomError(fundingRound, 'VotesNotTallied') }) it('prevents adding batches of tally results if maci has not completed tallying', async function () { - // increase the number of signup to simulate incomplete tallying - await poll.mock.numSignUpsAndMessages.returns(numSignUps * 2, 1) - + await tally.mock.tallyBatchNum.returns(0) await expect( addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) - ).to.be.revertedWith('VotesNotTallied') + ).to.be.revertedWithCustomError(fundingRound, 'VotesNotTallied') }) it('prevent adding more tally results if already finalized', async () => { - //await maci.mock.treeDepths.returns(10, 10, tallyTreeDepth) - await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) - await finalizeRound(fundingRound, totalSpent, totalSpentSalt) + const { spent, salt } = smallTallyTestData.totalSpentVoiceCredits + await fundingRound.finalize( + spent, + salt, + newResultCommitment, + perVOSpentVoiceCreditsHash + ) await expect( addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) - ).to.be.revertedWith('RoundAlreadyFinalized') + ).to.be.revertedWithCustomError(fundingRound, 'RoundAlreadyFinalized') }) it('prevents adding tally results that were already verified', async function () { await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) await expect( addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, 5 ) - ).to.revertedWith('VoteResultsAlreadyVerified') + ).to.revertedWithCustomError(fundingRound, 'VoteResultsAlreadyVerified') }) it('returns correct proccessed count in the callback for processing tally results', async () => { @@ -1548,7 +1484,7 @@ describe('Funding Round', () => { const total = smallTallyTestData.results.tally.length const lastBatch = Math.ceil(total / batchSize) await addTallyResultsBatch( - fundingRound.connect(coordinator), + fundingRoundAsCoordinator, tallyTreeDepth, smallTallyTestData, batchSize, @@ -1568,45 +1504,6 @@ describe('Funding Round', () => { }) }) - describe('getRecipientTallyResultsBatch', () => { - const treeDepth = 5 - const batchSize = 5 - const total = smallTallyTestData.results.tally.length - for (const startIndex of [0, Math.floor(total / 2)]) { - it(`should pass with startIndex ${startIndex}`, () => { - const data = getRecipientTallyResultsBatch( - startIndex, - treeDepth, - smallTallyTestData, - batchSize - ) - expect(data).to.have.lengthOf(3) - expect(data[1]).to.have.lengthOf(5) - }) - } - it(`should pass with startIndex ${total - 1}`, () => { - const data = getRecipientTallyResultsBatch( - total - 1, - treeDepth, - smallTallyTestData, - batchSize - ) - expect(data).to.have.lengthOf(3) - expect(data[1]).to.have.lengthOf(1) - }) - it(`should fail with startIndex ${total}`, () => { - const startIndex = total - expect(() => { - getRecipientTallyResultsBatch( - startIndex, - treeDepth, - smallTallyTestData, - batchSize - ) - }).to.throw('Recipient index out of bound') - }) - }) - describe('Alpha calculation', () => { it('fails alpha calculation if budget less than contributions', async function () { const totalBudget = 99 @@ -1614,16 +1511,19 @@ describe('Funding Round', () => { const totalSpent = 100 await expect( fundingRound.calcAlpha(totalBudget, totalVotesSquares, totalSpent) - ).to.be.revertedWith('InvalidBudget') + ).to.be.revertedWithCustomError(fundingRound, 'InvalidBudget') }) - it('fails alpha calculation if no project has more than 1 vote', async function () { - const totalBudget = ethers.utils.parseEther('200') + it('fails alpha calculation if total votes square less than total spent', async function () { + const totalBudget = parseEther('200') const totalVotesSquares = 88 const totalSpent = 100 await expect( fundingRound.calcAlpha(totalBudget, totalVotesSquares, totalSpent) - ).to.be.revertedWith('NoProjectHasMoreThanOneVote') + ).to.be.revertedWithCustomError( + fundingRound, + 'NoProjectHasMoreThanOneVote' + ) }) }) }) diff --git a/contracts/tests/userRegistry.ts b/contracts/tests/userRegistry.ts index 7721bb744..63d05e76f 100644 --- a/contracts/tests/userRegistry.ts +++ b/contracts/tests/userRegistry.ts @@ -1,19 +1,18 @@ -import { ethers, waffle } from 'hardhat' -import { use, expect } from 'chai' -import { solidity } from 'ethereum-waffle' +import { ethers } from 'hardhat' +import { expect } from 'chai' import { Contract } from 'ethers' import { ZERO_ADDRESS } from '../utils/constants' - -use(solidity) +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' describe('Simple User Registry', () => { - const provider = waffle.provider - const [, deployer, user] = provider.getWallets() - let registry: Contract + let user: HardhatEthersSigner beforeEach(async () => { + let deployer: HardhatEthersSigner + ;[, deployer, user] = await ethers.getSigners() + const SimpleUserRegistry = await ethers.getContractFactory( 'SimpleUserRegistry', deployer @@ -44,7 +43,7 @@ describe('Simple User Registry', () => { }) it('allows only owner to add users', async () => { - const registryAsUser = registry.connect(user) + const registryAsUser = registry.connect(user) as Contract await expect(registryAsUser.addUser(user.address)).to.be.revertedWith( 'Ownable: caller is not the owner' ) @@ -66,7 +65,7 @@ describe('Simple User Registry', () => { it('allows only owner to remove users', async () => { await registry.addUser(user.address) - const registryAsUser = registry.connect(user) + const registryAsUser = registry.connect(user) as Contract await expect(registryAsUser.removeUser(user.address)).to.be.revertedWith( 'Ownable: caller is not the owner' ) diff --git a/contracts/tests/userRegistryBrightId.ts b/contracts/tests/userRegistryBrightId.ts index d92ed49fb..687d0a47f 100644 --- a/contracts/tests/userRegistryBrightId.ts +++ b/contracts/tests/userRegistryBrightId.ts @@ -1,15 +1,18 @@ -import { ethers, waffle } from 'hardhat' -import { use, expect } from 'chai' -import { solidity } from 'ethereum-waffle' -import { Contract, providers, utils } from 'ethers' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { + Wallet, + Contract, + SigningKey, + encodeBytes32String, + solidityPackedKeccak256, +} from 'ethers' import { ZERO_ADDRESS } from '../utils/constants' +import { time } from '@nomicfoundation/hardhat-network-helpers' +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' -use(solidity) - -const verifier = ethers.Wallet.createRandom() -const signingKey = new utils.SigningKey(verifier.privateKey) - -const context = utils.formatBytes32String('clrfund-goerli') +const verifier = Wallet.createRandom() +const context = encodeBytes32String('clrfund-goerli') const verificationHash = '0xc99d46ae8baaa7ed2766cbf34e566de43decc2ff7d8e3da5cb80e72f3b5e20de' @@ -24,26 +27,18 @@ type Verification = { verificationHash: string } -async function getBlockTimestamp( - provider: providers.Provider -): Promise { - const blockNumber = await provider.getBlockNumber() - const block = await provider.getBlock(blockNumber) - return block.timestamp -} - -function generateVerification( +async function generateVerification( appUserId: string, timestamp: number, - anotherSigner?: utils.SigningKey -): Verification { - const message = utils.solidityKeccak256( + anotherSigner?: SigningKey +): Promise { + const message = solidityPackedKeccak256( ['bytes32', 'address', 'bytes32', 'uint256'], [context, appUserId, verificationHash, timestamp] ) - const sig = anotherSigner - ? anotherSigner.signDigest(message) - : signingKey.signDigest(message) + + const signer = anotherSigner ?? verifier.signingKey + const sig = await signer.sign(message) return { appUserId, @@ -65,19 +60,24 @@ function register(registry: Contract, verification: Verification) { ) } -describe('BrightId User Registry', () => { - const provider = waffle.provider - const [, deployer, user] = provider.getWallets() - +describe('BrightId User Registry', async () => { let registry: Contract let sponsor: Contract + let user: HardhatEthersSigner + let sponsorAddress: string + + before(async () => { + ;[, , user] = await ethers.getSigners() + }) beforeEach(async () => { + const [, deployer] = await ethers.getSigners() const BrightIdSponsor = await ethers.getContractFactory( 'BrightIdSponsor', deployer ) sponsor = await BrightIdSponsor.deploy() + sponsorAddress = await sponsor.getAddress() const BrightIdUserRegistry = await ethers.getContractFactory( 'BrightIdUserRegistry', @@ -86,7 +86,7 @@ describe('BrightId User Registry', () => { registry = await BrightIdUserRegistry.deploy( context, verifier.address, - sponsor.address + sponsorAddress ) }) @@ -108,19 +108,22 @@ describe('BrightId User Registry', () => { }) it('allows valid sponsor', async () => { - await expect(registry.setSponsor(sponsor.address)) + await expect(registry.setSponsor(sponsorAddress)) .to.emit(registry, 'SponsorChanged') - .withArgs(sponsor.address) + .withArgs(sponsorAddress) }) describe('registration', () => { let blockTimestamp: number beforeEach(async () => { - blockTimestamp = await getBlockTimestamp(provider) + blockTimestamp = await time.latest() }) it('allows valid verified user to register', async () => { - const verification = generateVerification(user.address, blockTimestamp) + const verification = await generateVerification( + user.address, + blockTimestamp + ) expect(await registry.isVerifiedUser(user.address)).to.equal(false) await expect(register(registry, verification)) .to.emit(registry, 'Registered') @@ -130,7 +133,7 @@ describe('BrightId User Registry', () => { }) it('rejects verifications with 0 timestamp', async () => { - const verification = generateVerification(user.address, 0) + const verification = await generateVerification(user.address, 0) await expect(register(registry, verification)).to.be.revertedWith( 'NEWER VERIFICATION REGISTERED BEFORE' ) @@ -140,8 +143,8 @@ describe('BrightId User Registry', () => { expect(await registry.isVerifiedUser(user.address)).to.equal(false) const oldTime = blockTimestamp const newTime = blockTimestamp + 1 - const oldVerification = generateVerification(user.address, oldTime) - const newVerification = generateVerification(user.address, newTime) + const oldVerification = await generateVerification(user.address, oldTime) + const newVerification = await generateVerification(user.address, newTime) await expect(register(registry, newVerification)) .to.emit(registry, 'Registered') .withArgs(user.address, newVerification.timestamp) @@ -155,17 +158,25 @@ describe('BrightId User Registry', () => { it('rejects invalid verifications', async () => { const timestamp = blockTimestamp - const signer = new utils.SigningKey(user.privateKey) - const verification = generateVerification(user.address, timestamp, signer) + const anotherSigner = Wallet.createRandom() + + const verification = await generateVerification( + user.address, + timestamp, + anotherSigner.signingKey + ) await expect(register(registry, verification)).to.be.revertedWith( 'NOT AUTHORIZED' ) }) it('rejects invalid context', async () => { - const verification = generateVerification(user.address, blockTimestamp) + const verification = await generateVerification( + user.address, + blockTimestamp + ) const tx = await registry.setSettings( - utils.formatBytes32String('invalid'), + encodeBytes32String('invalid'), verifier.address ) await tx.wait() diff --git a/contracts/tests/userRegistryMerkle.ts b/contracts/tests/userRegistryMerkle.ts index 5ee1467eb..87c021a55 100644 --- a/contracts/tests/userRegistryMerkle.ts +++ b/contracts/tests/userRegistryMerkle.ts @@ -1,50 +1,53 @@ -import { ethers, waffle } from 'hardhat' -import { use, expect } from 'chai' -import { solidity } from 'ethereum-waffle' -import { Contract, utils, Wallet } from 'ethers' +import { ethers } from 'hardhat' +import { expect } from 'chai' +import { Contract, Wallet, zeroPadBytes, randomBytes } from 'ethers' import { loadUserMerkleTree, getUserMerkleProof } from '@clrfund/common' - -use(solidity) +import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers' describe('Merkle User Registry', () => { - const provider = waffle.provider - const [, deployer, user1, user2] = provider.getWallets() - - const signers = { [user1.address]: user1, [user2.address]: user2 } - const authorizedUsers = [user1.address, user2.address] let registry: Contract let tree: any + let user1: HardhatEthersSigner + let user2: HardhatEthersSigner + const signers: Record = {} + + before(async () => { + ;[, , user1, user2] = await ethers.getSigners() + signers[user1.address] = user1 + signers[user2.address] = user2 + }) beforeEach(async () => { + const [, deployer] = await ethers.getSigners() const MerkleUserRegistry = await ethers.getContractFactory( 'MerkleUserRegistry', deployer ) registry = await MerkleUserRegistry.deploy() - tree = loadUserMerkleTree(authorizedUsers) + tree = loadUserMerkleTree(Object.keys(signers)) const tx = await registry.setMerkleRoot(tree.root, 'test') await tx.wait() }) it('rejects zero merkle root', async () => { await expect( - registry.setMerkleRoot(utils.hexZeroPad('0x0', 32), 'testzero') + registry.setMerkleRoot(zeroPadBytes('0x00', 32), 'testzero') ).to.be.revertedWith('MerkleUserRegistry: Merkle root is zero') }) it('should not allow non-owner to set the merkle root', async () => { - const registryAsUser = registry.connect(signers[user1.address]) + const registryAsUser = registry.connect(signers[user1.address]) as Contract await expect( - registryAsUser.setMerkleRoot(utils.hexZeroPad('0x1', 32), 'non owner') + registryAsUser.setMerkleRoot(randomBytes(32), 'non owner') ).to.be.revertedWith('Ownable: caller is not the owner') }) describe('registration', () => { it('allows valid verified user to register', async () => { - for (const user of authorizedUsers) { + for (const user of Object.keys(signers)) { const proof = getUserMerkleProof(user, tree) - const registryAsUser = registry.connect(signers[user]) + const registryAsUser = registry.connect(signers[user]) as Contract await expect(registryAsUser.addUser(user, proof)) .to.emit(registryAsUser, 'UserAdded') .withArgs(user, tree.root) @@ -55,7 +58,9 @@ describe('Merkle User Registry', () => { it('rejects unauthorized user', async () => { const user = ethers.Wallet.createRandom() const proof = tree.getProof(0) - const registryAsUser = registry.connect(signers[user1.address]) + const registryAsUser = registry.connect( + signers[user1.address] + ) as Contract await expect( registryAsUser.addUser(user.address, proof) ).to.be.revertedWith('MerkleUserRegistry: User is not authorized') @@ -65,16 +70,16 @@ describe('Merkle User Registry', () => { it('should be able load 10k users', async function () { this.timeout(200000) - const allAuthorizedUsers = Array.from(authorizedUsers) + const allAuthorizedUsers = Object.keys(signers) for (let i = 0; i < 10000; i++) { - const randomWallet = new Wallet(utils.randomBytes(32)) + const randomWallet = Wallet.createRandom() allAuthorizedUsers.push(randomWallet.address) } tree = loadUserMerkleTree(allAuthorizedUsers) const tx = await registry.setMerkleRoot(tree.root, 'test') await tx.wait() - const registryAsUser = registry.connect(user1) + const registryAsUser = registry.connect(user1) as Contract const proof = getUserMerkleProof(user1.address, tree) await expect(registryAsUser.addUser(user1.address, proof)) .to.emit(registryAsUser, 'UserAdded') diff --git a/contracts/tests/userRegistrySnapshot.ts b/contracts/tests/userRegistrySnapshot.ts index 97a4422e2..2426cd048 100644 --- a/contracts/tests/userRegistrySnapshot.ts +++ b/contracts/tests/userRegistrySnapshot.ts @@ -1,12 +1,11 @@ import { ethers } from 'hardhat' -import { use, expect } from 'chai' -import { solidity } from 'ethereum-waffle' +import { expect } from 'chai' import { Contract, ContractTransaction, - providers, - constants, - utils, + InfuraProvider, + ZeroAddress, + ZeroHash, } from 'ethers' import { Block, @@ -16,11 +15,9 @@ import { rlpEncodeProof, } from '@clrfund/common' -use(solidity) - // Accounts from arbitrum-goerli to call eth_getProof as hardhat network // does not support eth_getProof -const provider = new providers.InfuraProvider('arbitrum-goerli') +const provider = new InfuraProvider('arbitrum-goerli') const tokens = [ { @@ -107,7 +104,7 @@ describe('SnapshotUserRegistry', function () { it('Should throw if token address is 0', async function () { await expect( userRegistry.setStorageRoot( - constants.AddressZero, + ZeroAddress, block.hash, block.stateRoot, token.storageSlot, @@ -120,7 +117,7 @@ describe('SnapshotUserRegistry', function () { await expect( userRegistry.setStorageRoot( token.address, - utils.hexZeroPad('0x00', 32), + ZeroHash, block.stateRoot, token.storageSlot, accountProofRlpBytes @@ -133,7 +130,7 @@ describe('SnapshotUserRegistry', function () { userRegistry.setStorageRoot( token.address, block.hash, - utils.hexZeroPad('0x00', 32), + ZeroHash, token.storageSlot, accountProofRlpBytes ) diff --git a/contracts/tsconfig.json b/contracts/tsconfig.json index 224cc64b7..4565eb4ff 100644 --- a/contracts/tsconfig.json +++ b/contracts/tsconfig.json @@ -4,13 +4,10 @@ "module": "commonjs", "strict": true, "esModuleInterop": true, - "outDir": "dist", + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, "resolveJsonModule": true }, - "include": ["./scripts", "./tests", "./cli"], - "files": [ - "./hardhat.config.ts", - "./node_modules/@nomiclabs/hardhat-ethers/src/type-extensions.d.ts", - "./node_modules/@nomiclabs/hardhat-waffle/src/type-extensions.d.ts" - ] + "include": ["./scripts", "./tests", "./cli", "./e2e", "./utils"], + "files": ["./hardhat.config.ts"] } diff --git a/contracts/utils/JSONFile.ts b/contracts/utils/JSONFile.ts index cf1a1eb95..e1d6e4c86 100644 --- a/contracts/utils/JSONFile.ts +++ b/contracts/utils/JSONFile.ts @@ -1,18 +1,26 @@ import fs from 'fs' +/** + * Used by JSON.stringify to convert bigint to string + * @param _key: key of the JSON entry to process + * @param value: value of the JSON entry to process + * @returns formated value + */ +function replacer(_key: string, value: any) { + if (typeof value === 'bigint') { + return value.toString() + } + return value +} + export class JSONFile { /** * Read the content of the JSON file - * * @param path The path of the JSON file * @returns */ static read(path: string) { - try { - return JSON.parse(fs.readFileSync(path).toString()) - } catch { - return {} - } + return JSON.parse(fs.readFileSync(path).toString()) } /** @@ -21,7 +29,22 @@ export class JSONFile { * @param data The new data to add to the JSON content */ static update(path: string, data: any) { - const state = JSONFile.read(path) + let state: any + try { + state = JSONFile.read(path) + } catch { + state = {} + } fs.writeFileSync(path, JSON.stringify({ ...state, ...data }, null, 2)) } + + /** + * Write the data to the JSON file + * @param path The path of the file + * @param data The data to write + */ + static write(path: string, data: any) { + const outputString = JSON.stringify(data, replacer, 2) + fs.writeFileSync(path, outputString + '\n') + } } diff --git a/contracts/utils/RecipientRegistryLogProcessor.ts b/contracts/utils/RecipientRegistryLogProcessor.ts index be07d3a14..615ab1cb0 100644 --- a/contracts/utils/RecipientRegistryLogProcessor.ts +++ b/contracts/utils/RecipientRegistryLogProcessor.ts @@ -1,4 +1,4 @@ -import { Contract, EventFilter, providers, constants, utils } from 'ethers' +import { Contract, EventFilter, Interface, ZeroAddress } from 'ethers' import { ProviderFactory } from './providers/ProviderFactory' import { Project } from './types' import { RecipientState } from './constants' @@ -6,11 +6,15 @@ import { ParserFactory } from './parsers/ParserFactory' import { Log } from './providers/BaseProvider' import { toDate } from './date' import { EVENT_ABIS } from './abi' +import { AbiInfo } from './types' -function getFilter(address: string, abi: string): EventFilter { - const eventInterface = new utils.Interface([abi]) - const events = Object.values(eventInterface.events) - const topic0 = eventInterface.getEventTopic(events[0].name) +function getFilter(address: string, abiInfo: AbiInfo): EventFilter { + const eventInterface = new Interface([abiInfo.abi]) + const event = eventInterface.getEvent(abiInfo.name) + if (!event) { + throw new Error(`Event ${abiInfo.name} not found`) + } + const topic0 = event.topicHash return { address, topics: [topic0] } } @@ -50,11 +54,12 @@ export class RecipientRegistryLogProcessor { // fetch event logs containing project information const lastBlock = endBlock ? endBlock - : await this.registry.provider.getBlockNumber() + : await this.registry.runner?.provider?.getBlockNumber() + const registryAddress = await this.registry.getAddress() console.log( `Fetching event logs from the recipient registry`, - this.registry.address + registryAddress ) const logProvider = ProviderFactory.createProvider({ @@ -66,7 +71,7 @@ export class RecipientRegistryLogProcessor { for (let i = 0; i < EVENT_ABIS.length; i++) { const { add, remove } = EVENT_ABIS[i] - const filter = getFilter(this.registry.address, add.abi) + const filter = getFilter(registryAddress, add) const addLogs = await logProvider.fetchLogs({ filter, startBlock, @@ -75,7 +80,7 @@ export class RecipientRegistryLogProcessor { }) if (addLogs.length > 0) { - const filter = getFilter(this.registry.address, remove.abi) + const filter = getFilter(registryAddress, remove) const removeLogs = await logProvider.fetchLogs({ filter, startBlock, @@ -99,7 +104,7 @@ export class RecipientRegistryLogProcessor { return logs } - async parseLogs(logs: providers.Log[]): Promise> { + async parseLogs(logs: Log[]): Promise> { const recipients: Record = {} for (let i = 0; i < logs.length; i++) { @@ -112,16 +117,18 @@ export class RecipientRegistryLogProcessor { } catch (err) { console.log('failed to parse', (err as Error).message) } - const address = parsed.recipientAddress || constants.AddressZero + const address = parsed.recipientAddress || ZeroAddress const id = parsed.id || '0' const [block, transaction] = await Promise.all([ - this.registry.provider.getBlock(log.blockNumber), - this.registry.provider.getTransactionReceipt(log.transactionHash), + this.registry.runner?.provider?.getBlock(log.blockNumber), + this.registry.runner?.provider?.getTransactionReceipt( + log.transactionHash + ), ]) - const blockTimestamp = toDate(block.timestamp) + const blockTimestamp = toDate(block?.timestamp || 0) const createdAt = parsed.createdAt || blockTimestamp - const requester = transaction.from + const requester = transaction?.from if (!recipients[id]) { recipients[id] = { diff --git a/contracts/utils/abi.ts b/contracts/utils/abi.ts index 7e2f0368c..98101978a 100644 --- a/contracts/utils/abi.ts +++ b/contracts/utils/abi.ts @@ -1,4 +1,4 @@ -import { utils } from 'ethers' +import { Interface } from 'ethers' import { AbiInfo } from './types' type EventAbiEntry = { @@ -16,52 +16,64 @@ export const EVENT_ABIS: EventAbiEntry[] = [ { add: { type: 'RequestSubmitted', + name: 'RequestSubmitted', abi: `event RequestSubmitted(bytes32 indexed _recipientId, uint8 indexed _type, address _recipient, string _metadata, uint256 _timestamp)`, }, remove: { type: 'RequestResolved', + name: 'RequestResolved', abi: `event RequestResolved(bytes32 indexed _recipientId, uint8 indexed _type, bool indexed _rejected, uint256 _recipientIndex, uint256 _timestamp)`, }, }, { add: { type: 'RecipientAdded', + name: 'RecipientAdded', abi: 'event RecipientAdded(bytes32 indexed _recipientId, address _recipient, string _metadata, uint256 _index, uint256 _timestamp)', }, remove: { type: 'RecipientRemoved', + name: 'RecipientRemoved', abi: 'event RecipientRemoved(bytes32 indexed _recipientId, uint256 _timestamp)', }, }, { add: { type: 'RecipientAddedV1', + name: 'RecipientAdded', abi: 'event RecipientAdded(address indexed _recipient, string _metadata, uint256 _index)', }, remove: { type: 'RecipientRemovedV1', + name: 'RecipientRemoved', abi: 'event RecipientRemoved(address indexed _recipient)', }, }, { add: { type: 'KlerosRecipientAdded', + name: 'RecipientAdded', abi: `event RecipientAdded(bytes32 indexed _recipientId, bytes _metadata, uint256 _index)`, }, remove: { type: 'KlerosRecipientRemoved', + name: 'RecipientRemoved', abi: `event RecipientRemoved(bytes32 indexed _recipientId)`, }, }, ] +/** + * Event topic abi + */ export const TOPIC_ABIS: Record = EVENT_ABIS.reduce( (records: Record, addAndRemoveGroup) => { - Object.values(addAndRemoveGroup).forEach(({ type, abi }) => { - const addInterface = new utils.Interface([abi]) - const events = Object.values(addInterface.events) - const topic0 = addInterface.getEventTopic(events[0].name) - records[topic0] = { type, abi } + Object.values(addAndRemoveGroup).forEach(({ type, name, abi }) => { + const addInterface = new Interface([abi]) + const event = addInterface.getEvent(name) + if (event) { + records[event.topicHash] = { type, name, abi } + } }) return records diff --git a/contracts/utils/circuits.ts b/contracts/utils/circuits.ts index 2d1a56691..bb344325f 100644 --- a/contracts/utils/circuits.ts +++ b/contracts/utils/circuits.ts @@ -1,44 +1,104 @@ // custom configuration for MACI parameters // See https://github.com/privacy-scaling-explorations/maci/wiki/Precompiled-v1.1.1 for parameter definition -// TODO: currently the version of MACI used in clrfund only supports circuit 6-8-2-3 because stateTreeDepth = 6 in MACI contract +// NOTE: currently the version of MACI used in clrfund only supports circuit 6-8-2-3 because +// the EmptyBallotRoots.sol published in MACI npm package is hardcoded for stateTreeDepth = 6 +import path from 'path' + +// This should match MACI.TREE_ARITY in the contract const TREE_ARITY = 5 export const DEFAULT_CIRCUIT = 'micro' -export const CIRCUITS: { [name: string]: any } = { +/** + * Information about the circuit + */ +export type CircuitInfo = { + processMessagesZkey: string + processWitness: string + processWasm: string + processDatFile: string + tallyVotesZkey: string + tallyWitness: string + tallyWasm: string + tallyDatFile: string + stateTreeDepth: number + treeDepths: { + messageTreeDepth: number + messageTreeSubDepth: number + voteOptionTreeDepth: number + intStateTreeDepth: number + } + maxValues: { + maxMessages: bigint + maxVoteOptions: bigint + } + messageBatchSize: bigint +} + +export const CIRCUITS: { [name: string]: CircuitInfo } = { micro: { processMessagesZkey: 'processmessages_6-8-2-3_final.zkey', processWitness: 'processMessages_6-8-2-3_test', processWasm: 'processmessages_6-8-2-3.wasm', + processDatFile: 'processMessages_6-8-2-3_test.dat', tallyVotesZkey: 'tallyvotes_6-2-3_final.zkey', tallyWitness: 'tallyVotes_6-2-3_test', tallyWasm: 'tallyvotes_6-2-3.wasm', + tallyDatFile: 'tallyVotes_6-2-3_test.dat', + // 1st param in processmessages_6-8-2-3 + stateTreeDepth: 6, treeDepths: { - // - stateTreeDepth: 6, + // 2nd param in processmessages_6-8-2-3 messageTreeDepth: 8, - // TODO: confirm if messageBatchTreeDepth is the same as _messageTreeSubDepth in TreeDepths. - // see https://github.com/clrfund/maci-v1/blob/b5ea1ed4a10c14dc133f8d61e886120cda240003/cli/ts/deployPoll.ts#L153 - // TODO: is messageBatchTreeDepth == intStateTreeDepth?? + // 3rd param in processmessages_6-8-2-3 messageTreeSubDepth: 2, + // last param of processMessages_6-8-2-3 and tallyvotes_6-2-3 voteOptionTreeDepth: 3, - // TODO: confirm if intStateTreeDepth is the 2nd param in tallyVotes.circom + // 2nd param in tallyvotes_6-2-3 intStateTreeDepth: 2, }, maxValues: { // maxMessages and maxVoteOptions are calculated using treeArity = 5 as seen in the following code: // https://github.com/privacy-scaling-explorations/maci/blob/master/contracts/contracts/Poll.sol#L115 // treeArity ** messageTreeDepth - maxMessages: TREE_ARITY ** 8, + maxMessages: BigInt(TREE_ARITY ** 8), // treeArity ** voteOptionTreeDepth - maxVoteOptions: TREE_ARITY ** 3, - }, - batchSizes: { - // TODO: confirm the following mapping - // https://github.com/privacy-scaling-explorations/maci/blob/master/contracts/contracts/MACI.sol#L259 - // treeArity ** messageBatchTreeDepth - messageBatchSize: TREE_ARITY ** 2, + maxVoteOptions: BigInt(TREE_ARITY ** 3), }, + messageBatchSize: BigInt(TREE_ARITY ** 2), }, } + +/** + * List of all the circuit files used by MACI commands + */ +export interface ZkFiles { + processZkFile: string + processWitness: string + processWasm: string + processDatFile: string + tallyZkFile: string + tallyWitness: string + tallyWasm: string + tallyDatFile: string +} + +/** + * Get the zkey file path + * @param name zkey file name + * @returns zkey file path + */ +export function getCircuitFiles(circuit: string, directory: string): ZkFiles { + const params = CIRCUITS[circuit] + return { + processZkFile: path.join(directory, params.processMessagesZkey), + processWitness: path.join(directory, params.processWitness), + processWasm: path.join(directory, params.processWasm), + processDatFile: path.join(directory, params.processDatFile), + tallyZkFile: path.join(directory, params.tallyVotesZkey), + tallyWitness: path.join(directory, params.tallyWitness), + tallyWasm: path.join(directory, params.tallyWasm), + tallyDatFile: path.join(directory, params.tallyDatFile), + } +} diff --git a/contracts/utils/constants.ts b/contracts/utils/constants.ts index af3274138..70687b10a 100644 --- a/contracts/utils/constants.ts +++ b/contracts/utils/constants.ts @@ -1,11 +1,18 @@ -import { BigNumber } from 'ethers' - export const DEFAULT_IPFS_GATEWAY = 'https://ipfs.io' export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' -export const UNIT = BigNumber.from(10).pow(BigNumber.from(18)) -export const VOICE_CREDIT_FACTOR = BigNumber.from(10).pow(4 + 18 - 9) -export const ALPHA_PRECISION = BigNumber.from(10).pow(18) +export const UNIT = 10n ** 18n +export const VOICE_CREDIT_FACTOR = 10n ** BigInt(4 + 18 - 9) +export const ALPHA_PRECISION = 10n ** 18n export const DEFAULT_SR_QUEUE_OPS = '4' +export const DEFAULT_GET_LOG_BATCH_SIZE = 20000 + +// brightid.clr.fund node uses this to sign messages +// see the ethSigningAddress in https://brightid.clr.fund +export const BRIGHTID_VERIFIER_ADDR = + '0xdbf0b2ee9887fe11934789644096028ed3febe9c' + +// This is brightid node signer address +// export const BRIGHTID_VERIFIER_ADDR = '0xb1d71F62bEe34E9Fc349234C201090c33BCdF6DB' export enum RecipientState { Registered = 'Registered', diff --git a/contracts/utils/contracts.ts b/contracts/utils/contracts.ts index f6ef16ef6..8dbc0734a 100644 --- a/contracts/utils/contracts.ts +++ b/contracts/utils/contracts.ts @@ -1,37 +1,19 @@ -import { BigNumber, Contract } from 'ethers' -import { TransactionResponse } from '@ethersproject/abstract-provider' +import { TransactionResponse } from 'ethers' +import { getEventArg } from '@clrfund/common' export async function getGasUsage( transaction: TransactionResponse ): Promise { const receipt = await transaction.wait() - return receipt.gasUsed.toNumber() + return receipt ? Number(receipt.gasUsed) : 0 } export async function getTxFee( transaction: TransactionResponse -): Promise { +): Promise { const receipt = await transaction.wait() // effectiveGasPrice was introduced by EIP1559 - return receipt.gasUsed.mul(receipt.effectiveGasPrice) + return receipt ? BigInt(receipt.gasUsed) * BigInt(receipt.gasPrice) : 0n } -export async function getEventArg( - transaction: TransactionResponse, - contract: Contract, - eventName: string, - argumentName: string -): Promise { - // eslint-disable-line @typescript-eslint/no-explicit-any - const receipt = await transaction.wait() - for (const log of receipt.logs || []) { - if (log.address != contract.address) { - continue - } - const event = contract.interface.parseLog(log) - if (event && event.name === eventName) { - return event.args[argumentName] - } - } - throw new Error('Event not found') -} +export { getEventArg } diff --git a/contracts/utils/date.ts b/contracts/utils/date.ts index 201fd9297..3e506ff13 100644 --- a/contracts/utils/date.ts +++ b/contracts/utils/date.ts @@ -1,5 +1,8 @@ -import { BigNumber, BigNumberish } from 'ethers' - -export function toDate(val: BigNumberish) { - return new Date(BigNumber.from(val).mul(1000).toNumber()) +/** + * Convert the unix timestamp in seconds to javascript date object + * @param unixTime unix timestamp in seconds + * @returns javascript date object + */ +export function toDate(unixTime: any) { + return new Date(Number(unixTime) * 1000) } diff --git a/contracts/utils/deployment.ts b/contracts/utils/deployment.ts index 3912e493f..94bde247c 100644 --- a/contracts/utils/deployment.ts +++ b/contracts/utils/deployment.ts @@ -1,13 +1,21 @@ -import { Signer, Contract, utils, BigNumber, ContractTransaction } from 'ethers' -import { link } from 'ethereum-waffle' +import { + Signer, + Contract, + ContractTransactionResponse, + encodeBytes32String, + BaseContract, +} from 'ethers' import path from 'path' import { readFileSync } from 'fs' -import { HardhatEthersHelpers } from '@nomiclabs/hardhat-ethers/types' +import { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types' import { DEFAULT_CIRCUIT } from './circuits' import { isPathExist } from './misc' import { MaciParameters } from './maciParameters' import { PrivKey, Keypair } from '@clrfund/common' +import { ZERO_ADDRESS } from './constants' +import { VkRegistry } from '../typechain-types' +import { IVerifyingKeyStruct } from 'maci-contracts' // Number.MAX_SAFE_INTEGER - 1 export const challengePeriodSeconds = '9007199254740990' @@ -35,17 +43,19 @@ export interface BrightIdParams { sponsor: string } -export function linkBytecode(bytecode: string, libraries: Libraries): string { - // Workarounds for https://github.com/nomiclabs/buidler/issues/611 - const linkable = { evm: { bytecode: { object: bytecode } } } - for (const [libraryName, libraryAddress] of Object.entries(libraries)) { - link(linkable, libraryName, libraryAddress.toLowerCase()) +type PoseidonName = 'PoseidonT3' | 'PoseidonT4' | 'PoseidonT5' | 'PoseidonT6' + +/** + * Log the message based on the quiet flag + * @param quiet whether to log the message + * @param message the message to log + */ +function logInfo(quiet = true, message: string, ...args: any[]) { + if (!quiet) { + console.log(message, ...args) } - return linkable.evm.bytecode.object } -type PoseidonName = 'PoseidonT3' | 'PoseidonT4' | 'PoseidonT5' | 'PoseidonT6' - /** * Deploy the Poseidon contracts. These contracts * have a custom artifact location that the hardhat library cannot @@ -103,13 +113,12 @@ export async function deployContract({ ethers, signer, }: deployContractOptions): Promise { - const contractFactory = await ethers.getContractFactory(name, { + const contract = await ethers.deployContract(name, contractArgs, { signer, libraries, }) - const contract = await contractFactory.deploy(...contractArgs) - return await contract.deployed() + return await contract.waitForDeployment() } /** @@ -137,8 +146,9 @@ export async function deployUserRegistry({ brightidVerifier?: string brightidSponsor?: string }): Promise { - let userRegistry: Contract + let contractArgs: any[] = [] const registryType = (userRegistryType || '').toLowerCase() + if (registryType === 'brightid') { if (!brightidContext) { throw new Error('Missing BrightId context') @@ -150,31 +160,24 @@ export async function deployUserRegistry({ throw new Error('Missing BrightId sponsor contract address') } - const BrightIdUserRegistry = await ethers.getContractFactory( - 'BrightIdUserRegistry', - signer - ) - - userRegistry = await BrightIdUserRegistry.deploy( - utils.formatBytes32String(brightidContext), + contractArgs = [ + encodeBytes32String(brightidContext), brightidVerifier, - brightidSponsor - ) - } else { - const userRegistryName = userRegistryNames[registryType] - if (!userRegistryName) { - throw new Error('unsupported user registry type: ' + registryType) - } + brightidSponsor, + ] + } - const UserRegistry = await ethers.getContractFactory( - userRegistryName, - signer - ) - userRegistry = await UserRegistry.deploy() + const userRegistryName = userRegistryNames[registryType] + if (!userRegistryName) { + throw new Error('unsupported user registry type: ' + registryType) } - await userRegistry.deployTransaction.wait() - return userRegistry + return deployContract({ + name: userRegistryName, + contractArgs, + ethers, + signer, + }) } /** @@ -197,7 +200,7 @@ export async function deployRecipientRegistry({ }: { type: string controller: string - deposit?: BigNumber + deposit?: bigint challengePeriod?: string ethers: HardhatEthersHelpers signer?: Signer @@ -222,10 +225,14 @@ export async function deployRecipientRegistry({ ? [controller] : [deposit, challengePeriod, controller] - const factory = await ethers.getContractFactory(registryName, signer) - const recipientRegistry = await factory.deploy(...args) + const recipientRegistry = await ethers.deployContract( + registryName, + args, + signer + ) - return await recipientRegistry.deployed() + await recipientRegistry.waitForDeployment() + return recipientRegistry } /** @@ -274,10 +281,10 @@ export async function deployPoseidonLibraries({ }) const libraries = { - PoseidonT3: PoseidonT3Contract.address, - PoseidonT4: PoseidonT4Contract.address, - PoseidonT5: PoseidonT5Contract.address, - PoseidonT6: PoseidonT6Contract.address, + PoseidonT3: await PoseidonT3Contract.getAddress(), + PoseidonT4: await PoseidonT4Contract.getAddress(), + PoseidonT5: await PoseidonT5Contract.getAddress(), + PoseidonT6: await PoseidonT6Contract.getAddress(), } return libraries } @@ -321,110 +328,95 @@ export async function deployPollFactory({ }) } -/** - * Deploy the contracts needed to run the proveOnChain script. - * If the poseidon contracts are not provided, it will create them - * using the byte codes in the artifactsPath - * - * libraries - poseidon libraries - * artifactsPath - path that contacts the poseidon abi and bytecode - * - * @returns the MessageProcessor and Tally contracts - */ -export async function deployMessageProcesorAndTally({ - artifactsPath, - libraries, - ethers, - signer, -}: { - libraries?: Libraries - artifactsPath?: string - signer?: Signer - ethers: HardhatEthersHelpers -}): Promise<{ - mpContract: Contract - tallyContract: Contract -}> { - if (!libraries) { - if (!artifactsPath) { - throw Error('Need the artifacts path to create the poseidon contracts') - } - libraries = await deployPoseidonLibraries({ - artifactsPath, - ethers, - signer, - }) - } - - const verifierContract = await deployContract({ - name: 'Verifier', - signer, - ethers, - }) - const tallyContract = await deployContract({ - name: 'Tally', - contractArgs: [verifierContract.address], - libraries, - ethers, - signer, - }) - - // deploy the message processing contract - const mpContract = await deployContract({ - name: 'MessageProcessor', - contractArgs: [verifierContract.address], - signer, - libraries, - ethers, - }) - - return { - mpContract, - tallyContract, - } -} - /** * Deploy an instance of MACI factory - * * libraries - poseidon contracts * ethers - hardhat ethers handle * signer - if signer is not provided, use default signer in ethers - * * @returns MACI factory contract */ export async function deployMaciFactory({ libraries, ethers, signer, + maciParameters, + quiet, }: { libraries: Libraries ethers: HardhatEthersHelpers signer?: Signer + maciParameters: MaciParameters + quiet?: boolean }): Promise { + const vkRegistry = await deployContract({ + name: 'VkRegistry', + ethers, + signer, + }) + logInfo(quiet, 'Deployed VkRegistry at', vkRegistry.target) + + await setVerifyingKeys( + vkRegistry as BaseContract as VkRegistry, + maciParameters + ) + + const verifier = await deployContract({ + name: 'Verifier', + ethers, + signer, + }) + logInfo(quiet, 'Deployed Verifier at', verifier.target) + const pollFactory = await deployContract({ name: 'PollFactory', libraries, ethers, signer, }) + logInfo(quiet, 'Deployed PollFactory at', pollFactory.target) - const vkRegistry = await deployContract({ - name: 'VkRegistry', + const tallyFactory = await deployContract({ + name: 'TallyFactory', + libraries, ethers, signer, }) + logInfo(quiet, 'Deployed TallyFactory at', tallyFactory.target) + + const messageProcessorFactory = await deployContract({ + name: 'MessageProcessorFactory', + libraries, + ethers, + signer, + }) + logInfo( + quiet, + 'Deployed MessageProcessorFactory at', + messageProcessorFactory.target + ) + + // all the factories to deploy MACI contracts + const factories = { + pollFactory: pollFactory.target, + tallyFactory: tallyFactory.target, + // subsidy is not currently used + subsidyFactory: ZERO_ADDRESS, + messageProcessorFactory: messageProcessorFactory.target, + } const maciFactory = await deployContract({ name: 'MACIFactory', libraries, - contractArgs: [vkRegistry.address, pollFactory.address], + contractArgs: [vkRegistry.target, factories, verifier.target], ethers, signer, }) + logInfo(quiet, 'Deployed MACIFactory at', maciFactory.target) - const transferTx = await vkRegistry.transferOwnership(maciFactory.address) - await transferTx.wait() + const setTx = await maciFactory.setMaciParameters( + ...maciParameters.asContractParam() + ) + await setTx.wait() return maciFactory } @@ -439,7 +431,7 @@ export async function setMaciParameters( maciFactory: Contract, directory: string, circuit = DEFAULT_CIRCUIT -): Promise { +): Promise { if (!isPathExist(directory)) { throw new Error(`Path ${directory} does not exists`) } @@ -452,9 +444,35 @@ export async function setMaciParameters( return setMaciTx } +/** + * Set Verifying key + * @param vkRegistry VKRegistry contract + * @param maciParameters MACI tree depths and verifying key information + * @returns transaction response + */ +export async function setVerifyingKeys( + vkRegistry: VkRegistry, + params: MaciParameters +): Promise { + const tx = await vkRegistry.setVerifyingKeys( + params.stateTreeDepth, + params.treeDepths.intStateTreeDepth, + params.treeDepths.messageTreeDepth, + params.treeDepths.voteOptionTreeDepth, + params.messageBatchSize, + params.processVk.asContractParam() as IVerifyingKeyStruct, + params.tallyVk.asContractParam() as IVerifyingKeyStruct + ) + + const receipt = await tx.wait() + if (receipt?.status !== 1) { + throw new Error('Failed to set verifying key; transaction receipt status 1') + } + return tx +} + /** * Set the coordinator address and maci public key in the funding round factory - * * @param fundingRoundFactory funding round factory contract * @param coordinatorAddress * @param MaciPrivateKey @@ -467,10 +485,10 @@ export async function setCoordinator({ clrfundContract: Contract coordinatorAddress: string coordinatorMacisk?: string -}): Promise { +}): Promise { // Generate or use the passed in coordinator key const privKey = coordinatorMacisk - ? PrivKey.unserialize(coordinatorMacisk) + ? PrivKey.deserialize(coordinatorMacisk) : undefined const keypair = new Keypair(privKey) diff --git a/contracts/utils/file.ts b/contracts/utils/file.ts deleted file mode 100644 index df83d0f7d..000000000 --- a/contracts/utils/file.ts +++ /dev/null @@ -1,7 +0,0 @@ -import fs from 'fs' - -export function writeToFile(filePath: string, data: any) { - const outputString = JSON.stringify(data, null, 2) - fs.writeFileSync(filePath, outputString + '\n') - console.log('Successfully written to ', filePath) -} diff --git a/contracts/utils/ipfs.ts b/contracts/utils/ipfs.ts index e2157a093..8fc5de275 100644 --- a/contracts/utils/ipfs.ts +++ b/contracts/utils/ipfs.ts @@ -1,17 +1,29 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const Hash = require('ipfs-only-hash') -import { utils } from 'ethers' +import { FetchRequest } from 'ethers' import { DEFAULT_IPFS_GATEWAY } from './constants' +/** + * Get the ipfs hash for the input object + * @param object a json object to get the ipfs hash for + * @returns the ipfs hash + */ export async function getIpfsHash(object: any): Promise { const data = Buffer.from(JSON.stringify(object, null, 4)) return await Hash.of(data) } export class Ipfs { + /** + * Get the content of the ipfs hash + * @param hash ipfs hash + * @param gatewayUrl ipfs gateway url + * @returns the content + */ static async fetchJson(hash: string, gatewayUrl?: string): Promise { const url = `${gatewayUrl || DEFAULT_IPFS_GATEWAY}/ipfs/${hash}` - const result = utils.fetchJson(url) - return result + const req = new FetchRequest(url) + const resp = await req.send() + return resp.bodyJson } } diff --git a/contracts/utils/maci.ts b/contracts/utils/maci.ts index 4e178d05b..2cf5828d7 100644 --- a/contracts/utils/maci.ts +++ b/contracts/utils/maci.ts @@ -1,4 +1,4 @@ -import { Contract, BigNumber, ContractReceipt } from 'ethers' +import { ContractTransactionReceipt } from 'ethers' import { bnSqrt, createMessage, @@ -17,34 +17,29 @@ import { mergeSignups, genProofs, proveOnChain, -} from '@clrfund/maci-cli' + GenProofsArgs, + genLocalState, + verify, + TallyData, +} from 'maci-cli' import { getTalyFilePath, isPathExist } from './misc' -import { CIRCUITS } from './circuits' -import path from 'path' +import { getCircuitFiles } from './circuits' +import { FundingRound } from '../typechain-types' -interface TallyResult { +interface TallyResultProof { recipientIndex: number result: string - proof: any[] -} - -export interface ZkFiles { - processZkFile: string - processWitness: string - processWasm: string - tallyZkFile: string - tallyWitness: string - tallyWasm: string + proof: bigint[][] } export const isOsArm = os.arch().includes('arm') -export function getRecipientTallyResult( +export function getTallyResultProof( recipientIndex: number, recipientTreeDepth: number, - tally: any -): TallyResult { + tally: TallyData +): TallyResultProof { // Create proof for tally result const result = tally.results.tally[recipientIndex] if (result == null) { @@ -59,55 +54,49 @@ export function getRecipientTallyResult( hash5 ) for (const leaf of tally.results.tally) { - resultTree.insert(leaf) + resultTree.insert(BigInt(leaf)) } - const resultProof = resultTree.genMerklePath(recipientIndex) + const resultProof = resultTree.genProof(recipientIndex) return { recipientIndex, result, - proof: resultProof.pathElements.map((x: bigint[]) => - x.map((y) => y.toString()) - ), + proof: resultProof.pathElements, } } -export function getRecipientTallyResultsBatch( +export function getTallyResultProofBatch( recipientStartIndex: number, recipientTreeDepth: number, tally: any, batchSize: number -): any[] { +): TallyResultProof[] { const tallyCount = tally.results.tally.length if (recipientStartIndex >= tallyCount) { throw new Error('Recipient index out of bound') } - const tallyData: TallyResult[] = [] + const proofs: TallyResultProof[] = [] const lastIndex = recipientStartIndex + batchSize > tallyCount ? tallyCount : recipientStartIndex + batchSize for (let i = recipientStartIndex; i < lastIndex; i++) { - tallyData.push(getRecipientTallyResult(i, recipientTreeDepth, tally)) + proofs.push(getTallyResultProof(i, recipientTreeDepth, tally)) } - return [ - tallyData.map((item) => item.recipientIndex), - tallyData.map((item) => item.result), - tallyData.map((item) => item.proof), - ] + return proofs } export async function addTallyResultsBatch( - fundingRound: Contract, + fundingRound: FundingRound, recipientTreeDepth: number, tallyData: any, batchSize: number, startIndex = 0, - callback?: (processed: number, receipt: ContractReceipt) => void -): Promise { - let totalGasUsed = BigNumber.from(0) + callback?: (processed: number, receipt: ContractTransactionReceipt) => void +): Promise { + let totalGasUsed = 0 const { tally } = tallyData.results const spentVoiceCreditsHash = hashLeftRight( @@ -142,7 +131,7 @@ export async function addTallyResultsBatch( } for (let i = startIndex; i < tally.length; i = i + batchSize) { - const data = getRecipientTallyResultsBatch( + const proofs = getTallyResultProofBatch( i, recipientTreeDepth, tallyData, @@ -150,46 +139,33 @@ export async function addTallyResultsBatch( ) const tx = await fundingRound.addTallyResultsBatch( - recipientTreeDepth, - ...data, - BigInt(tallyData.results.salt).toString(), - spentVoiceCreditsHash.toString(), - BigInt(perVOSpentVoiceCreditsHash).toString() + proofs.map((i) => i.recipientIndex), + proofs.map((i) => i.result), + proofs.map((i) => i.proof), + BigInt(tallyData.results.salt), + spentVoiceCreditsHash, + BigInt(perVOSpentVoiceCreditsHash) ) const receipt = await tx.wait() + if (receipt?.status !== 1) { + throw new Error('Failed to add tally results on chain') + } + if (callback) { // the 2nd element in the data array has the array of // recipients to be processed for the batch - const totalProcessed = i + data[1].length + const totalProcessed = i + proofs.length callback(totalProcessed, receipt) } - totalGasUsed = totalGasUsed.add(receipt.gasUsed) + totalGasUsed = totalGasUsed + Number(receipt.gasUsed) } return totalGasUsed } -/** - * Get the zkey file path - * @param name zkey file name - * @returns zkey file path - */ -export function getCircuitFiles(circuit: string, directory: string): ZkFiles { - const params = CIRCUITS[circuit] - return { - processZkFile: path.join(directory, params.processMessagesZkey), - processWitness: path.join(directory, params.processWitness), - processWasm: path.join(directory, params.processWasm), - tallyZkFile: path.join(directory, params.tallyVotesZkey), - tallyWitness: path.join(directory, params.tallyWitness), - tallyWasm: path.join(directory, params.tallyWasm), - } -} - /* Input to getGenProofArgs() */ type getGenProofArgsInput = { maciAddress: string - providerUrl: string - pollId: string + pollId: bigint // coordinator's MACI serialized secret key coordinatorMacisk: string // the transaction hash of the creation of the MACI contract @@ -197,103 +173,101 @@ type getGenProofArgsInput = { // the key get zkeys file mapping, see utils/circuits.ts circuitType: string circuitDirectory: string - rapidSnark?: string + rapidsnark?: string // where the proof will be produced outputDir: string -} - -type getGenProofArgsResult = { - contract: string - eth_provider: string - poll_id: string - tally_file: string - rapidsnark?: string - process_witnessgen?: string - tally_witnessgen?: string - process_wasm?: string - tally_wasm?: string - process_zkey: string - tally_zkey: string - transaction_hash?: string - output: string - privkey: string - macistate: string - cleanup: boolean + // number of blocks of logs to fetch per batch + blocksPerBatch: number + // fetch logs from MACI from these start and end blocks + startBlock?: number + endBlock?: number + // MACI state file + maciStateFile?: string + // flag to turn on verbose logging in MACI cli + quiet?: boolean } /* * Get the arguments to pass to the genProof function */ -export function getGenProofArgs( - args: getGenProofArgsInput -): getGenProofArgsResult { +export function getGenProofArgs(args: getGenProofArgsInput): GenProofsArgs { const { maciAddress, - providerUrl, pollId, coordinatorMacisk, maciTxHash, circuitType, circuitDirectory, - rapidSnark, + rapidsnark, outputDir, + blocksPerBatch, + startBlock, + endBlock, + maciStateFile, + quiet, } = args const tallyFile = getTalyFilePath(outputDir) - const maciStateFile = path.join(outputDir, `macistate`) const { processZkFile, tallyZkFile, processWitness, processWasm, + processDatFile, tallyWitness, tallyWasm, + tallyDatFile, } = getCircuitFiles(circuitType, circuitDirectory) - // do not cleanup threads after calling genProofs, - // the script will exit and end threads at the end - const cleanup = false if (isOsArm) { return { - contract: maciAddress, - eth_provider: providerUrl, - poll_id: pollId.toString(), - tally_file: tallyFile, - process_wasm: processWasm, - process_zkey: processZkFile, - tally_zkey: tallyZkFile, - tally_wasm: tallyWasm, - transaction_hash: maciTxHash, - output: outputDir, - privkey: coordinatorMacisk, - macistate: maciStateFile, - cleanup, + outputDir, + tallyFile, + tallyZkey: tallyZkFile, + processZkey: processZkFile, + pollId, + coordinatorPrivKey: coordinatorMacisk, + maciAddress, + transactionHash: maciTxHash, + processWasm, + tallyWasm, + useWasm: true, + blocksPerBatch, + startBlock, + endBlock, + stateFile: maciStateFile, + quiet, } } else { - if (!rapidSnark) { + if (!rapidsnark) { throw new Error('Please specify the path to the rapidsnark binary') } - if (!isPathExist(rapidSnark)) { - throw new Error(`Path ${rapidSnark} does not exist`) + if (!isPathExist(rapidsnark)) { + throw new Error(`Path ${rapidsnark} does not exist`) } return { - contract: maciAddress, - eth_provider: providerUrl, - poll_id: pollId.toString(), - tally_file: tallyFile, - rapidsnark: rapidSnark, - process_witnessgen: processWitness, - tally_witnessgen: tallyWitness, - process_zkey: processZkFile, - tally_zkey: tallyZkFile, - transaction_hash: maciTxHash, - output: outputDir, - privkey: coordinatorMacisk, - macistate: maciStateFile, - cleanup, + outputDir, + tallyFile, + tallyZkey: tallyZkFile, + processZkey: processZkFile, + pollId, + processWitgen: processWitness, + processDatFile, + tallyWitgen: tallyWitness, + tallyDatFile, + coordinatorPrivKey: coordinatorMacisk, + maciAddress, + transactionHash: maciTxHash, + rapidsnark, + useWasm: false, + blocksPerBatch, + startBlock, + endBlock, + stateFile: maciStateFile, + quiet, } } } @@ -301,26 +275,36 @@ export function getGenProofArgs( /** * Merge MACI message and signups subtrees * Must merge the subtrees before calling genProofs - * * @param maciAddress MACI contract address * @param pollId Poll id - * @param numOperations Number of operations to perform for the merge + * @param numQueueOps Number of operations to perform for the merge + * @param quiet Whether to log output */ -export async function mergeMaciSubtrees( - maciAddress: string, - pollId: string, - numOperations: number -) { +export async function mergeMaciSubtrees({ + maciAddress, + pollId, + numQueueOps, + quiet, +}: { + maciAddress: string + pollId: bigint + numQueueOps?: string + quiet?: boolean +}) { + if (!maciAddress) throw new Error('Missing MACI address') + await mergeMessages({ - contract: maciAddress, - poll_id: pollId, - num_queue_ops: numOperations, + pollId, + maciContractAddress: maciAddress, + numQueueOps, + quiet, }) await mergeSignups({ - contract: maciAddress, - poll_id: pollId, - num_queue_ops: numOperations, + pollId, + maciContractAddress: maciAddress, + numQueueOps, + quiet, }) } @@ -340,4 +324,13 @@ export function newMaciPrivateKey(): string { return secretKey } -export { createMessage, getRecipientClaimData, bnSqrt, proveOnChain, genProofs } +export { + createMessage, + getRecipientClaimData, + bnSqrt, + genProofs, + proveOnChain, + verify, + genLocalState, + TallyData, +} diff --git a/contracts/utils/maciParameters.ts b/contracts/utils/maciParameters.ts index 1798ac3d2..965326ea6 100644 --- a/contracts/utils/maciParameters.ts +++ b/contracts/utils/maciParameters.ts @@ -1,74 +1,60 @@ import { Contract } from 'ethers' -import { VerifyingKey } from '@clrfund/maci-domainobjs' -import { extractVk } from '@clrfund/maci-circuits' -import { CIRCUITS } from './circuits' -import path from 'path' +import { VerifyingKey } from 'maci-domainobjs' +import { extractVk } from 'maci-circuits' +import { CIRCUITS, getCircuitFiles } from './circuits' -export interface ZkFiles { - processZkFile: string - processWitness: string - processWasm: string - tallyZkFile: string - tallyWitness: string - tallyWasm: string +type TreeDepths = { + intStateTreeDepth: number + messageTreeSubDepth: number + messageTreeDepth: number + voteOptionTreeDepth: number } -/** - * Get the zkey file path - * @param name zkey file name - * @returns zkey file path - */ -export function getCircuitFiles(circuit: string, directory: string): ZkFiles { - const params = CIRCUITS[circuit] - return { - processZkFile: path.join(directory, params.processMessagesZkey), - processWitness: path.join(directory, params.processWitness), - processWasm: path.join(directory, params.processWasm), - tallyZkFile: path.join(directory, params.tallyVotesZkey), - tallyWitness: path.join(directory, params.tallyWitness), - tallyWasm: path.join(directory, params.tallyWasm), - } +type MaxValues = { + maxMessages: bigint + maxVoteOptions: bigint } export class MaciParameters { stateTreeDepth: number - intStateTreeDepth: number - messageTreeSubDepth: number - messageTreeDepth: number - voteOptionTreeDepth: number - maxMessages: number - maxVoteOptions: number - messageBatchSize: number + messageBatchSize: bigint processVk: VerifyingKey tallyVk: VerifyingKey + treeDepths: TreeDepths + maxValues: MaxValues constructor(parameters: { [name: string]: any } = {}) { this.stateTreeDepth = parameters.stateTreeDepth - this.intStateTreeDepth = parameters.intStateTreeDepth - this.messageTreeSubDepth = parameters.messageTreeSubDepth - this.messageTreeDepth = parameters.messageTreeDepth - this.voteOptionTreeDepth = parameters.voteOptionTreeDepth - this.maxMessages = parameters.maxMessages - this.maxVoteOptions = parameters.maxVoteOptions this.messageBatchSize = parameters.messageBatchSize this.processVk = parameters.processVk this.tallyVk = parameters.tallyVk + this.treeDepths = { + intStateTreeDepth: parameters.intStateTreeDepth, + messageTreeSubDepth: parameters.messageTreeSubDepth, + messageTreeDepth: parameters.messageTreeDepth, + voteOptionTreeDepth: parameters.voteOptionTreeDepth, + } + this.maxValues = { + maxMessages: parameters.maxMessages, + maxVoteOptions: parameters.maxVoteOptions, + } } asContractParam(): any[] { return [ this.stateTreeDepth, { - intStateTreeDepth: this.intStateTreeDepth, - messageTreeSubDepth: this.messageTreeSubDepth, - messageTreeDepth: this.messageTreeDepth, - voteOptionTreeDepth: this.voteOptionTreeDepth, + intStateTreeDepth: this.treeDepths.intStateTreeDepth, + messageTreeSubDepth: this.treeDepths.messageTreeSubDepth, + messageTreeDepth: this.treeDepths.messageTreeDepth, + voteOptionTreeDepth: this.treeDepths.voteOptionTreeDepth, + }, + { + maxMessages: this.maxValues.maxMessages, + maxVoteOptions: this.maxValues.maxVoteOptions, }, - { maxMessages: this.maxMessages, maxVoteOptions: this.maxVoteOptions }, this.messageBatchSize, - this.processVk.asContractParam(), - this.tallyVk.asContractParam(), ] } @@ -86,9 +72,10 @@ export class MaciParameters { ) return new MaciParameters({ + stateTreeDepth: params.stateTreeDepth, ...params.maxValues, ...params.treeDepths, - ...params.batchSizes, + messageBatchSize: params.messageBatchSize, processVk, tallyVk, }) @@ -117,8 +104,11 @@ export class MaciParameters { }) } - static mock(circuit: string): MaciParameters { + static mock(): MaciParameters { const processVk = VerifyingKey.fromObj({ + protocol: 1, + curve: 1, + nPublic: 1, vk_alpha_1: [1, 2], vk_beta_2: [ [1, 2], @@ -132,13 +122,26 @@ export class MaciParameters { [1, 2], [1, 2], ], + vk_alphabeta_12: [[[1, 2, 3]]], IC: [[1, 2]], }) - const params = CIRCUITS[circuit] + + // use smaller voteOptionTreeDepth for testing + const params = { + maxValues: { maxMessages: BigInt(390625), maxVoteOptions: BigInt(25) }, + treeDepths: { + intStateTreeDepth: 2, + messageTreeSubDepth: 2, + messageTreeDepth: 8, + voteOptionTreeDepth: 2, + }, + } + return new MaciParameters({ + stateTreeDepth: 6, ...params.maxValues, ...params.treeDepths, - ...params.batchSizes, + messageBatchSize: BigInt(25), processVk, tallyVk: processVk.copy(), }) diff --git a/contracts/utils/misc.ts b/contracts/utils/misc.ts index 8583d6190..34bb53482 100644 --- a/contracts/utils/misc.ts +++ b/contracts/utils/misc.ts @@ -3,7 +3,6 @@ import fs from 'fs' /** * Get the tally file path - * * @param outputDir The output directory * @returns The tally file path */ @@ -11,12 +10,29 @@ export function getTalyFilePath(outputDir: string) { return path.join(outputDir, 'tally.json') } +/** + * Get the MACI state file path + * @param directory The directory containing the MACI state file + * @returns The path of the MACI state file + */ +export function getMaciStateFilePath(directory: string) { + return path.join(directory, 'maci-state.json') +} + /** * Check if the path exist - * * @param path The path to check for existence * @returns true if the path exists */ export function isPathExist(path: string): boolean { return fs.existsSync(path) } + +/** + * Returns the directory of the path + * @param file The file path + * @returns The directory of the file + */ +export function getDirname(file: string): string { + return path.dirname(file) +} diff --git a/contracts/utils/parsers/BaseParser.ts b/contracts/utils/parsers/BaseParser.ts index 3ca0547a3..50507ae5e 100644 --- a/contracts/utils/parsers/BaseParser.ts +++ b/contracts/utils/parsers/BaseParser.ts @@ -1,11 +1,27 @@ -import { Log } from '@ethersproject/abstract-provider' +import { Log, Interface } from 'ethers' import { Project } from '../types' +import { TOPIC_ABIS } from '../abi' export abstract class BaseParser { topic0: string + private parser: Interface constructor(topic0: string) { this.topic0 = topic0 + + const abiInfo = TOPIC_ABIS[this.topic0] + if (!abiInfo) { + throw new Error(`topic ${this.topic0} not found`) + } + this.parser = new Interface([abiInfo.abi]) + } + + protected getEventArgs(log: Log): any { + const parsedLog = this.parser.parseLog({ + data: log.data, + topics: [...log.topics], + }) + return parsedLog?.args || {} } abstract parse(log: Log): Partial diff --git a/contracts/utils/parsers/KlerosRecipientAddedParser.ts b/contracts/utils/parsers/KlerosRecipientAddedParser.ts index a44aee9da..ebd4c42dd 100644 --- a/contracts/utils/parsers/KlerosRecipientAddedParser.ts +++ b/contracts/utils/parsers/KlerosRecipientAddedParser.ts @@ -1,13 +1,12 @@ import { Log } from '../providers/BaseProvider' import { Project } from '../types' -import { utils, BigNumber, constants } from 'ethers' -import { TOPIC_ABIS } from '../abi' -import { RecipientState } from '../constants' +import { toUtf8String as tryToUtf8String, decodeRlp } from 'ethers' +import { RecipientState, ZERO_ADDRESS } from '../constants' import { BaseParser } from './BaseParser' function toUtf8String(hex: string): string | undefined { try { - return utils.toUtf8String(hex) + return tryToUtf8String(hex) } catch { return undefined } @@ -25,16 +24,25 @@ function sanitizeImageHash(url: string | undefined): string | undefined { function decodeMetadata(rawMetadata: string): any { // best effort to parse the rlp encoded metadata try { - const decoded = utils.RLP.decode(rawMetadata) - const utf8s = decoded.map(toUtf8String) + const decoded = decodeRlp(rawMetadata) + + if (!Array.isArray(decoded)) { + throw new Error('Unexpected metadata format, expecting array') + } + + const utf8s = decoded.map((val) => { + if (typeof val !== 'string') { + throw new Error('Unexpected decoded format, expecting string') + } + return typeof val === 'string' ? toUtf8String(val) : val + }) const name = utf8s[0] const recipientAddress = decoded[1] - const imageHash = sanitizeImageHash(utf8s[2]) + const imageHash = sanitizeImageHash(utf8s[2] as string) const description = utf8s[3] const websiteUrl = utf8s[4] const twitterUrl = utf8s[5] - return { name, recipientAddress, @@ -55,19 +63,13 @@ export class KlerosRecipientAddedParser extends BaseParser { } parse(log: Log): Partial { - const abiInfo = TOPIC_ABIS[this.topic0] - if (!abiInfo) { - throw new Error(`topic ${this.topic0} not found`) - } - - const parser = new utils.Interface([abiInfo.abi]) - const { args } = parser.parseLog(log) + const args = this.getEventArgs(log) const id = args._recipientId - const recipientIndex = BigNumber.from(args._index).toNumber() + const recipientIndex = Number(args._index) const state = RecipientState.Accepted const rawMetadata = args._metadata const metadata = decodeMetadata(args._metadata) - const recipientAddress = metadata?.recipientAddress || constants.AddressZero + const recipientAddress = metadata?.recipientAddress || ZERO_ADDRESS const name = metadata?.name || '?' return { diff --git a/contracts/utils/parsers/KlerosRecipientRemovedParser.ts b/contracts/utils/parsers/KlerosRecipientRemovedParser.ts index 468237a49..fa23898da 100644 --- a/contracts/utils/parsers/KlerosRecipientRemovedParser.ts +++ b/contracts/utils/parsers/KlerosRecipientRemovedParser.ts @@ -1,7 +1,5 @@ import { Log } from '../providers/BaseProvider' import { Project } from '../types' -import { utils } from 'ethers' -import { TOPIC_ABIS } from '../abi' import { RecipientState } from '../constants' import { BaseParser } from './BaseParser' @@ -11,13 +9,7 @@ export class KlerosRecipientRemovedParser extends BaseParser { } parse(log: Log): Partial { - const abiInfo = TOPIC_ABIS[this.topic0] - if (!abiInfo) { - throw new Error(`topic ${this.topic0} not found`) - } - - const parser = new utils.Interface([abiInfo.abi]) - const { args } = parser.parseLog(log) + const args = this.getEventArgs(log) const id = args._recipientId const state = RecipientState.Removed diff --git a/contracts/utils/parsers/RecipientAddedParser.ts b/contracts/utils/parsers/RecipientAddedParser.ts index bcbe47fd1..b3525c182 100644 --- a/contracts/utils/parsers/RecipientAddedParser.ts +++ b/contracts/utils/parsers/RecipientAddedParser.ts @@ -1,7 +1,5 @@ import { Log } from '../providers/BaseProvider' import { Project } from '../types' -import { BigNumber, utils } from 'ethers' -import { TOPIC_ABIS } from '../abi' import { RecipientState } from '../constants' import { BaseParser } from './BaseParser' import { toDate } from '../date' @@ -12,14 +10,9 @@ export class RecipientAddedParser extends BaseParser { } parse(log: Log): Project { - const abiInfo = TOPIC_ABIS[this.topic0] - if (!abiInfo) { - throw new Error(`topic ${this.topic0} not found`) - } - const parser = new utils.Interface([abiInfo.abi]) - const { args } = parser.parseLog(log) + const args = this.getEventArgs(log) const id = args._recipientId - const recipientIndex = BigNumber.from(args._index).toNumber() + const recipientIndex = Number(args._index) const recipientAddress = args._recipient const addedAt = args._timestamp let metadata: any diff --git a/contracts/utils/parsers/RecipientAddedV1Parser.ts b/contracts/utils/parsers/RecipientAddedV1Parser.ts index 5334f2038..30331891c 100644 --- a/contracts/utils/parsers/RecipientAddedV1Parser.ts +++ b/contracts/utils/parsers/RecipientAddedV1Parser.ts @@ -1,7 +1,5 @@ import { Log } from '../providers/BaseProvider' import { Project } from '../types' -import { BigNumber, utils } from 'ethers' -import { TOPIC_ABIS } from '../abi' import { RecipientState } from '../constants' import { BaseParser } from './BaseParser' @@ -11,14 +9,9 @@ export class RecipientAddedV1Parser extends BaseParser { } parse(log: Log): Project { - const abiInfo = TOPIC_ABIS[this.topic0] - if (!abiInfo) { - throw new Error(`topic ${this.topic0} not found`) - } - const parser = new utils.Interface([abiInfo.abi]) - const { args } = parser.parseLog(log) + const args = this.getEventArgs(log) const id = args._recipient - const recipientIndex = BigNumber.from(args._index).toNumber() + const recipientIndex = Number(args._index) const recipientAddress = args._recipient let metadata: any let name: string diff --git a/contracts/utils/parsers/RecipientRemovedParser.ts b/contracts/utils/parsers/RecipientRemovedParser.ts index b7f557de4..f179d11e3 100644 --- a/contracts/utils/parsers/RecipientRemovedParser.ts +++ b/contracts/utils/parsers/RecipientRemovedParser.ts @@ -1,7 +1,5 @@ import { Log } from '../providers/BaseProvider' import { Project } from '../types' -import { utils } from 'ethers' -import { TOPIC_ABIS } from '../abi' import { RecipientState } from '../constants' import { BaseParser } from './BaseParser' import { toDate } from '../date' @@ -12,13 +10,7 @@ export class RecipientRemovedParser extends BaseParser { } parse(log: Log): Partial { - const abiInfo = TOPIC_ABIS[this.topic0] - if (!abiInfo) { - throw new Error(`topic ${this.topic0} not found`) - } - - const parser = new utils.Interface([abiInfo.abi]) - const { args } = parser.parseLog(log) + const args = this.getEventArgs(log) const id = args._recipientId const state = RecipientState.Removed const removedAt = toDate(args._timestamp) diff --git a/contracts/utils/parsers/RecipientRemovedV1Parser.ts b/contracts/utils/parsers/RecipientRemovedV1Parser.ts index bbc80215a..7cb37c201 100644 --- a/contracts/utils/parsers/RecipientRemovedV1Parser.ts +++ b/contracts/utils/parsers/RecipientRemovedV1Parser.ts @@ -1,7 +1,5 @@ import { Log } from '../providers/BaseProvider' import { Project } from '../types' -import { utils } from 'ethers' -import { TOPIC_ABIS } from '../abi' import { RecipientState } from '../constants' import { BaseParser } from './BaseParser' @@ -11,13 +9,7 @@ export class RecipientRemovedV1Parser extends BaseParser { } parse(log: Log): Partial { - const abiInfo = TOPIC_ABIS[this.topic0] - if (!abiInfo) { - throw new Error(`topic ${this.topic0} not found`) - } - - const parser = new utils.Interface([abiInfo.abi]) - const { args } = parser.parseLog(log) + const args = this.getEventArgs(log) const id = args._recipient const state = RecipientState.Removed diff --git a/contracts/utils/parsers/RequestResolvedParser.ts b/contracts/utils/parsers/RequestResolvedParser.ts index fbf739d67..271cb563e 100644 --- a/contracts/utils/parsers/RequestResolvedParser.ts +++ b/contracts/utils/parsers/RequestResolvedParser.ts @@ -1,7 +1,5 @@ import { Log } from '../providers/BaseProvider' import { Project } from '../types' -import { utils, BigNumber } from 'ethers' -import { TOPIC_ABIS } from '../abi' import { RecipientState } from '../constants' import { BaseParser } from './BaseParser' import { toDate } from '../date' @@ -12,13 +10,7 @@ export class RequestResolvedParser extends BaseParser { } parse(log: Log): Partial { - const abiInfo = TOPIC_ABIS[this.topic0] - if (!abiInfo) { - throw new Error(`topic ${this.topic0} not found`) - } - - const parser = new utils.Interface([abiInfo.abi]) - const { args } = parser.parseLog(log) + const args = this.getEventArgs(log) const id = args._recipientId const timestamp = toDate(args._timestamp) @@ -31,7 +23,7 @@ export class RequestResolvedParser extends BaseParser { const recipientIndex = state === RecipientState.Accepted - ? BigNumber.from(args._recipientIndex).toNumber() + ? Number(args._recipientIndex) : undefined const createdAt = state === RecipientState.Accepted ? timestamp : undefined const removedAt = state === RecipientState.Accepted ? undefined : timestamp diff --git a/contracts/utils/parsers/RequestSubmittedParser.ts b/contracts/utils/parsers/RequestSubmittedParser.ts index 9bd363c77..777b38fbb 100644 --- a/contracts/utils/parsers/RequestSubmittedParser.ts +++ b/contracts/utils/parsers/RequestSubmittedParser.ts @@ -1,7 +1,5 @@ import { Log } from '../providers/BaseProvider' import { Project } from '../types' -import { utils } from 'ethers' -import { TOPIC_ABIS } from '../abi' import { RecipientState } from '../constants' import { BaseParser } from './BaseParser' import { toDate } from '../date' @@ -12,17 +10,14 @@ export class RequestSubmittedParser extends BaseParser { } parse(log: Log): Partial { - const abiInfo = TOPIC_ABIS[this.topic0] - if (!abiInfo) { - throw new Error(`topic ${this.topic0} not found`) - } - const parser = new utils.Interface([abiInfo.abi]) - const { args } = parser.parseLog(log) + const args = this.getEventArgs(log) const id = args._recipientId const recipientIndex = args._index const state = - args._type === 0 ? RecipientState.Registered : RecipientState.Removed + BigInt(args._type) === BigInt(0) + ? RecipientState.Registered + : RecipientState.Removed const timestamp = toDate(args._timestamp) const createdAt = diff --git a/contracts/utils/providers/BaseProvider.ts b/contracts/utils/providers/BaseProvider.ts index 707fdc56b..3eb57d4cb 100644 --- a/contracts/utils/providers/BaseProvider.ts +++ b/contracts/utils/providers/BaseProvider.ts @@ -1,9 +1,9 @@ -import { EventFilter, Log } from '@ethersproject/abstract-provider' +import { EventFilter, Log } from 'ethers' export interface FetchLogArgs { filter: EventFilter startBlock: number - lastBlock: number + lastBlock?: number blocksPerBatch: number } diff --git a/contracts/utils/providers/EtherscanProvider.ts b/contracts/utils/providers/EtherscanProvider.ts index 9d8383532..73a6205c0 100644 --- a/contracts/utils/providers/EtherscanProvider.ts +++ b/contracts/utils/providers/EtherscanProvider.ts @@ -1,10 +1,11 @@ import { BaseProvider, FetchLogArgs, Log } from './BaseProvider' -import { utils, BigNumber } from 'ethers' +import { FetchRequest } from 'ethers' const EtherscanApiUrl: Record = { xdai: 'https://api.gnosisscan.io', arbitrum: 'https://api.arbiscan.io', 'arbitrum-goerli': 'https://api-goerli.arbiscan.io', + 'arbitrum-sepolia': 'https://api-sepolia.arbiscan.io', } export class EtherscanProvider extends BaseProvider { @@ -30,11 +31,14 @@ export class EtherscanProvider extends BaseProvider { } const topic0 = filter.topics?.[0] || '' + const toBlockQuery = lastBlock ? `&toBlock=${lastBlock}` : '' const url = `${baseUrl}/api?module=logs&action=getLogs&address=${filter.address}` + - `&topic0=${topic0}&fromBlock=${startBlock}&&toBlock=${lastBlock}&apikey=${this.apiKey}` + `&topic0=${topic0}&fromBlock=${startBlock}${toBlockQuery}&apikey=${this.apiKey}` - const result = await utils.fetchJson(url) + const req = new FetchRequest(url) + const resp = await req.send() + const result = resp.bodyJson if (result.status === '0' && result.message === 'No records found') { return [] @@ -45,7 +49,7 @@ export class EtherscanProvider extends BaseProvider { } return result.result.map((res: any) => ({ - blockNumber: BigNumber.from(res.blockNumber).toNumber(), + blockNumber: Number(res.blockNumber), blockHash: res.blockHash, transactionIndex: res.transactionIndex, removed: false, diff --git a/contracts/utils/testutils.ts b/contracts/utils/testutils.ts new file mode 100644 index 000000000..0b5caf970 --- /dev/null +++ b/contracts/utils/testutils.ts @@ -0,0 +1,147 @@ +import { Signer, Contract } from 'ethers' +import { MockContract, deployMockContract } from '@clrfund/waffle-mock-contract' +import { artifacts, ethers, config } from 'hardhat' +import { deployMaciFactory, deployPoseidonLibraries } from './deployment' +import { MaciParameters } from './maciParameters' +import { PubKey } from '@clrfund/common' +import { getEventArg } from './contracts' + +/** + * Deploy a mock contract with the given contract name + * @param signer signer of the mock contract deployment + * @param name name of the contract to mock + * @returns a mock contract + */ +export async function deployMockContractByName( + name: string, + signer: Signer +): Promise { + const ContractArtifacts = await artifacts.readArtifact(name) + return deployMockContract(signer, ContractArtifacts.abi) +} + +/** + * Output from the deployTestFundingRound() function + */ +export type DeployTestFundingRoundOutput = { + token: Contract + fundingRound: Contract + mockUserRegistry: MockContract + mockRecipientRegistry: MockContract + mockVerifier: MockContract + mockTally: MockContract +} + +/** + * Deploy an instance of funding round contract for testing + * @param tokenSupply initial supply for the native token + * @param coordinatorAddress the coordinator wallet address + * @param deployer singer for the contract deployment + * @returns all the deployed objects in DeployTestFundingRoundOutput + */ +export async function deployTestFundingRound( + tokenSupply: bigint, + coordinatorAddress: string, + coordinatorPubKey: PubKey, + roundDuration: number, + deployer: Signer +): Promise { + const token = await ethers.deployContract( + 'AnyOldERC20Token', + [tokenSupply], + deployer + ) + + const mockUserRegistry = await deployMockContractByName( + 'IUserRegistry', + deployer + ) + const mockRecipientRegistry = await deployMockContractByName( + 'IRecipientRegistry', + deployer + ) + + const fundingRound = await ethers.deployContract( + 'FundingRound', + [ + token.target, + mockUserRegistry.target, + mockRecipientRegistry.target, + coordinatorAddress, + ], + deployer + ) + + const libraries = await deployPoseidonLibraries({ + signer: deployer, + ethers, + artifactsPath: config.paths.artifacts, + }) + + const maciParameters = MaciParameters.mock() + const maciFactory = await deployMaciFactory({ + libraries, + ethers, + signer: deployer, + maciParameters, + }) + const factories = await maciFactory.factories() + const topupToken = await ethers.deployContract('TopupToken', deployer) + const vkRegistry = await ethers.deployContract('VkRegistry', deployer) + const mockVerifier = await deployMockContractByName('Verifier', deployer) + const mockTally = await deployMockContractByName('Tally', deployer) + + const maciInstance = await ethers.deployContract( + 'MACI', + [ + factories.pollFactory, + factories.messageProcessorFactory, + factories.tallyFactory, + factories.subsidyFactory, + fundingRound.target, + fundingRound.target, + topupToken.target, + maciParameters.stateTreeDepth, + ], + { + signer: deployer, + libraries, + } + ) + + const deployPollTx = await maciInstance.deployPoll( + roundDuration, + maciParameters.maxValues, + maciParameters.treeDepths, + coordinatorPubKey.asContractParam(), + mockVerifier.target, + vkRegistry.target, + // pass false to not deploy the subsidy contract + false + ) + const pollAddr = await getEventArg( + deployPollTx, + maciInstance, + 'DeployPoll', + 'pollAddr' + ) + + // swap out the tally contract with with a mock for testing + const pollContracts = { + tally: await mockTally.getAddress(), + poll: pollAddr.poll, + messageProcessor: pollAddr.messageProcessor, + subsidy: pollAddr.subsidy, + } + + await fundingRound.setMaci(maciInstance.target, pollContracts) + + return { + token, + fundingRound, + mockRecipientRegistry, + mockUserRegistry, + mockVerifier, + mockTally, + } +} diff --git a/contracts/utils/types.ts b/contracts/utils/types.ts index 221063da9..473853ac1 100644 --- a/contracts/utils/types.ts +++ b/contracts/utils/types.ts @@ -39,6 +39,8 @@ export interface Round { recipientRegistryAddress: string recipientDepositAmount?: string maciAddress: string + pollAddress?: string + pollId?: bigint contributorCount: number totalSpent: string matchingPoolSize: string @@ -51,6 +53,11 @@ export interface Round { nativeTokenDecimals: number startTime: number endTime: number + signUpDuration: number + votingDuration: number + messages: bigint + maxMessages: bigint + maxRecipients: bigint blogUrl?: string } @@ -66,6 +73,7 @@ export type EventType = export type AbiInfo = { type: EventType + name: string abi: string } diff --git a/docs/deployment.md b/docs/deployment.md index e82ec0680..3a87f99be 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,5 +1,28 @@ # Deploy to a network +## Install MACI dependencies + +### Install rapidsnark (if on an intel chip) + +Check the MACI doc, https://maci.pse.dev/docs/installation#install-rapidsnark-if-on-an-intel-chip, on how to install the rapidsnark. + + +### Install C++ dependencies (if on intel chip) + +``` +sudo apt-get install libgmp-dev nlohmann-json3-dev nasm g++ +``` + +### Download MACI circuit files + +The following script will download the files in the params folder under the current folder where the script is run + +``` +monorepo/.github/scripts/download-6-8-2-3.sh +``` + +Make a note of this `params` folder as you'll need it to run the tally script. + ## Setup BrightID If using BrightID as the user registry type: @@ -20,10 +43,17 @@ Once the app is registered, you will get an appId which will be set to `BRIGHTID ## Deploy Contracts +Goto the `contracts` folder. + ### Deploy the BrightID sponsor contract (if using BrightID) -1. Run `yarn hardhat --network {network} deploy-sponsor` -2. Verify the contract by running `yarn hardhat --network arbitrum-goerli verify {contract address}` +1. Deploy the BrightID sponsor contract + +``` +HARDHAT_NETWORK={network} yarn ts-node cli/deploySponsor.ts +``` + +2. Verify the contract by running `yarn hardhat --network {network} verify {contract address}` 3. Set `BRIGHTID_SPONSOR` to the contract address in the next step ### Edit the `/contracts/.env` file @@ -31,33 +61,44 @@ Once the app is registered, you will get an appId which will be set to `BRIGHTID E.g. ``` -RECIPIENT_REGISTRY_TYPE=simple -USER_REGISTRY_TYPE=simple JSONRPC_HTTP_URL=https://NETWORK.alchemyapi.io/v2/ADD_API_KEY WALLET_PRIVATE_KEY= -NATIVE_TOKEN_ADDRESS= -BRIGHTID_CONTEXT= -BRIGHTID_SPONSOR= +ARBISCAN_API_KEY= ``` ### Run the deploy script +Use the `-h` switch to print the command line help menu for all the scripts in the `cli` folder. For hardhat help, use `yarn hardhat help`. + + +1. Deploy an instance of ClrFund + +``` +HARDHAT_NETWORK=localhost yarn ts-node cli/newClrFund.ts \ + --directory \ + --token \ + --coordinator \ + --user-registry-type \ + --recipient-registry-type +``` + +2. deploy new funding round +``` +HARDHAT_NETWOR=localhost yarn ts-node cli/newRound.ts \ + --clrfund \ + --duration +``` -1. Adjust the `/contracts/scripts/deploy.ts` as you wish. -2. Run `yarn hardhat run --network {network} scripts/deploy.ts` or use one of the `yarn deploy:{network}` available in `/contracts/package.json`. 3. Make sure to save in a safe place the serializedCoordinatorPrivKey, you are going to need it for tallying the votes in future steps. -4. To deploy a new funding round, update the .env file: + + +4. To load a list of users into the simple user registry, ``` -# .env -# The funding round factory address -FACTORY_ADDRESS= -# The coordinator MACI private key (serializedCoordinatorPrivKey saved in step 3) -COORDINATOR_PK= -# The coordinator wallet private key -COORDINATOR_ETH_PK= +yarn hardhat load-users --file-path addresses.txt --user-registry
--network ``` -5. If using a snapshot user registry, run the `set-storage-root` task to set the storage root for the block snapshot for user account verification + +If using a snapshot user registry, run the `set-storage-root` task to set the storage root for the block snapshot for user account verification ``` yarn hardhat --network {network} set-storage-root --registry 0x7113b39Eb26A6F0a4a5872E7F6b865c57EDB53E0 --slot 2 --token 0x65bc8dd04808d99cf8aa6749f128d55c2051edde --block 34677758 --network arbitrum-goerli @@ -75,16 +116,10 @@ yarn hardhat load-merkle-users --address-file ./addresses.txt --user-registry 0x Note: Make sure to upload generated merkle tree file to IPFS. -6. Run the `newRound.ts` script to deploy a new funding round: - -``` -yarn hardhat run --network {network} scripts/newRound.ts -``` - -5. Verify all deployed contracts: +8. Verify all deployed contracts: ``` -yarn hardhat verify-all {funding-round-factory-address} --network {network} +yarn hardhat verify-all --clrfund {clrfund-address} --network {network} ``` ### Deploy the subgraph @@ -119,7 +154,7 @@ VITE_INFURA_ID= VITE_IPFS_API_KEY= VITE_IPFS_SECRET_API_KEY= VITE_SUBGRAPH_URL= -VITE_CLRFUND_FACTORY_ADDRESS= +VITE_CLRFUND_ADDRESS= VITE_USER_REGISTRY_TYPE= VITE_BRIGHTID_CONTEXT= VITE_BRIGHTID_SPONSOR_KEY= diff --git a/docs/tally-verify.md b/docs/tally-verify.md index 57ab13bb3..3e60f429c 100644 --- a/docs/tally-verify.md +++ b/docs/tally-verify.md @@ -1,198 +1,29 @@ # How to tally votes -A funding round coordinator can tally votes using the MACI CLI, Docker or clrfund scripts. +Only a funding round coordinator can tally votes. -## Using MACI CLI - -### Clone the [MACI repo](https://github.com/privacy-scaling-explorations/maci) and switch to version v0.10.1: - -``` -git clone https://github.com/privacy-scaling-explorations/maci.git -cd maci/ -git checkout v0.10.1 -``` - -Follow instructions in README.md to install necessary dependencies. - -### Download circuits parameters - -Download the [zkSNARK parameters](https://gateway.pinata.cloud/ipfs/QmbVzVWqNTjEv5S3Vvyq7NkLVkpqWuA9DGMRibZYJXKJqy) for 'batch 64' circuits into the `circuits/params/` directory. - -Change the permission of the c binaries to be executable: -``` -cd circuits/params -chmod u+x qvt32 batchUst32 -``` - -Or, run the script monorepo/.github/scripts/download-batch64-params.sh to download the parameter files. - - -The contract deployment scripts, `deploy*.ts` in the [clrfund repository](https://github.com/clrfund/monorepo/tree/develop/contracts/scripts) currently use the `batch 64` circuits, if you want to use a smaller size circuits, you can find them [here](../contracts/contracts/snarkVerifiers/README.md). You will need to update the deploy script to call `deployMaciFactory()` with your circuit and redeploy the contracts. - -``` - // e.g. to use the x32 circuits - const circuit = 'x32' // defined in contracts/utils/deployment.ts - const maciFactory = await deployMaciFactory(deployer, circuit) -``` - -### Recompile the contracts: -Compile the contracts to generate the ABI that the MACI command lines use in the next step. - -``` -cd ../contracts -npm run compileSol -``` - -### Generate coordinator key -Generate the coordinator key used to encrypt messages. The key will be used when deploying new round. - -``` -cd ../cli -node build/index.js genMaciKeypair -``` - -A single key can be used to coordinate multiple rounds. ### Tally votes -Download the logs to be fed to the `proveOnChain` step. This step is useful -especially to avoid hitting rating limiting from the node. Make sure to run this -step againts a node that has archiving enabled, e.g. could use the alchemy node: - -``` -cd ../cli -node build/index.js fetchLogs \ - --eth-provider \ - --contract \ - --start-block \ - --num-blocks-per-request \ - --output logs -``` - -Decrypt messages, tally the votes and generate proofs: - -``` -node build/index.js genProofs \ - --eth-provider \ - --contract \ - --privkey \ - --tally-file tally.json \ - --logs-file logs \ - --macistate macistate \ - --output proofs.json -``` - -The coordinator private key (`COORDINATOR_PRIVKEY`) must be in the MACI key format (starts with `macisk`). It is used to decrypt messages. - -The `genProofs` command will create two files: `proofs.json` and `tally.json`. The `proofs.json` file will be needed to run the next command, `proveOnChain`, which submits proofs to the MACI contract: - -``` -node build/index.js proveOnChain \ - --eth-provider \ - --contract \ - --eth-privkey \ - --proof-file proofs.json -``` - -The Ethereum private key (`eth-private-key`) can be any private key that controls the necessary amount of ETH to pay for gas. - -The process may take several hours. Results can be found in `tally.json` file, which must then be published via IPFS. - -Finally, the [CID](https://docs.ipfs.tech/concepts/content-addressing/) of tally file must be submitted to `FundingRound` contract: - -``` -await fundingRound.publishTallyHash('') -``` - -## Using Docker - -In case you are in a different OS than Linux, you can run all the previous MACI CLI commands by running the Docker image located in the MACI repo. - -**Note:** the batch 64 zkSNARK parameters have been tested using Ubuntu 22.04 + Node v16.13.2 - -### Use the docker image +Install MACI dependencies (see the github action, `.github/workflows/test-scripts.yml` for all the dependencies to install) -First, install [docker](https://docs.docker.com/engine/install/) and [docker-compose](https://docs.docker.com/compose/install/) - -Inside the maci repo, run: - -``` -docker-compose up -``` - -Once the container is built, in a different terminal, grab the container id: - -``` -docker container ls -``` - -Get inside the container and execute the scripts you want: - -``` -docker exec -it {CONTAINER_ID} bash - -# inside the container -cd cli/ -node build/index.js genProofs ... -``` - -## Using clrfund scripts - -### Generate coordinator key - -``` -cd contracts/ -yarn ts-node scripts/generate-key.ts -``` - -A single key can be used to coordinate multiple rounds. - -### Tally votes - -Install [zkutil](https://github.com/poma/zkutil) (see instructions in [MACI readme](https://github.com/appliedzkp/maci#get-started)). - -Switch to `contracts` directory: - -``` -cd contracts/ -``` - -Download [zkSNARK parameters](https://gateway.pinata.cloud/ipfs/QmbVzVWqNTjEv5S3Vvyq7NkLVkpqWuA9DGMRibZYJXKJqy) for 'batch 64' circuits to `snark-params` directory. Example: - -``` -ipfs get --output snark-params QmbVzVWqNTjEv5S3Vvyq7NkLVkpqWuA9DGMRibZYJXKJqy -``` - -Change the permission of the c binaries to be executable: -``` -cd snark-params -chmod u+x qvt32 batchUst32 -``` - -Or, run the script monorepo/.github/scripts/download-batch64-params.sh to download the parameter files. - - - -Set the path to downloaded parameter files and also the path to `zkutil` binary (if needed): - -``` -export NODE_CONFIG='{"snarkParamsPath": "path-to/snark-params/", "zkutil_bin": "/usr/bin/zkutil"}' -``` +Run the script monorepo/.github/scripts/download-6-8-2-3.sh to download the parameter files. Set the following env vars in `.env`: ``` # private key for decrypting messages -COORDINATOR_PK= +COORDINATOR_MACISK= # private key for interacting with contracts -COORDINATOR_ETH_PK= +WALLET_MNEMONIC= +WALLET_PRIVATE_KEY ``` Decrypt messages and tally the votes: ``` -yarn hardhat tally --network {network} --round-address {funding-round-address} --start-block {maci-contract-start-block} +HARDHAT_NETWORK= yarn ts-node cli/tally.ts --clrfund --start-block ``` If there's error and the tally task was stopped prematurely, it can be resumed by passing 2 additional parameters, '--maci-logs' and/or '--maci-state-file', if the files were generated. @@ -214,34 +45,27 @@ ipfs add tally.json Make sure you have the following env vars in `.env`. Ignore this if you are running a local test round in `localhost`, the script will know these values itself. ``` -FACTORY_ADDRESS= -COORDINATOR_ETH_PK= +# private key of the owner of the clrfund contract for interacting with the contract +WALLET_MNEMONIC= +WALLET_PRIVATE_KEY= ``` Once you have the `tally.json` from the tally script, run: ``` -yarn hardhat run --network {network} scripts/finalize.ts +HARDHAT_NETWORK= yarn ts-node cli/finalize.ts \ + --clrfund + --tally-file ``` # How to verify the tally results -Anyone can verify the tally results using the MACI cli or clrfund scripts. - -### Using MACI CLI - -Follow the steps in tallying votes to get the MACI cli, circuit parameters, and tally file, and verify using the following command: - -``` -node build/index.js verify -t tally.json -``` - -### Using clrfund scripts +Anyone can verify the tally results in the tally.json. From the clrfund contracts folder, run the following command to verify the result: ``` -yarn ts-node scripts/verify.ts tally.json +HARDHAT_NETWORK= yarn ts-node cli/verify.ts -f -t ``` # How to enable the leaderboard view diff --git a/package.json b/package.json index f1c00d285..2fbf1b1ef 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "build:web": "yarn workspace @clrfund/vue-app run build", "build:subgraph": "yarn workspace @clrfund/subgraph run codegen && yarn workspace @clrfund/subgraph run build", "start:dev": "yarn deploy:local && yarn start:subgraph && yarn start:web", - "start:registry": "yarn deploy:local-registry && yarn start:subgraph && yarn start:web", "start:node": "yarn workspace @clrfund/contracts run node", "start:web": "yarn workspace @clrfund/vue-app run serve", "start:subgraph": "yarn workspace @clrfund/subgraph run prepare:hardhat && yarn build:subgraph && yarn deploy:local-subgraph", @@ -46,11 +45,8 @@ "test:format": "yarn prettier --check", "test:lint-i18n": "echo yarn workspace @clrfund/vue-app run test:lint-i18n --ci", "deploy:subgraph": "yarn workspace @clrfund/subgraph run deploy", - "deploy:local": "yarn deploy:local-registry && yarn deploy:local-round", - "deploy:local-registry": "yarn workspace @clrfund/contracts run deploy:local", - "deploy:local-round": "yarn workspace @clrfund/contracts run deployTestRound:local", + "deploy:local": "yarn workspace @clrfund/contracts run deploy-local", "deploy:local-subgraph": "yarn workspace @clrfund/subgraph run create-local && yarn workspace @clrfund/subgraph run deploy-local", - "upgrade:local": "yarn workspace @clrfund/contracts run upgrade:local", "lint": "yarn workspaces run lint", "lint:contracts": "yarn workspace @clrfund/contracts run lint", "lint:web": "yarn workspace @clrfund/vue-app run lint", diff --git a/subgraph/abis/ClrFund.json b/subgraph/abis/ClrFund.json index a9b7eb499..a87993fb7 100644 --- a/subgraph/abis/ClrFund.json +++ b/subgraph/abis/ClrFund.json @@ -24,11 +24,6 @@ "name": "InvalidMaciFactory", "type": "error" }, - { - "inputs": [], - "name": "NoCoordinator", - "type": "error" - }, { "inputs": [], "name": "NoCurrentRound", @@ -36,32 +31,27 @@ }, { "inputs": [], - "name": "NoRecipientRegistry", - "type": "error" - }, - { - "inputs": [], - "name": "NoToken", + "name": "NotAuthorized", "type": "error" }, { "inputs": [], - "name": "NoUserRegistry", + "name": "NotFinalized", "type": "error" }, { "inputs": [], - "name": "NotAuthorized", + "name": "NotInitialized", "type": "error" }, { "inputs": [], - "name": "NotFinalized", + "name": "NotOwnerOfMaciFactory", "type": "error" }, { "inputs": [], - "name": "NotOwnerOfMaciFactory", + "name": "RecipientRegistryNotSet", "type": "error" }, { @@ -79,7 +69,27 @@ }, { "anonymous": false, - "inputs": [], + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_roundFactory", + "type": "address" + } + ], + "name": "FundingRoundFactoryChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_template", + "type": "address" + } + ], "name": "FundingRoundTemplateChanged", "type": "event" }, @@ -115,6 +125,19 @@ "name": "Initialized", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_maciFactory", + "type": "address" + } + ], + "name": "MaciFactoryChanged", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -136,8 +159,15 @@ }, { "anonymous": false, - "inputs": [], - "name": "RecipientRegistrySet", + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_recipientRegistry", + "type": "address" + } + ], + "name": "RecipientRegistryChanged", "type": "event" }, { @@ -181,10 +211,30 @@ }, { "anonymous": false, - "inputs": [], - "name": "UserRegistrySet", + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "_userRegistry", + "type": "address" + } + ], + "name": "UserRegistryChanged", "type": "event" }, + { + "inputs": [], + "name": "MESSAGE_DATA_LENGTH", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -261,7 +311,7 @@ "name": "getCurrentRound", "outputs": [ { - "internalType": "contract FundingRound", + "internalType": "contract IFundingRound", "name": "_currentRound", "type": "address" } @@ -306,12 +356,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "isInit", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "maciFactory", "outputs": [ { - "internalType": "contract MACIFactory", + "internalType": "contract IMACIFactory", "name": "", "type": "address" } @@ -383,7 +446,7 @@ "name": "roundFactory", "outputs": [ { - "internalType": "contract FundingRoundFactory", + "internalType": "contract IFundingRoundFactory", "name": "", "type": "address" } @@ -411,7 +474,7 @@ "type": "uint256" } ], - "internalType": "struct IPubKey.PubKey", + "internalType": "struct DomainObjs.PubKey", "name": "_coordinatorPubKey", "type": "tuple" } @@ -421,6 +484,32 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_roundFactory", + "type": "address" + } + ], + "name": "setFundingRoundFactory", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_maciFactory", + "type": "address" + } + ], + "name": "setMaciFactory", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/subgraph/config/arbitrum-sepolia.json b/subgraph/config/arbitrum-sepolia.json new file mode 100644 index 000000000..aee545b9a --- /dev/null +++ b/subgraph/config/arbitrum-sepolia.json @@ -0,0 +1,6 @@ +{ + "network": "arbitrum-sepolia", + "address": "0x22Ff798925A76B21f8122C04D10f177ea52D6411", + "clrFundStartBlock": 9007610, + "recipientRegistryStartBlock": 9007610 +} diff --git a/subgraph/config/hardhat.json b/subgraph/config/hardhat.json index 308788d1d..0520de5d5 100644 --- a/subgraph/config/hardhat.json +++ b/subgraph/config/hardhat.json @@ -1,6 +1,6 @@ { "network": "hardhat", - "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", - "factoryStartBlock": 0, + "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82", + "clrFundStartBlock": 0, "recipientRegistryStartBlock": 0 } diff --git a/subgraph/generated/ClrFund/ClrFund.ts b/subgraph/generated/ClrFund/ClrFund.ts index e89208f27..5358cdfc0 100644 --- a/subgraph/generated/ClrFund/ClrFund.ts +++ b/subgraph/generated/ClrFund/ClrFund.ts @@ -28,6 +28,24 @@ export class CoordinatorChanged__Params { } } +export class FundingRoundFactoryChanged extends ethereum.Event { + get params(): FundingRoundFactoryChanged__Params { + return new FundingRoundFactoryChanged__Params(this); + } +} + +export class FundingRoundFactoryChanged__Params { + _event: FundingRoundFactoryChanged; + + constructor(event: FundingRoundFactoryChanged) { + this._event = event; + } + + get _roundFactory(): Address { + return this._event.parameters[0].value.toAddress(); + } +} + export class FundingRoundTemplateChanged extends ethereum.Event { get params(): FundingRoundTemplateChanged__Params { return new FundingRoundTemplateChanged__Params(this); @@ -40,6 +58,10 @@ export class FundingRoundTemplateChanged__Params { constructor(event: FundingRoundTemplateChanged) { this._event = event; } + + get _template(): Address { + return this._event.parameters[0].value.toAddress(); + } } export class FundingSourceAdded extends ethereum.Event { @@ -92,6 +114,24 @@ export class Initialized__Params { } } +export class MaciFactoryChanged extends ethereum.Event { + get params(): MaciFactoryChanged__Params { + return new MaciFactoryChanged__Params(this); + } +} + +export class MaciFactoryChanged__Params { + _event: MaciFactoryChanged; + + constructor(event: MaciFactoryChanged) { + this._event = event; + } + + get _maciFactory(): Address { + return this._event.parameters[0].value.toAddress(); + } +} + export class OwnershipTransferred extends ethereum.Event { get params(): OwnershipTransferred__Params { return new OwnershipTransferred__Params(this); @@ -114,18 +154,22 @@ export class OwnershipTransferred__Params { } } -export class RecipientRegistrySet extends ethereum.Event { - get params(): RecipientRegistrySet__Params { - return new RecipientRegistrySet__Params(this); +export class RecipientRegistryChanged extends ethereum.Event { + get params(): RecipientRegistryChanged__Params { + return new RecipientRegistryChanged__Params(this); } } -export class RecipientRegistrySet__Params { - _event: RecipientRegistrySet; +export class RecipientRegistryChanged__Params { + _event: RecipientRegistryChanged; - constructor(event: RecipientRegistrySet) { + constructor(event: RecipientRegistryChanged) { this._event = event; } + + get _recipientRegistry(): Address { + return this._event.parameters[0].value.toAddress(); + } } export class RoundFinalized extends ethereum.Event { @@ -182,18 +226,22 @@ export class TokenChanged__Params { } } -export class UserRegistrySet extends ethereum.Event { - get params(): UserRegistrySet__Params { - return new UserRegistrySet__Params(this); +export class UserRegistryChanged extends ethereum.Event { + get params(): UserRegistryChanged__Params { + return new UserRegistryChanged__Params(this); } } -export class UserRegistrySet__Params { - _event: UserRegistrySet; +export class UserRegistryChanged__Params { + _event: UserRegistryChanged; - constructor(event: UserRegistrySet) { + constructor(event: UserRegistryChanged) { this._event = event; } + + get _userRegistry(): Address { + return this._event.parameters[0].value.toAddress(); + } } export class ClrFund__coordinatorPubKeyResult { @@ -226,6 +274,29 @@ export class ClrFund extends ethereum.SmartContract { return new ClrFund("ClrFund", address); } + MESSAGE_DATA_LENGTH(): i32 { + let result = super.call( + "MESSAGE_DATA_LENGTH", + "MESSAGE_DATA_LENGTH():(uint8)", + [] + ); + + return result[0].toI32(); + } + + try_MESSAGE_DATA_LENGTH(): ethereum.CallResult { + let result = super.tryCall( + "MESSAGE_DATA_LENGTH", + "MESSAGE_DATA_LENGTH():(uint8)", + [] + ); + if (result.reverted) { + return new ethereum.CallResult(); + } + let value = result.value; + return ethereum.CallResult.fromValue(value[0].toI32()); + } + coordinator(): Address { let result = super.call("coordinator", "coordinator():(address)", []); @@ -320,6 +391,21 @@ export class ClrFund extends ethereum.SmartContract { return ethereum.CallResult.fromValue(value[0].toBigInt()); } + isInit(): boolean { + let result = super.call("isInit", "isInit():(bool)", []); + + return result[0].toBoolean(); + } + + try_isInit(): ethereum.CallResult { + let result = super.tryCall("isInit", "isInit():(bool)", []); + if (result.reverted) { + return new ethereum.CallResult(); + } + let value = result.value; + return ethereum.CallResult.fromValue(value[0].toBoolean()); + } + maciFactory(): Address { let result = super.call("maciFactory", "maciFactory():(address)", []); @@ -667,6 +753,66 @@ export class SetCoordinatorCall_coordinatorPubKeyStruct extends ethereum.Tuple { } } +export class SetFundingRoundFactoryCall extends ethereum.Call { + get inputs(): SetFundingRoundFactoryCall__Inputs { + return new SetFundingRoundFactoryCall__Inputs(this); + } + + get outputs(): SetFundingRoundFactoryCall__Outputs { + return new SetFundingRoundFactoryCall__Outputs(this); + } +} + +export class SetFundingRoundFactoryCall__Inputs { + _call: SetFundingRoundFactoryCall; + + constructor(call: SetFundingRoundFactoryCall) { + this._call = call; + } + + get _roundFactory(): Address { + return this._call.inputValues[0].value.toAddress(); + } +} + +export class SetFundingRoundFactoryCall__Outputs { + _call: SetFundingRoundFactoryCall; + + constructor(call: SetFundingRoundFactoryCall) { + this._call = call; + } +} + +export class SetMaciFactoryCall extends ethereum.Call { + get inputs(): SetMaciFactoryCall__Inputs { + return new SetMaciFactoryCall__Inputs(this); + } + + get outputs(): SetMaciFactoryCall__Outputs { + return new SetMaciFactoryCall__Outputs(this); + } +} + +export class SetMaciFactoryCall__Inputs { + _call: SetMaciFactoryCall; + + constructor(call: SetMaciFactoryCall) { + this._call = call; + } + + get _maciFactory(): Address { + return this._call.inputValues[0].value.toAddress(); + } +} + +export class SetMaciFactoryCall__Outputs { + _call: SetMaciFactoryCall; + + constructor(call: SetMaciFactoryCall) { + this._call = call; + } +} + export class SetRecipientRegistryCall extends ethereum.Call { get inputs(): SetRecipientRegistryCall__Inputs { return new SetRecipientRegistryCall__Inputs(this); diff --git a/subgraph/generated/schema.ts b/subgraph/generated/schema.ts index 4aa73bc1b..f611654e8 100644 --- a/subgraph/generated/schema.ts +++ b/subgraph/generated/schema.ts @@ -314,74 +314,6 @@ export class ClrFund extends Entity { } } - get batchUstVerifier(): Bytes | null { - let value = this.get("batchUstVerifier"); - if (!value || value.kind == ValueKind.NULL) { - return null; - } else { - return value.toBytes(); - } - } - - set batchUstVerifier(value: Bytes | null) { - if (!value) { - this.unset("batchUstVerifier"); - } else { - this.set("batchUstVerifier", Value.fromBytes(value)); - } - } - - get qvtVerifier(): Bytes | null { - let value = this.get("qvtVerifier"); - if (!value || value.kind == ValueKind.NULL) { - return null; - } else { - return value.toBytes(); - } - } - - set qvtVerifier(value: Bytes | null) { - if (!value) { - this.unset("qvtVerifier"); - } else { - this.set("qvtVerifier", Value.fromBytes(value)); - } - } - - get signUpDuration(): BigInt | null { - let value = this.get("signUpDuration"); - if (!value || value.kind == ValueKind.NULL) { - return null; - } else { - return value.toBigInt(); - } - } - - set signUpDuration(value: BigInt | null) { - if (!value) { - this.unset("signUpDuration"); - } else { - this.set("signUpDuration", Value.fromBigInt(value)); - } - } - - get votingDuration(): BigInt | null { - let value = this.get("votingDuration"); - if (!value || value.kind == ValueKind.NULL) { - return null; - } else { - return value.toBigInt(); - } - } - - set votingDuration(value: BigInt | null) { - if (!value) { - this.unset("votingDuration"); - } else { - this.set("votingDuration", Value.fromBigInt(value)); - } - } - get maxUsers(): BigInt | null { let value = this.get("maxUsers"); if (!value || value.kind == ValueKind.NULL) { diff --git a/subgraph/graph-node/.gitignore b/subgraph/graph-node/.gitignore new file mode 100644 index 000000000..1269488f7 --- /dev/null +++ b/subgraph/graph-node/.gitignore @@ -0,0 +1 @@ +data diff --git a/subgraph/graph-node/README.md b/subgraph/graph-node/README.md new file mode 100644 index 000000000..e08120a10 --- /dev/null +++ b/subgraph/graph-node/README.md @@ -0,0 +1,9 @@ +# Graph Node Docker Image + +Preconfigured Docker image for running a Graph Node. + +To start a graph node for development of clrfund. Make sure to start a local hardhat node first (`yarn start:node` from the root folder). + +```sh +docker compose up -d +``` diff --git a/subgraph/graph-node/docker-compose.yml b/subgraph/graph-node/docker-compose.yml new file mode 100644 index 000000000..2265944ab --- /dev/null +++ b/subgraph/graph-node/docker-compose.yml @@ -0,0 +1,50 @@ +version: '3' +services: + graph-node: + image: graphprotocol/graph-node + ports: + - '8000:8000' + - '8001:8001' + - '8020:8020' + - '8030:8030' + - '8040:8040' + depends_on: + - ipfs + - postgres + extra_hosts: + - host.docker.internal:host-gateway + environment: + postgres_host: postgres + postgres_user: graph-node + postgres_pass: let-me-in + postgres_db: graph-node + ipfs: 'ipfs:5001' + ethereum: 'hardhat:http://host.docker.internal:18545' + GRAPH_LOG: info + ipfs: + image: ipfs/kubo:v0.14.0 + ports: + - '5001:5001' + volumes: + - ./data/ipfs:/data/ipfs + postgres: + image: postgres:14 + ports: + - '5432:5432' + command: + [ + "postgres", + "-cshared_preload_libraries=pg_stat_statements", + "-cmax_connections=200" + ] + environment: + POSTGRES_USER: graph-node + POSTGRES_PASSWORD: let-me-in + POSTGRES_DB: graph-node + # FIXME: remove this env. var. which we shouldn't need. Introduced by + # , maybe as a + # workaround for https://github.com/docker/for-mac/issues/6270? + PGDATA: "/var/lib/postgresql/data" + POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C" + volumes: + - ./data/postgres:/var/lib/postgresql/data diff --git a/subgraph/package.json b/subgraph/package.json index f71fcfdc3..1efeb5db7 100644 --- a/subgraph/package.json +++ b/subgraph/package.json @@ -26,7 +26,7 @@ "deploy": "graph deploy --node https://api.thegraph.com/deploy/ --ipfs https://api.thegraph.com/ipfs/ clrfund/clrfund", "create-local": "graph create --node http://localhost:8020/ clrfund/clrfund", "remove-local": "graph remove --node http://localhost:8020/ clrfund/clrfund", - "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 clrfund/clrfund", + "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 clrfund/clrfund --version-label=v0.0.1", "gen-uml": "graphqlviz https://api.thegraph.com/subgraphs/name/clrfund/clrfund | dot -Tpng -o subgraphUML.png" }, "dependencies": { diff --git a/subgraph/schema.graphql b/subgraph/schema.graphql index 1f86045f5..3465701bb 100644 --- a/subgraph/schema.graphql +++ b/subgraph/schema.graphql @@ -17,10 +17,6 @@ type ClrFund @entity { voteOptionTreeDepth: BigInt tallyBatchSize: BigInt messageBatchSize: BigInt - batchUstVerifier: Bytes - qvtVerifier: Bytes - signUpDuration: BigInt - votingDuration: BigInt maxUsers: BigInt maxMessages: BigInt diff --git a/subgraph/src/ClrFundMapping.ts b/subgraph/src/ClrFundMapping.ts index e5e8da56a..bcabfedcb 100644 --- a/subgraph/src/ClrFundMapping.ts +++ b/subgraph/src/ClrFundMapping.ts @@ -7,6 +7,8 @@ import { RoundFinalized, RoundStarted, TokenChanged, + UserRegistryChanged, + RecipientRegistryChanged, ClrFund as ClrFundContract, } from '../generated/ClrFund/ClrFund' @@ -309,3 +311,19 @@ export function handleTokenChanged(event: TokenChanged): void { log.info('handleTokenChanged {}', [event.params._token.toHexString()]) createOrUpdateClrFund(event.address, event.block.timestamp) } + +export function handleRecipientRegistryChanged( + event: RecipientRegistryChanged +): void { + log.info('handleRecipientRegistryChanged {}', [ + event.params._recipientRegistry.toHexString(), + ]) + createOrUpdateClrFund(event.address, event.block.timestamp) +} + +export function handleUserRegistryChanged(event: UserRegistryChanged): void { + log.info('handleUserRegistryChanged {}', [ + event.params._userRegistry.toHexString(), + ]) + createOrUpdateClrFund(event.address, event.block.timestamp) +} diff --git a/subgraph/src/MACIMapping.ts b/subgraph/src/MACIMapping.ts index cbc2c17d7..2b8e98a0f 100644 --- a/subgraph/src/MACIMapping.ts +++ b/subgraph/src/MACIMapping.ts @@ -1,4 +1,4 @@ -import { log, ByteArray, crypto, BigInt } from '@graphprotocol/graph-ts' +import { log } from '@graphprotocol/graph-ts' import { SignUp } from '../generated/templates/MACI/MACI' import { FundingRound, PublicKey } from '../generated/schema' @@ -21,7 +21,11 @@ import { makePublicKeyId } from './PublicKey' // - contract.verifier(...) export function handleSignUp(event: SignUp): void { + let fundingRoundAddress = event.transaction.to! + let fundingRoundId = fundingRoundAddress.toHex() + let publicKeyId = makePublicKeyId( + fundingRoundId, event.params._userPubKey.x, event.params._userPubKey.y ) @@ -34,11 +38,8 @@ export function handleSignUp(event: SignUp): void { publicKey.x = event.params._userPubKey.x publicKey.y = event.params._userPubKey.y publicKey.stateIndex = event.params._stateIndex - publicKey.voiceCreditBalance = event.params._voiceCreditBalance - let fundingRoundAddress = event.transaction.to! - let fundingRoundId = fundingRoundAddress.toHex() let fundingRound = FundingRound.load(fundingRoundId) if (fundingRound == null) { log.error('Error: handleSignUp failed, fundingRound not registered', []) diff --git a/subgraph/src/PollMapping.ts b/subgraph/src/PollMapping.ts index 59360c3c3..867d023a1 100644 --- a/subgraph/src/PollMapping.ts +++ b/subgraph/src/PollMapping.ts @@ -1,4 +1,4 @@ -import { log, ByteArray, crypto, BigInt } from '@graphprotocol/graph-ts' +import { log } from '@graphprotocol/graph-ts' import { PublishMessage } from '../generated/templates/Poll/Poll' import { FundingRound, Message, PublicKey } from '../generated/schema' @@ -37,6 +37,7 @@ export function handlePublishMessage(event: PublishMessage): void { message.submittedBy = event.transaction.from let publicKeyId = makePublicKeyId( + fundingRoundId, event.params._encPubKey.x, event.params._encPubKey.y ) diff --git a/subgraph/src/PublicKey.ts b/subgraph/src/PublicKey.ts index 9859dfe8e..d34f33706 100644 --- a/subgraph/src/PublicKey.ts +++ b/subgraph/src/PublicKey.ts @@ -1,12 +1,17 @@ import { ByteArray, crypto, BigInt } from '@graphprotocol/graph-ts' -import { FundingRound, PublicKey } from '../generated/schema' // Create the PublicKey entity id used in subgraph // using MACI public key x and y values -export function makePublicKeyId(x: BigInt, y: BigInt): string { +export function makePublicKeyId( + fundingRoundId: string, + x: BigInt, + y: BigInt +): string { let publicKeyX = x.toString() let publicKeyY = y.toString() - let publicKeyXY = ByteArray.fromUTF8(publicKeyX + '.' + publicKeyY) + let publicKeyXY = ByteArray.fromUTF8( + fundingRoundId + '.' + publicKeyX + '.' + publicKeyY + ) let publicKeyId = crypto.keccak256(publicKeyXY).toHexString() return publicKeyId } diff --git a/subgraph/subgraph.template.yaml b/subgraph/subgraph.template.yaml index de35fe47f..1b8bf245b 100644 --- a/subgraph/subgraph.template.yaml +++ b/subgraph/subgraph.template.yaml @@ -53,6 +53,10 @@ dataSources: handler: handleRoundStarted - event: TokenChanged(address) handler: handleTokenChanged + - event: UserRegistryChanged(address) + handler: handleUserRegistryChanged + - event: RecipientRegistryChanged(address) + handler: handleRecipientRegistryChanged file: ./src/ClrFundMapping.ts - kind: ethereum/contract name: OptimisticRecipientRegistry diff --git a/subgraph/subgraph.yaml b/subgraph/subgraph.yaml index 815835a5e..0a5b17677 100644 --- a/subgraph/subgraph.yaml +++ b/subgraph/subgraph.yaml @@ -53,6 +53,10 @@ dataSources: handler: handleRoundStarted - event: TokenChanged(address) handler: handleTokenChanged + - event: UserRegistryChanged(address) + handler: handleUserRegistryChanged + - event: RecipientRegistryChanged(address) + handler: handleRecipientRegistryChanged file: ./src/ClrFundMapping.ts - kind: ethereum/contract name: OptimisticRecipientRegistry diff --git a/vue-app/.env.example b/vue-app/.env.example index e74e2fcbc..7686aea11 100644 --- a/vue-app/.env.example +++ b/vue-app/.env.example @@ -19,7 +19,7 @@ VITE_IPFS_SECRET_API_KEY= VITE_SUBGRAPH_URL=http://localhost:8000/subgraphs/name/clrfund/clrfund -VITE_CLRFUND_ADDRESS=0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 +VITE_CLRFUND_ADDRESS=0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82 # Supported values: simple, brightid, snapshot, merkle VITE_USER_REGISTRY_TYPE=simple diff --git a/vue-app/package.json b/vue-app/package.json index c263e342b..b580674e6 100644 --- a/vue-app/package.json +++ b/vue-app/package.json @@ -32,7 +32,7 @@ "@walletconnect/modal": "^2.6.0", "crypto-js": "^4.1.1", "ethereum-blockies-base64": "^1.0.2", - "ethers": "^5.7.2", + "ethers": "^6.9.2", "floating-vue": "^2.0.0-beta.20", "google-spreadsheet": "^3.3.0", "graphql": "^16.6.0", diff --git a/vue-app/src/App.vue b/vue-app/src/App.vue index 665c833b0..e981f5053 100644 --- a/vue-app/src/App.vue +++ b/vue-app/src/App.vue @@ -166,13 +166,18 @@ onMounted(async () => { } appReady.value = true - await appStore.loadClrFundInfo() - await appStore.loadMACIFactoryInfo() - await appStore.loadRoundInfo() - await recipientStore.loadRecipientRegistryInfo() - appStore.isAppReady = true + try { + await appStore.loadClrFundInfo() + await appStore.loadMACIFactoryInfo() + await appStore.loadRoundInfo() + await recipientStore.loadRecipientRegistryInfo() + appStore.isAppReady = true - setupLoadIntervals() + setupLoadIntervals() + } catch (err) { + /* eslint-disable-next-line no-console */ + console.warn('Failed to load application data:', err) + } }) onBeforeUnmount(() => { diff --git a/vue-app/src/api/abi.ts b/vue-app/src/api/abi.ts index 9add92259..41a05e8c3 100644 --- a/vue-app/src/api/abi.ts +++ b/vue-app/src/api/abi.ts @@ -2,8 +2,8 @@ import { abi as ERC20 } from '../../../contracts/build/contracts/@openzeppelin/c import { abi as ClrFund } from '../../../contracts/build/contracts/contracts/ClrFund.sol/ClrFund.json' import { abi as FundingRound } from '../../../contracts/build/contracts/contracts/FundingRound.sol/FundingRound.json' import { abi as MACIFactory } from '../../../contracts/build/contracts/contracts/MACIFactory.sol/MACIFactory.json' -import { abi as MACI } from '../../../contracts/build/contracts/@clrfund/maci-contracts/contracts/MACI.sol/MACI.json' -import { abi as Poll } from '../../../contracts/build/contracts/@clrfund/maci-contracts/contracts/Poll.sol/Poll.json' +import { abi as MACI } from '../../../contracts/build/contracts/maci-contracts/contracts/MACI.sol/MACI.json' +import { abi as Poll } from '../../../contracts/build/contracts/maci-contracts/contracts/Poll.sol/Poll.json' import { abi as UserRegistry } from '../../../contracts/build/contracts/contracts/userRegistry/IUserRegistry.sol/IUserRegistry.json' import { abi as BrightIdUserRegistry } from '../../../contracts/build/contracts/contracts/userRegistry/BrightIdUserRegistry.sol/BrightIdUserRegistry.json' import { abi as SnapshotUserRegistry } from '../../../contracts/build/contracts/contracts/userRegistry/SnapshotUserRegistry.sol/SnapshotUserRegistry.json' diff --git a/vue-app/src/api/bright-id.ts b/vue-app/src/api/bright-id.ts index 6e051150e..0aba9a8ad 100644 --- a/vue-app/src/api/bright-id.ts +++ b/vue-app/src/api/bright-id.ts @@ -1,6 +1,5 @@ -import { Contract, Signer, utils } from 'ethers' -import type { TransactionResponse } from '@ethersproject/abstract-provider' -import { formatBytes32String } from '@ethersproject/strings' +import { Contract, encodeBytes32String, toUtf8Bytes, decodeBase64, encodeBase64 } from 'ethers' +import type { TransactionResponse, Signer } from 'ethers' import { BrightIdUserRegistry } from './abi' import { brightIdSponsorKey, brightIdNodeUrl } from './core' @@ -138,7 +137,7 @@ export async function registerUser( ): Promise { const registry = new Contract(registryAddress, BrightIdUserRegistry, signer) const transaction = await registry.register( - formatBytes32String(CONTEXT), + encodeBytes32String(CONTEXT), verification.appUserId, '0x' + verification.verificationHash, verification.timestamp, @@ -221,10 +220,10 @@ export async function brightIdSponsor(userAddress: string): Promise } const message = JSON.stringify(op) - const arrayedMessage = utils.toUtf8Bytes(message) - const arrayedKey = utils.base64.decode(brightIdSponsorKey) + const arrayedMessage = toUtf8Bytes(message) + const arrayedKey = decodeBase64(brightIdSponsorKey) const signature = nacl.sign.detached(arrayedMessage, arrayedKey) - op.sig = utils.base64.encode(signature) + op.sig = encodeBase64(signature) const res = await fetch(endpoint, { method: 'POST', diff --git a/vue-app/src/api/cart.ts b/vue-app/src/api/cart.ts index f5935763b..c95993530 100644 --- a/vue-app/src/api/cart.ts +++ b/vue-app/src/api/cart.ts @@ -1,7 +1,6 @@ import { type CartItem, getContributorMessages } from './contributions' import type { RoundInfo } from './round' import { Keypair, Command } from '@clrfund/common' -import { BigNumber } from 'ethers' import { getProjectByIndex } from './projects' import { formatAmount } from '@/utils/amounts' import { maxDecimals } from './core' @@ -37,7 +36,7 @@ export async function getCommittedCart( const { voteOptionIndex, newVoteWeight } = command const voteWeightString = newVoteWeight.toString() - const amount = BigNumber.from(voteWeightString).mul(voteWeightString).mul(voiceCreditFactor) + const amount = BigInt(voteWeightString) * BigInt(voteWeightString) * voiceCreditFactor const project = await getProjectByIndex(recipientRegistryAddress, Number(voteOptionIndex)) @@ -49,7 +48,7 @@ export async function getCommittedCart( // cannot be reduced, isCleared is used to mark deleted items return { amount: formatAmount(amount, nativeTokenDecimals, null, maxDecimals), - isCleared: amount.isZero(), + isCleared: amount === 0n, ...project, } }) diff --git a/vue-app/src/api/claims.ts b/vue-app/src/api/claims.ts index 82da4d757..bf76eff3a 100644 --- a/vue-app/src/api/claims.ts +++ b/vue-app/src/api/claims.ts @@ -1,4 +1,4 @@ -import { Contract, BigNumber } from 'ethers' +import { Contract } from 'ethers' import sdk from '@/graphql/sdk' import { FundingRound } from './abi' @@ -9,7 +9,7 @@ export async function getAllocatedAmount( tokenDecimals: number, result: string, spent: string, -): Promise { +): Promise { const fundingRound = new Contract(fundingRoundAddress, FundingRound, provider) const allocatedAmount = await fundingRound.getAllocatedAmount(result, spent) return allocatedAmount diff --git a/vue-app/src/api/clrFund.ts b/vue-app/src/api/clrFund.ts index f9e28b3aa..80de3acdc 100644 --- a/vue-app/src/api/clrFund.ts +++ b/vue-app/src/api/clrFund.ts @@ -1,5 +1,4 @@ -import { BigNumber } from 'ethers' -import { clrFundContract } from './core' +import { clrfundContractAddress, clrFundContract } from './core' import sdk from '@/graphql/sdk' export interface ClrFund { @@ -8,20 +7,20 @@ export interface ClrFund { nativeTokenDecimals: number userRegistryAddress: string recipientRegistryAddress: string - matchingPool: BigNumber + matchingPool: bigint } export async function getClrFundInfo() { let nativeTokenAddress = '' let nativeTokenSymbol = '' let nativeTokenDecimals = 0 - let matchingPool = BigNumber.from(0) + let matchingPool = BigInt(0) let userRegistryAddress = '' let recipientRegistryAddress = '' try { const data = await sdk.GetClrFundInfo({ - clrFundAddress: clrFundContract.address.toLowerCase(), + clrFundAddress: clrfundContractAddress.toLowerCase(), }) const nativeTokenInfo = data.clrFund?.nativeTokenInfo @@ -55,7 +54,7 @@ export async function getClrFundInfo() { } } -export async function getMatchingFunds(nativeTokenAddress: string): Promise { +export async function getMatchingFunds(nativeTokenAddress: string): Promise { const matchingFunds = await clrFundContract.getMatchingFunds(nativeTokenAddress) return matchingFunds } diff --git a/vue-app/src/api/contributions.ts b/vue-app/src/api/contributions.ts index 74bbafba9..fbca2bb3f 100644 --- a/vue-app/src/api/contributions.ts +++ b/vue-app/src/api/contributions.ts @@ -1,8 +1,6 @@ -import { BigNumber, Contract, Signer, FixedNumber } from 'ethers' -import { parseFixed } from '@ethersproject/bignumber' - -import type { TransactionResponse } from '@ethersproject/abstract-provider' -import { Keypair, PubKey, PrivKey, Message, Command, getPubKeyId } from '@clrfund/common' +import { Contract, FixedNumber, parseUnits, id } from 'ethers' +import type { TransactionResponse, Signer } from 'ethers' +import { Keypair, PubKey, PrivKey, Message, Command } from '@clrfund/common' import type { RoundInfo } from './round' import { FundingRound, ERC20 } from './abi' @@ -27,6 +25,17 @@ export interface Contributor { stateIndex: number } +/** + * get the id of the subgraph public key entity from the pubKey value + * @param fundingRoundAddress funding round address + * @param pubKey MACI public key + * @returns the id for the subgraph public key entity + */ +function getPubKeyId(fundingRoundAddress = '', pubKey: PubKey): string { + const pubKeyPair = pubKey.asContractParam() + return id(fundingRoundAddress.toLowerCase() + '.' + pubKeyPair.x + '.' + pubKeyPair.y) +} + export function getCartStorageKey(roundAddress: string): string { return `cart-${roundAddress.toLowerCase()}` } @@ -61,19 +70,16 @@ export function serializeContributorData(contributor: Contributor): string { export function deserializeContributorData(data: string | null): Contributor | null { if (data) { const parsed = JSON.parse(data) - const keypair = new Keypair(PrivKey.unserialize(parsed.privateKey)) + const keypair = new Keypair(PrivKey.deserialize(parsed.privateKey)) return { keypair, stateIndex: parsed.stateIndex } } else { return null } } -export async function getContributionAmount( - fundingRoundAddress: string, - contributorAddress: string, -): Promise { +export async function getContributionAmount(fundingRoundAddress: string, contributorAddress: string): Promise { if (!fundingRoundAddress) { - return BigNumber.from(0) + return 0n } const data = await sdk.GetContributionsAmount({ fundingRoundAddress: fundingRoundAddress.toLowerCase(), @@ -81,13 +87,13 @@ export async function getContributionAmount( }) if (!data.contributions.length) { - return BigNumber.from(0) + return 0n } - return BigNumber.from(data.contributions[0].amount) + return BigInt(data.contributions[0].amount) } -export async function getTotalContributed(fundingRoundAddress: string): Promise<{ count: number; amount: BigNumber }> { +export async function getTotalContributed(fundingRoundAddress: string): Promise<{ count: number; amount: bigint }> { const nativeTokenAddress = await clrFundContract.nativeToken() const nativeToken = new Contract(nativeTokenAddress, ERC20, provider) const balance = await nativeToken.balanceOf(fundingRoundAddress) @@ -97,7 +103,7 @@ export async function getTotalContributed(fundingRoundAddress: string): Promise< }) if (!data.fundingRound?.contributorCount) { - return { count: 0, amount: BigNumber.from(0) } + return { count: 0, amount: 0n } } const count = parseInt(data.fundingRound.contributorCount) @@ -128,19 +134,16 @@ export function isContributionAmountValid(value: string, currentRound: RoundInfo // return true // } const { nativeTokenDecimals, voiceCreditFactor } = currentRound - let amount + let amount: bigint try { - amount = parseFixed(value, nativeTokenDecimals) + amount = parseUnits(value, nativeTokenDecimals) } catch { return false } - if (amount.lte(BigNumber.from(0))) { + if (amount <= 0n) { return false } - const normalizedValue = FixedNumber.fromValue( - amount.div(voiceCreditFactor).mul(voiceCreditFactor), - nativeTokenDecimals, - ) + const normalizedValue = FixedNumber.fromValue((amount / voiceCreditFactor) * voiceCreditFactor, nativeTokenDecimals) .toUnsafeFloat() .toString() return normalizedValue === value @@ -156,7 +159,7 @@ export async function getContributorIndex(fundingRoundAddress: string, pubKey: P if (!fundingRoundAddress) { return null } - const id = getPubKeyId(pubKey) + const id = getPubKeyId(fundingRoundAddress, pubKey) const data = await sdk.GetContributorIndex({ fundingRoundAddress: fundingRoundAddress.toLowerCase(), publicKeyId: id, @@ -191,7 +194,7 @@ export async function getContributorMessages({ return [] } - const key = getPubKeyId(contributorKey.pubKey) + const key = getPubKeyId(fundingRoundAddress, contributorKey.pubKey) const result = await sdk.GetContributorMessages({ fundingRoundAddress: fundingRoundAddress.toLowerCase(), pubKey: key, @@ -207,10 +210,10 @@ export async function getContributorMessages({ let latestTransaction: Transaction | null = null const latestMessages = result.messages .filter(message => { - const { iv, data, blockNumber, transactionIndex } = message + const { msgType, data, blockNumber, transactionIndex } = message try { - const maciMessage = new Message(iv, data || []) + const maciMessage = new Message(BigInt(msgType), data || []) const { command, signature } = Command.decrypt(maciMessage, sharedKey) if (!command.verifySignature(signature, contributorKey.pubKey)) { // Not signed by this user, filter it out @@ -240,8 +243,8 @@ export async function getContributorMessages({ return latestTransaction && tx.compare(latestTransaction) === 0 }) .map(message => { - const { iv, data } = message - return new Message(iv, data || []) + const { msgType, data } = message + return new Message(BigInt(msgType), data || []) }) return latestMessages diff --git a/vue-app/src/api/core.ts b/vue-app/src/api/core.ts index 9f5049ca2..4aa23cf68 100644 --- a/vue-app/src/api/core.ts +++ b/vue-app/src/api/core.ts @@ -1,4 +1,4 @@ -import { ethers } from 'ethers' +import { JsonRpcProvider, Contract } from 'ethers' import { ClrFund } from './abi' import { CHAIN_INFO } from '@/utils/chains' @@ -16,8 +16,8 @@ if (!walletConnectProjectId) { throw new Error('Please provide wallet connect project id') } -export const mainnetProvider = new ethers.providers.StaticJsonRpcProvider(import.meta.env.VITE_ETHEREUM_MAINNET_API_URL) -export const provider = new ethers.providers.StaticJsonRpcProvider(rpcUrl) +export const mainnetProvider = new JsonRpcProvider(import.meta.env.VITE_ETHEREUM_MAINNET_API_URL) +export const provider = new JsonRpcProvider(rpcUrl) export const chainId = Number(import.meta.env.VITE_ETHEREUM_API_CHAINID) export const chain = CHAIN_INFO[chainId] if (!chain) throw new Error('invalid chain id') @@ -37,7 +37,8 @@ if (!ipfsPinningJwt && !(ipfsApiKey && ipfsSecretApiKey)) { //TODO: need to be able to pass the clrfund contract address dynamically, note all places this is used make clrfund address a parameter that defaults to the env. variable set //NOTE: these calls will be replaced by subgraph queries eventually. -export const clrFundContract = new ethers.Contract(import.meta.env.VITE_CLRFUND_ADDRESS as string, ClrFund, provider) +export const clrFundContract = new Contract(import.meta.env.VITE_CLRFUND_ADDRESS as string, ClrFund, provider) +export const clrfundContractAddress = import.meta.env.VITE_CLRFUND_ADDRESS as string export const userRegistryType = import.meta.env.VITE_USER_REGISTRY_TYPE export enum UserRegistryType { BRIGHT_ID = 'brightid', diff --git a/vue-app/src/api/maci-factory.ts b/vue-app/src/api/maci-factory.ts index 9dfd1bd1f..d8ecc9f96 100644 --- a/vue-app/src/api/maci-factory.ts +++ b/vue-app/src/api/maci-factory.ts @@ -1,4 +1,4 @@ -import { Contract } from 'ethers' +import { Contract, getNumber } from 'ethers' import { MACIFactory as MACIFactoryABI } from './abi' import { clrFundContract, provider } from './core' @@ -15,6 +15,6 @@ export async function getMACIFactoryInfo(): Promise { return { maciFactoryAddress, - maxRecipients: 5 ** treeDepths.voteOptionTreeDepth - 1, + maxRecipients: 5 ** getNumber(treeDepths.voteOptionTreeDepth) - 1, } } diff --git a/vue-app/src/api/projects.ts b/vue-app/src/api/projects.ts index 1733dcce6..22a2b1d46 100644 --- a/vue-app/src/api/projects.ts +++ b/vue-app/src/api/projects.ts @@ -1,5 +1,5 @@ -import { BigNumber, Contract, Signer } from 'ethers' -import type { TransactionResponse } from '@ethersproject/abstract-provider' +import { Contract, Interface, getNumber } from 'ethers' +import type { TransactionResponse, Signer } from 'ethers' import { FundingRound, OptimisticRecipientRegistry } from './abi' import { clrFundContract, provider, recipientRegistryType, ipfsGatewayUrl } from './core' @@ -9,7 +9,6 @@ import KlerosRegistry from './recipient-registry-kleros' import sdk from '@/graphql/sdk' import { getLeaderboardData } from '@/api/leaderboard' import type { RecipientApplicationData } from '@/api/types' -import { getEventArg } from '@/utils/contracts' export interface LeaderboardProject { id: string // Address or another ID depending on registry implementation @@ -18,9 +17,9 @@ export interface LeaderboardProject { bannerImageUrl?: string thumbnailImageUrl?: string imageUrl?: string - allocatedAmount: BigNumber - votes: BigNumber - donation: BigNumber + allocatedAmount: bigint + votes: bigint + donation: bigint } export interface Project { @@ -158,15 +157,22 @@ export async function getProjectByIndex( export async function getRecipientIdByHash(transactionHash: string): Promise { try { const receipt = await provider.getTransactionReceipt(transactionHash) + if (!receipt) { + return null + } + const eventName = 'RequestSubmitted' + const argumentName = '_recipientId' + const registryInterface = new Interface(OptimisticRecipientRegistry) // should only have 1 event, just in case, return the first matching event for (const log of receipt.logs) { - const registry = new Contract(log.address, OptimisticRecipientRegistry, provider) - try { - const recipientId = getEventArg(receipt, registry, 'RequestSubmitted', '_recipientId') - return recipientId - } catch { - // try next log + const event = registryInterface.parseLog({ + data: log.data, + topics: [...log.topics], + }) + // eslint-disable-next-line + if (event && event.name === eventName) { + return event.args[argumentName] } } } catch { @@ -180,11 +186,11 @@ export function toLeaderboardProject(project: any): LeaderboardProject { return { id: project.id, name: project.name, - index: project.recipientIndex, + index: getNumber(project.recipientIndex), imageUrl, - allocatedAmount: BigNumber.from(project.allocatedAmount || '0'), - votes: BigNumber.from(project.tallyResult || '0'), - donation: BigNumber.from(project.spentVoiceCredits || '0'), + allocatedAmount: BigInt(project.allocatedAmount || '0'), + votes: BigInt(project.tallyResult || '0'), + donation: BigInt(project.spentVoiceCredits || '0'), } } diff --git a/vue-app/src/api/recipient-registry-kleros.ts b/vue-app/src/api/recipient-registry-kleros.ts index 2a260ebf8..7ab229938 100644 --- a/vue-app/src/api/recipient-registry-kleros.ts +++ b/vue-app/src/api/recipient-registry-kleros.ts @@ -1,5 +1,5 @@ -import { Contract, type Event, Signer, BigNumber } from 'ethers' -import type { TransactionResponse } from '@ethersproject/abstract-provider' +import { Contract, toNumber } from 'ethers' +import type { TransactionResponse, Signer, EventLog } from 'ethers' import { gtcrDecode } from '@kleros/gtcr-encoder' import { KlerosGTCR, KlerosGTCRAdapter } from './abi' @@ -26,7 +26,7 @@ async function getTcrColumns(tcr: Contract): Promise { const metaEvidenceEvents = await tcr.queryFilter(metaEvidenceFilter, 0) // Take last event with even index const regMetaEvidenceEvent = metaEvidenceEvents[metaEvidenceEvents.length - 2] - const ipfsPath = (regMetaEvidenceEvent.args as any)._evidence + const ipfsPath = (regMetaEvidenceEvent as EventLog).args._evidence const tcrDataResponse = await fetch(`${ipfsGatewayUrl}${ipfsPath}`) const tcrData = await tcrDataResponse.json() return tcrData.metadata.columns @@ -56,7 +56,7 @@ function decodeTcrItemData( } } -function decodeRecipientAdded(event: Event, columns: TcrColumn[]): Project { +function decodeRecipientAdded(event: EventLog, columns: TcrColumn[]): Project { const args = event.args as any return { id: args._tcrItemId, @@ -78,17 +78,17 @@ export async function getProjects(registryAddress: string, startTime?: number, e const recipientRemovedEvents = await registry.queryFilter(recipientRemovedFilter, 0) const projects: Project[] = [] for (const event of recipientAddedEvents) { - const project = decodeRecipientAdded(event, tcrColumns) - const addedAt = (event.args as any)._timestamp.toNumber() + const project = decodeRecipientAdded(event as EventLog, tcrColumns) + const addedAt = toNumber((event as EventLog).args._timestamp) if (endTime && addedAt >= endTime) { // Hide recipient if it is added after the end of round. project.isHidden = true } const removed = recipientRemovedEvents.find(event => { - return (event.args as any)._tcrItemId === project.id + return (event as EventLog).args._tcrItemId === project.id }) if (removed) { - const removedAt = (removed.args as any)._timestamp.toNumber() + const removedAt = toNumber((removed as EventLog).args._timestamp) if (!startTime || removedAt <= startTime) { // Start time not specified // or recipient had been removed before start time @@ -106,7 +106,7 @@ export async function getProjects(registryAddress: string, startTime?: number, e const tcrItemSubmittedFilter = tcr.filters.ItemSubmitted() const tcrItemSubmittedEvents = await tcr.queryFilter(tcrItemSubmittedFilter, 0) for (const event of tcrItemSubmittedEvents) { - const tcrItemId = (event.args as any)._itemID + const tcrItemId = (event as EventLog).args._itemID const registered = projects.find(item => item.id === tcrItemId) if (registered) { // Already registered (or registered and removed) @@ -151,7 +151,7 @@ export async function getProject(registryAddress: string, recipientId: string): isHidden: false, isLocked: false, extra: { - tcrItemStatus: tcrItemStatus.toNumber(), + tcrItemStatus: toNumber(tcrItemStatus), tcrItemUrl: `${KLEROS_CURATE_URL}/${recipientId}`, }, } @@ -159,7 +159,7 @@ export async function getProject(registryAddress: string, recipientId: string): const recipientAddedEvents = await registry.queryFilter(recipientAddedFilter, 0) if (recipientAddedEvents.length !== 0) { const recipientAddedEvent = recipientAddedEvents[0] - project.index = (recipientAddedEvent.args as any)._index.toNumber() + project.index = toNumber((recipientAddedEvent as EventLog).args._index) } const recipientRemovedFilter = registry.filters.RecipientRemoved(recipientId) const recipientRemovedEvents = await registry.queryFilter(recipientRemovedFilter, 0) @@ -190,7 +190,7 @@ async function getRegistryInfo(registryAddress: string): Promise { // older BaseRecipientRegistry contract did not have recipientCount // set it to zero as this information is only // used during current round for space calculation - recipientCount = BigNumber.from(0) + recipientCount = BigInt(0) } // Kleros registry does not have owner @@ -198,7 +198,7 @@ async function getRegistryInfo(registryAddress: string): Promise { // deposit, depositToken and challengePeriodDuration are only relevant to the optimistic registry return { - deposit: BigNumber.from(0), + deposit: BigInt(0), depositToken: '', challengePeriodDuration: 0, recipientCount: recipientCount.toNumber(), diff --git a/vue-app/src/api/recipient-registry-optimistic.ts b/vue-app/src/api/recipient-registry-optimistic.ts index 1ecae4bb9..2ce7270b1 100644 --- a/vue-app/src/api/recipient-registry-optimistic.ts +++ b/vue-app/src/api/recipient-registry-optimistic.ts @@ -1,8 +1,6 @@ -import { BigNumber, Contract, Signer } from 'ethers' -import type { TransactionResponse, TransactionReceipt } from '@ethersproject/abstract-provider' -import { isHexString } from '@ethersproject/bytes' +import { Contract, toNumber, isHexString } from 'ethers' +import type { TransactionResponse, TransactionReceipt, Signer } from 'ethers' import { DateTime } from 'luxon' -import { getEventArg } from '@/utils/contracts' import { chain } from '@/api/core' import { OptimisticRecipientRegistry } from './abi' @@ -25,14 +23,14 @@ async function getRegistryInfo(registryAddress: string): Promise { // older BaseRecipientRegistry contract did not have recipientCount // set it to zero as this information is only // used during current round for space calculation - recipientCount = BigNumber.from(0) + recipientCount = BigInt(0) } const owner = await registry.owner() return { deposit, depositToken: chain.currency, - challengePeriodDuration: challengePeriodDuration.toNumber(), - recipientCount: recipientCount.toNumber(), + challengePeriodDuration: toNumber(challengePeriodDuration), + recipientCount: toNumber(recipientCount), owner, } } @@ -186,7 +184,7 @@ export async function getRequests(registryInfo: RegistryInfo, registryAddress: s async function addRecipient( registryAddress: string, recipientApplicationData: RecipientApplicationData, - deposit: BigNumber, + deposit: bigint, signer: Signer, ): Promise { const registry = new Contract(registryAddress, OptimisticRecipientRegistry, signer) @@ -196,11 +194,6 @@ async function addRecipient( return transaction } -export function getRequestId(receipt: TransactionReceipt, registryAddress: string): string { - const registry = new Contract(registryAddress, OptimisticRecipientRegistry) - return getEventArg(receipt, registry, 'RequestSubmitted', '_recipientId') -} - function decodeProject(recipient: Partial): Project { if (!recipient.id) { throw new Error('Incorrect recipient data') diff --git a/vue-app/src/api/recipient-registry-simple.ts b/vue-app/src/api/recipient-registry-simple.ts index 8fda10a91..852e85f4c 100644 --- a/vue-app/src/api/recipient-registry-simple.ts +++ b/vue-app/src/api/recipient-registry-simple.ts @@ -1,7 +1,5 @@ -import { Contract, BigNumber, Signer } from 'ethers' -import type { Event } from 'ethers' -import { isHexString } from '@ethersproject/bytes' -import type { TransactionResponse } from '@ethersproject/abstract-provider' +import { Contract, toNumber, isHexString } from 'ethers' +import type { EventLog, ContractTransactionResponse, Signer } from 'ethers' import { SimpleRecipientRegistry } from './abi' import { provider, ipfsGatewayUrl } from './core' @@ -9,7 +7,7 @@ import type { Project } from './projects' import type { RegistryInfo, RecipientApplicationData } from './types' import { formToRecipientData } from './recipient' -function decodeRecipientAdded(event: Event): Project { +function decodeRecipientAdded(event: EventLog): Project { const args = event.args as any const metadata = JSON.parse(args._metadata) return { @@ -46,21 +44,21 @@ export async function getProjects(registryAddress: string, startTime?: number, e for (const event of recipientAddedEvents) { let project: Project try { - project = decodeRecipientAdded(event) + project = decodeRecipientAdded(event as EventLog) } catch { // Invalid metadata continue } - const addedAt = (event.args as any)._timestamp.toNumber() + const addedAt = toNumber((event as EventLog).args._timestamp) if (endTime && addedAt >= endTime) { // Hide recipient if it is added after the end of round project.isHidden = true } const removed = recipientRemovedEvents.find(event => { - return (event.args as any)._recipientId === project.id - }) + return (event as EventLog).args._recipientId === project.id + }) as EventLog if (removed) { - const removedAt = (removed.args as any)._timestamp.toNumber() + const removedAt = toNumber(removed.args._timestamp) if (!startTime || removedAt <= startTime) { // Start time not specified // or recipient had been removed before start time @@ -89,7 +87,7 @@ export async function getProject(registryAddress: string, recipientId: string): } let project try { - project = decodeRecipientAdded(recipientAddedEvents[0]) + project = decodeRecipientAdded(recipientAddedEvents[0] as EventLog) } catch { // Invalid metadata return null @@ -108,23 +106,23 @@ export async function getProject(registryAddress: string, recipientId: string): async function getRegistryInfo(registryAddress: string): Promise { const registry = new Contract(registryAddress, SimpleRecipientRegistry, provider) - let recipientCount + let recipientCount: bigint try { recipientCount = await registry.getRecipientCount() } catch { // older BaseRecipientRegistry contract did not have recipientCount // set it to zero as this information is only // used during current round for space calculation - recipientCount = BigNumber.from(0) + recipientCount = 0n } const owner = await registry.owner() // deposit, depositToken and challengePeriodDuration are only relevant to the optimistic registry return { - deposit: BigNumber.from(0), + deposit: 0n, depositToken: '', challengePeriodDuration: 0, - recipientCount: recipientCount.toNumber(), + recipientCount: toNumber(recipientCount), owner, } } @@ -133,7 +131,7 @@ async function addRecipient( registryAddress: string, recipientApplicationData: RecipientApplicationData, signer: Signer, -): Promise { +): Promise { const registry = new Contract(registryAddress, SimpleRecipientRegistry, signer) const recipientData = formToRecipientData(recipientApplicationData) const { address, ...metadata } = recipientData diff --git a/vue-app/src/api/recipient-registry.ts b/vue-app/src/api/recipient-registry.ts index dd26dea8e..0c0690cef 100644 --- a/vue-app/src/api/recipient-registry.ts +++ b/vue-app/src/api/recipient-registry.ts @@ -3,8 +3,7 @@ import { recipientRegistryType } from './core' import SimpleRegistry from './recipient-registry-simple' import OptimisticRegistry from './recipient-registry-optimistic' import KlerosRegistry from './recipient-registry-kleros' -import type { BigNumber, Signer } from 'ethers' -import type { TransactionResponse } from '@ethersproject/abstract-provider' +import type { ContractTransactionResponse, Signer } from 'ethers' export async function getRegistryInfo(registryAddress: string): Promise { if (recipientRegistryType === 'simple') { @@ -21,9 +20,9 @@ export async function getRegistryInfo(registryAddress: string): Promise { +): Promise { if (recipientRegistryType === 'simple') { return await SimpleRegistry.addRecipient(registryAddress, recipientApplicationData, signer) } else if (recipientRegistryType === 'optimistic') { diff --git a/vue-app/src/api/round.ts b/vue-app/src/api/round.ts index 755837fea..a9c99d750 100644 --- a/vue-app/src/api/round.ts +++ b/vue-app/src/api/round.ts @@ -1,4 +1,4 @@ -import { BigNumber, Contract, utils } from 'ethers' +import { Contract, toNumber, getAddress, hexlify, randomBytes } from 'ethers' import { DateTime } from 'luxon' import { PubKey } from '@clrfund/common' @@ -26,14 +26,14 @@ export interface RoundInfo { nativeTokenAddress: string nativeTokenSymbol: string nativeTokenDecimals: number - voiceCreditFactor: BigNumber + voiceCreditFactor: bigint status: string startTime: DateTime signUpDeadline: DateTime votingDeadline: DateTime - totalFunds: BigNumber - matchingPool: BigNumber - contributions: BigNumber + totalFunds: bigint + matchingPool: bigint + contributions: bigint contributors: number messages: number blogUrl?: string @@ -67,32 +67,32 @@ export async function getCurrentRound(): Promise { export function toRoundInfo(data: any, network: string): RoundInfo { const nativeTokenDecimals = Number(data.nativeTokenDecimals) // leaderboard does not need coordinator key, generate a dummy number - const keypair = Keypair.createFromSeed(utils.hexlify(utils.randomBytes(32))) + const keypair = Keypair.createFromSeed(hexlify(randomBytes(32))) const coordinatorPubKey = keypair.pubKey - const voiceCreditFactor = BigNumber.from(data.voiceCreditFactor) - const contributions = BigNumber.from(data.totalSpent).mul(voiceCreditFactor) - const matchingPool = BigNumber.from(data.matchingPoolSize) + const voiceCreditFactor = BigInt(data.voiceCreditFactor) + const contributions = BigInt(data.totalSpent) * voiceCreditFactor + const matchingPool = BigInt(data.matchingPoolSize) let status = RoundStatus.Cancelled if (data.isCancelled) { status = RoundStatus.Cancelled } else if (data.isFinalized) { status = RoundStatus.Finalized } - const totalFunds = contributions.add(matchingPool) + const totalFunds = contributions + matchingPool return { fundingRoundAddress: data.address, - recipientRegistryAddress: utils.getAddress(data.recipientRegistryAddress), - userRegistryAddress: utils.getAddress(data.userRegistryAddress), - maciAddress: utils.getAddress(data.maciAddress), + recipientRegistryAddress: getAddress(data.recipientRegistryAddress), + userRegistryAddress: getAddress(data.userRegistryAddress), + maciAddress: getAddress(data.maciAddress), pollId: BigInt(data.pollId || 0), recipientTreeDepth: 0, maxContributors: 0, maxRecipients: data.maxRecipients, maxMessages: data.maxMessages, coordinatorPubKey, - nativeTokenAddress: utils.getAddress(data.nativeTokenAddress), + nativeTokenAddress: getAddress(data.nativeTokenAddress), nativeTokenSymbol: data.nativeTokenSymbol, nativeTokenDecimals, voiceCreditFactor, @@ -167,7 +167,7 @@ export async function getRoundInfo( coordinatorPubKeyY, } = data.fundingRound - const voiceCreditFactor = BigNumber.from(data.fundingRound.voiceCreditFactor) + const voiceCreditFactor = BigInt(data.fundingRound.voiceCreditFactor) const poll = new Contract(pollAddress, Poll, provider) const [, messages] = await poll.numSignUpsAndMessages() @@ -186,15 +186,15 @@ export async function getRoundInfo( const contributionsInfo = await getTotalContributed(fundingRoundAddress) const contributors = contributionsInfo.count let status: string - let contributions: BigNumber - let matchingPool: BigNumber + let contributions: bigint + let matchingPool: bigint if (isCancelled) { status = RoundStatus.Cancelled - contributions = BigNumber.from(0) - matchingPool = BigNumber.from(0) + contributions = 0n + matchingPool = 0n } else if (isFinalized) { status = RoundStatus.Finalized - contributions = (await fundingRound.totalSpent()).mul(voiceCreditFactor) + contributions = (await fundingRound.totalSpent()) * voiceCreditFactor matchingPool = await fundingRound.matchingPoolSize() } else if (messages >= maxMessages) { status = RoundStatus.Tallying @@ -213,20 +213,20 @@ export async function getRoundInfo( matchingPool = await clrFundContract.getMatchingFunds(nativeTokenAddress) } - const totalFunds = matchingPool.add(contributions) + const totalFunds = matchingPool + contributions return { fundingRoundAddress, - recipientRegistryAddress: utils.getAddress(recipientRegistryAddress), - userRegistryAddress: utils.getAddress(userRegistryAddress), - maciAddress: utils.getAddress(maciAddress), + recipientRegistryAddress: getAddress(recipientRegistryAddress), + userRegistryAddress: getAddress(userRegistryAddress), + maciAddress: getAddress(maciAddress), pollId: BigInt(pollId || 0), recipientTreeDepth: voteOptionTreeDepth || 1, maxContributors, maxRecipients: voteOptionTreeDepth ? 5 ** voteOptionTreeDepth - 1 : 0, maxMessages, coordinatorPubKey, - nativeTokenAddress: utils.getAddress(nativeTokenAddress), + nativeTokenAddress: getAddress(nativeTokenAddress), nativeTokenSymbol, nativeTokenDecimals, voiceCreditFactor, @@ -238,6 +238,6 @@ export async function getRoundInfo( matchingPool, contributions, contributors, - messages: messages.toNumber(), + messages: toNumber(messages), } } diff --git a/vue-app/src/api/subgraph.ts b/vue-app/src/api/subgraph.ts index db01532a2..9f1a58819 100644 --- a/vue-app/src/api/subgraph.ts +++ b/vue-app/src/api/subgraph.ts @@ -1,4 +1,4 @@ -import type { TransactionReceipt } from '@ethersproject/abstract-provider' +import type { TransactionReceipt } from 'ethers' import sdk from '@/graphql/sdk' /** diff --git a/vue-app/src/api/types.ts b/vue-app/src/api/types.ts index 96d3897b5..0252f4fb0 100644 --- a/vue-app/src/api/types.ts +++ b/vue-app/src/api/types.ts @@ -1,8 +1,6 @@ -import type { BigNumber } from 'ethers' - // Recipient registry info export interface RegistryInfo { - deposit: BigNumber + deposit: bigint depositToken: string challengePeriodDuration: number recipientCount: number diff --git a/vue-app/src/api/user.ts b/vue-app/src/api/user.ts index 9cee1f442..0b2ec51ba 100644 --- a/vue-app/src/api/user.ts +++ b/vue-app/src/api/user.ts @@ -1,9 +1,8 @@ import makeBlockie from 'ethereum-blockies-base64' -import { BigNumber, Contract, Signer, type ContractTransaction } from 'ethers' -import type { Web3Provider } from '@ethersproject/providers' +import { Contract, type ContractTransaction, type Signer, BrowserProvider } from 'ethers' import { FundingRound, UserRegistry, ERC20 } from './abi' -import { clrFundContract, ipfsGatewayUrl, provider, operator } from './core' +import { clrfundContractAddress, ipfsGatewayUrl, provider, operator } from './core' import type { BrightId } from './bright-id' import { SnapshotUserRegistry, MerkleUserRegistry } from './abi' import { @@ -21,17 +20,17 @@ To get logged in, sign this message to prove you have access to this wallet. Thi You will be asked to sign each time you load the app. -Contract address: ${clrFundContract.address.toLowerCase()}.` +Contract address: ${clrfundContractAddress.toLowerCase()}.` export interface User { walletAddress: string - walletProvider: Web3Provider + walletProvider: BrowserProvider encryptionKey?: string brightId?: BrightId isRegistered?: boolean // If is in user registry - balance?: BigNumber | null - etherBalance?: BigNumber | null - contribution?: BigNumber | null + balance?: bigint | null + etherBalance?: bigint | null + contribution?: bigint | null ensName?: string | null } @@ -59,12 +58,12 @@ export async function isRegisteredUser(fundingRoundAddress: string, walletAddres return contributor.isRegistered } -export async function getTokenBalance(tokenAddress: string, walletAddress: string): Promise { +export async function getTokenBalance(tokenAddress: string, walletAddress: string): Promise { const token = new Contract(tokenAddress, ERC20, provider) return await token.balanceOf(walletAddress) } -export async function getEtherBalance(walletAddress: string): Promise { +export async function getEtherBalance(walletAddress: string): Promise { return await provider.getBalance(walletAddress) } diff --git a/vue-app/src/components/Cart.vue b/vue-app/src/components/Cart.vue index a8a602e27..08f77b294 100644 --- a/vue-app/src/components/Cart.vue +++ b/vue-app/src/components/Cart.vue @@ -113,7 +113,7 @@
{{ $t('cart.div9') }}
- + {{ formatAmount(contribution.sub(getTotal())) }} + + {{ formatAmount(contribution - getTotal()) }} {{ tokenSymbol }}
{{ $t('cart.div12', { - total: formatAmount(contribution.sub(getTotal())), + total: formatAmount(contribution - getTotal()), tokenSymbol: tokenSymbol, contribution: formatAmount(contribution), }) @@ -176,7 +176,7 @@ @click="submitCart" :disabled="Boolean(errorMessage) || (hasUserContributed && hasUserVoted && !isDirty)" > -