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 committed Sep 11, 2024
1 parent d032084 commit 56adbe4
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 0 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've received an attestation of a specific schema from a trusted attester
contract MerkleProofGatekeeper is SignUpGatekeeper, Ownable(msg.sender) {
// the merkle tree root
bytes32 public root;

Check warning

Code scanning / Slither

State variables that could be declared immutable Warning

MerkleProofGatekeeper.root should be immutable

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

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

/// @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 attestation is not valid or just complete silently
/// @param _user The user's Ethereum address.
/// @param _data The proof athat 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 (registeredProofs[_user]) revert AlreadyRegistered();

// register the user so it cannot be called again with the same one
registeredProofs[_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
136 changes: 136 additions & 0 deletions packages/contracts/tests/MerkleProofGatekeeper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { expect } from "chai";
import { AbiCoder, Signer, ZeroAddress, encodeBytes32String } from "ethers";
import { Keypair } from "maci-domainobjs";

import { deployContract } from "../ts/deploy";
import { getDefaultSigner, getSigners } 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;

const root = "0xfa6f66f1be66f2815b61179f5bd043b6839ac27cc1c8a0e20dd4400d89d90861";
const invalidRoot = encodeBytes32String("");
const validProof = [
"0xc65a4d494d5b974c8114f7ba4ae9081d0749b8d65119343a5f985822e15c8da1",
"0x39947c197009ff867a25c375bcc13beca66c7563d245e9d3a8c27ed20f5a1924",
"0x6b9eb4531492f03c01e616823bbc4fecbe264dce549d622150f422e341c0923a",
"0x90c638228d7b7880a9366d6253944af00e83f30147b3e38df6420dc246f81c22",
"0x167658642d62274440db47e688cce708eeb068e63e8b1a30068aa16afde5b799",
"0x53253f8482986107dde4e9aa1ac16c25bca9fe12261bb38f8ff647ab3b15d4da",
"0x7cb1946ff766827d71b942d2e7e8a03b5153c7526b309513bb335de63ca8b37f",
"0x88cf65a418b67f086e6b44dc6179aacff7638c2eb2b5793d829a5259ef7b0b08",
"0x11dc42813498e23ccb198ca8ad75c62d9006367d71b8232dff8beb0ddac61e58",
"0x3647c988dd6073eb730d74dbbf723908ee5a6a65ae4568b1a8eaa3cb10048854",
"0xbd85ee8d862baccee1c3dd36f038f485460effd932a7724e13e8b959613aee0a",
];
const invalidProof = [
"0xc65a4d494d5b974c8114f7ba4ae9081d0749b8d65119343a5f985822e15c8da1",
"0x39947c197009ff867a25c375bcc13beca66c7563d245e9d3a8c27ed20f5a1924",
"0x6b9eb4531492f03c01e616823bbc4fecbe264dce549d622150f422e341c0923a",
"0x90c638228d7b7880a9366d6253944af00e83f30147b3e38df6420dc246f81c22",
"0x167658642d62274440db47e688cce708eeb068e63e8b1a30068aa16afde5b799",
"0x53253f8482986107dde4e9aa1ac16c25bca9fe12261bb38f8ff647ab3b15d4da",
"0x7cb1946ff766827d71b942d2e7e8a03b5153c7526b309513bb335de63ca8b37f",
"0x88cf65a418b67f086e6b44dc6179aacff7638c2eb2b5793d829a5259ef7b0b08",
"0x11dc42813498e23ccb198ca8ad75c62d9006367d71b8232dff8beb0ddac61e58",
"0x3647c988dd6073eb730d74dbbf723908ee5a6a65ae4568b1a8eaa3cb10048854",
"0x00000000000000fa6f66f1be66f2815b61179f5bd043b6839ac0000000000000",
];

const user = new Keypair();

before(async () => {
signer = await getDefaultSigner();
signerAddress = await signer.getAddress();
merkleProofGatekeeper = await deployContract("MerkleProofGatekeeper", signer, true, 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;
});

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

0 comments on commit 56adbe4

Please sign in to comment.