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

Create the Contract File

Create a new file at packages/hardhat/contracts/DirectGrants.sol.

Allocate Tokens

AlloKit includes an Allocator extension contract that handles transferring tokens and emitting events for indexing.

By using the same interface for allocate across all strategies, AlloKit ensures that this custom data is indexed in a consistent manner—no matter what strategy you’re using.

//SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { Strategy } from "./base/Strategy.sol"; import { Allocator } from "./extensions/Allocator.sol"; contract DirectGrants is Strategy, Allocator, Ownable { constructor(address owner) Ownable(owner) Strategy("DirectGrants") {} function allocate( address[] memory recipients, uint256[] memory amounts, address token, bytes[] memory data // Only the owner can give out grants (optional - we can also allow anyone to donate) ) public override onlyOwner { // Use the default implementation from Allocator super.allocate(recipients, amounts, token, data); } }

Register projects

We also need a way to list projects we want to give out grants to. There are many ways to do this, we could use EAS Attestations similar to what Optimism is doing with RetroFunding. However, to do that we would need to set up the EAS SDK, create schemas, and write functions to create attestations.

For now, we will just use the AlloKit Registry to register projects. The Registry extension provides functions to register and approve projects. When we extend this contract we get access to calling register and approve functions. If we wish to make any changes we can simply override them.

We explore using EAS as a Registry in the RetroFunding tutorial.

import { Registry } from "./extensions/Registry.sol"; // Add the Registry extension to the contract contract DirectGrants is Strategy, Allocator, Registry, Ownable { // This override is optional since we're not changing any logic. function register( address project, string memory metadataURI, bytes memory data ) public override { super.register(project, metadataURI, data); } function approve( address project, uint256 index, string memory metadataURI, bytes memory data // Only the owner can approve projects ) public override onlyOwner { super.approve(project, index, metadataURI, data); } // Same as before }

Enforcing Approved Projects Only

We can also add a check to make sure only approved projects can receive grants. The easiest way to do this is to override the internal _allocate function.

import { Registry, IRegistry } from "./extensions/Registry.sol"; contract DirectGrants is Strategy, Allocator, Registry, Ownable { // Same as before function _allocate( address to, uint256 amount, address token, bytes memory data ) internal override { require(projects[to][0].status == IRegistry.Status.approved, "Project not approved"); super._allocate(to, amount, token, data); } }

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: ["0xYourAdminAddress"], // Replace with your wallet address 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: Replace the Ownable pattern with AccessControl to allow more granular permissions on who can approve projects and allocate funds.
  • Admin-Only Views: Limit the ApplicationsPage so that only admins can view the applications.
  • EAS Registry: While this tutorial uses the simple AlloKit Registry for project registration, you might explore using EAS Attestations (or check out the RetroFunding tutorial).
Last updated on