NanoQF a minimal grants protocol on EAS

NanoQF

This document explores what a minimal protocol for grants could look like with the goals of being simple and easy to extend. It’s built on Safe and EAS.

The goals with NanoQF are:

  • attest projects and voters to be eligible to be included in round
  • ability to fund projects
  • ability to pool funds into a Safe
  • distribute funds to projects based on events to and from attested voters and projects
  • easy to query data

It’s guided by these questions:

  • What if each project is represented by any address
    • EOA, AA, Escrow, PaymentSplitter (the platform can decide how projects are created)
  • What if token transfers to addresses is the basis for voting rather than contract calls
    • CountingService filters approved voters and projects
  • What if Ethereum Attestation Service is used to approve projects and voters

Components

  • Round contract: pools matching funds and sets Merkle tree for distribution claims. This is a Safe Multisig.
  • Attestation service: approves projects valid for distribution of funds
  • Attestation service: approves voters to include in counting
  • Counting service: counts votes for each project, calculates distribution, and updates MerkleTree

The attestation and counting services are meant to be modular. Anyone can build these and share. See code examples further down.

Overview of Flow

  1. Organizers deploy a Round contract to pool matching funds.
  2. Supporters transfer funds to the Round contract.
  3. Creators have their projects attested. (can be re-used in other rounds)
  4. Voters have their addresses attested. (can be re-used in other rounds)
  5. Voting on projects takes place. (any token transfer to project address)
  6. Votes are calculated and a Merkle Tree of distribution claims is created.
  7. Projects claim their matching funds.

Key Features

  • Only one contract to deploy, making it more efficient than existing platforms.
  • Ethereum Attestation Service (EAS) is used to approve projects and voters. By using EAS, a variety of attestation methods can be utilized, such as Gitcoin Passport, Zuzalu, Stripe KYC, and others. This also acts as a project registry.
  • Voting can be done without calling any contracts by simply transferring tokens within the rounds’ start and end blocks.

Code Implementation

Here are code snippets for Vote Counting Service, Attestation Service, and the Round contract:

Vote Counting Service

The vote counting service does the following:

  • Get the attested projects and voters from specified attester services
  • Get all the voting token transfers within start and end block
  • Get the funding token balance of the Round contract
  • Calculate the quadratic distribution (or any other strategy)
  • Generate a Merkle Tree with distribution so project owners can claim their matching funds

This can theoretically run on either backend or frontend. Repl.it is also viable and simple.

const round = new Contract(roundAddress, roundABI);
const config = await round.config();

const fundingToken = new Contract(config.fundingToken, erc20ABI);
const votingToken = new Contract(config.votingToken, erc20ABI);

// Query EAS for attestations
const approvedVoters = await eas.query({
  schemaId: approvedVotersSchema,
  // voter with any of these attestations considered valid
  attester: { in: [passportAttester, stripeAttester, zupassAttester] },
});
const approvedProjects = await eas.query({
  schemaId: approvedProjectsSchema,
  attester: { in: [zupassAttester] },
});

// How much funding in the contract - used for qf calculation
const matchingFunds = await fundingToken.balanceOf(roundAddress);


votingToken.queryFilter(
    // Get all token transfers within the round time span
    votingToken.filters.Transfer(),
    config.startBlock,
    config.endBlock
  )
  .then((votes) =>
    votes.filter(
      (vote) =>
        // Vote must be cast to an approved project from an approved user
        approvedProjects.includes(vote.args.to) &&
        approvedVoters.includes(vote.args.from)
    )
  )
  .then(async (validVotes) =>
    // Calculate QF and create Merkle from the filtered votes
    createMerkleTree(calculateQuadratic(validVotes, matchingFunds))
  )
  .then((merkle) => 
    // Update Round contract with distribution Merkle
    round.setMerkle(merkle)
   );

function calculateQuadratic(votes, funds) {
  // Calculate distribution...
}
function createMerkleTree(values) {
  // https://github.com/OpenZeppelin/merkle-tree
  return StandardMerkleTree.of(values, ["address", "uint256"]);
}

Attestation Service

Attestation Services can be created and run by anyone. Here’s an example of an api endpoint that verifies a Zuzalu membership before creating an attestation.

function zupassAttestation(req, res) {
  await verifyZupassProof(req.body.proof);

  const attestation = await createAttestation(req.body);
  res.status(201).send({ attestation });
}

Round Contract

The round contract does the following:

  • Contain a pointer to metadata (use Ceramic so data can be updated?)
  • Configuration for funding and voting tokens
  • Timespan for when to count votes
  • Function to set merkle root by the owners
  • Function to claim matching funds by the project owners
contract Round is SafeMultisig {
  struct Config {
    string metadata; // title, description, ...
    address fundingToken;
    address votingToken;
    uint256 startBlock;
    uint256 endBlock;
  };
  bytes32 public root;
  Config public config;

  function initialize(Config _config) initializer {...}
  function setMerkle(_root) authorized public {...}
  function claim(bytes32 calldata proof, uint256 amount) {...}
}

Questions

Why this solution?

Only one contract with clear responsibilities: a multisig contract that holds and distribute funds. Safe multisig handles logic for authorizing.

Simple services that attest legitimacy of projects and voters as well as strategies for counting votes. These can easily be shared in a registry. Easy to query projects from EAS based on schemaId, attestationId or other params.

Take any list of addresses and create attestations to approve them for round. For example CSV, Excel, Airtable, …

By using Transfer events to count votes we can easily choose the action to count as vote. It’s trivial to query ERC721 or ERC1155 Transfer events and build new counting services. We could even query the chain for governance Vote or any other event.

If votes are ERC20 Transfers, how do we know what round the vote belongs to?

We don’t. The VoteCounting service will count all transfers to the project address coming from a valid voter as a vote.

If this is important, there are a few potential solutions:

  • Changing the approved voters list - The Round has an AttestationService just for this round. Each voter would need to get their address attested.
  • Changing the approved projects list - The Project has a unique address just for this Round.

How can the voters trust the Vote counter services?

Anyone can query the chain for donations and calculate the quadratic distribution and come up with the correct distribution of matching funds.

1 Like