-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(contracts): implement semaphore gatekeeper
- Loading branch information
Showing
5 changed files
with
282 additions
and
0 deletions.
There are no files selected for viewing
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,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 { | ||
Check warning Code scanning / Slither Conformance to Solidity naming conventions Warning
Parameter SemaphoreGatekeeper.setMaciInstance(address)._maci is not in mixedCase
|
||
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 { | ||
// 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(); | ||
} | ||
} |
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,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); | ||
} |
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,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; | ||
} | ||
} |
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,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"); | ||
}); | ||
}); | ||
}); |