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