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(contracts): implement semaphore gatekeeper #1579

Merged
merged 1 commit into from
Jun 21, 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
73 changes: 73 additions & 0 deletions contracts/contracts/gatekeepers/SemaphoreGatekeeper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

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

/// @title SemaphoreGatekeeper
/// @notice A gatekeeper contract which allows users to sign up to MACI
/// only if they can prove they are part of a semaphore group.
/// @dev Please note that once a identity is used to register, it cannot be used again.
/// This is because we store the nullifier which is
/// hash(secret, groupId)
contract SemaphoreGatekeeper is SignUpGatekeeper, Ownable(msg.sender) {
/// @notice The group id of the semaphore group
uint256 public immutable groupId;

/// @notice The semaphore contract
ISemaphore public immutable semaphoreContract;

/// @notice The address of the MACI contract
address public maci;

/// @notice The registered identities
mapping(uint256 => bool) public registeredIdentities;

/// @notice Errors
error ZeroAddress();
error OnlyMACI();
error AlreadyRegistered();
error InvalidGroup();
error InvalidProof();

/// @notice Create a new instance of the gatekeeper
/// @param _semaphoreContract The address of the semaphore contract
/// @param _groupId The group id of the semaphore group
constructor(address _semaphoreContract, uint256 _groupId) payable {
if (_semaphoreContract == address(0)) revert ZeroAddress();
semaphoreContract = ISemaphore(_semaphoreContract);
groupId = _groupId;
}

/// @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 if they can prove they belong to a semaphore group
/// @dev Throw if the proof is not valid or just complete silently
/// @param _data The ABI-encoded schemaId as a uint256.
function register(address /*_user*/, bytes memory _data) public override {
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed
// decode the argument
ISemaphore.SemaphoreProof memory proof = abi.decode(_data, (ISemaphore.SemaphoreProof));

// ensure that the caller is the MACI contract
if (maci != msg.sender) revert OnlyMACI();

// ensure that the nullifier has not been registered yet
if (registeredIdentities[proof.nullifier]) revert AlreadyRegistered();

// check that the scope is equal to the group id
if (proof.scope != groupId) revert InvalidGroup();

// register the nullifier so it cannot be called again with the same one
registeredIdentities[proof.nullifier] = true;

// check if the proof validates
if (!semaphoreContract.verifyProof(proof.scope, proof)) revert InvalidProof();
}
}
20 changes: 20 additions & 0 deletions contracts/contracts/interfaces/ISemaphore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

Check warning

Code scanning / Slither

Incorrect versions of Solidity Warning

Version constraint 0.8.20 contains known severe issues (https://solidity.readthedocs.io/en/latest/bugs.html)
- VerbatimInvalidDeduplication
- FullInlinerNonExpressionSplitArgumentEvaluationOrder
- MissingSideEffectsOnSelectorAccess.
It is used by:
- 0.8.20

/// @title Semaphore contract interface.
interface ISemaphore {
/// It defines all the Semaphore proof parameters used by Semaphore.sol.
struct SemaphoreProof {
uint256 merkleTreeDepth;
uint256 merkleTreeRoot;
uint256 nullifier;
uint256 message;
uint256 scope;
uint256[8] points;
}

/// @dev Verifies a zero-knowledge proof by returning true or false.
/// @param groupId: Id of the group.
/// @param proof: Semaphore zero-knowledge proof.
function verifyProof(uint256 groupId, SemaphoreProof calldata proof) external view returns (bool);
}
29 changes: 29 additions & 0 deletions contracts/contracts/mocks/MockSemaphore.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

/// @title MockSemaphore
/// @notice A mock contract to test the Semaphore gatekeeper
contract MockSemaphore is ISemaphore {
/// @notice The group id
uint256 public immutable groupId;

bool public valid = true;

/// @notice Create a new instance
/// @param _groupId The group id
constructor(uint256 _groupId) {
groupId = _groupId;
}

/// @notice mock function to flip the valid state
function flipValid() external {
valid = !valid;
}

/// @notice Verify a proof for the group
function verifyProof(uint256 _groupId, SemaphoreProof calldata proof) external view returns (bool) {
return valid;
}
}
1 change: 1 addition & 0 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"test:hats_gatekeeper": "pnpm run test ./tests/HatsGatekeeper.test.ts",
"test:gitcoin_gatekeeper": "pnpm run test ./tests/GitcoinPassportGatekeeper.test.ts",
"test:zupass_gatekeeper": "pnpm run test ./tests/ZupassGatekeeper.test.ts",
"test:semaphore_gatekeeper": "pnpm run test ./tests/SemaphoreGatekeeper.test.ts",
"deploy": "hardhat deploy-full",
"deploy-poll": "hardhat deploy-poll",
"verify": "hardhat verify-full",
Expand Down
159 changes: 159 additions & 0 deletions contracts/tests/SemaphoreGatekeeper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { expect } from "chai";
import { AbiCoder, Signer, ZeroAddress } from "ethers";
import { Keypair } from "maci-domainobjs";

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

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

describe("Semaphore Gatekeeper", () => {
let semaphoreGatekeeper: SemaphoreGatekeeper;
let mockSemaphore: MockSemaphore;
let signer: Signer;
let signerAddress: string;

const user = new Keypair();

const validGroupId = 0n;
const invalidGroupId = 1n;

const proof = {
merkleTreeDepth: 1n,
merkleTreeRoot: 0n,
nullifier: 0n,
message: 0n,
scope: validGroupId,
points: [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n],
};

const invalidProof = {
merkleTreeDepth: 1n,
merkleTreeRoot: 0n,
nullifier: 0n,
message: 0n,
scope: invalidGroupId,
points: [1n, 0n, 0n, 0n, 0n, 0n, 0n, 0n],
};

const encodedProof = AbiCoder.defaultAbiCoder().encode(
["uint256", "uint256", "uint256", "uint256", "uint256", "uint256[8]"],
[proof.merkleTreeDepth, proof.merkleTreeRoot, proof.nullifier, proof.message, proof.scope, proof.points],
);

const encodedProofInvalidGroupId = AbiCoder.defaultAbiCoder().encode(
["uint256", "uint256", "uint256", "uint256", "uint256", "uint256[8]"],
[proof.merkleTreeDepth, proof.merkleTreeRoot, proof.nullifier, proof.message, invalidGroupId, proof.points],
);

const encodedInvalidProof = AbiCoder.defaultAbiCoder().encode(
["uint256", "uint256", "uint256", "uint256", "uint256", "uint256[8]"],
[
invalidProof.merkleTreeDepth,
invalidProof.merkleTreeRoot,
invalidProof.nullifier,
invalidProof.message,
validGroupId,
invalidProof.points,
],
);

before(async () => {
signer = await getDefaultSigner();
mockSemaphore = await deployContract("MockSemaphore", signer, true, validGroupId);
const mockSemaphoreAddress = await mockSemaphore.getAddress();
signerAddress = await signer.getAddress();
semaphoreGatekeeper = await deployContract("SemaphoreGatekeeper", signer, true, mockSemaphoreAddress, validGroupId);
});

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

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

before(async () => {
const r = await deployTestContracts(
initialVoiceCreditBalance,
STATE_TREE_DEPTH,
signer,
true,
semaphoreGatekeeper,
);

maciContract = r.maciContract;
});

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

expect(await semaphoreGatekeeper.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(
semaphoreGatekeeper.connect(secondSigner).setMaciInstance(signerAddress),
).to.be.revertedWithCustomError(semaphoreGatekeeper, "OwnableUnauthorizedAccount");
});

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

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

await expect(
maciContract.signUp(
user.pubKey.asContractParam(),
encodedProofInvalidGroupId,
AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
),
).to.be.revertedWithCustomError(semaphoreGatekeeper, "InvalidGroup");
});

it("should revert if the proof is invalid (mock)", async () => {
await mockSemaphore.flipValid();
await expect(
maciContract.signUp(
user.pubKey.asContractParam(),
encodedInvalidProof,
AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
),
).to.be.revertedWithCustomError(semaphoreGatekeeper, "InvalidProof");
await mockSemaphore.flipValid();
});

it("should register a user if the register function is called with the valid data", async () => {
const tx = await maciContract.signUp(
user.pubKey.asContractParam(),
encodedProof,
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(),
encodedProof,
AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
),
).to.be.revertedWithCustomError(semaphoreGatekeeper, "AlreadyRegistered");
});
});
});
Loading