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
Create the Contract File
Create a new file at packages/hardhat/contracts/QuandraticFunding.sol
.
Donation and Matching Tokens
We want to allow donations of a specific token to be allocated to projects. We will then use these donations to calculate a quadratic funding amount for each project to be awarded from the matching pool.
As described in the Allocator documentation:
- Allocate - tokens going from sender to a recipient. We will use this for donations and funding the matching pool.
- Distribute - tokens going from strategy contract to a recipient. This is for distributing the matching funds to projects.
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { Allocator } from "../extensions/Allocator.sol";
import { Registry, IRegistry } from "../extensions/Registry.sol";
import { Strategy } from "../base/Strategy.sol";
contract QuadraticFunding is Strategy, Allocator, Registry, Ownable {
address public immutable donationToken;
address public immutable matchingToken;
constructor(
address owner,
address _donationToken,
address _matchingToken
) Ownable(owner) Strategy("QuadraticFunding") {
donationToken = _donationToken;
matchingToken = _matchingToken;
}
// Register is inherited from the Registry contract so we don't need to override it
// Owner can approve projects
function approve(
address project,
uint256 index,
string memory metadataURI,
bytes memory data
) public override onlyOwner {
super.approve(project, index, metadataURI, data);
}
// Override allocate function to check if tokens are either donation or matching
function _allocate(address to, uint256 amount, address token, bytes memory data) internal override nonReentrant {
if (to == address(this)) require(token == matchingToken, "Matching funds to strategy must be matching token");
if (to != address(this)) require(token == donationToken, "Allocations to projects must be donation token");
super._allocate(to, amount, token, data);
}
// Distribution of matching funds to projects (must be by owner)
function distribute(
address[] memory recipients,
uint256[] memory amounts,
address token,
bytes[] memory data
) public override onlyOwner {
require(token == matchingToken, "Must be matching token");
super.distribute(recipients, amounts, token, data);
}
}
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.