Quadratic Funding System Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Quadratic Funding System Development
Complex
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Developing quadratic funding system

Quadratic funding is a matching fund distribution mechanism that mathematically amplifies broad popular support over concentrated donations. Invented by Glen Weyl and Vitalik Buterin. Gitcoin Grants uses it to distribute $50M+ among Web3 public goods. Technical implementation is non-trivial: you need to collect individual contributions, calculate square roots, aggregate, and distribute matching — all with Sybil attack protection.

Mathematics: how QF works

Classic formula for matching sum of a project:

matching = (Σ √contributionᵢ)² - Σ contributionᵢ

Where the sum is over all donors to the project. Important consequence: 100 donations of $1 give more matching than one donation of $100.

Example:

Project Donations Sum Matching calc Matching
A 1 × $100 $100 (√100)² - 100 = 100 - 100 = 0 $0
B 100 × $1 $100 (100 × √1)² - 100 = 10000 - 100 $9900
C 10 × $10 $100 (10 × √10)² - 100 ≈ 10000 - 100 ~$900

Final matching is normalized by matching pool: if total matching > pool, all sums are proportionally decreased.

System architecture

QF system consists of four components:

1. Grant Registry: stores project list with metadata
2. Round Contract: manages round — donation period, matching pool
3. Donation Collector: accepts contributions, emits events
4. Distribution Engine: calculates matching and makes payouts

Matching calculation is done off-chain (too expensive on-chain) and verified through merkle proof or ZK proof before payout.

Smart contracts

Round Controller

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract QFRound is Ownable {
    using SafeERC20 for IERC20;

    IERC20 public immutable donationToken;  // USDC or DAI
    uint256 public immutable matchingPool;  // fixed at round creation
    uint256 public immutable roundStart;
    uint256 public immutable roundEnd;

    // project ID => donor => amount
    mapping(uint256 => mapping(address => uint256)) public donations;
    // project ID => total donations
    mapping(uint256 => uint256) public projectTotalDonations;
    // project ID => list of donors (for off-chain calculation)
    mapping(uint256 => address[]) public projectDonors;

    bytes32 public matchingMerkleRoot;  // set after round end
    mapping(uint256 => bool) public matchingClaimed;

    event DonationMade(
        uint256 indexed projectId,
        address indexed donor,
        uint256 amount
    );
    event MatchingDistributed(uint256 indexed projectId, uint256 amount);

    constructor(
        address _token,
        uint256 _matchingPool,
        uint256 _start,
        uint256 _end
    ) Ownable(msg.sender) {
        donationToken = IERC20(_token);
        matchingPool = _matchingPool;
        roundStart = _start;
        roundEnd = _end;
    }

    function donate(uint256 projectId, uint256 amount) external {
        require(block.timestamp >= roundStart, "Round not started");
        require(block.timestamp <= roundEnd, "Round ended");
        require(amount > 0, "Zero amount");

        // If first donation — add to donor list
        if (donations[projectId][msg.sender] == 0) {
            projectDonors[projectId].push(msg.sender);
        }

        donations[projectId][msg.sender] += amount;
        projectTotalDonations[projectId] += amount;

        donationToken.safeTransferFrom(msg.sender, address(this), amount);
        emit DonationMade(projectId, msg.sender, amount);
    }

    // Set after off-chain calculation
    function setMatchingRoot(bytes32 _root) external onlyOwner {
        require(block.timestamp > roundEnd, "Round not ended");
        matchingMerkleRoot = _root;
    }

    // Project claims matching via merkle proof
    function claimMatching(
        uint256 projectId,
        address recipient,
        uint256 matchingAmount,
        bytes32[] calldata proof
    ) external {
        require(!matchingClaimed[projectId], "Already claimed");
        require(matchingMerkleRoot != bytes32(0), "Root not set");

        bytes32 leaf = keccak256(bytes.concat(
            keccak256(abi.encode(projectId, recipient, matchingAmount))
        ));

        require(
            MerkleProof.verify(proof, matchingMerkleRoot, leaf),
            "Invalid proof"
        );

        matchingClaimed[projectId] = true;
        donationToken.safeTransfer(recipient, matchingAmount);

        emit MatchingDistributed(projectId, matchingAmount);
    }
}

Off-chain matching calculation

interface Donation {
  projectId: number;
  donor: string;
  amount: bigint;
}

interface ProjectMatching {
  projectId: number;
  recipient: string;
  matchingAmount: bigint;
}

function calculateQFMatching(
  donations: Donation[],
  matchingPool: bigint
): ProjectMatching[] {
  // Group donations by projects
  const projectDonations = new Map<number, Map<string, bigint>>();

  for (const d of donations) {
    if (!projectDonations.has(d.projectId)) {
      projectDonations.set(d.projectId, new Map());
    }
    const donors = projectDonations.get(d.projectId)!;
    donors.set(d.donor, (donors.get(d.donor) ?? 0n) + d.amount);
  }

  // Calculate QF score for each project
  const projectScores = new Map<number, bigint>();
  let totalScore = 0n;

  for (const [projectId, donors] of projectDonations) {
    // Σ √contributionᵢ with fixed-point arithmetic
    // Use 1e9 scale for accuracy with bigint
    const SCALE = 1_000_000_000n;
    let sumSqrt = 0n;

    for (const amount of donors.values()) {
      // Integer square root with scaling
      const scaledAmount = amount * SCALE * SCALE;
      const sqrt = isqrt(scaledAmount);
      sumSqrt += sqrt;
    }

    // QF score = (Σ √contributionᵢ)²
    const score = (sumSqrt * sumSqrt) / SCALE / SCALE;
    projectScores.set(projectId, score);
    totalScore += score;
  }

  if (totalScore === 0n) return [];

  // Normalize: matching = score / totalScore * matchingPool
  const result: ProjectMatching[] = [];
  for (const [projectId, score] of projectScores) {
    const matchingAmount = (score * matchingPool) / totalScore;
    result.push({
      projectId,
      recipient: getProjectRecipient(projectId),
      matchingAmount
    });
  }

  return result;
}

// Integer square root (Newton's method)
function isqrt(n: bigint): bigint {
  if (n < 0n) throw new Error("Negative input");
  if (n < 2n) return n;
  let x = n;
  let y = (x + 1n) / 2n;
  while (y < x) {
    x = y;
    y = (x + n / x) / 2n;
  }
  return x;
}

Sybil resistance

Quadratic funding without Sybil protection is just uniform distribution, only more expensive. Attacker creates 100 wallets, makes minimal donations from each and gets maximum matching.

Defense methods

Gitcoin Passport: decentralized identity score. User verifies via GitHub, Twitter, ENS, Lens, BrightID and other providers. Each stamp gives score. Participation threshold in QF: usually 15-20 points.

// Check Gitcoin Passport score before donation
async function checkPassportScore(address: string): Promise<boolean> {
  const response = await fetch(
    `https://api.scorer.gitcoin.co/registry/score/${SCORER_ID}/${address}`,
    { headers: { "X-API-Key": PASSPORT_API_KEY } }
  );
  const { score } = await response.json();
  return parseFloat(score) >= MINIMUM_PASSPORT_SCORE; // 15.0
}

Proof of Humanity / WorldID: biometric verification of unique person. WorldID uses ZK-proof to confirm "I'm a unique human" without revealing identity.

Connection-weighted QF (COCM): Gitcoin Grants 19+ algorithm. Accounts for donor connections in social graph. Donations from correlated wallet clusters get lower weight — reduces Sybil effectiveness even with verified accounts.

Pairwise coordination subsidy (PCS)

QF extension that reduces matching for donations from correlated donors:

adjustedMatching = originalMatching × (1 - correlationFactor)

If two donors frequently donate to same projects — their combined contribution gets penalized. This reduces coordinated group influence without complete exclusion.

Verifying calculations via ZK proof

Problem with merkle approach: operator could theoretically set wrong merkle root. ZK-proof allows verifying calculation correctness on-chain without revealing all data.

Uses Circom + SnarkJS to build ZK circuit proving:

  • All donations from specific commitment set
  • QF formula applied correctly
  • Final matching sums match merkle leaves

This is an actively developing area — Gitcoin Allo Protocol moves in this direction. For production 2024-2025 — merkle approach with trusted operator (DAO multisig) remains practical.

Integration with Gitcoin Allo Protocol

Gitcoin Allo Protocol v2 — open-source framework for capital allocation with QF strategies. Instead of building from scratch:

import { IAllo } from "@gitcoin/allo-v2/contracts/core/interfaces/IAllo.sol";

// Create QF pool through Allo
allo.createPool(
    profileId,           // Gitcoin registry profile
    QF_STRATEGY,         // address of QFVotingStrategy contract
    initData,            // round parameters
    token,               // USDC
    matchingAmount,      // matching pool
    metadata,            // IPFS metadata
    managers             // who manages round
);

Allo Protocol already has QF strategy, Sybil protection via Passport, and project interface. Custom implementation makes sense for specific requirements — like native token instead of stablecoins or non-standard weighting formula.