Skip to Content
TutorialsQuadratic Funding

Quadratic Funding

This tutorial builds on the Direct Grants tutorial. These are the changes and additions we will make:

  • Contract is deployed with a specified donation and matching token
  • Matching funds can be added that will be distributed to projects based on quadratic funding calculation
  • Donations can be made by anyone to projects with the donation token
  • Distribution of matching funds to projects (only by owner)
  • Add a Distribute page to add matching funds and distribute them to projects

It will have the following features:

  • Distribute Page: Add a page to distribute matching funds to projects.
  • Add Matching Funds: Add a way to add and withdraw matching funds from the strategy.
  • Calculate Matching Funds: Calculate the matching funds for each project based on the donation amount.
  • List Project Allocations: List the allocations for each project.
  • Distribute Matching Funds: Add a way to distribute matching funds to projects.

Smart contract

Understanding the Contract Structure

The QuadraticFunding strategy is located at packages/hardhat/contracts/strategies/QuadraticFunding.sol and extends the base Pool contract.

Key Features

The QuadraticFunding contract provides:

  • Access Control: Uses OpenZeppelin’s AccessControl for admin management
  • Token Validation: Ensures allocations use the correct tokens (allocation vs distribution)
  • Registration Management: Projects can register and be reviewed by admins
  • Allocation Control: Validates recipients are approved before allocation
  • Distribution Control: Only admins can distribute funds from the pool
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {Context} from "@openzeppelin/contracts/utils/Context.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {Pool, PoolConfig} from "../Pool.sol"; contract QuadraticFunding is Pool, Context, AccessControl, ReentrancyGuard { constructor(string memory _name, string memory _schema, string memory _metadataURI) Pool(_name, _schema, _metadataURI) {} function initialize(PoolConfig memory _config, bytes memory data) public override { super.initialize(_config, data); _grantRole(DEFAULT_ADMIN_ROLE, _config.owner); for (uint256 i = 0; i < _config.admins.length; i++) { _grantRole(DEFAULT_ADMIN_ROLE, _config.admins[i]); } } // Configure pool (admin only) function configure(PoolConfig memory _config) public override onlyRole(DEFAULT_ADMIN_ROLE) { super._configure(_config); _grantRole(DEFAULT_ADMIN_ROLE, _config.owner); for (uint256 i = 0; i < _config.admins.length; i++) { _grantRole(DEFAULT_ADMIN_ROLE, _config.admins[i]); } } // Anyone can register a project function register(address project, string memory _metadataURI, bytes memory data) external override { _register(project, _metadataURI, data); } // Only admins can review applications function review(address project, uint8 status, string memory _metadataURI, bytes memory data) external override onlyRole(DEFAULT_ADMIN_ROLE) { _review(project, status, _metadataURI, data); } // Allocate with token validation and reentrancy protection function allocate(address[] memory recipients, uint256[] memory amounts, address token, bytes[] memory data) external override nonReentrant { require( token == config.allocationToken || token == config.distributionToken, "Allocations to projects must be allocation or distribution token" ); _allocate(recipients, amounts, token, data); } // Only admins can distribute funds from the pool function distribute(address[] memory recipients, uint256[] memory amounts, address token, bytes[] memory data) external override onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { require(token == config.distributionToken, "Distributions must be distribution token"); _distribute(recipients, amounts, token, data); } // Validation before allocation function _beforeAllocate(address recipient, uint256 amount, address token, bytes memory data) internal override { require( recipient == address(this) || registrations[recipient].status == Status.approved, "Recipient is not approved" ); if (recipient == address(this)) { uint256 balance = IERC20(token).balanceOf(address(this)); require(config.maxAmount == 0 || amount + balance <= config.maxAmount, "Max amount reached"); } } // Validation before distribution function _beforeDistribute(address recipient, uint256 amount, address token, bytes memory data) internal override { uint256 balance = IERC20(token).balanceOf(address(this)); require(amount <= balance, "Amount exceeds balance"); require(registrations[recipient].status == Status.approved, "Recipient is not approved"); } }

Deploying the contract

Open the deploy script at packages/hardhat/deploy/00_deploy_your_contract.ts and add the new contract:

For the sake of simplicity, we will use the ERC20Mock token for both the donation and matching tokens. We can easily mint these tokens from the UI.

const mockToken = await hre.ethers.getContract<Contract>("ERC20Mock", deployer); const tokenAddress = await mockToken.getAddress(); await deploy("QuandraticFunding", { from: deployer, args: [ "0xYourAdminAddress", // Replace with your wallet address tokenAddress, // Donation token tokenAddress, // Matching token ], log: true, autoMine: true, });

Frontend

AlloKit provides React components and hooks to interact with your contract. We will create pages for project registration, listing applications, browsing approved projects, and allocating tokens.

Distribute Page

Create a new page at app/distribute/page.tsx.

import { calculateQuadraticMatching, getContributions } from "~/lib/quadratic"; export default function DistributePage() { const invalidate = useInvalidate(); const { QuandraticFunding, ERC20Mock } = useContracts(); const strategyAddress = QuandraticFunding?.address; const donationToken = ERC20Mock?.address; const matchingTokenAddress = ERC20Mock?.address; // Get all donations to projects const allocations = useAllocations({ where: { // Only fetch allocations for this strategy strategy_in: [strategyAddress], // Not any transfers to or from Strategy contract (fund / withdraw of matching) to_not_in: [strategyAddress], from_not_in: [strategyAddress], }, }); const donations = allocations.data?.items ?? []; const matchingToken = useToken(matchingTokenAddress, strategyAddress); const matchingFunds = matchingToken.data?.balance ?? BigInt(0); const matching = calculateQuadraticMatching(donations, matchingFunds); const distribute = useDistribute({ strategyAddress }); return ( <div className="space-y-6"> <div className="flex justify-end"> <DistributeButton strategyAddress={strategyAddress} tokenAddress={matchingTokenAddress} onSuccess={() => invalidate([matchingToken.queryKey, allocations.queryKey]) } /> </div> <MatchingFunds strategyAddress={strategyAddress} tokenAddress={tokenAddress} /> <AllocationsDistributions strategyAddress={strategyAddress} tokenAddress={tokenAddress} /> </div> ); }

Matching Funds

Add matching funds that will be used to distribute to projects.

Next Steps

After completing the basics, consider the following improvements:

  • Access Control: Replace the Ownable pattern with AccessControl to allow more granular permissions on who can approve projects and allocate funds.
  • Token Support: Handle different ERC20 tokens for donation and matching. Perhaps add a way to add approved tokens.
Last updated on