Разработка квадратичного финансирования (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) и верифицируется through 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 или нестандартная формула взвешивания.







