Direct Grants
In this tutorial, we’ll create a Direct Grants strategy that allows an admin to grant funds to a list of recipients. The main features are:
- Project Registration: Grantees register their projects.
- Application Listing: Admins list the submitted project applications.
- Application Approval: Admins review and approve each application.
- Token Allocation: Approved projects are displayed with an input field to enter the grant amount.
AlloKit makes it easy to build such strategies by providing smart contract extensions and pre-built React components.
AlloKit standardizes functions like allocate
across strategies, which means
the same interface is used everywhere—making it straightforward for indexing.
Notice that many functions (like allocate
and register
) include a data
parameter that allows you to pass custom information. This parameter can be
used to add custom functionality that may be useful for your allocation logic
or for off-chain indexing.
Read more about the rationale behind this in the Strategy Contracts overview page.
You can copy and paste this tutorial in Cursor Composer to get started.
Smart contract
Understanding the Contract Structure
A DirectGrants strategy would extend the base Pool contract. While not included in the current codebase, here’s how you would create one following the same patterns as QuadraticFunding.
Creating a DirectGrants Contract
Create a new file at packages/hardhat/contracts/strategies/DirectGrants.sol
following the Pool-based architecture:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.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 DirectGrants 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);
}
// 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);
}
// Only admins can allocate grants
function allocate(address[] memory recipients, uint256[] memory amounts, address token, bytes[] memory data)
external override onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant {
require(token == config.allocationToken, "Must use allocation token");
_allocate(recipients, amounts, token, data);
}
// Validation: only approved projects can receive grants
function _beforeAllocate(address recipient, uint256 amount, address token, bytes memory data) internal override {
require(registrations[recipient].status == Status.approved, "Recipient must be approved");
}
}
Key Differences from QuadraticFunding
- Admin-Only Allocation: Only admins can allocate grants (vs. open donations in QF)
- Simpler Logic: Straightforward approval -> allocation workflow
Pool Configuration
The DirectGrants strategy uses the PoolConfig structure:
struct PoolConfig {
address owner; // Pool owner
address[] admins; // Additional admins who can review/allocate
address allocationToken; // Token used for grant allocations
address distributionToken; // Token used for distributions (often same as allocation)
uint256 maxAmount; // Maximum pool funding (0 = no limit)
uint64[] timestamps; // Important dates
string metadataURI; // Pool metadata
}
Registration and Review Process
- Registration: Projects call
register()
with their metadata - Review: Admins call
review()
to approve/reject applications - Allocation: Admins can allocate grants to approved projects
- Distribution: Pool can distribute funds directly to recipients
Deploying the contract
Open the deploy script at packages/hardhat/deploy/00_deploy_your_contract.ts
and add the new contract:
await deploy("DirectGrants", {
from: deployer,
args: [
"DirectGrants", // Pool name
"()", // Schema (empty for basic usage)
"ipfs metadata cid", // Pool metadata URI
],
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.
Register Project
Create a new page at app/project/register/page.tsx
. This page uses the RegistrationForm
component along with the useContracts
hook to access the deployed DirectGrants
contract.
import { useRouter } from "next/navigation";
import { useContracts } from "~/hooks/use-contracts";
import { RegistrationForm } from "~/components/registration/registration-form";
export default function RegisterProjectPage() {
const router = useRouter();
const { DirectGrants } = useContracts();
return (
<RegistrationForm
strategyAddress={DirectGrants?.address}
onSuccess={({ project }) => router.push(`/project/${project}`)}
/>
);
}
Visiting http://localhost:3000/projects/register should display the registration form. You can modify the form inputs by editing packages/allo-app/components/registration/registration-form.tsx
.
List Applications
Create a page at app/application/page.tsx
to list project applications that have not yet been approved.
import { useContracts } from "~/hooks/use-contracts";
import { ApplicationsList } from "~/components/registration/applications-list";
export default function ApplicationsPage() {
const { DirectGrants } = useContracts();
return (
<ApplicationsList
query={{
where: {
// Include applications for DirectGrants strategy
strategy_in: [DirectGrants?.address],
isApproved: false, // Only show unapproved applications
},
}}
/>
);
}
Opening http://localhost:3000/applications should display a list of unapproved applications with a button to approve them.
Browse Projects
Create a page at app/project/page.tsx
to display a grid of approved projects. Projects can be added to a cart that will persist across page navigation and reloads.
Note that both the ProjectsList
and ApplicationsList
components use the
same Grid
component with the useRegistrations
hook. This
shared functionality helps maintain consistency in how project data is fetched
and displayed. You can open these components to see how they are implemented.
import { useContracts } from "~/hooks/use-contracts";
import { ProjectsList } from "~/components/registration/projects-list";
export default function BrowseProjectsPage() {
const { DirectGrants } = useContracts();
return (
<ProjectsList
query={{
where: {
strategy_in: [DirectGrants?.address],
isApproved: true, // Only show projects that are approved
},
}}
/>
);
}
Navigating to http://localhost:3000/project should display a grid of approved projects.
Allocation Page
Next we want to allocate tokens to the projects we have added to our cart. Create a new page: app/checkout/page.tsx
.
The AllocationForm
component renders the Projects we have added to our cart and allows us to allocate tokens to them.
On local development, we deploy an ERC20Mock token to make it easy to test Allo Apps. We can use the MintTokens
component to mint tokens to the address of the wallet we’re connected with.
import { useContracts } from "~/hooks/use-contracts";
import { AllocationForm } from "~/components/allocation/allocation-form";
import { MintTokens } from "~/components/mint-tokens";
export default function CheckoutPage() {
const { DirectGrants, ERC20Mock } = useContracts();
return (
<div className="space-y-4">
<AllocationForm
strategyAddress={DirectGrants?.address}
tokenAddress={ERC20Mock?.address}
/>
<MintTokens tokenAddress={ERC20Mock?.address} />
</div>
);
}
Next Steps
After completing the basics, consider the following improvements:
- Access Control: The current implementation uses AccessControl for granular permissions on who can approve projects and allocate funds.
- Admin-Only Views: Limit the
ApplicationsPage
so that only admins can view the applications. - Pool Factory: Use the PoolFactory to deploy new DirectGrants instances dynamically.
- Advanced Features: Add features like milestones, reporting requirements, or multi-sig approvals.