snapshot
Blog
  • Welcome to Snapshot docs
  • User guides
    • Spaces
      • What is a space?
      • Create a space
        • Register an ENS domain
        • Alternative way to create a space
      • Settings
      • Sub-spaces
      • Space verification
      • Space hibernation
      • Add a custom domain
      • Add a skin
      • Space roles
      • Space badges
      • Snapshot Pro
      • Space handbook
        • Most common
        • Voting threshold
        • Anti-whale
        • Sybil resistance, scam & spam prevention
        • Liquidity / staking pool
        • Delegation
        • NFT voting
          • Most common: ERC-721
          • Multi-token: ERC-1155
          • POAP - Proof of Attendance
        • Custom calculations
    • Create a proposal
    • Voting
      • Vote on a proposal
      • Delegate your voting power
    • Voting strategies
    • Validation strategies
    • Using Safe multi-sig
    • Delegation
  • Developer Guides
    • Create a voting strategy
    • Create a validation strategy
    • Identify integrator activity
    • Add a network
  • Tools
    • Tools overview
    • Snapshot.js
    • API
      • API Keys
    • Webhooks
    • Subgraphs
    • Mobile notifications
    • Bots
  • Snapshot X
    • Overview
    • User guides
      • Create a space
      • Proposals
      • Voting
      • Safe execution setup
    • Protocol
      • Overview
      • Space actions
      • Space controller actions
      • Authenticators
      • Proposal validations
      • Voting strategies
      • Starknet specifics
      • Execution strategies
      • Audits
    • Services
      • Architecture
      • API
      • SX.js
      • UI
      • Mana
  • V1 interface
    • Email notifications
    • Plugins
      • What is a plugin?
      • Create a plugin
      • oSnap
      • SafeSnap
      • POAP
      • Quorum
      • Domino notifications
      • Galxe
    • Boost
  • Community
    • Help center
    • Discord
    • X (Twitter)
    • GitHub
Powered by GitBook
On this page
  • What is a validation strategy?
  • How to use validation strategies:
  • Authors only mode
  • Validation strategy example - Basic
  • Validation strategy example - Gitcoin Passport
  • Create a custom validation
  • Find more voting validations here:

Was this helpful?

Edit on GitHub
Export as PDF
  1. User guides

Validation strategies

PreviousVoting strategiesNextUsing Safe multi-sig

Last updated 9 months ago

Was this helpful?

What is a validation strategy?

A voting validation is a JavaScript function that returns a boolean (true or false) for the connected account. Voting validations are being used on Snapshot to decide if an account can vote or create a proposal in a specific space. Each space can use one voting validation for all of its proposals at a time. While voting strategies calculate the Voting Power mainly on the basis of the monetary assets, the validation strategy can serve as a protection against Sybil attacks. It can take into consideration how many POAPs an account owns or track the account activity to assess if the account is a bot or a real human.

The default validation is checking if the address has any voting power. If the voting power is higher than 0 the connected account is validated. A validation strategy can send a call to a node or subgraph.

When setting the Validation Strategy up it’s important to keep in mind that it is meant to make it difficult for users outside of your community to post scam proposals or post spam votes.

Therefore for Proposal Validation make sure to use a high threshold, for example $100 worth of your organization’s token. A good idea would be to check the holdings of previous proposal creators, both legitimate and scammers, to assess a reasonable value.

Spaces are required to use Proposal Validation. Learn how to set it up on this page or read .

Spaces using only a are required to set a Voting Validation to secure their spaces and ensure a fair voting process preventing spam. Learn here how to set it up: Voting Validation in Space Settings

How to use validation strategies:

Validation strategies can be used for two purposes:

  • proposal validation - determine if the account can create a new proposal,

  • voting validation - determine if the account can take part in the voting process.

Proposal Validation in Space Settings

Head to Proposals tab in the sidebar to update the configuration:

Voting Validation in Space Settings

Head to Voting tab in the sidebar to update the configuration:

If you want to allow addresses with any voting power to vote you can use the default voting validation.

Authors only mode

If you wish to limit proposal creators to Admins, Moderators and Authors only, you can do so by enabling the Authors only setting in the Proposal tab in the space settings. Make sure to give the Author role to the users you trust!

Validation strategy example - Basic

The Basic validation strategy allows you to use existing Voting Strategies configured for your space or define a custom setup to determine if a user is eligible to create a proposal or cast a vote.

In order to use existing setup of Voting Strategies you can simply chose Basic Validation and define a required threshold as on the screenshot below. 100 corresponds to user's Voting Power calculated on the basis of the Voting Strategies.

If you wish to use a different configuration, toggle the Use custom strategies button and define the strategies for your use case:

Validation strategy example - Gitcoin Passport

Validation strategy built together with Gitcoin Passport. You can select individual or multiple stamps that matter for your space. You can also decide if they need to meet all of these criteria or only one. The more criteria you select, the more sybil resistant your space is.

Implementation

Have a look at the example of the Gitcoin Passport validation strategy.

import snapshot from '@snapshot-labs/snapshot.js';
import Validation from '../validation';
import {
  getPassport,
  getVerifiedStamps,
  hasValidIssuanceAndExpiration
} from '../passport-weighted/helper';

export default class extends Validation {
  public id = 'passport-gated';
  public github = 'snapshot-labs';
  public version = '0.1.0';

  async validate(): Promise<boolean> {
    const requiredStamps = this.params.stamps;
    const passport: any = await getPassport(this.author);
    if (!passport) return false;
    if (!passport.stamps?.length || !requiredStamps?.length) return false;

    const verifiedStamps: any[] = await getVerifiedStamps(
      passport,
      this.author,
      requiredStamps.map((stamp) => ({
        id: stamp
      }))
    );
    if (!verifiedStamps.length) return false;

    const provider = snapshot.utils.getProvider(this.network);
    const proposalTs = (await provider.getBlock(this.snapshot)).timestamp;

    const operator = this.params.operator;

    // check issuance and expiration
    const validStamps = verifiedStamps
      .filter((stamp) =>
        hasValidIssuanceAndExpiration(stamp.credential, proposalTs)
      )
      .map((stamp) => stamp.provider);

    if (operator === 'AND') {
      return requiredStamps.every((stamp) => validStamps.includes(stamp));
    } else if (operator === 'OR') {
      return requiredStamps.some((stamp) => validStamps.includes(stamp));
    } else {
      return false;
    }
  }
}

Voting validation can be specified in your space settings at https://snapshot.page/#/<SPACE ADDRESS>/settings.

Create a custom validation

The possibilities are endless! You can build a custom validation strategy for your space. Please have a look at Create a validation strategyfor more details.

Find more voting validations here:

If you are using only a Voting Strategy for your space you are required to use a for Voting to protect your space from spam votes.

ticket
Gitcoin Passport Validation
our article
ticket strategy
Use current setup and define a strong threshold to avoid spam in your space.
Use a custom setup using various Voting Strategies to calculate if a user is eligible to create a proposal or vote.
https://github.com/snapshot-labs/snapshot-strategies/tree/master/src/validations
https://github.com/snapshot-labs/snapshot-strategies/blob/master/src/validations/passport-gated/index.ts
import snapshot from '@snapshot-labs/snapshot.js';
import { customFetch } from '../../utils';

import STAMPS from './stampsMetadata.json';
import Validation from '../validation';

// Create one from https://scorer.gitcoin.co/#/dashboard/api-keys
const API_KEY = process.env.PASSPORT_API_KEY || '';
const SCORER_ID = process.env.PASSPORT_SCORER_ID || '';

const headers = API_KEY
  ? {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY
    }
  : undefined;

// const GET_STAMPS_METADATA_URI = `https://api.scorer.gitcoin.co/registry/stamp-metadata`;
const GET_PASSPORT_STAMPS_URI = `https://api.scorer.gitcoin.co/registry/stamps/`;
const GET_PASSPORT_SCORE_URI = `https://api.scorer.gitcoin.co/registry/score/${SCORER_ID}/`;
const POST_SUBMIT_PASSPORT_URI = `https://api.scorer.gitcoin.co/registry/submit-passport`;

const PASSPORT_SCORER_MAX_ATTEMPTS = 2;

const stampCredentials = STAMPS.map((stamp) => {
  return {
    id: stamp.id,
    name: stamp.name,
    description: stamp.description,
    credentials: stamp.groups
      .flatMap((group) => group.stamps)
      .map((credential) => credential.name)
  };
});

// Useful to get stamp metadata and update `stampsMetata.json`
// console.log('stampCredentials', JSON.stringify(stampCredentials.map((s) => ({"const": s.id, title: s.name}))));

function hasValidIssuanceAndExpiration(credential: any, proposalTs: string) {
  const issuanceDate = Number(
    new Date(credential.issuanceDate).getTime() / 1000
  ).toFixed(0);
  const expirationDate = Number(
    new Date(credential.expirationDate).getTime() / 1000
  ).toFixed(0);
  if (issuanceDate <= proposalTs && expirationDate >= proposalTs) {
    return true;
  }
  return false;
}

function hasStampCredential(stampId: string, credentials: Array<string>) {
  const stamp = stampCredentials.find((stamp) => stamp.id === stampId);
  if (!stamp) {
    console.log('[passport] Stamp not supported', stampId);
    throw new Error('Stamp not supported');
  }
  return credentials.some((credential) =>
    stamp.credentials.includes(credential)
  );
}

async function validateStamps(
  currentAddress: string,
  operator: string,
  proposalTs: string,
  requiredStamps: Array<string> = []
): Promise<boolean> {
  if (requiredStamps.length === 0) return true;

  const stampsResponse = await customFetch(
    GET_PASSPORT_STAMPS_URI + currentAddress,
    {
      headers
    }
  );
  const stampsData = await stampsResponse.json();

  if (!stampsData?.items) {
    console.log('[passport] Stamps Unknown error', stampsData);
    throw new Error('Unknown error');
  }
  if (stampsData.items.length === 0) return false;

  // check expiration for all stamps
  const validStamps = stampsData.items
    .filter((stamp: any) =>
      hasValidIssuanceAndExpiration(stamp.credential, proposalTs)
    )
    .map((stamp: any) => stamp.credential.credentialSubject.provider);

  if (operator === 'AND') {
    return requiredStamps.every((stampId) =>
      hasStampCredential(stampId, validStamps)
    );
  } else if (operator === 'OR') {
    return requiredStamps.some((stampId) =>
      hasStampCredential(stampId, validStamps)
    );
  }
  return false;
}

function evalPassportScore(scoreData: any, minimumThreshold = 0): boolean {
  // scoreData.evidence?.type === 'ThresholdScoreCheck' -> Returned if using Boolean Unique Humanity Scorer (should not be used)
  if (scoreData.evidence?.type === 'ThresholdScoreCheck') {
    return (
      Number(scoreData.evidence.rawScore) > Number(scoreData.evidence.threshold)
    );
  }
  // scoreData.score -> Returned if using Unique Humanity Score
  return Number(scoreData.score) >= minimumThreshold;
}

async function validatePassportScore(
  currentAddress: string,
  scoreThreshold: number
): Promise<boolean> {
  // always hit the /submit-passport endpoint to get the latest passport score
  const submittedPassport = await customFetch(POST_SUBMIT_PASSPORT_URI, {
    headers,
    method: 'POST',
    body: JSON.stringify({ address: currentAddress, scorer_id: SCORER_ID })
  });
  const submissionData =
    submittedPassport.ok && (await submittedPassport.json());

  if (!submittedPassport.ok) {
    const reason = !SCORER_ID
      ? 'SCORER_ID missing'
      : submittedPassport.statusText;
    console.log('[passport] Scorer error', reason);
    throw new Error(`Scorer error: ${reason}`);
  }

  // Scorer done calculating passport score during submission
  if (submittedPassport.ok && submissionData.status === 'DONE') {
    return evalPassportScore(submissionData, scoreThreshold);
  }

  // Try to fetch Passport Score if still processing (submittedPassport.status === 'PROCESSING')
  for (let i = 0; i < PASSPORT_SCORER_MAX_ATTEMPTS; i++) {
    const scoreResponse = await customFetch(
      GET_PASSPORT_SCORE_URI + currentAddress,
      {
        headers
      }
    );
    const scoreData = await scoreResponse.json();

    if (scoreResponse.ok && scoreData.status === 'DONE') {
      return evalPassportScore(scoreData, scoreThreshold);
    }
    console.log(
      `[passport] Waiting for scorer... (${i}/${PASSPORT_SCORER_MAX_ATTEMPTS})`
    );
    await snapshot.utils.sleep(3e3);
  }
  const reason =
    'Failed to fetch Passport Score. Reached PASSPORT_SCORER_MAX_ATTEMPTS';
  console.log('[passport] Scorer error', reason);
  throw new Error(`Scorer error: ${reason}`);
}

export default class extends Validation {
  public id = 'passport-gated';
  public github = 'snapshot-labs';
  public version = '1.0.0';
  public title = 'Gitcoin Passport Gated';
  public description =
    'Protect your proposals from spam and vote manipulation by requiring users to have a valid Gitcoin Passport.';

  async validate(currentAddress = this.author): Promise<boolean> {
    const requiredStamps = this.params.stamps || [];
    const operator = this.params.operator;
    const scoreThreshold = this.params.scoreThreshold || 0;

    if (requiredStamps.length > 0 && (!operator || operator === 'NONE'))
      throw new Error('Operator is required when selecting required stamps');

    const provider = snapshot.utils.getProvider(this.network);
    const proposalTs = (await provider.getBlock(this.snapshot)).timestamp;
    const validStamps = await validateStamps(
      currentAddress,
      operator,
      proposalTs,
      requiredStamps
    );

    if (scoreThreshold === 0) {
      return validStamps;
    }

    const validScore = await validatePassportScore(
      currentAddress,
      scoreThreshold
    );

    return validStamps && validScore;
  }
}