Skip to main content
Authenticators verify that governance actions (proposals, votes, updates) are authorized by the correct user. They act as the entry point for all user interactions with a space.

What are Authenticators?

An authenticator is a contract that:
  1. Verifies user identity - Confirms the action comes from the claimed address
  2. Validates signatures or transactions - Checks cryptographic proofs
  3. Calls the space contract - Forwards the authenticated action
Each space can enable multiple authenticators, allowing users to choose their preferred authentication method.

Authenticator Interface

EVM Authenticators

EVM authenticators create contract calls from envelopes:
interface Authenticator {
  type: string;
  
  createCall(
    envelope: Envelope<Propose | UpdateProposal | Vote>,
    selector: string,
    calldata: string[]
  ): Call;
}

type Call = {
  abi: ContractInterface;
  args: any[];
};

Starknet Authenticators

Starknet authenticators create separate calls for each action type:
interface Authenticator {
  type: string;
  
  createProposeCall(
    envelope: Envelope<Propose>,
    args: ProposeCallArgs
  ): Call;
  
  createVoteCall(
    envelope: Envelope<Vote>,
    args: VoteCallArgs
  ): Call;
  
  createUpdateProposalCall(
    envelope: Envelope<UpdateProposal>,
    args: UpdateProposalCallArgs
  ): Call;
}

EVM Authenticators

Vanilla Authenticator

The simplest authenticator - uses direct transaction authentication.
function createVanillaAuthenticator(): Authenticator {
  return {
    type: 'vanilla',
    createCall(
      envelope: Envelope<Propose | UpdateProposal | Vote>,
      selector: string,
      calldata: string[]
    ): Call {
      const { space } = envelope.data;
      
      return {
        abi: VanillaAuthenticatorAbi,
        args: [space, selector, ...calldata]
      };
    }
  };
}
How it works: The transaction sender is authenticated by the transaction itself. No signature required. Use case: Direct onchain interaction where the user sends the transaction themselves.

EthTx Authenticator

Authenticates via direct Ethereum transactions with additional validation.
function createEthTxAuthenticator(): Authenticator {
  return {
    type: 'ethTx',
    createCall(
      envelope: Envelope<Propose | UpdateProposal | Vote>,
      selector: string,
      calldata: string[]
    ): Call {
      const { space } = envelope.data;
      
      return {
        abi: EthTxAuthenticatorAbi,
        args: [space, selector, ...calldata]
      };
    }
  };
}
How it works: Similar to vanilla, but with additional onchain checks. Use case: Direct transactions with enhanced validation.

EthSig Authenticator

Authenticates using EIP-712 typed signatures for gasless voting.
function createEthSigAuthenticator(
  type: 'ethSig' | 'ethSigV2'
): Authenticator {
  return {
    type,
    createCall(
      envelope: Envelope<Propose | UpdateProposal | Vote>,
      selector: string,
      calldata: string[]
    ): Call {
      const { signatureData, data } = envelope;
      const { space } = data;
      
      if (!signatureData)
        throw new Error('signatureData is required for this authenticator');
      
      const { r, s, v } = getRSVFromSig(signatureData.signature);
      
      const args = [
        v,
        hexPadLeft(r.toHex()),
        hexPadLeft(s.toHex()),
        signatureData.message.salt || '0x00',
        space,
        selector,
        ...calldata
      ];
      
      return {
        abi: EthSigAuthenticatorAbi,
        args
      };
    }
  };
}
How it works:
1

User Signs Message

User signs an EIP-712 typed data message with their wallet
2

Submit to Relayer

Signature is submitted to Mana relayer or sent directly
3

Verify Signature

Authenticator contract recovers signer address from signature
4

Execute Action

If signature is valid, the action is executed on behalf of the signer
EIP-712 message format for voting:
type EIP712VoteMessage = {
  space: string;
  voter: string;
  proposalId: number;
  choice: number;
  userVotingStrategies: IndexedConfig[];
  voteMetadataURI: string;
};
Use case: Gasless voting where a relayer pays transaction fees. Users sign messages instead of sending transactions.

Starknet Authenticators

Starknet Vanilla Authenticator

Direct transaction authentication on Starknet. Use case: Direct Starknet transactions.

StarkSig Authenticator

Authenticates using Starknet signature verification.
function createStarkSigAuthenticator(): Authenticator {
  return {
    type: 'starkSig',
    createProposeCall(
      envelope: Envelope<Propose>,
      args: ProposeCallArgs
    ): Call {
      const { authenticator } = envelope.data;
      
      if (!envelope.signatureData?.signature) {
        throw new Error('signature is required for this authenticator');
      }
      
      const compiled = callData.compile('authenticate_propose', [
        envelope.signatureData.signature,
        envelope.data.space,
        envelope.signatureData.address,
        shortString.splitLongString(args.metadataUri),
        {
          address: args.executionStrategy.address,
          params: args.executionStrategy.params
        },
        args.strategiesParams,
        envelope.signatureData.message.salt
      ]);
      
      return {
        contractAddress: authenticator,
        entrypoint: 'authenticate_propose',
        calldata: compiled
      };
    },
    createVoteCall(envelope: Envelope<Vote>, args: VoteCallArgs): Call {
      const { authenticator } = envelope.data;
      
      if (!envelope.signatureData?.signature) {
        throw new Error('signature is required for this authenticator');
      }
      
      const compiled = callData.compile('authenticate_vote', [
        envelope.signatureData.signature,
        envelope.data.space,
        envelope.signatureData.address,
        uint256.bnToUint256(args.proposalId),
        getChoiceEnum(args.choice),
        args.votingStrategies.map(strategy => ({
          index: strategy.index,
          params: strategy.params
        })),
        shortString.splitLongString(args.metadataUri)
      ]);
      
      return {
        contractAddress: authenticator,
        entrypoint: 'authenticate_vote',
        calldata: compiled
      };
    },
    createUpdateProposalCall(
      envelope: Envelope<UpdateProposal>,
      args: UpdateProposalCallArgs
    ): Call {
      const { authenticator } = envelope.data;
      
      if (!envelope.signatureData?.signature) {
        throw new Error('signature is required for this authenticator');
      }
      
      const compiled = callData.compile('authenticate_update_proposal', [
        envelope.signatureData.signature,
        envelope.data.space,
        envelope.signatureData.address,
        uint256.bnToUint256(args.proposalId),
        {
          address: args.executionStrategy.address,
          params: args.executionStrategy.params
        },
        shortString.splitLongString(args.metadataUri),
        envelope.signatureData.message.salt
      ]);
      
      return {
        contractAddress: authenticator,
        entrypoint: 'authenticate_update_proposal',
        calldata: compiled
      };
    }
  };
}
How it works: Similar to EthSig but using Starknet’s native signature verification. Use case: Gasless voting on Starknet.

StarkTx Authenticator

Authenticates via direct Starknet transactions. Use case: Direct Starknet transaction authentication.

Cross-Chain Authenticators

Starknet also supports Ethereum authenticators for cross-chain governance:
  • EthSig on Starknet: Verify Ethereum signatures on Starknet
  • EthTx on Starknet: Accept Ethereum transaction proofs on Starknet

Signature Data Structure

The SignatureData type contains all information needed to verify a signature:
type SignatureData = {
  address: string;                              // Signer address
  signature?: string | string[];                // Signature bytes
  message?: Record<string, any>;                // Signed message
  domain?: TypedDataDomain;                     // EIP-712 domain
  types?: Record<string, TypedDataField[]>;    // EIP-712 types
  primaryType?: string;                         // EIP-712 primary type
  
  // For commit-reveal schemes
  commitTxId?: string;                          // Commit transaction ID
  commitHash?: string;                          // Commit hash
};

Using Authenticators

Configure Space Authenticators

Enable authenticators when creating a space:
const { address, txId } = await client.deploySpace({
  signer,
  params: {
    // ... other params
    authenticators: [
      '0x...', // Vanilla authenticator
      '0x...', // EthSig authenticator
      '0x...'  // EthTx authenticator
    ]
  }
});

Voting with Vanilla Authenticator

Direct transaction (user pays gas):
await client.vote({
  signer,
  envelope: {
    data: {
      space: '0x...',
      proposal: 1,
      choice: Choice.For,
      authenticator: vanillaAuthenticatorAddress,
      strategies: [...],
      metadataUri: ''
    }
    // No signatureData needed
  }
});

Voting with EthSig Authenticator

Gasless signature-based voting:
// 1. Create the vote envelope
const envelope = {
  data: {
    space: '0x...',
    proposal: 1,
    choice: Choice.For,
    authenticator: ethSigAuthenticatorAddress,
    strategies: [...],
    metadataUri: ''
  }
};

// 2. Sign the typed data
const domain = {
  name: 'snapshot-x',
  version: '1',
  chainId: 1,
  verifyingContract: envelope.data.space
};

const types = {
  Vote: [
    { name: 'space', type: 'address' },
    { name: 'voter', type: 'address' },
    { name: 'proposalId', type: 'uint256' },
    { name: 'choice', type: 'uint8' },
    { name: 'userVotingStrategies', type: 'IndexedStrategy[]' },
    { name: 'voteMetadataURI', type: 'string' }
  ],
  IndexedStrategy: [
    { name: 'index', type: 'uint8' },
    { name: 'params', type: 'bytes' }
  ]
};

const message = {
  space: envelope.data.space,
  voter: await signer.getAddress(),
  proposalId: envelope.data.proposal,
  choice: envelope.data.choice,
  userVotingStrategies: strategiesParams,
  voteMetadataURI: ''
};

const signature = await signer._signTypedData(domain, types, message);

// 3. Submit to relayer or execute directly
const envelopeWithSig = {
  ...envelope,
  signatureData: {
    address: await signer.getAddress(),
    signature,
    domain,
    types,
    message
  }
};

// Send to Mana relayer
await fetch(manaUrl, {
  method: 'POST',
  body: JSON.stringify({
    method: 'vote',
    params: envelopeWithSig
  })
});

Authenticator Resolution

Authenticators are resolved from network configuration:
// EVM
function getAuthenticator(
  address: string,
  networkConfig: EvmNetworkConfig
): Authenticator | null {
  const authenticator = networkConfig.authenticators[address];
  if (!authenticator) return null;
  
  if (authenticator.type === 'vanilla') return createVanillaAuthenticator();
  if (authenticator.type === 'ethTx') return createEthTxAuthenticator();
  if (authenticator.type === 'ethSig' || authenticator.type === 'ethSigV2') {
    return createEthSigAuthenticator(authenticator.type);
  }
  
  return null;
}

// Starknet
function getAuthenticator(
  address: string,
  networkConfig: NetworkConfig
): Authenticator | null {
  const authenticator = networkConfig.authenticators[hexPadLeft(address)];
  if (!authenticator) return null;
  
  if (authenticator.type === 'vanilla') return createVanillaAuthenticator();
  if (authenticator.type === 'ethSig') return createEthSigAuthenticator();
  if (authenticator.type === 'ethTx') return createEthTxAuthenticator();
  if (authenticator.type === 'starkSig') return createStarkSigAuthenticator();
  if (authenticator.type === 'starkTx') return createStarkTxAuthenticator();
  
  return null;
}

Authentication Flow

Authenticator Types by Network

AuthenticatorEVMStarknetGaslessDescription
vanillaDirect transaction
ethTxEthereum transaction with validation
ethSigEIP-712 signature
ethSigV2Updated EIP-712 signature
starkSigStarknet signature
starkTxStarknet transaction

Spaces

Learn about governance spaces

Strategies

Understand voting power

Creating Proposals

Create your first proposal

Gasless Voting

Implement gasless voting

Build docs developers (and LLMs) love