diff --git a/packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol b/packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol new file mode 100644 index 0000000000..b675ddbd30 --- /dev/null +++ b/packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { SignUpGatekeeper } from "./SignUpGatekeeper.sol"; +import { IAnonAadhaar } from "../interfaces/IAnonAadhaar.sol"; + +/// @title AnonAadhaarGatekeeper +/// @notice A gatekeeper contract which allows users to sign up to MACI +/// only if they can prove they are valid Aadhaar owners. +/// @dev Please note that once a identity is used to register, it cannot be used again. +/// This is because we store the nullifier of the proof. +contract AnonAadhaarGatekeeper is SignUpGatekeeper, Ownable(msg.sender) { + /// @notice The anonAadhaar contract + IAnonAadhaar public immutable anonAadhaarContract; + + /// @notice The address of the MACI contract + address public maci; + + /// @notice The registered identities + mapping(uint256 => bool) public registeredAadhaars; + + /// @notice The nullifier seed + uint256 public immutable nullifierSeed; + + /// @notice Errors + error ZeroAddress(); + error OnlyMACI(); + error AlreadyRegistered(); + error InvalidProof(); + error InvalidSignal(); + error InvalidNullifierSeed(); + + /// @notice Create a new instance of the gatekeeper + /// @param _anonAadhaarVerifierAddr The address of the anonAadhaar contract + /// @param _nullifierSeed The nullifier seed specific to the app + constructor(address _anonAadhaarVerifierAddr, uint256 _nullifierSeed) payable { + if (_anonAadhaarVerifierAddr == address(0)) revert ZeroAddress(); + anonAadhaarContract = IAnonAadhaar(_anonAadhaarVerifierAddr); + nullifierSeed = _nullifierSeed; + } + + /// @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 { + if (_maci == address(0)) revert ZeroAddress(); + maci = _maci; + } + + /// @notice Register an user if they can prove anonAadhaar proof + /// @dev Throw if the proof is not valid or just complete silently + /// @param _data The ABI-encoded data containing nullifierSeed, nullifier, timestamp, signal, revealArray, + /// and groth16Proof. + function register(address _user, bytes memory _data) public override { + // decode the argument + ( + uint256 providedNullifierSeed, + uint256 nullifier, + uint256 timestamp, + uint256 signal, + uint256[4] memory revealArray, + uint256[8] memory groth16Proof + ) = abi.decode(_data, (uint256, uint256, uint256, uint256, uint256[4], uint256[8])); + + // ensure that the caller is the MACI contract + if (maci != msg.sender) revert OnlyMACI(); + + // ensure that the provided nullifier seed matches the stored nullifier seed + if (providedNullifierSeed != nullifierSeed) revert InvalidNullifierSeed(); + + // ensure that the signal is correct + if (signal != addressToUint256(_user)) revert InvalidSignal(); + + // ensure that the nullifier has not been registered yet + if (registeredAadhaars[nullifier]) revert AlreadyRegistered(); + + // register the nullifier so it cannot be called again with the same one + registeredAadhaars[nullifier] = true; + + // check if the proof validates + if ( + !anonAadhaarContract.verifyAnonAadhaarProof( + providedNullifierSeed, + nullifier, + timestamp, + signal, + revealArray, + groth16Proof + ) + ) revert InvalidProof(); + } + + /// @dev Convert an address to uint256, used to check against signal. + /// @param _addr: msg.sender address. + /// @return Address msg.sender's address in uint256 + function addressToUint256(address _addr) private pure returns (uint256) { + return uint256(uint160(_addr)); + } + + /// @notice Get the trait of the gatekeeper + /// @return The type of the gatekeeper + function getTrait() public pure override returns (string memory) { + return "AnonAadhaar"; + } +} diff --git a/packages/contracts/contracts/interfaces/IAnonAadhaar.sol b/packages/contracts/contracts/interfaces/IAnonAadhaar.sol new file mode 100644 index 0000000000..fce97fbba5 --- /dev/null +++ b/packages/contracts/contracts/interfaces/IAnonAadhaar.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IAnonAadhaar { + function verifyAnonAadhaarProof( + uint256 nullifierSeed, + uint256 nullifier, + uint256 timestamp, + uint256 signal, + uint256[4] memory revealArray, + uint256[8] memory groth16Proof + ) external view returns (bool); +} diff --git a/packages/contracts/contracts/mocks/MockAnonAadhaar.sol b/packages/contracts/contracts/mocks/MockAnonAadhaar.sol new file mode 100644 index 0000000000..bf0d2779e2 --- /dev/null +++ b/packages/contracts/contracts/mocks/MockAnonAadhaar.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import { IAnonAadhaar } from "../interfaces/IAnonAadhaar.sol"; + +/// @title MockAnonAadhaar +/// @notice A mock contract to test the AnonAadhaarGatekeeper +contract MockAnonAadhaar is IAnonAadhaar { + bool public valid = true; + + /// @notice Mock function to flip the valid state + function flipValid() external { + valid = !valid; + } + + /// @notice Mock implementation of verifyAnonAadhaarProof + function verifyAnonAadhaarProof( + uint256 nullifierSeed, + uint256 nullifier, + uint256 timestamp, + uint256 signal, + uint256[4] memory revealArray, + uint256[8] memory groth16Proof + ) external view override returns (bool) { + return valid; + } +} diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 0dd6c6268c..63f94df0c2 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -59,6 +59,7 @@ "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", + "test:anon_aadhaar_gatekeeper": "pnpm run test ./tests/AnonAadhaarGatekeeper.test.ts", "test:simpleProjectRegistry": "pnpm run test ./tests/extensions/SimpleProjectRegistry.test.ts", "test:simplePayout": "pnpm run test ./tests/extensions/SimplePayout.test.ts", "deploy": "hardhat deploy-full", diff --git a/packages/contracts/tests/AnonAadhaarGatekeeper.test.ts b/packages/contracts/tests/AnonAadhaarGatekeeper.test.ts new file mode 100644 index 0000000000..aee68ec283 --- /dev/null +++ b/packages/contracts/tests/AnonAadhaarGatekeeper.test.ts @@ -0,0 +1,185 @@ +import { expect } from "chai"; +import { AbiCoder, Signer, ZeroAddress } from "ethers"; +import { Keypair } from "maci-domainobjs"; + +import { deployAnonAadhaarGatekeeper, deployContract } from "../ts/deploy"; +import { getDefaultSigner, getSigners } from "../ts/utils"; +import { MACI, AnonAadhaarGatekeeper, MockAnonAadhaar } from "../typechain-types"; + +import { STATE_TREE_DEPTH, initialVoiceCreditBalance } from "./constants"; +import { deployTestContracts } from "./utils"; + +describe("AnonAadhaar Gatekeeper", () => { + let anonAadhaarGatekeeper: AnonAadhaarGatekeeper; + let mockAnonAadhaar: MockAnonAadhaar; + let signer: Signer; + let signerAddressUint256: bigint; + let signerAddress: string; + let encodedProof: string; + + const user = new Keypair(); + + // Define the constant nullifierSeed + const nullifierSeed = 1234; + + // Mock AnonAadhaar proof + const mockProof = { + timestamp: Math.floor(new Date().getTime() / 1000) - 2 * 60 * 60, + nullifierSeed: nullifierSeed.toString(), + nullifier: "7946664694698614794431553425553810756961743235367295886353548733878558886762", + ageAbove18: "1", + gender: "77", + pincode: "110051", + state: "452723500356", + packedGroth16Proof: ["0", "1", "2", "3", "4", "5", "6", "7"], + }; + + before(async () => { + signer = await getDefaultSigner(); + mockAnonAadhaar = await deployContract("MockAnonAadhaar", signer, true); + const mockAnonAadhaarAddress = await mockAnonAadhaar.getAddress(); + signerAddress = await signer.getAddress(); + anonAadhaarGatekeeper = await deployAnonAadhaarGatekeeper(mockAnonAadhaarAddress, nullifierSeed, signer, true); + signerAddressUint256 = BigInt(signerAddress); + encodedProof = AbiCoder.defaultAbiCoder().encode( + ["uint256", "uint256", "uint256", "uint256", "uint256[4]", "uint256[8]"], + [ + mockProof.nullifierSeed, + mockProof.nullifier, + mockProof.timestamp, + signerAddressUint256, + [mockProof.ageAbove18, mockProof.gender, mockProof.pincode, mockProof.state], + mockProof.packedGroth16Proof, + ], + ); + }); + + describe("Deployment", () => { + it("The gatekeeper should be deployed correctly", () => { + expect(anonAadhaarGatekeeper).to.not.eq(undefined); + }); + }); + + describe("Gatekeeper", () => { + let maciContract: MACI; + + before(async () => { + const r = await deployTestContracts({ + initialVoiceCreditBalance, + stateTreeDepth: STATE_TREE_DEPTH, + signer, + gatekeeper: anonAadhaarGatekeeper, + }); + + maciContract = r.maciContract; + }); + + it("sets MACI instance correctly", async () => { + const maciAddress = await maciContract.getAddress(); + await anonAadhaarGatekeeper.setMaciInstance(maciAddress).then((tx) => tx.wait()); + + expect(await anonAadhaarGatekeeper.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( + anonAadhaarGatekeeper.connect(secondSigner).setMaciInstance(signerAddress), + ).to.be.revertedWithCustomError(anonAadhaarGatekeeper, "OwnableUnauthorizedAccount"); + }); + + it("should fail to set MACI instance when the MACI instance is not valid", async () => { + await expect(anonAadhaarGatekeeper.setMaciInstance(ZeroAddress)).to.be.revertedWithCustomError( + anonAadhaarGatekeeper, + "ZeroAddress", + ); + }); + + it("should revert if the nullifier seed is invalid", async () => { + const invalidNullifierSeedProof = { + ...mockProof, + nullifierSeed: "5678", + }; + + const encodedInvalidNullifierSeedProof = AbiCoder.defaultAbiCoder().encode( + ["uint256", "uint256", "uint256", "uint256", "uint256[4]", "uint256[8]"], + [ + invalidNullifierSeedProof.nullifierSeed, + invalidNullifierSeedProof.nullifier, + invalidNullifierSeedProof.timestamp, + signerAddressUint256, + [ + invalidNullifierSeedProof.ageAbove18, + invalidNullifierSeedProof.gender, + invalidNullifierSeedProof.pincode, + invalidNullifierSeedProof.state, + ], + invalidNullifierSeedProof.packedGroth16Proof, + ], + ); + + await expect( + maciContract.signUp( + user.pubKey.asContractParam(), + encodedInvalidNullifierSeedProof, + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(anonAadhaarGatekeeper, "InvalidNullifierSeed"); + }); + + it("should revert if the signal is invalid", async () => { + const encodedInvalidProof = AbiCoder.defaultAbiCoder().encode( + ["uint256", "uint256", "uint256", "uint256", "uint256[4]", "uint256[8]"], + [ + mockProof.nullifierSeed, + mockProof.nullifier, + mockProof.timestamp, + BigInt(ZeroAddress), + [mockProof.ageAbove18, mockProof.gender, mockProof.pincode, mockProof.state], + mockProof.packedGroth16Proof, + ], + ); + await expect( + maciContract.signUp( + user.pubKey.asContractParam(), + encodedInvalidProof, + AbiCoder.defaultAbiCoder().encode(["uint256"], [0]), + ), + ).to.be.revertedWithCustomError(anonAadhaarGatekeeper, "InvalidSignal"); + }); + + it("should revert if the proof is invalid (mock)", async () => { + await mockAnonAadhaar.flipValid(); + await expect( + maciContract.signUp( + user.pubKey.asContractParam(), + encodedProof, + AbiCoder.defaultAbiCoder().encode(["uint256"], [1]), + ), + ).to.be.revertedWithCustomError(anonAadhaarGatekeeper, "InvalidProof"); + await mockAnonAadhaar.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(anonAadhaarGatekeeper, "AlreadyRegistered"); + }); + }); +}); diff --git a/packages/contracts/ts/deploy.ts b/packages/contracts/ts/deploy.ts index 436f675d90..c92cf880e5 100644 --- a/packages/contracts/ts/deploy.ts +++ b/packages/contracts/ts/deploy.ts @@ -31,6 +31,7 @@ import { TallyFactory__factory as TallyFactoryFactory, GitcoinPassportGatekeeper, SemaphoreGatekeeper, + AnonAadhaarGatekeeper, } from "../typechain-types"; import { genEmptyBallotRoots } from "./genEmptyBallotRoots"; @@ -185,6 +186,27 @@ export const deploySemaphoreGatekeeper = async ( ): Promise => deployContract("SemaphoreGatekeeper", signer, quiet, semaphoreAddress, groupId.toString()); +/** + * Deploy an AnonAadhaarGatekeeper contract + * @param verifierAddress - the address of the Verifier contract + * @param proofValidTime - the time in seconds that a proof is valid for + * @returns the deployed AnonAadhaarGatekeeper contract + */ + +export const deployAnonAadhaarGatekeeper = async ( + verifierAddress: string, + proofValidTime: number, + signer?: Signer, + quiet = false, +): Promise => + deployContract( + "AnonAadhaarGatekeeper", + signer, + quiet, + verifierAddress, + proofValidTime.toString(), + ); + /** * Deploy Poseidon contracts * @param signer - the signer to use to deploy the contracts