Skip to main content
Voting strategies determine how much voting power each user has when participating in governance. Strategies can read token balances, check whitelists, verify cross-chain proofs, or implement custom logic.

What are Voting Strategies?

A voting strategy is a contract that:
  1. Validates user eligibility - Checks if a user can vote or propose
  2. Calculates voting power - Returns the user’s voting weight
  3. Provides parameters - Generates proof data needed for the calculation
Each space can configure multiple voting strategies, allowing users to choose which strategy to use when voting.

Strategy Interface

All strategies implement a common interface:
interface Strategy {
  type: string;
  
  // Generate parameters for voting/proposing
  getParams(
    call: 'propose' | 'vote',
    strategyConfig: StrategyConfig,
    signerAddress: string,
    metadata: Record<string, any> | null,
    data: Propose | Vote,
    clientConfig: ClientConfig
  ): Promise<string>;
  
  // Calculate voting power
  getVotingPower(
    strategyAddress: string,
    voterAddress: string,
    metadata: Record<string, any> | null,
    block: number | null,
    params: string,
    provider: Provider
  ): Promise<bigint>;
}

EVM Strategies

Snapshot X supports multiple strategies for EVM-based spaces:

Vanilla Strategy

The simplest strategy - gives everyone 1 vote.
function createVanillaStrategy(): Strategy {
  return {
    type: 'vanilla',
    async getParams(): Promise<string> {
      return '0x00';
    },
    async getVotingPower(): Promise<bigint> {
      return 1n;
    }
  };
}
Use case: One person, one vote governance where all members have equal power.

Token Balance Strategies

OpenZeppelin Votes (ozVotes)

Reads voting power from ERC20Votes or ERC721Votes tokens:
function createOzVotesStrategy(): Strategy {
  return {
    type: 'ozVotes',
    async getParams(): Promise<string> {
      return '0x00';
    },
    async getVotingPower(
      strategyAddress: string,
      voterAddress: string,
      metadata: Record<string, any> | null,
      block: number | null,
      params: string,
      provider: Provider
    ): Promise<bigint> {
      const votesContract = new Contract(params, IVotes, provider);
      const votingPower = await votesContract.getVotes(voterAddress, {
        blockTag: block ?? 'latest'
      });
      return BigInt(votingPower.toString());
    }
  };
}
Configuration: The params field contains the token contract address. Use case: Token-weighted voting using OpenZeppelin’s Votes extension.

Compound (comp)

Reads voting power from Compound-style governance tokens:
function createCompStrategy(): Strategy {
  return {
    type: 'comp',
    async getParams(): Promise<string> {
      return '0x00';
    },
    async getVotingPower(
      strategyAddress: string,
      voterAddress: string,
      metadata: Record<string, any> | null,
      block: number | null,
      params: string,
      provider: Provider
    ): Promise<bigint> {
      const compContract = new Contract(params, ICompAbi, provider);
      const votingPower = await compContract.getCurrentVotes(voterAddress, {
        blockTag: block ?? 'latest'
      });
      return BigInt(votingPower.toString());
    }
  };
}
Configuration: The params field contains the COMP token address. Use case: Token-weighted voting using Compound’s voting token interface.

Whitelist Strategy (merkleWhitelist)

Allows only whitelisted addresses with predefined voting power:
type Entry = {
  address: string;
  votingPower: string;
};

function createMerkleWhitelist(): Strategy {
  return {
    type: 'whitelist',
    async getParams(
      call: 'propose' | 'vote',
      strategyConfig: StrategyConfig,
      signerAddress: string,
      metadata: Record<string, any> | null,
      data: Propose | Vote,
      clientConfig: ClientConfig
    ): Promise<string> {
      const tree: Entry[] = metadata?.tree;
      if (!tree) throw new Error('Invalid metadata. Missing tree');
      
      const voterIndex = tree.findIndex(
        entry => entry.address.toLowerCase() === signerAddress.toLowerCase()
      );
      if (voterIndex === -1) {
        throw new Error('Signer is not in whitelist');
      }
      
      const whitelist = tree.map(
        entry => [entry.address, BigInt(entry.votingPower)] as [string, bigint]
      );
      
      const merkleTree = StandardMerkleTree.of(whitelist, ['address', 'uint96']);
      const proof = merkleTree.getProof(voterIndex);
      
      const abiCoder = new AbiCoder();
      return abiCoder.encode(
        ['bytes32[]', 'tuple(address, uint96)'],
        [proof, whitelist[voterIndex]]
      );
    },
    async getVotingPower(
      strategyAddress: string,
      voterAddress: string,
      metadata: Record<string, any> | null
    ): Promise<bigint> {
      const tree: Entry[] = metadata?.tree;
      if (!tree) return 0n;
      
      const match = tree.find(
        entry => entry.address.toLowerCase() === voterAddress.toLowerCase()
      );
      return match ? BigInt(match.votingPower) : 0n;
    }
  };
}
Configuration:
  • params: Merkle root hash
  • metadata: Array of { address, votingPower } entries
Use case: Curated member lists with custom voting power per member.

Starknet Strategies

Starknet spaces support additional strategies for cross-chain governance:

EVM Slot Value (evmSlotValue)

Reads storage slots from Ethereum contracts and proves them on Starknet:
function createEvmSlotValueStrategy(): Strategy {
  return {
    type: 'evmSlotValue',
    async getParams(
      call: 'propose' | 'vote',
      signerAddress: string,
      address: string,
      index: number,
      params: string,
      metadata: Record<string, any> | null,
      envelope: Envelope<Vote>,
      clientConfig: ClientConfig
    ): Promise<string[]> {
      if (call === 'propose') throw new Error('Not supported for proposing');
      
      const { contractAddress, slotIndex } = metadata;
      const { starkProvider, ethUrl, networkConfig } = clientConfig;
      
      // Get L1 block number cached for this proposal
      const spaceContract = new Contract(
        SpaceAbi,
        envelope.data.space,
        starkProvider
      );
      const proposalStruct = await spaceContract.call('proposals', [
        envelope.data.proposal
      ]);
      const startTimestamp = proposalStruct.start_timestamp;
      
      const contract = new Contract(EVMSlotValue, address, starkProvider);
      const l1BlockNumber = await contract.cached_timestamps(startTimestamp);
      
      // Generate storage proof from Ethereum
      const provider = new StaticJsonRpcProvider(
        ethUrl,
        networkConfig.herodotusAccumulatesChainId
      );
      const proof = await provider.send('eth_getProof', [
        contractAddress,
        [getSlotKey(signerAddress, slotIndex)],
        `0x${l1BlockNumber.toString(16)}`
      ]);
      
      return CallData.compile({
        storageProof: proof.storageProof[0].proof
      });
    },
    async getVotingPower(
      strategyAddress: string,
      voterAddress: string,
      metadata: Record<string, any> | null,
      timestamp: number | null,
      params: string[],
      clientConfig: ClientConfig
    ): Promise<bigint> {
      if (!metadata || voterAddress.length !== 42) return 0n;
      
      const { contractAddress, slotIndex } = metadata;
      const { starkProvider, ethUrl, networkConfig } = clientConfig;
      
      if (!timestamp) {
        // Read current value from Ethereum
        const provider = new StaticJsonRpcProvider(
          ethUrl,
          networkConfig.herodotusAccumulatesChainId
        );
        const storage = await provider.getStorageAt(
          contractAddress,
          getSlotKey(voterAddress, slotIndex)
        );
        return BigInt(storage);
      }
      
      // Read proven value from Starknet strategy contract
      const contract = new Contract(
        EVMSlotValue,
        strategyAddress,
        starkProvider
      );
      const l1BlockNumber = await contract.cached_timestamps(timestamp);
      
      return await contract.get_voting_power(
        timestamp,
        getUserAddressEnum('ETHEREUM', voterAddress),
        params,
        CallData.compile({ storageProof })
      );
    }
  };
}
Configuration:
  • params: Comma-separated config values
  • metadata: { contractAddress, slotIndex }
Use case: Allow Ethereum token holders to vote on Starknet without bridging tokens.

ERC20 Votes (erc20Votes)

Reads voting power from ERC20Votes tokens on Starknet. Use case: Token-weighted voting using Starknet tokens.

OZ Votes Storage Proof (ozVotesStorageProof)

Proves OpenZeppelin Votes balances from Ethereum to Starknet using storage proofs. Configuration: params: { trace: 208 | 224 } for different proof types. Use case: Cross-chain voting with cryptographic proofs instead of bridges.

Offchain Strategies

Offchain spaces can use custom validation strategies:

Only Members

Only allows space members to vote:
function createOnlyMembersStrategy(): Strategy {
  // Validates against space member list
}

Remote VP

Delegates voting power calculation to Snapshot’s scoring API:
function createRemoteVpStrategy(): Strategy {
  // Calls Snapshot API for voting power
}

Remote Validate

Delegates validation to external services:
function createRemoteValidateStrategy(name: string): Strategy {
  // Supports: 'any', 'basic', 'passport-gated', 'arbitrum', 'karma-eas-attestation'
}

Using Strategies

When creating a space, configure voting strategies:
const { address, txId } = await client.deploySpace({
  signer,
  params: {
    // ... other params
    votingStrategies: [
      {
        addr: '0x...', // ozVotes strategy address
        params: '0x...' // Token contract address
      },
      {
        addr: '0x...', // whitelist strategy address
        params: '0x...' // Merkle root
      }
    ],
    votingStrategiesMetadata: [
      '', // No metadata for token strategy
      'ipfs://...' // Whitelist metadata
    ]
  }
});
When voting, users select which strategy to use:
await client.vote({
  signer,
  envelope: {
    data: {
      space: '0x...',
      proposal: 1,
      choice: Choice.For,
      authenticator: '0x...',
      strategies: [
        {
          index: 0, // Use first strategy (token voting)
          address: '0x...',
          params: '0x...',
          metadata: null
        }
      ],
      metadataUri: ''
    }
  }
});

Strategy Resolution

Strategies are resolved from network configuration:
function getStrategy(
  address: string,
  networkConfig: NetworkConfig
): Strategy | null {
  const strategy = networkConfig.strategies[address];
  if (!strategy) return null;
  
  if (strategy.type === 'vanilla') return createVanillaStrategy();
  if (strategy.type === 'comp') return createCompStrategy();
  if (strategy.type === 'ozVotes') return createOzVotesStrategy();
  if (strategy.type === 'whitelist') return createMerkleWhitelist();
  if (strategy.type === 'evmSlotValue') return createEvmSlotValueStrategy();
  // ...
  
  return null;
}

Spaces

Learn about governance spaces

Authenticators

Understand user authentication

Creating Proposals

Create your first proposal

Custom Strategies

Build custom voting strategies

Build docs developers (and LLMs) love