Розробка квадратичного фінансування (quadratic funding)
Quadratic funding — це механізм розподілу matching funds, який математично посилює широку народну підтримку над концентрованими донаціями. Придумали Глен Вейл та Віталік Бутерін. Gitcoin Grants використовує його для розподілу $50M+ серед публічних благ Web3. Реалізація технічно нетривіальна: потрібно збирати індивідуальні вклади, рахувати квадратні корені, агрегувати та розподіляти matching — все з захистом від Sybil атак.
Математика: як працює QF
Класична формула для matching суми проекту:
matching = (Σ √contributionᵢ)² - Σ contributionᵢ
Де сума береться по всім донаторам проекту. Важливе наслідок: 100 донацій по $1 дають більш matching, ніж одна донація $100.
Приклад:
| Проект | Донації | Сума | Matching розрахунок | 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 |
Фінальний matching нормалізується по matching pool: якщо total matching > pool, всі суми пропорційно зменшуються.
Архітектура системи
QF система складається з чотирьох компонентів:
1. Grant Registry: зберігає список проектів з метаданими
2. Round Contract: управляє раундом — період прийому донацій, matching pool
3. Donation Collector: приймає внески, емітить события
4. Distribution Engine: розраховує matching та виплачує
Розрахунок matching робиться off-chain (занадто дорого on-chain) та верифікується через merkle proof або ZK proof перед виплатою.
Смарт-контракти
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 або DAI
uint256 public immutable matchingPool; // фіксується при створенні раунду
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 => список донаторів (для off-chain розрахунку)
mapping(uint256 => address[]) public projectDonors;
bytes32 public matchingMerkleRoot; // встановлюється після 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 (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);
}
// Встановлюється після off-chain розрахунку
function setMatchingRoot(bytes32 _root) external onlyOwner {
require(block.timestamp > roundEnd, "Round not ended");
matchingMerkleRoot = _root;
}
// Проект клеймит свій matching через 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
interface Donation {
projectId: number;
donor: string;
amount: bigint;
}
interface ProjectMatching {
projectId: number;
recipient: string;
matchingAmount: bigint;
}
function calculateQFMatching(
donations: Donation[],
matchingPool: bigint
): ProjectMatching[] {
// Групуємо донації по проектам
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);
}
// Розраховуємо QF score для кожного проекту
const projectScores = new Map<number, bigint>();
let totalScore = 0n;
for (const [projectId, donors] of projectDonations) {
// Σ √contributionᵢ з fixed-point арифметикою
// Використовуємо 1e9 scale для точності при роботі з bigint
const SCALE = 1_000_000_000n;
let sumSqrt = 0n;
for (const amount of donors.values()) {
// Цілочисленний квадратний корінь з масштабуванням
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 [];
// Нормалізуємо: 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;
}
// Цілочисленний квадратний корінь (метод Ньютона)
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 без Sybil захисту — це просто рівномірний розподіл, тільки дороже. Зловмисник створює 100 кошельків, робить мінімальні донації з кожного та отримує максимальний matching.
Методи захисту
Gitcoin Passport: децентралізований identity score. Користувач верифікується через GitHub, Twitter, ENS, Lens, BrightID та інші провайдери. Кожен stamp дає score. Поріг для участі в QF: зазвичай 15-20 балів.
// Перевірка Gitcoin Passport score перед донацією
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: біометрична верифікація унікальної людини. WorldID використовує ZK-proof для підтвердження "я — унікальна людина" без розкриття особистості.
Connection-weighted QF (COCM): алгоритм Gitcoin Grants 19+. Враховує зв'язки між донаторами в соціальному графі. Пожертвування від кластерів пов'язаних кошельків отримують менший вес — це знижує ефективність Sybil атаки навіть при реальних верифікованих аккаунтах.
Pairwise coordination subsidy (PCS)
Розширення QF, яке знижує matching для пожертвувань від корельованих донаторів:
adjustedMatching = originalMatching × (1 - correlationFactor)
Якщо два донатори часто жертвують в одні й ті ж проекти — їх спільний вклад отримує штраф. Це знижує вплив координованих груп без повного виключення.
Верифікація розрахунків через ZK proof
Проблема merkle підходу: оператор теоретично може виставити неверний merkle root. ZK-proof дозволяє верифікувати коректність розрахунку on-chain без розкриття всіх даних.
Використовується Circom + SnarkJS для побудови ZK circuit, який доказує:
- Всі донації з певного набору commitment-ів
- Розрахунок QF формули застосований коректно
- Итогові matching суми відповідають merkle листям
Це активно розвивається область — Gitcoin Allo Protocol рухається в цьому напрямку. Для production 2024-2025 — merkle підхід з довіреним оператором (DAO multisig) залишається практичним варіантом.
Інтеграція з Gitcoin Allo Protocol
Gitcoin Allo Protocol v2 — open-source фреймворк для capital allocation з QF стратегіями. Замість написання з нуля:
import { IAllo } from "@gitcoin/allo-v2/contracts/core/interfaces/IAllo.sol";
// Створення QF пулу через Allo
allo.createPool(
profileId, // Gitcoin registry profile
QF_STRATEGY, // адрес QFVotingStrategy контракту
initData, // параметри раунду
token, // USDC
matchingAmount, // matching pool
metadata, // IPFS metadata
managers // хто управляє раундом
);
Allo Protocol уже має QF стратегію, Sybil захист через Passport та інтерфейс для проектів. Кастомна реалізація має смисл при специфічних вимогах — наприклад, нативний токен замість stablecoins або нестандартна формула взвешивання.







