Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(merkleroot-gatekeeper): adds a gatekeeper that uses merkle tree #1821

Merged
merged 1 commit into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

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

/// @title MerkleProofGatekeeper
/// @notice A gatekeeper contract which allows users to sign up to MACI
/// only if they are part of the tree
contract MerkleProofGatekeeper is SignUpGatekeeper, Ownable(msg.sender) {
// the merkle tree root
bytes32 public immutable root;

/// @notice the reference to the MACI contract
address public maci;

// a mapping of addresses that have already registered
mapping(address => bool) public registeredAddresses;

/// @notice custom errors
error InvalidProof();
error AlreadyRegistered();
error OnlyMACI();
error ZeroAddress();
error InvalidRoot();

/// @notice Deploy an instance of MerkleProofGatekeeper
/// @param _root The tree root
constructor(bytes32 _root) payable {
if (_root == bytes32(0)) revert InvalidRoot();
root = _root;
}

/// @notice Adds an uninitialised MACI instance to allow for token signups
/// @param _maci The MACI contract interface to be stored
function setMaciInstance(address _maci) public override onlyOwner {
Dismissed Show dismissed Hide dismissed
if (_maci == address(0)) revert ZeroAddress();
maci = _maci;
}

/// @notice Register an user based on being part of the tree
/// @dev Throw if the proof is not valid or the user has already been registered
/// @param _user The user's Ethereum address.
/// @param _data The proof that the user is part of the tree.
function register(address _user, bytes memory _data) public override {
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
// ensure that the caller is the MACI contract
if (maci != msg.sender) revert OnlyMACI();

bytes32[] memory proof = abi.decode(_data, (bytes32[]));

// ensure that the user has not been registered yet
if (registeredAddresses[_user]) revert AlreadyRegistered();

// register the user so it cannot be called again with the same one
registeredAddresses[_user] = true;

// get the leaf
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(_user))));
ctrlc03 marked this conversation as resolved.
Show resolved Hide resolved

// check the proof
if (!MerkleProof.verify(proof, root, leaf)) revert InvalidProof();
}

/// @notice Get the trait of the gatekeeper
/// @return The type of the gatekeeper
function getTrait() public pure override returns (string memory) {
return "MerkleProof";
}
}
1 change: 1 addition & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"@nomicfoundation/hardhat-ethers": "^3.0.6",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@openzeppelin/contracts": "^5.0.2",
"@openzeppelin/merkle-tree": "^1.0.7",
"circomlibjs": "^0.1.7",
"ethers": "^6.13.2",
"hardhat": "^2.22.8",
Expand Down
124 changes: 124 additions & 0 deletions packages/contracts/tests/MerkleProofGatekeeper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
import { expect } from "chai";
import { AbiCoder, Signer, ZeroAddress, encodeBytes32String } from "ethers";
import { Keypair } from "maci-domainobjs";

import { deployContract } from "../ts/deploy";
import { getDefaultSigner, getSigners, generateMerkleTree } from "../ts/utils";
import { MerkleProofGatekeeper, MACI } from "../typechain-types";

import { STATE_TREE_DEPTH, initialVoiceCreditBalance } from "./constants";
import { deployTestContracts } from "./utils";

describe("MerkleProof Gatekeeper", () => {
let merkleProofGatekeeper: MerkleProofGatekeeper;
let signer: Signer;
let signerAddress: string;
let tree: StandardMerkleTree<string[]>;
let validProof: string[];

const allowedAddress = [
["0x2fbca3862a7d99486c61e0275b6f5660180fb1b3"],
["0x70564145fa8e8a15348ef0190e6b7c07a2120462"],
["0x27cfc88640089f340aeaec182baff0ddf15b1b37"],
["0xccde65cf4e39a2d28b50e3030fdab60c463fe215"],
["0x9bae2cfa33280a8332da9a3bd589f91935b12804"],
];

const invalidRoot = encodeBytes32String("");
const invalidProof = ["0x0000000000000000000000000000000000000000000000000000000000000000"];

const user = new Keypair();

before(async () => {
signer = await getDefaultSigner();
signerAddress = await signer.getAddress();
allowedAddress.push([signerAddress]);
tree = generateMerkleTree(allowedAddress);
merkleProofGatekeeper = await deployContract("MerkleProofGatekeeper", signer, true, tree.root);
});

describe("Deployment", () => {
it("The gatekeeper should be deployed correctly", async () => {
expect(merkleProofGatekeeper).to.not.eq(undefined);
expect(await merkleProofGatekeeper.getAddress()).to.not.eq(ZeroAddress);
});

it("should fail to deploy when the root is not valid", async () => {
await expect(deployContract("MerkleProofGatekeeper", signer, true, invalidRoot)).to.be.revertedWithCustomError(
merkleProofGatekeeper,
"InvalidRoot",
);
});
});

describe("MerkleProofGatekeeper", () => {
let maciContract: MACI;

before(async () => {
const r = await deployTestContracts({
initialVoiceCreditBalance,
stateTreeDepth: STATE_TREE_DEPTH,
signer,
gatekeeper: merkleProofGatekeeper,
});

maciContract = r.maciContract;
validProof = tree.getProof([signerAddress]);
});

it("sets MACI instance correctly", async () => {
const maciAddress = await maciContract.getAddress();
await merkleProofGatekeeper.setMaciInstance(maciAddress).then((tx) => tx.wait());

expect(await merkleProofGatekeeper.maci()).to.eq(maciAddress);
});

it("should fail to set MACI instance when the caller is not the owner", async () => {
const [, secondSigner] = await getSigners();
await expect(
merkleProofGatekeeper.connect(secondSigner).setMaciInstance(signerAddress),
).to.be.revertedWithCustomError(merkleProofGatekeeper, "OwnableUnauthorizedAccount");
});

it("should fail to set MACI instance when the MACI instance is not valid", async () => {
await expect(merkleProofGatekeeper.setMaciInstance(ZeroAddress)).to.be.revertedWithCustomError(
merkleProofGatekeeper,
"ZeroAddress",
);
});

it("should throw when the proof is invalid)", async () => {
await merkleProofGatekeeper.setMaciInstance(signerAddress).then((tx) => tx.wait());

await expect(
merkleProofGatekeeper.register(signerAddress, AbiCoder.defaultAbiCoder().encode(["bytes32[]"], [invalidProof])),
).to.be.revertedWithCustomError(merkleProofGatekeeper, "InvalidProof");
});

it("should register a user if the register function is called with the valid data", async () => {
await merkleProofGatekeeper.setMaciInstance(await maciContract.getAddress()).then((tx) => tx.wait());

// signup via MACI
const tx = await maciContract.signUp(
user.pubKey.asContractParam(),
AbiCoder.defaultAbiCoder().encode(["bytes32[]"], [validProof]),
AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
);

const receipt = await tx.wait();

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

it("should prevent signing up twice", async () => {
await expect(
maciContract.signUp(
user.pubKey.asContractParam(),
AbiCoder.defaultAbiCoder().encode(["bytes32[]"], [validProof]),
AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
),
).to.be.revertedWithCustomError(merkleProofGatekeeper, "AlreadyRegistered");
});
});
});
6 changes: 6 additions & 0 deletions packages/contracts/ts/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { StandardMerkleTree } from "@openzeppelin/merkle-tree";

import type { Action, SnarkProof, Groth16Proof } from "./types";
import type { Ownable } from "../typechain-types";
import type { BigNumberish, FeeData, Network, Signer } from "ethers";
Expand Down Expand Up @@ -143,3 +145,7 @@ export const transferOwnership = async <T extends Ownable>(
export function asHex(value: BigNumberish): string {
return `0x${BigInt(value).toString(16)}`;
}

export function generateMerkleTree(elements: string[][]): StandardMerkleTree<string[]> {
return StandardMerkleTree.of(elements, ["address"]);
}
Loading
Loading