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 anon aadhaar gatekeeper #1846

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
105 changes: 105 additions & 0 deletions packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol
crisgarner marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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 {
Dismissed Show dismissed Hide dismissed
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 {
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed
Dismissed Show dismissed Hide dismissed
// 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;
lordshashank marked this conversation as resolved.
Show resolved Hide resolved

// 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) {
Dismissed Show dismissed Hide dismissed
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";
}
}
13 changes: 13 additions & 0 deletions packages/contracts/contracts/interfaces/IAnonAadhaar.sol
Original file line number Diff line number Diff line change
@@ -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);
}
27 changes: 27 additions & 0 deletions packages/contracts/contracts/mocks/MockAnonAadhaar.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
185 changes: 185 additions & 0 deletions packages/contracts/tests/AnonAadhaarGatekeeper.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
22 changes: 22 additions & 0 deletions packages/contracts/ts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
TallyFactory__factory as TallyFactoryFactory,
GitcoinPassportGatekeeper,
SemaphoreGatekeeper,
AnonAadhaarGatekeeper,
} from "../typechain-types";

import { genEmptyBallotRoots } from "./genEmptyBallotRoots";
Expand Down Expand Up @@ -185,6 +186,27 @@ export const deploySemaphoreGatekeeper = async (
): Promise<SemaphoreGatekeeper> =>
deployContract<SemaphoreGatekeeper>("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<AnonAadhaarGatekeeper> =>
deployContract<AnonAadhaarGatekeeper>(
"AnonAadhaarGatekeeper",
signer,
quiet,
verifierAddress,
proofValidTime.toString(),
);

/**
* Deploy Poseidon contracts
* @param signer - the signer to use to deploy the contracts
Expand Down
Loading