diff --git a/.github/workflows/hardhat-tasks.yml b/.github/workflows/hardhat-tasks.yml index ef17f8b6..c90c7524 100644 --- a/.github/workflows/hardhat-tasks.yml +++ b/.github/workflows/hardhat-tasks.yml @@ -58,6 +58,8 @@ jobs: run: | cp ./deploy-config-example.json ./deploy-config.json pnpm deploy:localhost + pnpm deploy-poll:localhost + pnpm initPoll:localhost --poll 0 working-directory: packages/contracts - name: Stop Hardhat diff --git a/packages/contracts/contracts/interfaces/IPoll.sol b/packages/contracts/contracts/interfaces/IPoll.sol index 8a894a2e..04c2696c 100644 --- a/packages/contracts/contracts/interfaces/IPoll.sol +++ b/packages/contracts/contracts/interfaces/IPoll.sol @@ -10,4 +10,8 @@ import { IOwnable } from "./IOwnable.sol"; interface IPoll is IPollBase, IOwnable { /// @notice The initialization function. function init() external; + + /// @notice Set the poll registry. + /// @param registryAddress The registry address + function setRegistry(address registryAddress) external; } diff --git a/packages/contracts/contracts/maci/MACI.sol b/packages/contracts/contracts/maci/MACI.sol index 54f2e948..cba96490 100644 --- a/packages/contracts/contracts/maci/MACI.sol +++ b/packages/contracts/contracts/maci/MACI.sol @@ -12,6 +12,7 @@ import { SignUpGatekeeper } from "maci-contracts/contracts/gatekeepers/SignUpGat import { ICommon } from "../interfaces/ICommon.sol"; import { IPoll } from "../interfaces/IPoll.sol"; import { IRegistryManager } from "../interfaces/IRegistryManager.sol"; +import { EASRegistry } from "../registry/EASRegistry.sol"; /// @title MACI - Minimum Anti-Collusion Infrastructure /// @notice A contract which allows users to sign up, and deploy new polls @@ -51,10 +52,11 @@ contract MACI is Ownable, BaseMACI, ICommon { /// @notice Initialize the poll by given poll id and transfer poll ownership to the caller. /// @param pollId The poll id - function initPoll(uint256 pollId) public onlyOwner { + function initPoll(uint256 pollId, address registryAddress) public onlyOwner { PollContracts memory pollAddresses = polls[pollId]; IPoll poll = IPoll(pollAddresses.poll); + poll.setRegistry(registryAddress); poll.init(); poll.transferOwnership(msg.sender); } diff --git a/packages/contracts/contracts/maci/Poll.sol b/packages/contracts/contracts/maci/Poll.sol index dc6fd8e8..9b27b922 100644 --- a/packages/contracts/contracts/maci/Poll.sol +++ b/packages/contracts/contracts/maci/Poll.sol @@ -4,18 +4,35 @@ pragma solidity ^0.8.20; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Poll as BasePoll } from "maci-contracts/contracts/Poll.sol"; +import { ICommon } from "../interfaces/ICommon.sol"; + /// @title Poll /// @notice A Poll contract allows voters to submit encrypted messages /// which can be either votes or key change messages. /// @dev Do not deploy this directly. Use PollFactory.deploy() which performs some /// checks on the Poll constructor arguments. -contract Poll is Ownable, BasePoll { +contract Poll is Ownable, BasePoll, ICommon { + /// @notice Poll specific registry + address public registry; + + /// @notice The timestamp of the block at which the Poll was deployed + uint256 internal initTime; + + /// @notice events + event SetRegistry(address indexed registry); + + /// @notice custom errors + error RegistryAlreadyInitialized(); + error RegistryNotInitialized(); + error PollNotInitialized(); + /// @notice Each MACI instance can have multiple Polls. /// When a Poll is deployed, its voting period starts immediately. /// @param duration The duration of the voting period, in seconds /// @param treeDepths The depths of the merkle trees /// @param coordinatorPubKey The coordinator's public key /// @param extContracts The external contracts + /// @param emptyBallotRoot The empty ballot root constructor( uint256 duration, TreeDepths memory treeDepths, @@ -28,8 +45,81 @@ contract Poll is Ownable, BasePoll { BasePoll(duration, treeDepths, coordinatorPubKey, extContracts, emptyBallotRoot) {} + /// @notice Check if poll is initialized + modifier isPollInitialized() { + if (!isInit) { + revert PollNotInitialized(); + } + + _; + } + + /// @notice Check if registry is initialized + modifier isRegistryInitialized() { + if (address(registry) == address(0)) { + revert RegistryNotInitialized(); + } + + _; + } + + /// @notice Check if registry is valid and not initialized + /// @param registryAddress Registry address + modifier isRegistryNotInitialized(address registryAddress) { + if (registryAddress == address(0)) { + revert InvalidAddress(); + } + + if (address(registry) != address(0)) { + revert RegistryAlreadyInitialized(); + } + + _; + } + + /// @notice A modifier that causes the function to revert if the voting period is + /// not over. + modifier isAfterVotingDeadline() override { + uint256 secondsPassed = block.timestamp - initTime; + + if (secondsPassed <= duration) { + revert VotingPeriodNotOver(); + } + + _; + } + + /// @notice A modifier that causes the function to revert if the voting period is + /// over + modifier isWithinVotingDeadline() override { + uint256 secondsPassed = block.timestamp - initTime; + + if (secondsPassed >= duration) { + revert VotingPeriodOver(); + } + + _; + } + + /// @notice Set poll registry. + /// @param registryAddress The registry address + function setRegistry(address registryAddress) public onlyOwner isRegistryNotInitialized(registryAddress) { + registry = registryAddress; + + emit SetRegistry(registryAddress); + } + /// @notice The initialization function. - function init() public override onlyOwner { + function init() public override onlyOwner isRegistryInitialized { + initTime = block.timestamp; super.init(); } + + /// @inheritdoc BasePoll + function publishMessage( + Message memory message, + PubKey calldata encPubKey + ) public override isPollInitialized isWithinVotingDeadline { + super.publishMessage(message, encPubKey); + } } diff --git a/packages/contracts/deploy-config-example.json b/packages/contracts/deploy-config-example.json index e401add4..da4e8725 100644 --- a/packages/contracts/deploy-config-example.json +++ b/packages/contracts/deploy-config-example.json @@ -36,6 +36,8 @@ "registryManager": "EASRegistryManager" }, "EASRegistryManager": { + "maxRecipients": 25, + "metadataUrl": "url", "easAddress": "0xC2679fBD37d54388Ce493F1DB75320D236e1815e" }, "VkRegistry": { @@ -101,6 +103,8 @@ "registryManager": "EASRegistryManager" }, "EASRegistryManager": { + "maxRecipients": 25, + "metadataUrl": "url", "easAddress": "0xaEF4103A04090071165F78D45D83A0C0782c2B2a" }, "VkRegistry": { @@ -167,6 +171,8 @@ "registryManager": "EASRegistryManager" }, "EASRegistryManager": { + "maxRecipients": 25, + "metadataUrl": "url", "easAddress": "0x4200000000000000000000000000000000000021" }, "VkRegistry": { @@ -233,6 +239,8 @@ "registryManager": "EASRegistryManager" }, "EASRegistryManager": { + "maxRecipients": 25, + "metadataUrl": "url", "easAddress": "0xC2679fBD37d54388Ce493F1DB75320D236e1815e" }, "VkRegistry": { @@ -299,6 +307,8 @@ "registryManager": "EASRegistryManager" }, "EASRegistryManager": { + "maxRecipients": 25, + "metadataUrl": "url", "easAddress": "0xC2679fBD37d54388Ce493F1DB75320D236e1815e" }, "VkRegistry": { @@ -365,6 +375,8 @@ "registryManager": "EASRegistryManager" }, "EASRegistryManager": { + "maxRecipients": 25, + "metadataUrl": "url", "easAddress": "0x4200000000000000000000000000000000000021" }, "VkRegistry": { @@ -436,6 +448,8 @@ "registryManager": "EASRegistryManager" }, "EASRegistryManager": { + "maxRecipients": 25, + "metadataUrl": "url", "easAddress": "0x4200000000000000000000000000000000000021" }, "VkRegistry": { diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index 6960b8b0..26bc11c6 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -5,6 +5,7 @@ import "hardhat-artifactor"; import "hardhat-contract-sizer"; import "maci-contracts/tasks/deploy"; import "maci-contracts/tasks/runner/deployFull"; +import "maci-contracts/tasks/runner/deployPoll"; import "maci-contracts/tasks/runner/verifyFull"; import "solidity-docgen"; @@ -13,6 +14,7 @@ import type { HardhatUserConfig } from "hardhat/config"; // Don't forget to import new tasks here import "./tasks/deploy"; import { EChainId, ESupportedChains, getEtherscanApiKeys, getNetworkRpcUrls } from "./tasks/helpers/constants"; +import "./tasks/runner/initPoll"; dotenv.config(); diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 337cccd1..941ee465 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -34,7 +34,11 @@ "coverage": "BLOCK_GAS_LIMIT=1599511627775 hardhat coverage", "test": "hardhat test --network hardhat", "deploy": "hardhat deploy-full", - "deploy:localhost": "pnpm run deploy" + "initPoll": "hardhat initPoll", + "deploy-poll": "hardhat deploy-poll", + "deploy:localhost": "pnpm run deploy", + "deploy-poll:localhost": "pnpm run deploy-poll", + "initPoll:localhost": "pnpm run initPoll" }, "dependencies": { "@nomicfoundation/hardhat-ethers": "^3.0.6", @@ -44,7 +48,7 @@ "ethers": "^6.13.2", "hardhat": "^2.22.8", "lowdb": "^1.0.0", - "maci-contracts": "0.0.0-ci.17a38c9", + "maci-contracts": "0.0.0-ci.a97f395", "maci-core": "^2.2.0", "maci-domainobjs": "^2.2.0", "solidity-docgen": "^0.6.0-beta.36" diff --git a/packages/contracts/tasks/deploy/index.ts b/packages/contracts/tasks/deploy/index.ts index 7e9c76a3..b35df05a 100644 --- a/packages/contracts/tasks/deploy/index.ts +++ b/packages/contracts/tasks/deploy/index.ts @@ -4,7 +4,7 @@ import path from "path"; /** * The same as individual imports but doesn't require to add new import line every time */ -["maci"].forEach((folder) => { +["maci", "poll"].forEach((folder) => { const tasksPath = path.resolve(__dirname, folder); if (fs.existsSync(tasksPath)) { diff --git a/packages/contracts/tasks/deploy/maci/10-registryManager.ts b/packages/contracts/tasks/deploy/maci/10-registryManager.ts index b2e44e65..8118d7d6 100644 --- a/packages/contracts/tasks/deploy/maci/10-registryManager.ts +++ b/packages/contracts/tasks/deploy/maci/10-registryManager.ts @@ -17,7 +17,7 @@ deployment.deployTask(EDeploySteps.RegistryManager, "Deploy registry manager").t const registryManagerType = deployment.getDeployConfigField(EContracts.MACI, "registryManager") || - EContracts.RegistryManager; + EContracts.EASRegistryManager; const registryManagerContractAddress = storage.getAddress(registryManagerType, hre.network.name); diff --git a/packages/contracts/tasks/deploy/poll/01-poll.ts b/packages/contracts/tasks/deploy/poll/01-poll.ts new file mode 100644 index 00000000..a309d41b --- /dev/null +++ b/packages/contracts/tasks/deploy/poll/01-poll.ts @@ -0,0 +1,185 @@ +/* eslint-disable no-console */ +import { encodeBytes32String } from "ethers"; +import { ContractStorage, Deployment, EMode } from "maci-contracts"; +import { PubKey } from "maci-domainobjs"; + +import { type Poll, type MACI } from "../../../typechain-types"; +import { EContracts, EDeploySteps, REGISTRY_TYPES, TRegistryManager } from "../../helpers/constants"; + +const deployment = Deployment.getInstance(); +const storage = ContractStorage.getInstance(); + +/** + * Deploy step registration and task itself + */ +deployment.deployTask(EDeploySteps.Poll, "Deploy poll").then((task) => + task.setAction(async (_, hre) => { + deployment.setHre(hre); + + const maciContractAddress = storage.getAddress(EContracts.MACI, hre.network.name); + const verifierContractAddress = storage.getAddress(EContracts.Verifier, hre.network.name); + const vkRegistryContractAddress = storage.getAddress(EContracts.VkRegistry, hre.network.name); + + if (!maciContractAddress) { + throw new Error("Need to deploy MACI contract first"); + } + + if (!verifierContractAddress) { + throw new Error("Need to deploy Verifier contract first"); + } + + if (!vkRegistryContractAddress) { + throw new Error("Need to deploy VkRegistry contract first"); + } + + const { MACI__factory: MACIFactory, Poll__factory: PollFactory } = await import("../../../typechain-types"); + + const maciContract = await deployment.getContract({ name: EContracts.MACI, abi: MACIFactory.abi }); + const pollId = await maciContract.nextPollId(); + + const coordinatorPubkey = deployment.getDeployConfigField(EContracts.Poll, "coordinatorPubkey"); + const pollDuration = deployment.getDeployConfigField(EContracts.Poll, "pollDuration"); + const intStateTreeDepth = deployment.getDeployConfigField(EContracts.VkRegistry, "intStateTreeDepth"); + const messageTreeSubDepth = deployment.getDeployConfigField(EContracts.VkRegistry, "messageBatchDepth"); + const messageTreeDepth = deployment.getDeployConfigField(EContracts.VkRegistry, "messageTreeDepth"); + const voteOptionTreeDepth = deployment.getDeployConfigField(EContracts.VkRegistry, "voteOptionTreeDepth"); + + const useQuadraticVoting = + deployment.getDeployConfigField(EContracts.Poll, "useQuadraticVoting") ?? false; + const unserializedKey = PubKey.deserialize(coordinatorPubkey); + const mode = useQuadraticVoting ? EMode.QV : EMode.NON_QV; + + const registryManagerType = + deployment.getDeployConfigField(EContracts.MACI, "registryManager") || + EContracts.EASRegistryManager; + const registryManagerAddress = storage.getAddress(registryManagerType, hre.network.name); + const maxRecipients = deployment.getDeployConfigField( + EContracts.EASRegistryManager, + "maxRecipients", + ); + const metadataUrl = deployment.getDeployConfigField( + EContracts.EASRegistryManager, + "metadataUrl", + ); + const easAddress = deployment.getDeployConfigField( + EContracts.EASRegistryManager, + "easAddress", + ); + + const registryArgs = [maxRecipients, encodeBytes32String(metadataUrl), easAddress, registryManagerAddress]; + const pollRegistry = await deployment.deployContract( + { name: REGISTRY_TYPES[registryManagerType] }, + ...registryArgs, + ); + + const tx = await maciContract.deployPoll( + pollDuration, + { + intStateTreeDepth, + messageTreeSubDepth, + messageTreeDepth, + voteOptionTreeDepth, + }, + unserializedKey.asContractParam(), + verifierContractAddress, + vkRegistryContractAddress, + mode, + ); + + const deployPollReceipt = await tx.wait(); + + if (deployPollReceipt?.status !== 1) { + throw new Error("Deploy poll transaction is failed"); + } + + const pollContracts = await maciContract.getPoll(pollId); + const pollContractAddress = pollContracts.poll; + const messageProcessorContractAddress = pollContracts.messageProcessor; + const tallyContractAddress = pollContracts.tally; + + const pollContract = await deployment.getContract({ + name: EContracts.Poll, + abi: PollFactory.abi, + address: pollContractAddress, + }); + const extContracts = await pollContract.extContracts(); + + const messageProcessorContract = await deployment.getContract({ + name: EContracts.MessageProcessor, + address: messageProcessorContractAddress, + }); + + const tallyContract = await deployment.getContract({ + name: EContracts.Tally, + address: tallyContractAddress, + }); + + const messageAccQueueContract = await deployment.getContract({ + name: EContracts.AccQueueQuinaryMaci, + address: extContracts[1], + }); + + // get the empty ballot root + const emptyBallotRoot = await pollContract.emptyBallotRoot(); + + await Promise.all([ + storage.register({ + id: EContracts.Poll, + key: `poll-${pollId}`, + contract: pollContract, + args: [ + pollDuration, + { + intStateTreeDepth, + messageTreeSubDepth, + messageTreeDepth, + voteOptionTreeDepth, + }, + unserializedKey.asContractParam(), + extContracts, + emptyBallotRoot.toString(), + ], + network: hre.network.name, + }), + + storage.register({ + id: REGISTRY_TYPES[registryManagerType], + key: `poll-${pollId}`, + contract: pollRegistry, + args: registryArgs, + network: hre.network.name, + }), + + storage.register({ + id: EContracts.MessageProcessor, + key: `poll-${pollId}`, + contract: messageProcessorContract, + args: [verifierContractAddress, vkRegistryContractAddress, pollContractAddress, mode], + network: hre.network.name, + }), + + storage.register({ + id: EContracts.Tally, + key: `poll-${pollId}`, + contract: tallyContract, + args: [ + verifierContractAddress, + vkRegistryContractAddress, + pollContractAddress, + messageProcessorContractAddress, + mode, + ], + network: hre.network.name, + }), + + storage.register({ + id: EContracts.AccQueueQuinaryMaci, + key: `poll-${pollId}`, + name: "contracts/trees/AccQueueQuinaryMaci.sol:AccQueueQuinaryMaci", + contract: messageAccQueueContract, + args: [messageTreeSubDepth], + network: hre.network.name, + }), + ]); + }), +); diff --git a/packages/contracts/tasks/helpers/constants/index.ts b/packages/contracts/tasks/helpers/constants/index.ts index bbb60ca4..ad14b968 100644 --- a/packages/contracts/tasks/helpers/constants/index.ts +++ b/packages/contracts/tasks/helpers/constants/index.ts @@ -19,8 +19,8 @@ export const EDeploySteps = { * Contracts for maci-platform related constacts */ export enum EPlatformContracts { - RegistryManager = "RegistryManager", EASRegistryManager = "EASRegistryManager", + EASRegistry = "EASRegistry", } /** @@ -31,6 +31,23 @@ export const EContracts = { ...EPlatformContracts, }; +/** + * Supported registry manager types + */ +export type TRegistryManager = EPlatformContracts.EASRegistryManager; + +/** + * Supported registry types + */ +export type TRegistry = EPlatformContracts.EASRegistry; + +/** + * Registry types by registry manager + */ +export const REGISTRY_TYPES: Record = { + [EPlatformContracts.EASRegistryManager]: EPlatformContracts.EASRegistry, +}; + /** * Supported networks for deployment and task running */ diff --git a/packages/contracts/tasks/runner/initPoll.ts b/packages/contracts/tasks/runner/initPoll.ts new file mode 100644 index 00000000..dd8396f9 --- /dev/null +++ b/packages/contracts/tasks/runner/initPoll.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-console */ +import { ZeroAddress } from "ethers"; +import { task, types } from "hardhat/config"; +import { ContractStorage, Deployment } from "maci-contracts"; + +import { type MACI } from "../../typechain-types"; +import { EContracts } from "../helpers/constants"; + +/** + * Interface that represents init poll params + */ +interface IInitPollParams { + /** + * Poll id + */ + poll: string; +} + +/** + * Command to merge signup and message queues of a MACI contract + */ +task("initPoll", "Initialize poll") + .addParam("poll", "The poll id", undefined, types.string) + .setAction(async ({ poll }: IInitPollParams, hre) => { + const deployment = Deployment.getInstance(hre); + const storage = ContractStorage.getInstance(); + const { MACI__factory: MACIFactory } = await import("../../typechain-types"); + + deployment.setHre(hre); + + const deployer = await deployment.getDeployer(); + + const maciContract = await deployment.getContract({ name: EContracts.MACI, abi: MACIFactory.abi }); + const pollContracts = await maciContract.polls(poll); + const registryAddress = storage.mustGetAddress( + EContracts.EASRegistry, + hre.network.name, + `poll-${poll}`, + ); + + if (pollContracts.poll === ZeroAddress) { + throw new Error(`No poll ${poll} found`); + } + + const startBalance = await deployer.provider.getBalance(deployer); + + console.log("Start balance: ", Number(startBalance / 10n ** 12n) / 1e6); + + const tx = await maciContract.initPoll(poll, registryAddress); + const receipt = await tx.wait(); + + if (receipt?.status !== 1) { + throw new Error("Poll init transaction is failed"); + } + + const endBalance = await deployer.provider.getBalance(deployer); + + console.log("End balance: ", Number(endBalance / 10n ** 12n) / 1e6); + console.log("Operation expenses: ", Number((startBalance - endBalance) / 10n ** 12n) / 1e6); + }); diff --git a/packages/contracts/tests/Maci.test.ts b/packages/contracts/tests/Maci.test.ts index 1b0bcb9e..2b566759 100644 --- a/packages/contracts/tests/Maci.test.ts +++ b/packages/contracts/tests/Maci.test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { Signer, ZeroAddress } from "ethers"; +import { encodeBytes32String, Signer, ZeroAddress } from "ethers"; import { Verifier, VkRegistry, EMode, getSigners, deployContract } from "maci-contracts"; import { MaciState } from "maci-core"; import { Keypair, Message, PubKey } from "maci-domainobjs"; @@ -16,7 +16,7 @@ import { } from "./constants"; import { deployTestContracts } from "./utils"; -describe("Poll", () => { +describe("Maci", () => { let maciContract: MACI; let pollId: bigint; let pollContract: PollContract; @@ -28,6 +28,8 @@ describe("Poll", () => { const coordinator = new Keypair(); const maciState = new MaciState(STATE_TREE_DEPTH); + const maxRecipients = 5; + const metadataUrl = encodeBytes32String("url"); describe("deployment", () => { before(async () => { @@ -80,21 +82,43 @@ describe("Poll", () => { }); it("should fail if unauthorized user tries to init the poll", async () => { - await expect(maciContract.initPoll(pollId)).not.to.be.revertedWithCustomError(pollContract, "PollAlreadyInit"); - await expect(maciContract.connect(user).initPoll(pollId)).to.be.revertedWithCustomError( + await expect(maciContract.initPoll(pollId, await user.getAddress())).not.to.be.revertedWithCustomError( + pollContract, + "PollAlreadyInit", + ); + await expect(maciContract.connect(user).initPoll(pollId, await user.getAddress())).to.be.revertedWithCustomError( pollContract, "OwnableUnauthorizedAccount", ); - await expect(pollContract.init()).to.be.revertedWithCustomError(pollContract, "PollAlreadyInit"); + }); + + it("should fail if try to set zero address as registry", async () => { + await expect(maciContract.initPoll(pollId, ZeroAddress)).not.to.be.revertedWithCustomError( + pollContract, + "InvalidAddress", + ); }); it("should not be possible to init the Poll contract twice", async () => { - await expect(maciContract.initPoll(pollId)).not.to.be.revertedWithCustomError(pollContract, "PollAlreadyInit"); - await expect(maciContract.initPoll(pollId)).to.be.revertedWithCustomError( + const registry = await deployContract( + "MockRegistry", + owner, + true, + maxRecipients, + metadataUrl, + await owner.getAddress(), + ); + + const registryAddress = await registry.getAddress(); + + await expect(maciContract.initPoll(pollId, registryAddress)).not.to.be.revertedWithCustomError( + pollContract, + "PollAlreadyInit", + ); + await expect(maciContract.initPoll(pollId, registryAddress)).to.be.revertedWithCustomError( pollContract, "OwnableUnauthorizedAccount", ); - await expect(pollContract.init()).to.be.revertedWithCustomError(pollContract, "PollAlreadyInit"); }); it("should not be possible to set zero address as registry manager", async () => { diff --git a/packages/contracts/tests/Poll.test.ts b/packages/contracts/tests/Poll.test.ts new file mode 100644 index 00000000..299a4406 --- /dev/null +++ b/packages/contracts/tests/Poll.test.ts @@ -0,0 +1,154 @@ +import { expect } from "chai"; +import { encodeBytes32String, ZeroAddress, type Signer } from "ethers"; +import hardhat from "hardhat"; +import { deployContract, Deployment, deployPoseidonContracts, genEmptyBallotRoots, getSigners } from "maci-contracts"; +import { Keypair, Message, PubKey } from "maci-domainobjs"; + +import { type Poll, Poll__factory as PollFactory, PollFactory as PollFactoryContract } from "../typechain-types"; + +import { DEFAULT_SR_QUEUE_OPS, STATE_TREE_DEPTH, treeDepths } from "./constants"; +import { timeTravel } from "./utils"; + +describe("Poll", () => { + let pollFactory: PollFactoryContract; + let pollContract: Poll; + let owner: Signer; + let user: Signer; + + const { pubKey: coordinatorPubKey } = new Keypair(); + + const deployment = Deployment.getInstance(hardhat); + + const emptyBallotRoots = genEmptyBallotRoots(STATE_TREE_DEPTH); + const emptyBallotRoot = emptyBallotRoots[treeDepths.voteOptionTreeDepth]; + const duration = 100; + const maxRecipients = 5; + const metadataUrl = encodeBytes32String("url"); + const message = new Message([0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]); + const key = new PubKey([ + 10457101036533406547632367118273992217979173478358440826365724437999023779287n, + 19824078218392094440610104313265183977899662750282163392862422243483260492317n, + ]); + + before(async () => { + [owner, user] = await getSigners(); + const { PoseidonT3Contract, PoseidonT4Contract, PoseidonT5Contract, PoseidonT6Contract } = + await deployPoseidonContracts(owner); + + const contractFactory = await hardhat.ethers.getContractFactory("contracts/maci/PollFactory.sol:PollFactory", { + signer: owner, + libraries: { + PoseidonT3: PoseidonT3Contract, + PoseidonT4: PoseidonT4Contract, + PoseidonT5: PoseidonT5Contract, + PoseidonT6: PoseidonT6Contract, + }, + }); + + pollFactory = await deployment.deployContractWithLinkedLibraries({ contractFactory, signer: owner }); + + const pollAddress = await pollFactory.deploy.staticCall( + duration, + treeDepths, + coordinatorPubKey.asContractParam(), + await owner.getAddress(), + emptyBallotRoot, + ); + + await pollFactory + .deploy("100", treeDepths, coordinatorPubKey.asContractParam(), await owner.getAddress(), emptyBallotRoot) + .then((tx) => tx.wait()); + + pollContract = PollFactory.connect(pollAddress, owner); + }); + + it("should fail if unauthorized user tries to set the poll registry", async () => { + const registry = await deployContract( + "MockRegistry", + owner, + true, + maxRecipients, + metadataUrl, + await owner.getAddress(), + ); + + await expect(pollContract.connect(user).setRegistry(await registry.getAddress())).to.be.revertedWithCustomError( + pollContract, + "OwnableUnauthorizedAccount", + ); + }); + + it("should fail if try to set the invalid poll registry", async () => { + await expect(pollContract.connect(owner).setRegistry(ZeroAddress)).to.be.revertedWithCustomError( + pollContract, + "InvalidAddress", + ); + }); + + it("should fail if try to initialize the poll without the registry", async () => { + await expect(pollContract.connect(owner).init()).to.be.revertedWithCustomError( + pollContract, + "RegistryNotInitialized", + ); + }); + + it("should fail if try to publish the message for the uninitialized poll", async () => { + await expect( + pollContract.connect(user).publishMessage(message.asContractParam(), key.asContractParam()), + ).to.be.revertedWithCustomError(pollContract, "PollNotInitialized"); + }); + + it("should fail if try to set the poll registry twice", async () => { + const address = await user.getAddress(); + + await expect(pollContract.connect(owner).setRegistry(address)) + .to.emit(pollContract, "SetRegistry") + .withArgs(address); + + await expect(pollContract.connect(owner).setRegistry(address)).to.be.revertedWithCustomError( + pollContract, + "RegistryAlreadyInitialized", + ); + }); + + it("should fail if non-owner tries to init the poll", async () => { + await expect(pollContract.connect(user).init()).to.be.revertedWithCustomError( + pollContract, + "OwnableUnauthorizedAccount", + ); + }); + + it("should fail if owner tries to init the poll twice", async () => { + await expect(pollContract.connect(owner).init()).not.to.be.revertedWithCustomError(pollContract, "PollAlreadyInit"); + await expect(pollContract.connect(owner).init()).to.be.revertedWithCustomError(pollContract, "PollAlreadyInit"); + }); + + it("should publish message properly", async () => { + const receipt = await pollContract + .connect(user) + .publishMessage(message.asContractParam(), key.asContractParam()) + .then((tx) => tx.wait()); + + expect(receipt?.status).to.equal(1); + }); + + it("should fail when trying to merge message queue subroots before poll is over", async () => { + await expect(pollContract.mergeMessageAqSubRoots(DEFAULT_SR_QUEUE_OPS)).to.be.revertedWithCustomError( + pollContract, + "VotingPeriodNotOver", + ); + }); + + it("should fail to publish a message if poll is over", async () => { + await timeTravel(duration, owner); + await expect( + pollContract.connect(user).publishMessage(message.asContractParam(), key.asContractParam()), + ).to.be.revertedWithCustomError(pollContract, "VotingPeriodOver"); + }); + + it("should merge message queue subroots properly", async () => { + const receipt = await pollContract.mergeMessageAqSubRoots(DEFAULT_SR_QUEUE_OPS).then((tx) => tx.wait()); + + expect(receipt?.status).to.equal(1); + }); +}); diff --git a/packages/contracts/tests/constants.ts b/packages/contracts/tests/constants.ts index d24d2aa1..821c0bd2 100644 --- a/packages/contracts/tests/constants.ts +++ b/packages/contracts/tests/constants.ts @@ -7,6 +7,7 @@ export const MESSAGE_TREE_DEPTH = 2; export const MESSAGE_TREE_SUBDEPTH = 1; export const messageBatchSize = MESSAGE_TREE_ARITY ** MESSAGE_TREE_SUBDEPTH; export const NOTHING_UP_MY_SLEEVE = 8370432830353022751713833565135785980866757267633941821328460903436894336785n; +export const DEFAULT_SR_QUEUE_OPS = 4; export const initialVoiceCreditBalance = 100; diff --git a/packages/contracts/tests/utils.ts b/packages/contracts/tests/utils.ts index b786cd6a..de1ecbc3 100644 --- a/packages/contracts/tests/utils.ts +++ b/packages/contracts/tests/utils.ts @@ -12,7 +12,7 @@ import { type VkRegistry, } from "maci-contracts"; -import type { ContractFactory, Signer } from "ethers"; +import type { ContractFactory, Signer, JsonRpcProvider } from "ethers"; import { type MACI } from "../typechain-types"; @@ -125,3 +125,17 @@ export const deployTestContracts = async ({ vkRegistryContract, }; }; + +/** + * Utility to travel in time when using a local blockchain + * @param seconds - the number of seconds to travel in time + * @param quiet - whether to log the output + */ +export const timeTravel = async (seconds: number, signer: Signer): Promise => { + // send the instructions to the provider + await (signer.provider as JsonRpcProvider).send("evm_increaseTime", [Number(seconds)]); + await (signer.provider as JsonRpcProvider).send("evm_mine", []); + + // eslint-disable-next-line no-console + console.log(`Fast-forwarded ${seconds} seconds`); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 187fe15b..cbbaefe3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,8 +105,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 maci-contracts: - specifier: 0.0.0-ci.17a38c9 - version: 0.0.0-ci.17a38c9(flxhpragqyg57es62n2ruk56xe) + specifier: 0.0.0-ci.a97f395 + version: 0.0.0-ci.a97f395(flxhpragqyg57es62n2ruk56xe) maci-core: specifier: ^2.2.0 version: 2.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -9260,8 +9260,8 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true - maci-circuits@0.0.0-ci.17a38c9: - resolution: {integrity: sha512-4D3dBdTHMF2q4Nqa2QNiz5xOLcYYobWCt9oHCI+GuZ50nZhsiNdOnjs55Xehdk2l1yy+Wn9k85+niWS0b7SuuQ==} + maci-circuits@0.0.0-ci.a97f395: + resolution: {integrity: sha512-qclWgZqK8mSuOVakqJ0fOUPx8i1Ex7b+YRM4VdJlAQ/1f+cshPp1+Fzk95PdUA5XjHteb7VaCfnxFyQ6ESN4sQ==} maci-circuits@2.1.0: resolution: {integrity: sha512-nYk8OM8NigLPhod+hPRK2sNpfoKjsOakBflsYqOKTA6N/fWHsMsfQUS83Qg6v7y5s1m5Nr/cATj+eWea2esd2Q==} @@ -9277,8 +9277,8 @@ packages: resolution: {integrity: sha512-byWUEeoZ7Z/QvMhe+/321sMNsLWwDAYEfYxTgyW4BJX0X4yX2SldcRIcQWs5U5tEqszlcwz4FH3oBMuYrjBZqQ==} hasBin: true - maci-contracts@0.0.0-ci.17a38c9: - resolution: {integrity: sha512-XHh+TfOsLZ2uwZWsnHEh66NJ8qXi2AIipCqWlP5NYE2sEuBMXhcFZl5butVovmMlB66PtiDLNfyCfo2wartD+Q==} + maci-contracts@0.0.0-ci.a97f395: + resolution: {integrity: sha512-imt+qTbAd1Zrd1/y8fspIeTEJV1q1ezuA3iFViGd/7XkGhyTaMnzrCU3advGdhkzRsyQ0marIGES3ngBEORIxw==} hasBin: true maci-contracts@2.1.0: @@ -9289,14 +9289,14 @@ packages: resolution: {integrity: sha512-X6NK47fDZ5Gt9K9V0MMFmkU4HwdwhX5bZ34pfme3oTnuagpOXhOWbnALfyKqPwikTjwPwj5190ahiwfTUCFnyw==} hasBin: true - maci-core@0.0.0-ci.17a38c9: - resolution: {integrity: sha512-n5KxNEoY1KXbCm6hKeZ1JjTKTsxhUQ1G6wmo4Wv7E3f98SR+Vtp+/mo7pwuIl75PXV1THBejOTgaZhWouaXBCw==} + maci-core@0.0.0-ci.a97f395: + resolution: {integrity: sha512-FJqjPWP0ziThWDL3gIjuDgkwn8LXeyv2qBXQWVzo9tlNB6wLmBZHVl/r3sieF85EYxkIDN2VVw+DDCOrVCWMBw==} maci-core@2.2.0: resolution: {integrity: sha512-jHS40/uGJZMYvslfDls3LUPXK8gAijVrc8L8o51SJQX44iocgR3aWpWycD8df9rBCGBxScZPbtn04CmtFT0lhQ==} - maci-crypto@0.0.0-ci.17a38c9: - resolution: {integrity: sha512-OzEujYBc8wwOA9XfNuvSp2ckIAWbr/i0XBwu1EEryJaJRExbDDNqHhodvor0w/uTPVXPpqK3NSAoQ4NeWrEdow==} + maci-crypto@0.0.0-ci.a97f395: + resolution: {integrity: sha512-Ky5/oGeX2/JPc7eqUa0KD/NooprJLbTCo9YteXmlp9f6qO1qkJbmjZdIDWhR31kyVpbniZR77AlkeKEyNEK+Yw==} maci-crypto@2.0.0: resolution: {integrity: sha512-bkgOoDA1ABG49MXDzzsQPsFVEijAkLk8ocJKGyeNQS7YpNhC3YEVVz/SE4g0td+N4xJhD3PbXsyHeaTM3ApIjw==} @@ -9304,8 +9304,8 @@ packages: maci-crypto@2.2.0: resolution: {integrity: sha512-kSbWfuAdDWOdtQsEyofvgDIdAE//+iRjFdYjluDpvXnk7//x4t+/U4VEQJlE0kJ3TbCVjmsAaGNcbkmwmU977Q==} - maci-domainobjs@0.0.0-ci.17a38c9: - resolution: {integrity: sha512-UbokT12pJCaTlsyIrV5bPF/9BfeU0G5UTgyQs9S8z9FvZEaiWg+4fDHZ2YU1NQdyLRacwCo0d3WtDhSWw8oczQ==} + maci-domainobjs@0.0.0-ci.a97f395: + resolution: {integrity: sha512-1pbnlibRqAc9icztxU4QmK/n8NbXW7iEfUesMas1jiwHaiwHVC5+nAYu/axKNLGzygYezYlw+dwejxed+VGKEw==} maci-domainobjs@2.0.0: resolution: {integrity: sha512-FmQdIC1omsWR/98wt8WvEJj0SDfnVTl9/2FMDp3N4WwUy1lzmmlVjUGKSFKj2+dj2Rx26DmBWsmKhbTIQeoPOQ==} @@ -25441,14 +25441,14 @@ snapshots: lz-string@1.5.0: {} - maci-circuits@0.0.0-ci.17a38c9(@types/snarkjs@0.7.8)(bufferutil@4.0.8)(utf-8-validate@5.0.10): + maci-circuits@0.0.0-ci.a97f395(@types/snarkjs@0.7.8)(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@zk-kit/circuits': 0.4.0 circomkit: 0.2.1(@types/snarkjs@0.7.8)(snarkjs@0.7.4) circomlib: 2.0.5 - maci-core: 0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10) - maci-crypto: 0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10) - maci-domainobjs: 0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-core: 0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-crypto: 0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-domainobjs: 0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10) snarkjs: 0.7.4 transitivePeerDependencies: - '@types/snarkjs' @@ -25557,7 +25557,7 @@ snapshots: - typescript - utf-8-validate - maci-contracts@0.0.0-ci.17a38c9(flxhpragqyg57es62n2ruk56xe): + maci-contracts@0.0.0-ci.a97f395(flxhpragqyg57es62n2ruk56xe): dependencies: '@nomicfoundation/hardhat-ethers': 3.0.6(ethers@6.13.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(hardhat@2.22.8(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10)) '@nomicfoundation/hardhat-toolbox': 5.0.0(croq2hzbpfcsi44fcj2jwym2jy) @@ -25566,10 +25566,10 @@ snapshots: ethers: 6.13.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) hardhat: 2.22.8(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10) lowdb: 1.0.0 - maci-circuits: 0.0.0-ci.17a38c9(@types/snarkjs@0.7.8)(bufferutil@4.0.8)(utf-8-validate@5.0.10) - maci-core: 0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10) - maci-crypto: 0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10) - maci-domainobjs: 0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-circuits: 0.0.0-ci.a97f395(@types/snarkjs@0.7.8)(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-core: 0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-crypto: 0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-domainobjs: 0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10) solidity-docgen: 0.6.0-beta.36(hardhat@2.22.8(bufferutil@4.0.8)(ts-node@10.9.2(@types/node@22.2.0)(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10)) uuid: 10.0.0 transitivePeerDependencies: @@ -25668,10 +25668,10 @@ snapshots: - typescript - utf-8-validate - maci-core@0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10): + maci-core@0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: - maci-crypto: 0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10) - maci-domainobjs: 0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-crypto: 0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-domainobjs: 0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -25684,7 +25684,7 @@ snapshots: - bufferutil - utf-8-validate - maci-crypto@0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10): + maci-crypto@0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@zk-kit/baby-jubjub': 1.0.1 '@zk-kit/eddsa-poseidon': 1.0.2 @@ -25714,9 +25714,9 @@ snapshots: - bufferutil - utf-8-validate - maci-domainobjs@0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10): + maci-domainobjs@0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: - maci-crypto: 0.0.0-ci.17a38c9(bufferutil@4.0.8)(utf-8-validate@5.0.10) + maci-crypto: 0.0.0-ci.a97f395(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate