-
Notifications
You must be signed in to change notification settings - Fork 160
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(merkleroot-gatekeeper): adds a gatekeeper that uses merkle tree
- Loading branch information
1 parent
d032084
commit f7fdba7
Showing
5 changed files
with
381 additions
and
41 deletions.
There are no files selected for viewing
71 changes: 71 additions & 0 deletions
71
packages/contracts/contracts/gatekeepers/MerkleProofGatekeeper.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
Parameter MerkleProofGatekeeper.setMaciInstance(address)._maci is not in mixedCase
|
||
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
Parameter MerkleProofGatekeeper.register(address,bytes)._data is not in mixedCase
Check warning Code scanning / Slither Conformance to Solidity naming conventions Warning
Parameter MerkleProofGatekeeper.register(address,bytes)._user is not in mixedCase
|
||
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.