From c29b336ca4b0ce98f2fdc41e1908774e0685e4fd Mon Sep 17 00:00:00 2001 From: 0xmad <0xmad@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:35:55 -0500 Subject: [PATCH] feat(contracts): add registry integration - [x] Add deploy poll task - [x] Add init poll task - [x] Link registry and poll - [x] Add deploy poll registry method for MACI --- .github/workflows/hardhat-tasks.yml | 2 + .../contracts/contracts/interfaces/IPoll.sol | 4 + packages/contracts/contracts/maci/MACI.sol | 18 +- packages/contracts/contracts/maci/Poll.sol | 25 ++- packages/contracts/deploy-config-example.json | 14 ++ packages/contracts/hardhat.config.ts | 2 + packages/contracts/package.json | 6 +- packages/contracts/tasks/deploy/index.ts | 2 +- .../contracts/tasks/deploy/poll/01-poll.ts | 197 ++++++++++++++++++ .../tasks/helpers/constants/index.ts | 1 + packages/contracts/tasks/runner/initPoll.ts | 60 ++++++ packages/contracts/tests/Maci.test.ts | 85 +++++++- 12 files changed, 406 insertions(+), 10 deletions(-) create mode 100644 packages/contracts/tasks/deploy/poll/01-poll.ts create mode 100644 packages/contracts/tasks/runner/initPoll.ts 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..92b57910 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 @@ -49,12 +50,27 @@ contract MACI is Ownable, BaseMACI, ICommon { ) {} + /// @notice Deploy a new instance of the registry contract + /// @param max The maximum number of projects that can be registered + /// @param url The metadata url + /// @param easAddress The EAS address + function deployPollRegistry( + uint256 max, + bytes32 url, + address easAddress + ) public onlyOwner returns (address) { + EASRegistry registry = new EASRegistry(max, url, easAddress, address(registryManager)); + + return address(registry); + } + /// @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..95ea7ba9 100644 --- a/packages/contracts/contracts/maci/Poll.sol +++ b/packages/contracts/contracts/maci/Poll.sol @@ -4,18 +4,27 @@ 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 custom errors + error RegistryAlreadyInitialized(); + /// @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,6 +37,20 @@ contract Poll is Ownable, BasePoll { BasePoll(duration, treeDepths, coordinatorPubKey, extContracts, emptyBallotRoot) {} + /// @notice Set poll registry. + /// @param registryAddress The registry address + function setRegistry(address registryAddress) public onlyOwner { + if (registryAddress == address(0)) { + revert InvalidAddress(); + } + + if (address(registry) != address(0)) { + revert RegistryAlreadyInitialized(); + } + + registry = registryAddress; + } + /// @notice The initialization function. function init() public override onlyOwner { super.init(); 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..52de49ee 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", 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/poll/01-poll.ts b/packages/contracts/tasks/deploy/poll/01-poll.ts new file mode 100644 index 00000000..a7bccc44 --- /dev/null +++ b/packages/contracts/tasks/deploy/poll/01-poll.ts @@ -0,0 +1,197 @@ +/* eslint-disable no-console */ +import { encodeBytes32String } from "ethers"; +import { ContractStorage, Deployment, EContracts as EMaciContracts, EMode } from "maci-contracts"; +import { PubKey } from "maci-domainobjs"; + +import { type Poll, type MACI, type EASRegistry } from "../../../typechain-types"; +import { EContracts, EDeploySteps } 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.RegistryManager; + 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 easRegistryAddress = await maciContract.deployPollRegistry.staticCall( + maxRecipients, + encodeBytes32String(metadataUrl), + easAddress, + ); + + const deployPollRegistryReceipt = await maciContract + .deployPollRegistry(maxRecipients, encodeBytes32String(metadataUrl), easAddress) + .then((transaction) => transaction.wait()); + + if (deployPollRegistryReceipt?.status !== 1) { + throw new Error("Deploy poll registry transaction is failed"); + } + + 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 easRegistry = await deployment.getContract({ + name: EContracts.EASRegistry as unknown as EMaciContracts, + address: easRegistryAddress, + }); + 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: EContracts.EASRegistry, + key: `poll-${pollId}`, + contract: easRegistry, + args: [maxRecipients, metadataUrl, easAddress, registryManagerAddress], + 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..5f452dfb 100644 --- a/packages/contracts/tasks/helpers/constants/index.ts +++ b/packages/contracts/tasks/helpers/constants/index.ts @@ -21,6 +21,7 @@ export const EDeploySteps = { export enum EPlatformContracts { RegistryManager = "RegistryManager", EASRegistryManager = "EASRegistryManager", + EASRegistry = "EASRegistry", } /** 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..c77088a4 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 () => { @@ -79,18 +81,62 @@ describe("Poll", () => { maciState.polls.get(pollId)?.publishMessage(message, padKey); }); + it("should fail if non-owner tries to deploy registry", async () => { + await expect(pollContract.init()).to.be.revertedWithCustomError(pollContract, "OwnableUnauthorizedAccount"); + }); + 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 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 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", ); @@ -104,6 +150,20 @@ describe("Poll", () => { ); }); + 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 set the poll registry twice", async () => { + await expect(pollContract.connect(owner).setRegistry(await user.getAddress())).to.be.revertedWithCustomError( + pollContract, + "RegistryAlreadyInitialized", + ); + }); + it("should not be possible to set registry manager by non-owner", async () => { const registryManager = await deployContract("RegistryManager", owner, true); @@ -121,5 +181,18 @@ describe("Poll", () => { expect(await maciContract.registryManager()).to.equal(registryManagerContractAddress); }); + + it("should fail if non-owner tries to deploy registry", async () => { + await expect( + maciContract.connect(user).deployPollRegistry(maxRecipients, metadataUrl, await user.getAddress()), + ).to.be.revertedWithCustomError(maciContract, "OwnableUnauthorizedAccount"); + }); + + it("should deploy registry properly", async () => { + const tx = await maciContract.deployPollRegistry(maxRecipients, metadataUrl, await user.getAddress()); + const receipt = await tx.wait(); + + expect(receipt?.status).to.equal(1); + }); }); });