Skip to content

Commit

Permalink
feat(contracts): add custom maci contracts
Browse files Browse the repository at this point in the history
- [x] Customize Poll and PollFactory
- [x] Customize MACI
- [x] Add tests
  • Loading branch information
0xmad committed Aug 16, 2024
1 parent 0e750c4 commit 9602484
Show file tree
Hide file tree
Showing 13 changed files with 533 additions and 107 deletions.
20 changes: 20 additions & 0 deletions packages/contracts/.solcover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const { buildPoseidonT3, buildPoseidonT4, buildPoseidonT5, buildPoseidonT6 } = require("maci-contracts");
const fs = require("fs");
const path = require("path");

const PATHS = [
path.resolve(__dirname, "..", "artifacts"),
path.resolve(__dirname, "..", "cache"),
path.resolve(__dirname, "..", "typechain-types"),
];

module.exports = {
onPreCompile: async () => {
await Promise.all(
PATHS.map((filepath) => fs.existsSync(filepath) && fs.promises.rm(filepath, { recursive: true })),
);
},
onCompileComplete: async () => {
await Promise.all([buildPoseidonT3(), buildPoseidonT4(), buildPoseidonT5(), buildPoseidonT6()]);
},
};
28 changes: 28 additions & 0 deletions packages/contracts/contracts/interfaces/IOwnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { IPoll as IPollBase } from "maci-contracts/contracts/interfaces/IPoll.sol";

Check warning on line 4 in packages/contracts/contracts/interfaces/IOwnable.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

imported name IPollBase is not used

/// @title IOwnable
/// @notice Ownable interface
interface IOwnable {
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) external;

/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() external;

/**
* @dev Returns the address of the current owner.
*/
function owner() external view returns (address);
}
13 changes: 13 additions & 0 deletions packages/contracts/contracts/interfaces/IPoll.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { IPoll as IPollBase } from "maci-contracts/contracts/interfaces/IPoll.sol";

import { IOwnable } from "./IOwnable.sol";

/// @title IPollBase
/// @notice Poll interface
interface IPoll is IPollBase, IOwnable {
/// @notice The initialization function.
function init() external;
}
56 changes: 56 additions & 0 deletions packages/contracts/contracts/maci/MACI.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { MACI as BaseMACI } from "maci-contracts/contracts/MACI.sol";
import { IPollFactory } from "maci-contracts/contracts/interfaces/IPollFactory.sol";
import { IMessageProcessorFactory } from "maci-contracts/contracts/interfaces/IMPFactory.sol";
import { ITallyFactory } from "maci-contracts/contracts/interfaces/ITallyFactory.sol";
import { InitialVoiceCreditProxy } from "maci-contracts/contracts/initialVoiceCreditProxy/InitialVoiceCreditProxy.sol";
import { SignUpGatekeeper } from "maci-contracts/contracts/gatekeepers/SignUpGatekeeper.sol";

import { IPoll } from "../interfaces/IPoll.sol";

/// @title MACI - Minimum Anti-Collusion Infrastructure
/// @notice A contract which allows users to sign up, and deploy new polls
contract MACI is Ownable, BaseMACI {
/// @notice Create a new instance of the MACI contract.
/// @param pollFactory The PollFactory contract
/// @param messageProcessorFactory The MessageProcessorFactory contract
/// @param tallyFactory The TallyFactory contract
/// @param signUpGatekeeper The SignUpGatekeeper contract
/// @param initialVoiceCreditProxy The InitialVoiceCreditProxy contract
/// @param stateTreeDepth The depth of the state tree
/// @param emptyBallotRoots The roots of the empty ballot trees
constructor(
IPollFactory pollFactory,
IMessageProcessorFactory messageProcessorFactory,
ITallyFactory tallyFactory,
SignUpGatekeeper signUpGatekeeper,
InitialVoiceCreditProxy initialVoiceCreditProxy,
uint8 stateTreeDepth,
uint256[5] memory emptyBallotRoots
)
payable
Ownable(msg.sender)
BaseMACI(
pollFactory,
messageProcessorFactory,
tallyFactory,
signUpGatekeeper,
initialVoiceCreditProxy,
stateTreeDepth,
emptyBallotRoots
)
{}

/// @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 {
PollContracts memory pollAddresses = polls[pollId];
IPoll poll = IPoll(pollAddresses.poll);

poll.init();
poll.transferOwnership(msg.sender);
}
}
35 changes: 35 additions & 0 deletions packages/contracts/contracts/maci/Poll.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { Poll as BasePoll } from "maci-contracts/contracts/Poll.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 {
/// @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
constructor(
uint256 duration,
TreeDepths memory treeDepths,
PubKey memory coordinatorPubKey,
ExtContracts memory extContracts,
uint256 emptyBallotRoot
)
payable
Ownable(address(extContracts.maci))
BasePoll(duration, treeDepths, coordinatorPubKey, extContracts, emptyBallotRoot)
{}

/// @notice The initialization function.
function init() public override onlyOwner {
super.init();
}
}
34 changes: 34 additions & 0 deletions packages/contracts/contracts/maci/PollFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { PollFactory as BasePollFactory } from "maci-contracts/contracts/PollFactory.sol";
import { IMACI } from "maci-contracts/contracts/interfaces/IMACI.sol";
import { AccQueue } from "maci-contracts/contracts/trees/AccQueue.sol";
import { AccQueueQuinaryMaci } from "maci-contracts/contracts/trees/AccQueueQuinaryMaci.sol";
import { Poll } from "./Poll.sol";

/// @title PollFactory
/// @notice A factory contract which deploys Poll contracts. It allows the MACI contract
/// size to stay within the limit set by EIP-170.
contract PollFactory is BasePollFactory {
/// @inheritdoc BasePollFactory
function deploy(
uint256 duration,
TreeDepths calldata treeDepths,
PubKey calldata coordinatorPubKey,
address maci,
uint256 emptyBallotRoot
) public virtual override returns (address pollAddr) {
/// @notice deploy a new AccQueue contract to store messages
AccQueue messageAq = new AccQueueQuinaryMaci(treeDepths.messageTreeSubDepth);

/// @notice the smart contracts that a Poll would interact with
ExtContracts memory extContracts = ExtContracts({ maci: IMACI(maci), messageAq: messageAq });

Poll poll = new Poll(duration, treeDepths, coordinatorPubKey, extContracts, emptyBallotRoot);

messageAq.transferOwnership(address(poll));

pollAddr = address(poll);
}
}
16 changes: 16 additions & 0 deletions packages/contracts/contracts/mocks/Mocker.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "maci-contracts/contracts/crypto/Hasher.sol";

Check warning on line 4 in packages/contracts/contracts/mocks/Mocker.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

global import of path maci-contracts/contracts/crypto/Hasher.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "maci-contracts/contracts/crypto/MockVerifier.sol";

Check warning on line 5 in packages/contracts/contracts/mocks/Mocker.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

global import of path maci-contracts/contracts/crypto/MockVerifier.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "maci-contracts/contracts/gatekeepers/FreeForAllSignUpGatekeeper.sol";

Check warning on line 6 in packages/contracts/contracts/mocks/Mocker.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

global import of path maci-contracts/contracts/gatekeepers/FreeForAllSignUpGatekeeper.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "maci-contracts/contracts/initialVoiceCreditProxy/ConstantInitialVoiceCreditProxy.sol";

Check warning on line 7 in packages/contracts/contracts/mocks/Mocker.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

global import of path maci-contracts/contracts/initialVoiceCreditProxy/ConstantInitialVoiceCreditProxy.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "maci-contracts/contracts/VkRegistry.sol";

Check warning on line 8 in packages/contracts/contracts/mocks/Mocker.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

global import of path maci-contracts/contracts/VkRegistry.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "maci-contracts/contracts/TallyFactory.sol";

Check warning on line 9 in packages/contracts/contracts/mocks/Mocker.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

global import of path maci-contracts/contracts/TallyFactory.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)
import "maci-contracts/contracts/MessageProcessorFactory.sol";

Check warning on line 10 in packages/contracts/contracts/mocks/Mocker.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

global import of path maci-contracts/contracts/MessageProcessorFactory.sol is not allowed. Specify names to import individually or bind all exports of the module into a name (import "path" as Name)

/// @title Mocker
/// @notice import all MACI protocol related contract for tests
contract Mocker {

Check warning on line 14 in packages/contracts/contracts/mocks/Mocker.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Code contains empty blocks

}
5 changes: 4 additions & 1 deletion packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"postbuild": "cp -r ./artifacts ./build",
"types": "tsc -p tsconfig.json --noEmit",
"docs": "hardhat docgen",
"coverage": "BLOCK_GAS_LIMIT=1599511627775 hardhat coverage",
"test": "hardhat test --network hardhat"
},
"dependencies": {
Expand All @@ -40,7 +41,9 @@
"ethers": "^6.13.2",
"hardhat": "^2.22.8",
"lowdb": "^1.0.0",
"maci-contracts": "^2.1.0",
"maci-contracts": "0.0.0-ci.b8d42a3",
"maci-core": "^2.0.0",
"maci-domainobjs": "^2.0.0",
"solidity-docgen": "^0.6.0-beta.36"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions packages/contracts/scripts/compileSol.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import hre from "hardhat";
import { buildPoseidonT3, buildPoseidonT4, buildPoseidonT5, buildPoseidonT6 } from "maci-contracts";

import fs from "fs";
import path from "path";
Expand All @@ -13,6 +14,8 @@ async function main(): Promise<void> {
await Promise.all(PATHS.map((filepath) => fs.existsSync(filepath) && fs.promises.rm(filepath, { recursive: true })));

await hre.run("compile");

await Promise.all([buildPoseidonT3(), buildPoseidonT4(), buildPoseidonT5(), buildPoseidonT6()]);
}

main();
100 changes: 100 additions & 0 deletions packages/contracts/tests/Poll.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { expect } from "chai";
import { Signer } from "ethers";
import { Verifier, VkRegistry, EMode, getSigners } from "maci-contracts";
import { MaciState } from "maci-core";
import { Keypair, Message, PubKey } from "maci-domainobjs";

import { MACI, Poll__factory as PollFactory, Poll as PollContract } from "../typechain-types";

import {
NOTHING_UP_MY_SLEEVE,
STATE_TREE_DEPTH,
duration,
initialVoiceCreditBalance,
messageBatchSize,
treeDepths,
} from "./constants";
import { deployTestContracts } from "./utils";

describe("Poll", () => {
let maciContract: MACI;
let pollId: bigint;
let pollContract: PollContract;
let verifierContract: Verifier;
let vkRegistryContract: VkRegistry;
let owner: Signer;
let user: Signer;
let deployTime: number;
const coordinator = new Keypair();

const maciState = new MaciState(STATE_TREE_DEPTH);

describe("deployment", () => {
before(async () => {
[owner, user] = await getSigners();

const contracts = await deployTestContracts({
initialVoiceCreditBalance,
stateTreeDepth: STATE_TREE_DEPTH,
signer: owner,
});
maciContract = contracts.maciContract;
verifierContract = contracts.mockVerifierContract as Verifier;
vkRegistryContract = contracts.vkRegistryContract;

// deploy on chain poll
const tx = await maciContract.deployPoll(
duration,
treeDepths,
coordinator.pubKey.asContractParam(),
verifierContract,
vkRegistryContract,
EMode.QV,
);
const receipt = await tx.wait();

const block = await owner.provider!.getBlock(receipt!.blockHash);
deployTime = block!.timestamp;

expect(receipt?.status).to.eq(1);

pollId = (await maciContract.nextPollId()) - 1n;

const pollContracts = await maciContract.getPoll(pollId);
pollContract = PollFactory.connect(pollContracts.poll, owner);

// deploy local poll
const p = maciState.deployPoll(BigInt(deployTime + duration), treeDepths, messageBatchSize, coordinator);
expect(p.toString()).to.eq(pollId.toString());
// publish the NOTHING_UP_MY_SLEEVE message
const messageData = [NOTHING_UP_MY_SLEEVE];
for (let i = 1; i < 10; i += 1) {
messageData.push(BigInt(0));
}
const message = new Message(messageData);
const padKey = new PubKey([
BigInt("10457101036533406547632367118273992217979173478358440826365724437999023779287"),
BigInt("19824078218392094440610104313265183977899662750282163392862422243483260492317"),
]);
maciState.polls.get(pollId)?.publishMessage(message, padKey);
});

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(
pollContract,
"OwnableUnauthorizedAccount",
);
await expect(pollContract.init()).to.be.revertedWithCustomError(pollContract, "PollAlreadyInit");
});

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(
pollContract,
"OwnableUnauthorizedAccount",
);
await expect(pollContract.init()).to.be.revertedWithCustomError(pollContract, "PollAlreadyInit");
});
});
});
20 changes: 20 additions & 0 deletions packages/contracts/tests/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { TreeDepths, STATE_TREE_ARITY, MESSAGE_TREE_ARITY } from "maci-core";

export const duration = 2_000;

export const STATE_TREE_DEPTH = 10;
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 initialVoiceCreditBalance = 100;

export const treeDepths: TreeDepths = {
intStateTreeDepth: 1,
messageTreeDepth: MESSAGE_TREE_DEPTH,
messageTreeSubDepth: MESSAGE_TREE_SUBDEPTH,
voteOptionTreeDepth: 2,
};

export const tallyBatchSize = STATE_TREE_ARITY ** treeDepths.intStateTreeDepth;
Loading

0 comments on commit 9602484

Please sign in to comment.