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.