This proposal introduces a Grant Registry contract intended for managing financial, research, or project-based grants that provide funding for projects across multiple blockchains. The contract standardizes the registration, management, and tracking of these grants by organizing data into distinct categories, enabling clear separation between immutable fields and mutable fields. It supports modular disbursement tracking and allows for external links to off-chain documentation. This registry emits lifecycle events, enabling external protocols to efficiently access grant data, which promotes transparency, interoperability, and enhanced insights into grant program performance.
Motivation
The Ethereum ecosystem currently lacks a standardized way to manage and track grants across different chains and programs, leading to inefficiencies and fragmentation. Each grant program has its own distinct interface, processes, and management mechanisms, which creates barriers for both funders and grantees. These issues hinder transparency, complicate the tracking of fund disbursements, and make it difficult to evaluate the overall effectiveness of grant programs across different networks.
The lack of interoperability between grant programs further exacerbates the problem, as projects and contributors often work across multiple blockchains. This makes it challenging to aggregate data, monitor milestones, and assess grantee performance in a consistent manner.
The Grant Registry contract solves these issues by introducing a unified standard that ensures all grants can be registered, tracked, and managed consistently, regardless of the underlying chain or program. This approach not only simplifies the lifecycle management of grants but also fosters better collaboration between communities, allowing for more competitiveness and better tracking of progress. Additionally, the standardization of data opens the door for more insightful analytics, enabling protocols to measure the impact of grants in a much more streamlined and transparent way.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Contract Interface
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.20;interfaceIGrantRegistry{/**
* @dev Thrown when the community name length is invalid (e.g., too short or too long).
*/errorInvalidCommunityNameLength();/**
* @dev Thrown when the caller is not the current grant manager.
*/errorInvalidGrantManager();/**
* @dev Thrown when a grant is already registered with the provided ID.
*/errorGrantAlreadyRegistered();/**
* @dev Thrown when attempting to add a grantee that is already present in the set.
*/errorGranteeAlreadyAdded();/**
* @dev Thrown when attempting to remove a grantee that is not found in the set.
*/errorGranteeNotFound();/**
* @dev Thrown when attempting to add or reference an invalid external link.
*/errorInvalidExternalLink();/**
* @dev Thrown when an invalid index is provided (e.g., out of bounds for an array).
*/errorInvalidIndex();/**
* @dev Thrown when a milestone date is invalid (e.g., earlier than the grant's start date).
*/errorInvalidStartDate();/**
* @dev Thrown when attempting to add a milestone date that is already present.
*/errorMilestoneDateAlreadyAdded();/**
* @dev Thrown when attempting to remove or reference a milestone date that is not found.
*/errorMilestoneDateNotFound();/**
* @dev Emitted when a new grant is registered.
* @param grantId The unique identifier for the grant.
* @param id The grant's unique numeric ID.
* @param chainid The chain ID where the grant is registered.
* @param community The name of the community that issued the grant.
* @param grantManager The address of the grant manager.
*/eventGrantRegistered(bytes32indexedgrantId,uint256indexedid,uint256chainid,stringindexedcommunity,addressgrantManager);/**
* @dev Emitted when the ownership of a grant is transferred.
* @param grantId The unique identifier of the grant.
* @param newGrantManager The address of the new grant manager.
*/eventOwnershipTransferred(bytes32indexedgrantId,addressindexednewGrantManager);/**
* @dev Emitted when a new grantee is added to the grant.
* @param grantId The unique identifier of the grant.
* @param grantee The address of the new grantee.
*/eventGranteeAdded(bytes32indexedgrantId,addressindexedgrantee);/**
* @dev Emitted when a grantee is removed from the grant.
* @param grantId The unique identifier of the grant.
* @param grantee The address of the removed grantee.
*/eventGranteeRemoved(bytes32indexedgrantId,addressindexedgrantee);/**
* @dev Emitted when the start date of a grant is set.
* @param grantId The unique identifier of the grant.
* @param startDate The timestamp representing the start date.
*/eventStartDateSet(bytes32indexedgrantId,uint256startDate);/**
* @dev Emitted when a new milestone date is added to the grant.
* @param grantId The unique identifier of the grant.
* @param milestoneDate The timestamp of the added milestone.
*/eventMilestoneDateAdded(bytes32indexedgrantId,uint256milestoneDate);/**
* @dev Emitted when a milestone date is removed from the grant.
* @param grantId The unique identifier of the grant.
* @param milestoneDate The timestamp of the removed milestone.
*/eventMilestoneDateRemoved(bytes32indexedgrantId,uint256milestoneDate);/**
* @dev Emitted when a disbursement is added to a milestone.
* @param grantId The unique identifier of the grant.
* @param milestoneDate The timestamp of the milestone.
* @param fundingToken The token used for the disbursement.
* @param fundingAmount The amount of the disbursement.
*/eventDisbursementAdded(bytes32indexedgrantId,uint256milestoneDate,addressindexedfundingToken,uint256fundingAmount);/**
* @dev Emitted when a disbursement is removed from a milestone.
* @param grantId The unique identifier of the grant.
* @param milestoneDate The timestamp of the milestone.
*/eventDisbursementRemoved(bytes32indexedgrantId,uint256milestoneDate);/**
* @dev Emitted when a disbursement status is updated.
* @param grantId The unique identifier of the grant.
* @param milestoneDate The timestamp of the milestone.
* @param isDisbursed Boolean indicating if the disbursement has been made.
*/eventDisbursementMade(bytes32indexedgrantId,uint256milestoneDate,boolisDisbursed);/**
* @dev Emitted when an external link is added to the grant.
* @param grantId The unique identifier of the grant.
* @param link The external URL added.
*/eventExternalLinkAdded(bytes32indexedgrantId,stringlink);/**
* @dev Emitted when an external link is removed from the grant.
* @param grantId The unique identifier of the grant.
* @param link The external URL removed.
*/eventExternalLinkRemoved(bytes32indexedgrantId,stringlink);/**
* @dev Registers a new grant with the provided details. `grantId` is generated by hashing the grant
* details and the current timestamp.
*
* Requirements:
*
* - The `grantManager` address must not be the zero address.
* - The `community` name must not be empty.
* - The grant must not already be registered.
*
* Emits a {GrantRegistered} event.
*
* @param id The unique identifier for the grant program.
* @param chainid The chain ID where the grant is being registered.
* @param community The name of the community or protocol issuing the grant.
* @param grantManager The address of the grant manager.
* @return The generated `grantId` as a bytes32 value.
*/functionregisterGrant(uint256id,uint256chainid,stringmemorycommunity,addressgrantManager)externalreturns(bytes32);/**
* @dev Transfers ownership of the grant to a new grant manager.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The `newGrantManager` address must not be the zero address.
*
* Emits an {OwnershipTransferred} event.
*
* @param grantId The unique identifier of the grant.
* @param newGrantManager The address of the new grant manager.
*/functiontransferOwnership(bytes32grantId,addressnewGrantManager)external;/**
* @dev Adds a new grantee to the grant.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The `grantee` address must not be the zero address.
*
* Emits a {GranteeAdded} event.
*
* @param grantId The unique identifier of the grant.
* @param grantee The address of the grantee to be added.
*/functionaddGrantee(bytes32grantId,addressgrantee)external;/**
* @dev Removes an existing grantee from the grant.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The `grantee` address must be present in the grant.
*
* Emits a {GranteeRemoved} event.
*
* @param grantId The unique identifier of the grant.
* @param grantee The address of the grantee to be removed.
*/functionremoveGrantee(bytes32grantId,addressgrantee)external;/**
* @dev Sets the start date for the grant.
*
* Requirements:
*
* - The caller must be the current grant manager.
*
* Emits a {StartDateSet} event.
*
* @param grantId The unique identifier of the grant.
* @param startDate The timestamp representing the start date.
*/functionsetStartDate(bytes32grantId,uint256startDate)external;/**
* @dev Adds a new milestone date for the grant.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The milestone date must not already exist in the grant.
*
* Emits a {MilestoneDateAdded} event.
*
* @param grantId The unique identifier of the grant.
* @param milestoneDate The timestamp representing the milestone date.
*/functionaddMilestoneDate(bytes32grantId,uint256milestoneDate)external;/**
* @dev Removes a milestone date from the grant.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The milestone date must exist in the grant.
*
* Emits a {MilestoneDateRemoved} event.
*
* @param grantId The unique identifier of the grant.
* @param milestoneDate The timestamp representing the milestone date to be removed.
*/functionremoveMilestoneDate(bytes32grantId,uint256milestoneDate)external;/**
* @dev Ovewrites a disbursement for a specific milestone.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The milestone date must exist in the grant.
*
* Emits a {DisbursementAdded} event.
*
* @param grantId The unique identifier of the grant.
* @param milestoneDate The milestone date associated with the disbursement.
* @param fundingToken The address of the token used for funding.
* @param fundingAmount The amount of tokens to be disbursed.
*/functionaddDisbursement(bytes32grantId,uint256milestoneDate,addressfundingToken,uint256fundingAmount)external;/**
* @dev Removes a disbursement for a specific milestone.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The milestone date must exist in the grant.
*
* Emits a {DisbursementRemoved} event.
*
* @param grantId The unique identifier of the grant.
* @param milestoneDate The milestone date associated with the disbursement.
*/functionremoveDisbursement(bytes32grantId,uint256milestoneDate)external;/**
* @dev Updates the disbursement status for a specific milestone.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The milestone date must exist in the grant.
*
* Emits a {DisbursementMade} event.
*
* @param grantId The unique identifier of the grant.
* @param milestoneDate The milestone date associated with the disbursement.
* @param isDisbursed A boolean value indicating if the disbursement has been made.
*/functionsetDisbursementStatus(bytes32grantId,uint256milestoneDate,boolisDisbursed)external;/**
* @dev Adds an external link related to the grant.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The link must not be empty.
*
* Emits an {ExternalLinkAdded} event.
*
* @param grantId The unique identifier of the grant.
* @param link The external URL to be added.
*/functionaddExternalLink(bytes32grantId,stringmemorylink)external;/**
* @dev Removes an external link associated with the grant.
*
* Requirements:
*
* - The caller must be the current grant manager.
* - The index must be within the bounds of the external links array.
*
* Emits an {ExternalLinkRemoved} event.
*
* @param grantId The unique identifier of the grant.
* @param index The index of the external link to be removed.
*/functionremoveExternalLink(bytes32grantId,uint256index)external;/**
* @dev Retrieves details of a specific grant by its ID
* @param grantId The unique identifier of the grant
* @return The `Grant` struct containing id, chainid, and community label
*/functiongetGrant(bytes32grantId)externalviewreturns(Grantmemory);/**
* @dev Retrieves the current grant manager for a specific grant
* @param grantId The unique identifier of the grant
* @return The address of the grant manager
*/functiongetGrantManager(bytes32grantId)externalviewreturns(address);/**
* @dev Retrieves the list of grantees associated with a specific grant
* @param grantId The unique identifier of the grant
* @return An array of addresses representing the grantees
*/functiongetGrantees(bytes32grantId)externalviewreturns(address[]memory);/**
* @dev Retrieves the start date and list of milestone dates for a specific grant
* @param grantId The unique identifier of the grant
* @return The start date and an array of milestone dates
*/functiongetMilestonesDates(bytes32grantId)externalviewreturns(uint256,uint256[]memory);/**
* @dev Retrieves the disbursement details for a specific milestone in a grant
* @param grantId The unique identifier of the grant
* @param milestoneDate The date of the milestone for which disbursement details are requested
* @return The `Disbursements` struct containing the token address, funding amount, and disbursement status
*/functiongetDisbursement(bytes32grantId,uint256milestoneDate)externalviewreturns(Disbursementsmemory);/**
* @dev Retrieves the list of external links associated with a specific grant
* @param grantId The unique identifier of the grant
* @return An array of strings representing the external links
*/functiongetExternalLinks(bytes32grantId)externalviewreturns(string[]memory);}
When calling the registerGrant function:
The grantManagerMUST submit a valid grantManager address that is not the zero address.
The community label MUST be a non-empty string.
The grant ID MUST be unique and not already registered in the system.
When editing overall grant details:
The grantManagerMUST be the current grant manager to make changes to the grant.
When adding a milestoneDate:
The milestoneDateMUST not exist in the milestonesDates set.
When editing disbursments:
The milestoneDateMUST be a valid milestone date associated with the grant.
When adding externalLinks:
The string MUST not be empty.
Rationale
The design of this Grant Registry Contract is driven by the need for a flexible and modular system that supports a wide range of grant programs across different chains. The rationale for the key design decisions is outlined below:
Separation of Fields: The division of fields into different categories, such as identification, grant data, and disbursement-related information, allows for a more efficient use of on-chain storage. Immutable fields like id, chainid, and community are kept separate from mutable fields, ensuring that core identification elements remain unchanged, while other aspects like milestones and participants can be updated throughout the grant lifecycle.
Modular Disbursement Handling: Not all grant programs will choose to perform disbursements on-chain. By allowing disbursements to be managed through external links, the contract remains modular and adaptable to different use cases. Programs that prefer to handle disbursements off-chain can still use the registry for status tracking, ensuring broad applicability across different ecosystems.
Dynamic Team Management: The participants structure uses EnumerableSet for grantees, allowing for team-based grants. This feature facilitates tracking of contributions and adjustments to the grant team over time, enabling more comprehensive reputation systems and transparency.
This design aims to create a scalable, efficient system that can evolve with the needs of different grant programs, while maintaining key benefits like transparency, modularity, and low gas usage.
Backwards Compatibility
No backward compatibility issues found.
Reference Implementation
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.20;import{IGrantRegistry}from"./IGrantRegistry.sol";import{EnumerableSet}from"@openzeppelin/contracts/utils/structs/EnumerableSet.sol";contractGrantRegistryisIGrantRegistry{usingEnumerableSetforEnumerableSet.AddressSet;usingEnumerableSetforEnumerableSet.UintSet;/**
* @dev Mapping to store the details of each grant, keyed by its unique grantId.
*/mapping(bytes32=>Grant)private_grants;/**
* @dev Stores information about the participants in each grant (manager and grantees), keyed by the grantId.
* This mapping allows tracking of the grant manager and the associated grantees for each grant.
*/mapping(bytes32=>Participants)private_participants;/**
* @dev Stores milestone-related data for each grant, keyed by the grantId.
* This includes the start date, milestone dates, and disbursements related to each milestone.
*/mapping(bytes32=>Milestones)private_milestones;/**
* @dev Stores external links related to each grant, such as proposal URLs or related documentation, keyed by grantId.
* External links provide references to off-chain information about the grant.
*/mapping(bytes32=>string[])private_externalLinks;/**
* @dev See {IGrantRegistry-registerGrant}.
*/functionregisterGrant(uint256id,uint256chainid,stringmemorycommunity,addressgrantManager)externalreturns(bytes32){bytes32grantId=keccak256(abi.encodePacked(id,chainid,community,block.timestamp));if(grantManager==address(0))revertInvalidGrantManager();if(bytes(community).length==0)revertInvalidCommunityNameLength();if(bytes(_grants[grantId].community).length>0)revertGrantAlreadyRegistered();_grants[grantId]=Grant(id,chainid,community);_participants[grantId].grantManager=grantManager;emitGrantRegistered(grantId,id,chainid,community,grantManager);returngrantId;}/**
* @dev See {IGrantRegistry-transferOwnership}.
*/functiontransferOwnership(bytes32grantId,addressnewGrantManager)external{_requireManager(grantId);if(newGrantManager==address(0))revertInvalidGrantManager();_participants[grantId].grantManager=newGrantManager;emitOwnershipTransferred(grantId,newGrantManager);}/**
* @dev See {IGrantRegistry-addGrantee}.
*/functionaddGrantee(bytes32grantId,addressgrantee)external{_requireManager(grantId);if(grantee==address(0))revertInvalidGrantManager();boolsuccess=_participants[grantId].grantees.add(grantee);if(!success)revertGranteeAlreadyAdded();emitGranteeAdded(grantId,grantee);}/**
* @dev See {IGrantRegistry-removeGrantee}.
*/functionremoveGrantee(bytes32grantId,addressgrantee)external{_requireManager(grantId);boolsuccess=_participants[grantId].grantees.remove(grantee);if(!success)revertGranteeNotFound();emitGranteeRemoved(grantId,grantee);}/**
* @dev See {IGrantRegistry-setStartDate}.
*/functionsetStartDate(bytes32grantId,uint256startDate)external{_requireManager(grantId);_milestones[grantId].startDate=startDate;emitStartDateSet(grantId,startDate);}/**
* @dev See {IGrantRegistry-addMilestoneDate}.
*/functionaddMilestoneDate(bytes32grantId,uint256milestoneDate)external{_requireManager(grantId);boolsuccess=_milestones[grantId].milestonesDates.add(milestoneDate);if(!success)revertMilestoneDateAlreadyAdded();emitMilestoneDateAdded(grantId,milestoneDate);}/**
* @dev See {IGrantRegistry-removeMilestoneDate}.
*/functionremoveMilestoneDate(bytes32grantId,uint256milestoneDate)external{_requireManager(grantId);boolsuccess=_milestones[grantId].milestonesDates.remove(milestoneDate);if(!success)revertMilestoneDateNotFound();emitMilestoneDateRemoved(grantId,milestoneDate);}/**
* @dev See {IGrantRegistry-addDisbursement}.
*/functionaddDisbursement(bytes32grantId,uint256milestoneDate,addressfundingToken,uint256fundingAmount)external{_requireManager(grantId);_requireMilestoneDate(grantId,milestoneDate);_milestones[grantId].disbursements[milestoneDate]=Disbursements(fundingToken,fundingAmount,false);emitDisbursementAdded(grantId,milestoneDate,fundingToken,fundingAmount);}/**
* @dev See {IGrantRegistry-removeDisbursement}.
*/functionremoveDisbursement(bytes32grantId,uint256milestoneDate)external{_requireManager(grantId);_requireMilestoneDate(grantId,milestoneDate);delete_milestones[grantId].disbursements[milestoneDate];emitDisbursementRemoved(grantId,milestoneDate);}/**
* @dev See {IGrantRegistry-setDisbursementStatus}.
*/functionsetDisbursementStatus(bytes32grantId,uint256milestoneDate,boolisDisbursed)external{_requireManager(grantId);_requireMilestoneDate(grantId,milestoneDate);_milestones[grantId].disbursements[milestoneDate].isDisbursed=isDisbursed;emitDisbursementMade(grantId,milestoneDate,isDisbursed);}/**
* @dev See {IGrantRegistry-addExternalLink}.
*/functionaddExternalLink(bytes32grantId,stringmemorylink)external{_requireManager(grantId);if(bytes(link).length==0)revertInvalidExternalLink();_externalLinks[grantId].push(link);emitExternalLinkAdded(grantId,link);}/**
* @dev See {IGrantRegistry-removeExternalLink}.
*/functionremoveExternalLink(bytes32grantId,uint256index)external{_requireManager(grantId);if(index>=_externalLinks[grantId].length)revertInvalidIndex();stringmemorylink=_externalLinks[grantId][index];_externalLinks[grantId][index]=_externalLinks[grantId][_externalLinks[grantId].length-1];_externalLinks[grantId].pop();emitExternalLinkRemoved(grantId,link);}/**
* @dev Ensures that the caller is the grant manager for the given grantId.
* Reverts with `InvalidGrantManager` if the caller is not the grant manager.
* @param grantId The unique identifier of the grant being checked.
*/function_requireManager(bytes32grantId)internalview{if(msg.sender!=_participants[grantId].grantManager)revertInvalidGrantManager();}/**
* @dev Ensures that the milestone date is present in the grant.
* Reverts with `MilestoneDateNotFound` if the milestone date is not present.
* @param grantId The unique identifier of the grant being checked.
* @param milestoneDate The milestone date being checked.
*/function_requireMilestoneDate(bytes32grantId,uint256milestoneDate)internalview{if(!_milestones[grantId].milestonesDates.contains(milestoneDate))revertMilestoneDateNotFound();}/**
* @dev See {IGrantRegistry-getGrant}.
*/functiongetGrant(bytes32grantId)externalviewreturns(Grantmemory){return_grants[grantId];}/**
* @dev See {IGrantRegistry-getGrantManager}.
*/functiongetGrantManager(bytes32grantId)externalviewreturns(address){return_participants[grantId].grantManager;}/**
* @dev See {IGrantRegistry-getGrantees}.
*/functiongetGrantees(bytes32grantId)externalviewreturns(address[]memory){return_participants[grantId].grantees.values();}/**
* @dev See {IGrantRegistry-getMilestonesDates}.
*/functiongetMilestonesDates(bytes32grantId)externalviewreturns(uint256,uint256[]memory){return(_milestones[grantId].startDate,_milestones[grantId].milestonesDates.values());}/**
* @dev See {IGrantRegistry-getDisbursement}.
*/functiongetDisbursement(bytes32grantId,uint256milestoneDate)externalviewreturns(Disbursementsmemory){return_milestones[grantId].disbursements[milestoneDate];}/**
* @dev See {IGrantRegistry-getExternalLinks}.
*/functiongetExternalLinks(bytes32grantId)externalviewreturns(string[]memory){return_externalLinks[grantId];}}
Key considerations for this implementation:
Gas Optimization: grantId utilizes immutable identification fields to minimize large gas consumption. This ensures that essential information is used with keccak256 efficiently, while the mutable data can be submitted or modified later as the project evolves without affecting the identification method.
Use of EnumerableSet: By leveraging EnumerableSet for managing participants and milestone dates, the contract allows for dynamic updates, such as team composition changes or new milestones. This approach offers flexibility without sacrificing the ability to efficiently track changes.