Skip to content

Commit

Permalink
feat(contracts): implement anon aadhaar gatekeeper
Browse files Browse the repository at this point in the history
  • Loading branch information
lordshashank committed Oct 19, 2024
1 parent a0a1309 commit 3a46c8e
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 0 deletions.
105 changes: 105 additions & 0 deletions packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol
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 {
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";
}
}
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

0 comments on commit 3a46c8e

Please sign in to comment.