Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/snapshot-labs/sx-monorepo/llms.txt

Use this file to discover all available pages before exploring further.

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