Skip to content

Commit

Permalink
feat(merkleroot-gatekeeper): adds a gatekeeper that uses merkle tree
Browse files Browse the repository at this point in the history
  • Loading branch information
Crisgarner authored and crisgarner committed Sep 11, 2024
1 parent d032084 commit f7fdba7
Show file tree
Hide file tree
Showing 5 changed files with 381 additions and 41 deletions.
71 changes: 71 additions & 0 deletions packages/contracts/contracts/gatekeepers/MerkleProofGatekeeper.sol
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 {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

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 {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

// 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))));

// 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";
}
}

Check warning

Code scanning / Slither

Contracts that lock Ether Medium

Contract locking ether found:
Contract MerkleProofGatekeeper has payable functions:
- MerkleProofGatekeeper.constructor(bytes32)
But does not have a function to withdraw the ether
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

0 comments on commit f7fdba7

Please sign in to comment.