-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
- Loading branch information
There are no files selected for viewing
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
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 { | ||
Check warning Code scanning / Slither Conformance to Solidity naming conventions Warning
Parameter SemaphoreGatekeeper.register(address,bytes)._data is not in mixedCase
|
||
// 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 |
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); | ||
} |
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
|
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"); | ||
}); | ||
}); | ||
}); |