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 4, 2024
1 parent 07552f4 commit 424857e
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 0 deletions.
104 changes: 104 additions & 0 deletions packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// 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 time window in which the proof is valid
uint256 public immutable proofValidTime;

/// @notice The registered identities
mapping(uint256 => bool) public registeredAadhaars;

/// @notice Errors
error ZeroAddress();
error OnlyMACI();
error AlreadyRegistered();
error InvalidProof();
error ProofTooOld();
error InvalidSignal();

/// @notice Create a new instance of the gatekeeper
/// @param _anonAadhaarVerifierAddr The address of the anonAadhaar contract
constructor(address _anonAadhaarVerifierAddr, uint256 _proofValidTime) payable {
if (_anonAadhaarVerifierAddr == address(0)) revert ZeroAddress();
anonAadhaarContract = IAnonAadhaar(_anonAadhaarVerifierAddr);
proofValidTime = _proofValidTime;
}

/// @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.

Check warning on line 52 in packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Line length must be no more than 120 but current length is 126
function register(address _user, bytes memory _data) public override {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

// decode the argument
(
uint nullifierSeed,

Check warning on line 56 in packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Rule is set with explicit type [var/s: uint]
uint nullifier,

Check warning on line 57 in packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Rule is set with explicit type [var/s: uint]
uint timestamp,

Check warning on line 58 in packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Rule is set with explicit type [var/s: uint]
uint signal,

Check warning on line 59 in packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Rule is set with explicit type [var/s: uint]
uint[4] memory revealArray,

Check warning on line 60 in packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Rule is set with explicit type [var/s: uint]
uint[8] memory groth16Proof

Check warning on line 61 in packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Rule is set with explicit type [var/s: uint]
) = abi.decode(_data, (uint, uint, uint, uint, uint[4], uint[8]));

Check warning on line 62 in packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Rule is set with explicit type [var/s: uint]

Check warning on line 62 in packages/contracts/contracts/gatekeepers/AnonAadhaarGatekeeper.sol

View workflow job for this annotation

GitHub Actions / check (lint:sol)

Rule is set with explicit type [var/s: uint]

// ensure that the caller is the MACI contract
if (maci != msg.sender) revert OnlyMACI();

// ensure that the proof is not too old
if (block.timestamp - timestamp > proofValidTime) revert ProofTooOld();

// 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(
nullifierSeed,
nullifier,
timestamp,
signal,
revealArray,
groth16Proof
)
) revert InvalidProof();
}

Check notice

Code scanning / Slither

Block timestamp Low


/// @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) {

Check warning

Code scanning / Slither

Conformance to Solidity naming conventions Warning

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(
uint nullifierSeed,
uint nullifier,
uint timestamp,
uint signal,
uint[4] memory revealArray,
uint[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(
uint nullifierSeed,
uint nullifier,
uint timestamp,
uint signal,
uint[4] memory revealArray,
uint[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
180 changes: 180 additions & 0 deletions packages/contracts/tests/AnonAadhaarGatekeeper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
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();

// set proof valid time to 3 hours
const proofValidTime = 3 * 60 * 60;

// Mock AnonAadhaar proof
const mockProof = {
timestamp: Math.floor(new Date().getTime() / 1000) - 2 * 60 * 60,
nullifierSeed: "1234",
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, proofValidTime, 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 proof is older than the proof valid time", async () => {
const oldProof = {
...mockProof,
timestamp: Math.floor(new Date().getTime() / 1000) - 4 * 60 * 60,
};

const encodedOldProof = AbiCoder.defaultAbiCoder().encode(
["uint256", "uint256", "uint256", "uint256", "uint256[4]", "uint256[8]"],
[
oldProof.nullifierSeed,
oldProof.nullifier,
oldProof.timestamp,
signerAddressUint256,
[oldProof.ageAbove18, oldProof.gender, oldProof.pincode, oldProof.state],
oldProof.packedGroth16Proof,
],
);

await expect(
maciContract.signUp(
user.pubKey.asContractParam(),
encodedOldProof,
AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
),
).to.be.revertedWithCustomError(anonAadhaarGatekeeper, "ProofTooOld");
});

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 424857e

Please sign in to comment.