Skip to content

Commit

Permalink
feat(contracts): implement semaphore gatekeeper
Browse files Browse the repository at this point in the history
  • Loading branch information
ctrlc03 committed Jun 20, 2024
1 parent 3a278fb commit 9fb4cd8
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 0 deletions.
83 changes: 83 additions & 0 deletions contracts/contracts/gatekeepers/SemaphoreGatekeeper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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 {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

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 {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

// decode the argument
(ISemaphore.SemaphoreProof memory proof, uint256 _groupId) = abi.decode(
_data,
(ISemaphore.SemaphoreProof, uint256)
);

// 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 proof is for this semaphore group
if (_groupId != groupId) revert InvalidGroup();

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

/// @notice force the scope to be the group id
/// this way we can ensure that the nullifier is
/// hash(secret, groupId)
/// and thus one identity can only be registered once via this
/// gatekeeper
proof.scope = groupId;

// check if the proof validates
if (!semaphoreContract.verifyProof(_groupId, proof)) revert InvalidProof();
}
}

Check warning

Code scanning / Slither

Contracts that lock Ether Medium

Contract locking ether found:
Contract SemaphoreGatekeeper has payable functions:
- SemaphoreGatekeeper.constructor(address,uint256)
But does not have a function to withdraw the ether
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 {
/// @notice The group id
uint256 public groupId;

Check warning

Code scanning / Slither

State variables that could be declared immutable Warning

MockSemaphore.groupId should be immutable

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, ISemaphore.SemaphoreProof calldata proof) external view returns (bool) {
return valid;
}
}

Check warning

Code scanning / Slither

Missing inheritance Warning

MockSemaphore should inherit from ISemaphore
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
176 changes: 176 additions & 0 deletions contracts/tests/SemaphoreGatekeeper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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 proof = {
merkleTreeDepth: 1n,
merkleTreeRoot: 0n,
nullifier: 0n,
message: 0n,
scope: 0n,
points: [0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n],
};

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

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

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

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

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

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

0 comments on commit 9fb4cd8

Please sign in to comment.