diff --git a/packages/cli/ts/commands/deployPoll.ts b/packages/cli/ts/commands/deployPoll.ts
index bff6d257f2..b69fb1a631 100644
--- a/packages/cli/ts/commands/deployPoll.ts
+++ b/packages/cli/ts/commands/deployPoll.ts
@@ -1,4 +1,4 @@
-import { MACI__factory as MACIFactory, EMode } from "maci-contracts";
+import { MACI__factory as MACIFactory, EMode, deployFreeForAllSignUpGatekeeper } from "maci-contracts";
 import { PubKey } from "maci-domainobjs";
 
 import {
@@ -26,6 +26,7 @@ export const deployPoll = async ({
   coordinatorPubkey,
   maciAddress,
   vkRegistryAddress,
+  gatekeeperAddress,
   signer,
   quiet = true,
   useQuadraticVoting = false,
@@ -49,6 +50,18 @@ export const deployPoll = async ({
 
   const maci = maciAddress || maciContractAddress;
 
+  const maciContract = MACIFactory.connect(maci, signer);
+  const pollId = await maciContract.nextPollId();
+
+  // check if we have a signupGatekeeper already deployed or passed as arg
+  let signupGatekeeperContractAddress =
+    gatekeeperAddress || (await readContractAddress(`SignUpGatekeeper-${pollId.toString()}`, network?.name));
+
+  if (!signupGatekeeperContractAddress) {
+    const contract = await deployFreeForAllSignUpGatekeeper(signer, true);
+    signupGatekeeperContractAddress = await contract.getAddress();
+  }
+
   // required arg -> poll duration
   if (pollDuration <= 0) {
     logError("Duration cannot be <= 0");
@@ -83,8 +96,6 @@ export const deployPoll = async ({
   // get the verifier contract
   const verifierContractAddress = await readContractAddress("Verifier", network?.name);
 
-  const maciContract = MACIFactory.connect(maci, signer);
-
   // deploy the poll
   let pollAddr = "";
   let messageProcessorContractAddress = "";
@@ -103,6 +114,7 @@ export const deployPoll = async ({
       verifierContractAddress,
       vkRegistry,
       useQuadraticVoting ? EMode.QV : EMode.NON_QV,
+      signupGatekeeperContractAddress,
       { gasLimit: 10000000 },
     );
 
@@ -130,8 +142,8 @@ export const deployPoll = async ({
     }
 
     // eslint-disable-next-line no-underscore-dangle
-    const pollId = log.args._pollId;
-    const pollContracts = await maciContract.getPoll(pollId);
+    const eventPollId = log.args._pollId;
+    const pollContracts = await maciContract.getPoll(eventPollId);
     pollAddr = pollContracts.poll;
     messageProcessorContractAddress = pollContracts.messageProcessor;
     tallyContractAddress = pollContracts.tally;
@@ -142,9 +154,18 @@ export const deployPoll = async ({
     logGreen(quiet, info(`Tally contract: ${tallyContractAddress}`));
 
     // store the address
-    await storeContractAddress(`MessageProcessor-${pollId.toString()}`, messageProcessorContractAddress, network?.name);
-    await storeContractAddress(`Tally-${pollId.toString()}`, tallyContractAddress, network?.name);
-    await storeContractAddress(`Poll-${pollId.toString()}`, pollAddr, network?.name);
+    await storeContractAddress(
+      `SignUpGatekeeper-${eventPollId.toString()}`,
+      signupGatekeeperContractAddress,
+      network?.name,
+    );
+    await storeContractAddress(
+      `MessageProcessor-${eventPollId.toString()}`,
+      messageProcessorContractAddress,
+      network?.name,
+    );
+    await storeContractAddress(`Tally-${eventPollId.toString()}`, tallyContractAddress, network?.name);
+    await storeContractAddress(`Poll-${eventPollId.toString()}`, pollAddr, network?.name);
   } catch (error) {
     logError((error as Error).message);
   }
@@ -154,5 +175,6 @@ export const deployPoll = async ({
     messageProcessor: messageProcessorContractAddress,
     tally: tallyContractAddress,
     poll: pollAddr,
+    signupGatekeeper: signupGatekeeperContractAddress,
   };
 };
diff --git a/packages/cli/ts/commands/joinPoll.ts b/packages/cli/ts/commands/joinPoll.ts
index c8dc02af6a..a78b52a6dc 100644
--- a/packages/cli/ts/commands/joinPoll.ts
+++ b/packages/cli/ts/commands/joinPoll.ts
@@ -10,7 +10,7 @@ import fs from "fs";
 
 import type { IJoinPollArgs, IJoinedUserArgs, IParsePollJoinEventsArgs, IJoinPollData } from "../utils";
 
-import { contractExists, logError, logYellow, info, logGreen, success, BLOCKS_STEP } from "../utils";
+import { contractExists, logError, logYellow, info, logGreen, success, BLOCKS_STEP, DEFAULT_SG_DATA } from "../utils";
 import { banner } from "../utils/banner";
 
 /**
@@ -195,6 +195,7 @@ export const joinPoll = async ({
   rapidsnark,
   pollWitgen,
   pollWasm,
+  sgDataArg,
   quiet = true,
 }: IJoinPollArgs): Promise<IJoinPollData> => {
   banner(quiet);
@@ -332,6 +333,8 @@ export const joinPoll = async ({
   let pollStateIndex = "";
   let receipt: ContractTransactionReceipt | null = null;
 
+  const sgData = sgDataArg || DEFAULT_SG_DATA;
+
   try {
     // generate the proof for this batch
     const proof = await generateAndVerifyProof(
@@ -351,6 +354,7 @@ export const joinPoll = async ({
       loadedCreditBalance!,
       currentStateRootIndex,
       proof,
+      sgData,
     );
     receipt = await tx.wait();
     logYellow(quiet, info(`Transaction hash: ${receipt!.hash}`));
diff --git a/packages/cli/ts/index.ts b/packages/cli/ts/index.ts
index 6051a4f856..c40a5166e9 100644
--- a/packages/cli/ts/index.ts
+++ b/packages/cli/ts/index.ts
@@ -216,6 +216,7 @@ program
   .description("join the poll")
   .requiredOption("-sk, --priv-key <privKey>", "the private key")
   .option("-i, --state-index <stateIndex>", "the user's state index", BigInt)
+  .requiredOption("-s, --sg-data <sgData>", "the signup gateway data")
   .requiredOption("-esk, --poll-priv-key <pollPrivKey>", "the user ephemeral private key for the poll")
   .option(
     "-nv, --new-voice-credit-balance <newVoiceCreditBalance>",
@@ -265,6 +266,7 @@ program
         useWasm: cmdObj.wasm,
         rapidsnark: cmdObj.rapidsnark,
         pollWitgen: cmdObj.pollWitnessgen,
+        sgDataArg: cmdObj.sgData,
       });
     } catch (error) {
       program.error((error as Error).message, { exitCode: 1 });
diff --git a/packages/cli/ts/utils/interfaces.ts b/packages/cli/ts/utils/interfaces.ts
index f1054c0b83..68467b9f7a 100644
--- a/packages/cli/ts/utils/interfaces.ts
+++ b/packages/cli/ts/utils/interfaces.ts
@@ -23,6 +23,7 @@ export interface PollContracts {
   poll: string;
   messageProcessor: string;
   tally: string;
+  signupGatekeeper: string;
 }
 
 /**
@@ -318,6 +319,11 @@ export interface DeployPollArgs {
    * Whether to use quadratic voting or not
    */
   useQuadraticVoting?: boolean;
+
+  /**
+   * The address of the gatekeeper contract
+   */
+  gatekeeperAddress?: string;
 }
 
 /**
@@ -447,6 +453,11 @@ export interface IJoinPollArgs {
    * Poll private key for the poll
    */
   pollPrivKey: string;
+
+  /**
+   * The signup gatekeeper data
+   */
+  sgDataArg?: string;
 }
 
 /**
diff --git a/packages/contracts/contracts/MACI.sol b/packages/contracts/contracts/MACI.sol
index 8a12ee01f3..3575d6a7cf 100644
--- a/packages/contracts/contracts/MACI.sol
+++ b/packages/contracts/contracts/MACI.sol
@@ -6,6 +6,7 @@ import { IMessageProcessorFactory } from "./interfaces/IMPFactory.sol";
 import { ITallyFactory } from "./interfaces/ITallyFactory.sol";
 import { IVerifier } from "./interfaces/IVerifier.sol";
 import { IVkRegistry } from "./interfaces/IVkRegistry.sol";
+import { ISignUpGatekeeper } from "./interfaces/ISignUpGatekeeper.sol";
 import { InitialVoiceCreditProxy } from "./initialVoiceCreditProxy/InitialVoiceCreditProxy.sol";
 import { SignUpGatekeeper } from "./gatekeepers/SignUpGatekeeper.sol";
 import { IMACI } from "./interfaces/IMACI.sol";
@@ -187,7 +188,8 @@ contract MACI is IMACI, DomainObjs, Params, Utilities {
     PubKey memory _coordinatorPubKey,
     address _verifier,
     address _vkRegistry,
-    Mode _mode
+    Mode _mode,
+    address _gatekeeper
   ) public virtual {
     // cache the poll to a local variable so we can increment it
     uint256 pollId = nextPollId;
@@ -208,7 +210,8 @@ contract MACI is IMACI, DomainObjs, Params, Utilities {
     ExtContracts memory extContracts = ExtContracts({
       maci: IMACI(address(this)),
       verifier: IVerifier(_verifier),
-      vkRegistry: IVkRegistry(_vkRegistry)
+      vkRegistry: IVkRegistry(_vkRegistry),
+      gatekeeper: ISignUpGatekeeper(_gatekeeper)
     });
 
     address p = pollFactory.deploy(
diff --git a/packages/contracts/contracts/Poll.sol b/packages/contracts/contracts/Poll.sol
index a183f39e00..d620c03906 100644
--- a/packages/contracts/contracts/Poll.sol
+++ b/packages/contracts/contracts/Poll.sol
@@ -6,6 +6,7 @@ import { SnarkCommon } from "./crypto/SnarkCommon.sol";
 import { LazyIMTData, InternalLazyIMT } from "./trees/LazyIMT.sol";
 import { IMACI } from "./interfaces/IMACI.sol";
 import { IPoll } from "./interfaces/IPoll.sol";
+import { ISignUpGatekeeper } from "./interfaces/ISignUpGatekeeper.sol";
 import { Utilities } from "./utilities/Utilities.sol";
 import { CurveBabyJubJub } from "./crypto/BabyJubJub.sol";
 
@@ -284,28 +285,31 @@ contract Poll is Params, Utilities, SnarkCommon, IPoll {
     PubKey calldata _pubKey,
     uint256 _newVoiceCreditBalance,
     uint256 _stateRootIndex,
-    uint256[8] calldata _proof
+    uint256[8] calldata _proof,
+    bytes memory _signUpGatekeeperData
   ) public virtual isWithinVotingDeadline {
     // Whether the user has already joined
     if (pollNullifier[_nullifier]) {
       revert UserAlreadyJoined();
     }
 
+    // Set nullifier for user's private key
+    pollNullifier[_nullifier] = true;
+
     // Verify user's proof
     if (!verifyPollProof(_nullifier, _newVoiceCreditBalance, _stateRootIndex, _pubKey, _proof)) {
       revert InvalidPollProof();
     }
 
+    // Check if the user is eligible to join the poll
+    extContracts.gatekeeper.register(msg.sender, _signUpGatekeeperData);
+
     // Store user in the pollStateTree
-    uint256 timestamp = block.timestamp;
-    uint256 stateLeaf = hashStateLeaf(StateLeaf(_pubKey, _newVoiceCreditBalance, timestamp));
+    uint256 stateLeaf = hashStateLeaf(StateLeaf(_pubKey, _newVoiceCreditBalance, block.timestamp));
     InternalLazyIMT._insert(pollStateTree, stateLeaf);
 
-    // Set nullifier for user's private key
-    pollNullifier[_nullifier] = true;
-
     uint256 pollStateIndex = pollStateTree.numberOfLeaves - 1;
-    emit PollJoined(_pubKey.x, _pubKey.y, _newVoiceCreditBalance, timestamp, _nullifier, pollStateIndex);
+    emit PollJoined(_pubKey.x, _pubKey.y, _newVoiceCreditBalance, block.timestamp, _nullifier, pollStateIndex);
   }
 
   /// @notice Verify the proof for Poll
diff --git a/packages/contracts/contracts/interfaces/IPoll.sol b/packages/contracts/contracts/interfaces/IPoll.sol
index 50318db07f..b17491e6ad 100644
--- a/packages/contracts/contracts/interfaces/IPoll.sol
+++ b/packages/contracts/contracts/interfaces/IPoll.sol
@@ -13,7 +13,8 @@ interface IPoll {
     DomainObjs.PubKey calldata _pubKey,
     uint256 _newVoiceCreditBalance,
     uint256 _stateRootIndex,
-    uint256[8] calldata _proof
+    uint256[8] calldata _proof,
+    bytes memory _signUpGatekeeperData
   ) external;
 
   /// @notice The number of messages which have been processed and the number of signups
diff --git a/packages/contracts/contracts/interfaces/ISignUpGatekeeper.sol b/packages/contracts/contracts/interfaces/ISignUpGatekeeper.sol
new file mode 100644
index 0000000000..ded9df5cfd
--- /dev/null
+++ b/packages/contracts/contracts/interfaces/ISignUpGatekeeper.sol
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.10;
+
+/// @title ISignUpGatekeeper
+/// @notice SignUpGatekeeper interface
+interface ISignUpGatekeeper {
+  /// @notice Register a user
+  /// @param _user User address
+  /// @param _data Data to register
+  function register(address _user, bytes memory _data) external;
+}
diff --git a/packages/contracts/contracts/utilities/Params.sol b/packages/contracts/contracts/utilities/Params.sol
index bde214b17e..91d9fae8e0 100644
--- a/packages/contracts/contracts/utilities/Params.sol
+++ b/packages/contracts/contracts/utilities/Params.sol
@@ -4,6 +4,7 @@ pragma solidity ^0.8.20;
 import { IMACI } from "../interfaces/IMACI.sol";
 import { IVerifier } from "../interfaces/IVerifier.sol";
 import { IVkRegistry } from "../interfaces/IVkRegistry.sol";
+import { ISignUpGatekeeper } from "../interfaces/ISignUpGatekeeper.sol";
 
 /// @title Params
 /// @notice This contracts contains a number of structures
@@ -24,5 +25,6 @@ contract Params {
     IMACI maci;
     IVerifier verifier;
     IVkRegistry vkRegistry;
+    ISignUpGatekeeper gatekeeper;
   }
 }
diff --git a/packages/contracts/deploy-config-example.json b/packages/contracts/deploy-config-example.json
index eb3daf87e4..04ab0e3417 100644
--- a/packages/contracts/deploy-config-example.json
+++ b/packages/contracts/deploy-config-example.json
@@ -65,7 +65,8 @@
     "Poll": {
       "pollDuration": 3600,
       "coordinatorPubkey": "macipk.9a59264310d95cfd8eb7083aebeba221b5c26e77427f12b7c0f50bc1cc35e621",
-      "useQuadraticVoting": false
+      "useQuadraticVoting": false,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "scroll_sepolia": {
@@ -133,7 +134,8 @@
     "Poll": {
       "pollDuration": 10800,
       "coordinatorPubkey": "macipk.0a1ce79a43fa676ee3d2882c79d9164a24d4a22bb6190e3d8fa25d97bffc069a",
-      "useQuadraticVoting": false
+      "useQuadraticVoting": false,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "optimism": {
@@ -202,7 +204,8 @@
     "Poll": {
       "pollDuration": 3600,
       "coordinatorPubkey": "macipk.9a59264310d95cfd8eb7083aebeba221b5c26e77427f12b7c0f50bc1cc35e621",
-      "useQuadraticVoting": false
+      "useQuadraticVoting": false,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "arbitrum_sepolia": {
@@ -271,7 +274,8 @@
     "Poll": {
       "pollDuration": 3600,
       "coordinatorPubkey": "macipk.9a59264310d95cfd8eb7083aebeba221b5c26e77427f12b7c0f50bc1cc35e621",
-      "useQuadraticVoting": false
+      "useQuadraticVoting": false,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "localhost": {
@@ -340,7 +344,8 @@
     "Poll": {
       "pollDuration": 30,
       "coordinatorPubkey": "macipk.29add77d27341c4cdfc2fb623175ecfd6527a286e3e7ded785d9fd7afbbdf399",
-      "useQuadraticVoting": false
+      "useQuadraticVoting": false,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "base_sepolia": {
@@ -409,7 +414,8 @@
     "Poll": {
       "pollDuration": 3600,
       "coordinatorPubkey": "macipk.9a59264310d95cfd8eb7083aebeba221b5c26e77427f12b7c0f50bc1cc35e621",
-      "useQuadraticVoting": false
+      "useQuadraticVoting": false,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "optimism_sepolia": {
@@ -483,7 +489,8 @@
     "Poll": {
       "pollDuration": 3600,
       "coordinatorPubkey": "macipk.9a59264310d95cfd8eb7083aebeba221b5c26e77427f12b7c0f50bc1cc35e621",
-      "useQuadraticVoting": false
+      "useQuadraticVoting": false,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "gnosis_chiado": {
@@ -557,7 +564,8 @@
     "Poll": {
       "pollDuration": 3600,
       "coordinatorPubkey": "macipk.0a1ce79a43fa676ee3d2882c79d9164a24d4a22bb6190e3d8fa25d97bffc069a",
-      "useQuadraticVoting": false
+      "useQuadraticVoting": false,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "gnosis": {
@@ -631,7 +639,8 @@
     "Poll": {
       "pollDuration": 3600,
       "coordinatorPubkey": "macipk.0a1ce79a43fa676ee3d2882c79d9164a24d4a22bb6190e3d8fa25d97bffc069a",
-      "useQuadraticVoting": false
+      "useQuadraticVoting": false,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "polygon_amoy": {
@@ -705,7 +714,8 @@
     "Poll": {
       "pollDuration": 3600,
       "coordinatorPubkey": "macipk.0a1ce79a43fa676ee3d2882c79d9164a24d4a22bb6190e3d8fa25d97bffc069a",
-      "useQuadraticVoting": true
+      "useQuadraticVoting": true,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   },
   "polygon": {
@@ -779,7 +789,8 @@
     "Poll": {
       "pollDuration": 3600,
       "coordinatorPubkey": "macipk.0a1ce79a43fa676ee3d2882c79d9164a24d4a22bb6190e3d8fa25d97bffc069a",
-      "useQuadraticVoting": true
+      "useQuadraticVoting": true,
+      "gatekeeper": "FreeForAllGatekeeper"
     }
   }
 }
diff --git a/packages/contracts/tasks/deploy/poll/01-gatekeepers.ts b/packages/contracts/tasks/deploy/poll/01-gatekeepers.ts
new file mode 100644
index 0000000000..856ebef3dc
--- /dev/null
+++ b/packages/contracts/tasks/deploy/poll/01-gatekeepers.ts
@@ -0,0 +1,298 @@
+import { hexToBigInt, uuidToBigInt } from "@pcd/util";
+
+import { HatsGatekeeperBase, MACI } from "../../../typechain-types";
+import { EDeploySteps, ESupportedChains } from "../../helpers/constants";
+import { ContractStorage } from "../../helpers/ContractStorage";
+import { Deployment } from "../../helpers/Deployment";
+import { EContracts, IDeployParams } from "../../helpers/types";
+
+const deployment = Deployment.getInstance();
+const storage = ContractStorage.getInstance();
+
+/**
+ * Deploy step registration and task itself
+ */
+deployment.deployTask(EDeploySteps.PollGatekeeper, "Deploy Poll gatekeepers").then((task) =>
+  task.setAction(async ({ incremental }: IDeployParams, hre) => {
+    deployment.setHre(hre);
+    const deployer = await deployment.getDeployer();
+
+    const maciContract = await deployment.getContract<MACI>({ name: EContracts.MACI });
+    const pollId = await maciContract.nextPollId();
+
+    const freeForAllGatekeeperContractAddress = storage.getAddress(
+      EContracts.FreeForAllGatekeeper,
+      hre.network.name,
+      `poll-${pollId}`,
+    );
+    const easGatekeeperContractAddress = storage.getAddress(
+      EContracts.EASGatekeeper,
+      hre.network.name,
+      `poll-${pollId}`,
+    );
+    const hatsGatekeeperContractAddress = storage.getAddress(
+      EContracts.HatsGatekeeper,
+      hre.network.name,
+      `poll-${pollId}`,
+    );
+    const gitcoinGatekeeperContractAddress = storage.getAddress(
+      EContracts.GitcoinPassportGatekeeper,
+      hre.network.name,
+      `poll-${pollId}`,
+    );
+    const zupassGatekeeperContractAddress = storage.getAddress(
+      EContracts.ZupassGatekeeper,
+      hre.network.name,
+      `poll-${pollId}`,
+    );
+    const semaphoreGatekeeperContractAddress = storage.getAddress(
+      EContracts.SemaphoreGatekeeper,
+      hre.network.name,
+      `poll-${pollId}`,
+    );
+    const merkleProofGatekeeperContractAddress = storage.getAddress(
+      EContracts.MerkleProofGatekeeper,
+      hre.network.name,
+      `poll-${pollId}`,
+    );
+
+    const gatekeeperToDeploy =
+      deployment.getDeployConfigField<EContracts | null>(EContracts.Poll, "gatekeeper") ||
+      EContracts.FreeForAllGatekeeper;
+
+    const skipDeployFreeForAllGatekeeper = gatekeeperToDeploy !== EContracts.FreeForAllGatekeeper;
+    const skipDeployEASGatekeeper = gatekeeperToDeploy !== EContracts.EASGatekeeper;
+    const skipDeployGitcoinGatekeeper = gatekeeperToDeploy !== EContracts.GitcoinPassportGatekeeper;
+    const skipDeployZupassGatekeeper = gatekeeperToDeploy !== EContracts.ZupassGatekeeper;
+    const skipDeploySemaphoreGatekeeper = gatekeeperToDeploy !== EContracts.SemaphoreGatekeeper;
+    const skipDeployHatsGatekeeper = gatekeeperToDeploy !== EContracts.HatsGatekeeper;
+    const skipDeployMerkleProofGatekeeper = gatekeeperToDeploy !== EContracts.MerkleProofGatekeeper;
+
+    const hasGatekeeperAddress = [
+      freeForAllGatekeeperContractAddress,
+      easGatekeeperContractAddress,
+      gitcoinGatekeeperContractAddress,
+      zupassGatekeeperContractAddress,
+      semaphoreGatekeeperContractAddress,
+      hatsGatekeeperContractAddress,
+      merkleProofGatekeeperContractAddress,
+    ].some(Boolean);
+
+    const isSkipable = [
+      skipDeployFreeForAllGatekeeper,
+      skipDeployEASGatekeeper,
+      skipDeployGitcoinGatekeeper,
+      skipDeployZupassGatekeeper,
+      skipDeploySemaphoreGatekeeper,
+      skipDeployHatsGatekeeper,
+      skipDeployMerkleProofGatekeeper,
+    ].some((skip) => !skip);
+
+    const canSkipDeploy = incremental && hasGatekeeperAddress && isSkipable;
+
+    if (canSkipDeploy) {
+      // eslint-disable-next-line no-console
+      console.log(`Skipping deployment of the Gatekeeper contract`);
+      return;
+    }
+
+    if (!skipDeployFreeForAllGatekeeper) {
+      const freeForAllGatekeeperContract = await deployment.deployContract({
+        name: EContracts.FreeForAllGatekeeper,
+        signer: deployer,
+      });
+
+      await storage.register({
+        id: EContracts.FreeForAllGatekeeper,
+        key: `poll-${pollId}`,
+        contract: freeForAllGatekeeperContract,
+        args: [],
+        network: hre.network.name,
+      });
+    }
+
+    const isSupportedEASGatekeeperNetwork = ![ESupportedChains.Hardhat, ESupportedChains.Coverage].includes(
+      hre.network.name as ESupportedChains,
+    );
+
+    if (!skipDeployEASGatekeeper && isSupportedEASGatekeeperNetwork) {
+      const easAddress = deployment.getDeployConfigField<string>(EContracts.EASGatekeeper, "easAddress", true);
+      const encodedSchema = deployment.getDeployConfigField<string>(EContracts.EASGatekeeper, "schema", true);
+      const attester = deployment.getDeployConfigField<string>(EContracts.EASGatekeeper, "attester", true);
+
+      const easGatekeeperContract = await deployment.deployContract(
+        {
+          name: EContracts.EASGatekeeper,
+          signer: deployer,
+        },
+        easAddress,
+        attester,
+        encodedSchema,
+      );
+
+      await storage.register({
+        id: EContracts.EASGatekeeper,
+        key: `poll-${pollId}`,
+        contract: easGatekeeperContract,
+        args: [easAddress, attester, encodedSchema],
+        network: hre.network.name,
+      });
+    }
+
+    const isSupportedGitcoinGatekeeperNetwork = ![
+      ESupportedChains.Hardhat,
+      ESupportedChains.Coverage,
+      ESupportedChains.Sepolia,
+    ].includes(hre.network.name as ESupportedChains);
+
+    if (!skipDeployGitcoinGatekeeper && isSupportedGitcoinGatekeeperNetwork) {
+      const gitcoinGatekeeperDecoderAddress = deployment.getDeployConfigField<string>(
+        EContracts.GitcoinPassportGatekeeper,
+        "decoderAddress",
+        true,
+      );
+      const gitcoinGatekeeperPassingScore = deployment.getDeployConfigField<number>(
+        EContracts.GitcoinPassportGatekeeper,
+        "passingScore",
+        true,
+      );
+      const gitcoinGatekeeperContract = await deployment.deployContract(
+        {
+          name: EContracts.GitcoinPassportGatekeeper,
+          signer: deployer,
+        },
+        gitcoinGatekeeperDecoderAddress,
+        gitcoinGatekeeperPassingScore,
+      );
+
+      await storage.register({
+        id: EContracts.GitcoinPassportGatekeeper,
+        key: `poll-${pollId}`,
+        contract: gitcoinGatekeeperContract,
+        args: [gitcoinGatekeeperDecoderAddress, gitcoinGatekeeperPassingScore],
+        network: hre.network.name,
+      });
+    }
+
+    if (!skipDeployZupassGatekeeper) {
+      const eventId = deployment.getDeployConfigField<string>(EContracts.ZupassGatekeeper, "eventId", true);
+      const validEventId = uuidToBigInt(eventId);
+      const signer1 = deployment.getDeployConfigField<string>(EContracts.ZupassGatekeeper, "signer1", true);
+      const validSigner1 = hexToBigInt(signer1);
+      const signer2 = deployment.getDeployConfigField<string>(EContracts.ZupassGatekeeper, "signer2", true);
+      const validSigner2 = hexToBigInt(signer2);
+      let verifier = deployment.getDeployConfigField<string | undefined>(EContracts.ZupassGatekeeper, "zupassVerifier");
+
+      if (!verifier) {
+        const verifierContract = await deployment.deployContract({
+          name: EContracts.ZupassGroth16Verifier,
+          signer: deployer,
+        });
+        verifier = await verifierContract.getAddress();
+      }
+
+      const ZupassGatekeeperContract = await deployment.deployContract(
+        {
+          name: EContracts.ZupassGatekeeper,
+          signer: deployer,
+        },
+        validEventId,
+        validSigner1,
+        validSigner2,
+        verifier,
+      );
+      await storage.register({
+        id: EContracts.ZupassGatekeeper,
+        key: `poll-${pollId}`,
+        contract: ZupassGatekeeperContract,
+        args: [validEventId.toString(), validSigner1.toString(), validSigner2.toString(), verifier],
+        network: hre.network.name,
+      });
+    }
+
+    if (!skipDeploySemaphoreGatekeeper) {
+      const semaphoreContractAddress = deployment.getDeployConfigField<string>(
+        EContracts.SemaphoreGatekeeper,
+        "semaphoreContract",
+        true,
+      );
+      const groupId = deployment.getDeployConfigField<number>(EContracts.SemaphoreGatekeeper, "groupId", true);
+
+      const semaphoreGatekeeperContract = await deployment.deployContract(
+        {
+          name: EContracts.SemaphoreGatekeeper,
+          signer: deployer,
+        },
+        semaphoreContractAddress,
+        groupId,
+      );
+
+      await storage.register({
+        id: EContracts.SemaphoreGatekeeper,
+        key: `poll-${pollId}`,
+        contract: semaphoreGatekeeperContract,
+        args: [semaphoreContractAddress, groupId.toString()],
+        network: hre.network.name,
+      });
+    }
+
+    if (!skipDeployHatsGatekeeper) {
+      // get args
+      const criterionHats = deployment.getDeployConfigField<string[]>(EContracts.HatsGatekeeper, "criterionHats", true);
+      const hatsProtocolAddress = deployment.getDeployConfigField<string>(
+        EContracts.HatsGatekeeper,
+        "hatsProtocolAddress",
+        true,
+      );
+
+      let hatsGatekeeperContract: HatsGatekeeperBase;
+      // if we have one we use the single gatekeeper
+      if (criterionHats.length === 1) {
+        hatsGatekeeperContract = await deployment.deployContract(
+          {
+            name: EContracts.HatsGatekeeperSingle,
+            signer: deployer,
+          },
+          hatsProtocolAddress,
+          criterionHats[0],
+        );
+      } else {
+        hatsGatekeeperContract = await deployment.deployContract(
+          {
+            name: EContracts.HatsGatekeeperMultiple,
+            signer: deployer,
+          },
+          hatsProtocolAddress,
+          criterionHats,
+        );
+      }
+
+      await storage.register({
+        id: EContracts.HatsGatekeeper,
+        key: `poll-${pollId}`,
+        contract: hatsGatekeeperContract,
+        args: [hatsProtocolAddress, criterionHats.length === 1 ? criterionHats[0] : criterionHats],
+        network: hre.network.name,
+      });
+    }
+
+    if (!skipDeployMerkleProofGatekeeper) {
+      const root = deployment.getDeployConfigField<string>(EContracts.MerkleProofGatekeeper, "root", true);
+
+      const MerkleProofGatekeeperContract = await deployment.deployContract(
+        {
+          name: EContracts.MerkleProofGatekeeper,
+          signer: deployer,
+        },
+        root,
+      );
+      await storage.register({
+        id: EContracts.MerkleProofGatekeeper,
+        key: `poll-${pollId}`,
+        contract: MerkleProofGatekeeperContract,
+        args: [root],
+        network: hre.network.name,
+      });
+    }
+  }),
+);
diff --git a/packages/contracts/tasks/deploy/poll/01-poll.ts b/packages/contracts/tasks/deploy/poll/02-poll.ts
similarity index 93%
rename from packages/contracts/tasks/deploy/poll/01-poll.ts
rename to packages/contracts/tasks/deploy/poll/02-poll.ts
index 4a6459019a..98b48ee5b8 100644
--- a/packages/contracts/tasks/deploy/poll/01-poll.ts
+++ b/packages/contracts/tasks/deploy/poll/02-poll.ts
@@ -49,6 +49,11 @@ deployment.deployTask(EDeploySteps.Poll, "Deploy poll").then((task) =>
     const unserializedKey = PubKey.deserialize(coordinatorPubkey);
     const mode = useQuadraticVoting ? EMode.QV : EMode.NON_QV;
 
+    const gatekeeper =
+      deployment.getDeployConfigField<EContracts | null>(EContracts.Poll, "gatekeeper") ||
+      EContracts.FreeForAllGatekeeper;
+    const gatekeeperContractAddress = storage.mustGetAddress(gatekeeper, hre.network.name, `poll-${pollId}`);
+
     const tx = await maciContract.deployPoll(
       pollDuration,
       {
@@ -60,6 +65,7 @@ deployment.deployTask(EDeploySteps.Poll, "Deploy poll").then((task) =>
       verifierContractAddress,
       vkRegistryContractAddress,
       mode,
+      gatekeeperContractAddress,
     );
 
     const receipt = await tx.wait();
diff --git a/packages/contracts/tasks/helpers/constants.ts b/packages/contracts/tasks/helpers/constants.ts
index 76e580ec17..f8bcfec6c2 100644
--- a/packages/contracts/tasks/helpers/constants.ts
+++ b/packages/contracts/tasks/helpers/constants.ts
@@ -11,6 +11,7 @@ export enum EDeploySteps {
   TallyFactory = "full:deploy-tally-factory",
   Maci = "full:deploy-maci",
   VkRegistry = "full:deploy-vk-registry",
+  PollGatekeeper = "poll:deploy-gatekeeper",
   Poll = "poll:deploy-poll",
 }
 
diff --git a/packages/contracts/tests/MACI.test.ts b/packages/contracts/tests/MACI.test.ts
index 19bd9d7b14..8c8d6a03ae 100644
--- a/packages/contracts/tests/MACI.test.ts
+++ b/packages/contracts/tests/MACI.test.ts
@@ -8,7 +8,14 @@ import { Keypair, PubKey, Message } from "maci-domainobjs";
 
 import { EMode } from "../ts/constants";
 import { getDefaultSigner, getSigners } from "../ts/utils";
-import { MACI, Poll as PollContract, Poll__factory as PollFactory, Verifier, VkRegistry } from "../typechain-types";
+import {
+  MACI,
+  Poll as PollContract,
+  Poll__factory as PollFactory,
+  Verifier,
+  VkRegistry,
+  SignUpGatekeeper,
+} from "../typechain-types";
 
 import { STATE_TREE_DEPTH, duration, initialVoiceCreditBalance, messageBatchSize, treeDepths } from "./constants";
 import { timeTravel, deployTestContracts } from "./utils";
@@ -19,6 +26,7 @@ describe("MACI", function test() {
   let maciContract: MACI;
   let vkRegistryContract: VkRegistry;
   let verifierContract: Verifier;
+  let signupGatekeeperContract: SignUpGatekeeper;
   let pollId: bigint;
 
   const coordinator = new Keypair();
@@ -40,6 +48,7 @@ describe("MACI", function test() {
       maciContract = r.maciContract;
       vkRegistryContract = r.vkRegistryContract;
       verifierContract = r.mockVerifierContract as Verifier;
+      signupGatekeeperContract = r.gatekeeperContract;
     });
 
     it("should have set the correct stateTreeDepth", async () => {
@@ -218,6 +227,7 @@ describe("MACI", function test() {
         verifierContract,
         vkRegistryContract,
         EMode.QV,
+        signupGatekeeperContract,
       );
       const receipt = await tx.wait();
 
@@ -252,6 +262,7 @@ describe("MACI", function test() {
         verifierContract,
         vkRegistryContract,
         EMode.QV,
+        signupGatekeeperContract,
       );
       const receipt = await tx.wait();
       expect(receipt?.status).to.eq(1);
@@ -270,6 +281,7 @@ describe("MACI", function test() {
           verifierContract,
           vkRegistryContract,
           EMode.QV,
+          signupGatekeeperContract,
         );
       const receipt = await tx.wait();
       expect(receipt?.status).to.eq(1);
diff --git a/packages/contracts/tests/MessageProcessor.test.ts b/packages/contracts/tests/MessageProcessor.test.ts
index 598558d1af..6702b30619 100644
--- a/packages/contracts/tests/MessageProcessor.test.ts
+++ b/packages/contracts/tests/MessageProcessor.test.ts
@@ -15,6 +15,7 @@ import {
   MessageProcessor__factory as MessageProcessorFactory,
   Verifier,
   VkRegistry,
+  SignUpGatekeeper,
 } from "../typechain-types";
 
 import {
@@ -34,7 +35,7 @@ describe("MessageProcessor", () => {
   let verifierContract: Verifier;
   let vkRegistryContract: VkRegistry;
   let mpContract: MessageProcessor;
-
+  let signupGatekeeperContract: SignUpGatekeeper;
   let pollId: bigint;
 
   // local poll and maci state
@@ -57,7 +58,7 @@ describe("MessageProcessor", () => {
     signer = await getDefaultSigner();
     verifierContract = r.mockVerifierContract as Verifier;
     vkRegistryContract = r.vkRegistryContract;
-
+    signupGatekeeperContract = r.gatekeeperContract;
     // deploy on chain poll
     const tx = await maciContract.deployPoll(
       duration,
@@ -67,6 +68,7 @@ describe("MessageProcessor", () => {
       verifierContract,
       vkRegistryContract,
       EMode.QV,
+      signupGatekeeperContract,
     );
     let receipt = await tx.wait();
 
diff --git a/packages/contracts/tests/Poll.test.ts b/packages/contracts/tests/Poll.test.ts
index 1bd6b5b4b0..935a731807 100644
--- a/packages/contracts/tests/Poll.test.ts
+++ b/packages/contracts/tests/Poll.test.ts
@@ -10,7 +10,14 @@ import { Keypair, Message, PCommand, PubKey } from "maci-domainobjs";
 import { EMode } from "../ts/constants";
 import { IVerifyingKeyStruct } from "../ts/types";
 import { getDefaultSigner } from "../ts/utils";
-import { Poll__factory as PollFactory, MACI, Poll as PollContract, Verifier, VkRegistry } from "../typechain-types";
+import {
+  Poll__factory as PollFactory,
+  MACI,
+  Poll as PollContract,
+  Verifier,
+  VkRegistry,
+  SignUpGatekeeper,
+} from "../typechain-types";
 
 import {
   STATE_TREE_DEPTH,
@@ -30,6 +37,7 @@ describe("Poll", () => {
   let pollContract: PollContract;
   let verifierContract: Verifier;
   let vkRegistryContract: VkRegistry;
+  let signupGatekeeperContract: SignUpGatekeeper;
   let signer: Signer;
   let deployTime: number;
   const coordinator = new Keypair();
@@ -51,6 +59,7 @@ describe("Poll", () => {
       maciContract = r.maciContract;
       verifierContract = r.mockVerifierContract as Verifier;
       vkRegistryContract = r.vkRegistryContract;
+      signupGatekeeperContract = r.gatekeeperContract;
 
       for (let i = 0; i < NUM_USERS; i += 1) {
         const timestamp = Math.floor(Date.now() / 1000);
@@ -74,6 +83,7 @@ describe("Poll", () => {
         verifierContract,
         vkRegistryContract,
         EMode.QV,
+        signupGatekeeperContract,
       );
       const receipt = await tx.wait();
 
@@ -177,6 +187,7 @@ describe("Poll", () => {
           r.mockVerifierContract as Verifier,
           r.vkRegistryContract,
           EMode.QV,
+          signupGatekeeperContract,
         ),
       ).to.be.revertedWithCustomError(testMaciContract, "InvalidPubKey");
     });
@@ -192,7 +203,14 @@ describe("Poll", () => {
         const mockNullifier = AbiCoder.defaultAbiCoder().encode(["uint256"], [i]);
         const voiceCreditBalance = AbiCoder.defaultAbiCoder().encode(["uint256"], [i]);
 
-        const response = await pollContract.joinPoll(mockNullifier, pubkey, voiceCreditBalance, i, mockProof);
+        const response = await pollContract.joinPoll(
+          mockNullifier,
+          pubkey,
+          voiceCreditBalance,
+          i,
+          mockProof,
+          AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
+        );
         const receipt = await response.wait();
         const logs = receipt!.logs[0];
         const event = iface.parseLog(logs as unknown as { topics: string[]; data: string }) as unknown as {
@@ -226,7 +244,14 @@ describe("Poll", () => {
       const mockProof = [0, 0, 0, 0, 0, 0, 0, 0];
 
       await expect(
-        pollContract.joinPoll(mockNullifier, pubkey, voiceCreditBalance, 0, mockProof),
+        pollContract.joinPoll(
+          mockNullifier,
+          pubkey,
+          voiceCreditBalance,
+          0,
+          mockProof,
+          AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
+        ),
       ).to.be.revertedWithCustomError(pollContract, "UserAlreadyJoined");
     });
   });
diff --git a/packages/contracts/tests/PollFactory.test.ts b/packages/contracts/tests/PollFactory.test.ts
index 89d4565101..55c3586818 100644
--- a/packages/contracts/tests/PollFactory.test.ts
+++ b/packages/contracts/tests/PollFactory.test.ts
@@ -33,7 +33,12 @@ describe("pollFactory", () => {
     maciContract = r.maciContract;
     verifierContract = r.mockVerifierContract as Verifier;
     vkRegistryContract = r.vkRegistryContract;
-    extContracts = { maci: maciContract, verifier: verifierContract, vkRegistry: vkRegistryContract };
+    extContracts = {
+      maci: maciContract,
+      verifier: verifierContract,
+      vkRegistry: vkRegistryContract,
+      gatekeeper: r.gatekeeperContract,
+    };
 
     pollFactory = (await deployPollFactory(signer, undefined, true)) as BaseContract as PollFactory;
   });
diff --git a/packages/contracts/tests/Tally.test.ts b/packages/contracts/tests/Tally.test.ts
index ed76475005..d8a25fd2de 100644
--- a/packages/contracts/tests/Tally.test.ts
+++ b/packages/contracts/tests/Tally.test.ts
@@ -19,6 +19,7 @@ import {
   MessageProcessor__factory as MessageProcessorFactory,
   Poll__factory as PollFactory,
   Tally__factory as TallyFactory,
+  SignUpGatekeeper,
 } from "../typechain-types";
 
 import {
@@ -41,6 +42,7 @@ describe("TallyVotes", () => {
   let mpContract: MessageProcessor;
   let verifierContract: Verifier;
   let vkRegistryContract: VkRegistry;
+  let signupGatekeeperContract: SignUpGatekeeper;
 
   const coordinator = new Keypair();
   let users: Keypair[];
@@ -63,6 +65,7 @@ describe("TallyVotes", () => {
     maciContract = r.maciContract;
     verifierContract = r.mockVerifierContract as Verifier;
     vkRegistryContract = r.vkRegistryContract;
+    signupGatekeeperContract = r.gatekeeperContract;
 
     // deploy a poll
     // deploy on chain poll
@@ -74,6 +77,7 @@ describe("TallyVotes", () => {
       verifierContract,
       vkRegistryContract,
       EMode.QV,
+      signupGatekeeperContract,
     );
     const receipt = await tx.wait();
 
@@ -231,6 +235,7 @@ describe("TallyVotes", () => {
         verifierContract,
         vkRegistryContract,
         EMode.QV,
+        signupGatekeeperContract,
       );
       const receipt = await tx.wait();
 
@@ -308,6 +313,7 @@ describe("TallyVotes", () => {
           BigInt(initialVoiceCreditBalance),
           i,
           [0, 0, 0, 0, 0, 0, 0, 0],
+          AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
         );
       }
 
@@ -538,6 +544,7 @@ describe("TallyVotes", () => {
         verifierContract,
         vkRegistryContract,
         EMode.QV,
+        signupGatekeeperContract,
       );
       const receipt = await tx.wait();
 
@@ -616,6 +623,7 @@ describe("TallyVotes", () => {
           BigInt(initialVoiceCreditBalance),
           i,
           [0, 0, 0, 0, 0, 0, 0, 0],
+          AbiCoder.defaultAbiCoder().encode(["uint256"], [1]),
         );
       }
 
diff --git a/packages/contracts/tests/TallyNonQv.test.ts b/packages/contracts/tests/TallyNonQv.test.ts
index 7d4e218e40..9187925de8 100644
--- a/packages/contracts/tests/TallyNonQv.test.ts
+++ b/packages/contracts/tests/TallyNonQv.test.ts
@@ -19,6 +19,7 @@ import {
   MessageProcessor__factory as MessageProcessorFactory,
   Poll__factory as PollFactory,
   Tally__factory as TallyFactory,
+  SignUpGatekeeper,
 } from "../typechain-types";
 
 import { STATE_TREE_DEPTH, duration, messageBatchSize, testProcessVk, testTallyVk, treeDepths } from "./constants";
@@ -32,6 +33,7 @@ describe("TallyVotesNonQv", () => {
   let mpContract: MessageProcessor;
   let verifierContract: Verifier;
   let vkRegistryContract: VkRegistry;
+  let gatekeeperContract: SignUpGatekeeper;
 
   const coordinator = new Keypair();
   let maciState: MaciState;
@@ -50,6 +52,7 @@ describe("TallyVotesNonQv", () => {
     maciContract = r.maciContract;
     verifierContract = r.mockVerifierContract as Verifier;
     vkRegistryContract = r.vkRegistryContract;
+    gatekeeperContract = r.gatekeeperContract;
 
     // deploy a poll
     // deploy on chain poll
@@ -61,6 +64,7 @@ describe("TallyVotesNonQv", () => {
       verifierContract,
       vkRegistryContract,
       EMode.NON_QV,
+      gatekeeperContract,
     );
     const receipt = await tx.wait();
 
diff --git a/packages/contracts/tests/constants.ts b/packages/contracts/tests/constants.ts
index 855b0e102e..ac2b747dcf 100644
--- a/packages/contracts/tests/constants.ts
+++ b/packages/contracts/tests/constants.ts
@@ -7,6 +7,7 @@ export interface ExtContractsStruct {
   maci: AddressLike;
   verifier: AddressLike;
   vkRegistry: AddressLike;
+  gatekeeper: AddressLike;
 }
 
 export const duration = 2_000;
diff --git a/packages/integrationTests/ts/__tests__/maci-keys.test.ts b/packages/integrationTests/ts/__tests__/maci-keys.test.ts
index 52a86fcfb4..081553e52f 100644
--- a/packages/integrationTests/ts/__tests__/maci-keys.test.ts
+++ b/packages/integrationTests/ts/__tests__/maci-keys.test.ts
@@ -72,7 +72,7 @@ describe("integration tests private/public/keypair", () => {
 
     before(async () => {
       signer = await getDefaultSigner();
-      const { maci, verifier, vkRegistry } = await deployTestContracts(
+      const { maci, verifier, vkRegistry, gatekeeper } = await deployTestContracts(
         initialVoiceCredits,
         STATE_TREE_DEPTH,
         signer,
@@ -91,6 +91,7 @@ describe("integration tests private/public/keypair", () => {
         verifier,
         vkRegistry,
         EMode.NON_QV,
+        gatekeeper,
       );
 
       // we know it's the first poll so id is 0
diff --git a/packages/integrationTests/ts/__tests__/utils/interfaces.ts b/packages/integrationTests/ts/__tests__/utils/interfaces.ts
index 9bfd6297ce..647a0b6ee5 100644
--- a/packages/integrationTests/ts/__tests__/utils/interfaces.ts
+++ b/packages/integrationTests/ts/__tests__/utils/interfaces.ts
@@ -1,4 +1,4 @@
-import { MACI, Verifier, VkRegistry } from "maci-contracts";
+import { MACI, Verifier, VkRegistry, FreeForAllGatekeeper } from "maci-contracts";
 
 /**
  * A util interface that represents a vote object
@@ -47,4 +47,5 @@ export interface IDeployedTestContracts {
   maci: MACI;
   verifier: Verifier;
   vkRegistry: VkRegistry;
+  gatekeeper: FreeForAllGatekeeper;
 }
diff --git a/packages/integrationTests/ts/__tests__/utils/utils.ts b/packages/integrationTests/ts/__tests__/utils/utils.ts
index 2859b7ecf1..5b59cb923b 100644
--- a/packages/integrationTests/ts/__tests__/utils/utils.ts
+++ b/packages/integrationTests/ts/__tests__/utils/utils.ts
@@ -198,5 +198,10 @@ export const deployTestContracts = async (
     quiet,
   });
 
-  return { maci: maciContract, verifier: mockVerifierContract as Verifier, vkRegistry: vkRegistryContract };
+  return {
+    maci: maciContract,
+    verifier: mockVerifierContract as Verifier,
+    vkRegistry: vkRegistryContract,
+    gatekeeper: gatekeeperContract,
+  };
 };