Skip to Content
TutorialsDirect Grants

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

  1. Admin-Only Allocation: Only admins can allocate grants (vs. open donations in QF)
  2. 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

  1. Registration: Projects call register() with their metadata
  2. Review: Admins call review() to approve/reject applications
  3. Allocation: Admins can allocate grants to approved projects
  4. 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.
Last updated on