Abstract

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
pragma solidity ^0.8.20;

interface IGrantRegistry {
  /**
   * @dev Thrown when the community name length is invalid (e.g., too short or too long).
   */
  error InvalidCommunityNameLength();

  /**
   * @dev Thrown when the caller is not the current grant manager.
   */
  error InvalidGrantManager();

  /**
   * @dev Thrown when a grant is already registered with the provided ID.
   */
  error GrantAlreadyRegistered();

  /**
   * @dev Thrown when attempting to add a grantee that is already present in the set.
   */
  error GranteeAlreadyAdded();

  /**
   * @dev Thrown when attempting to remove a grantee that is not found in the set.
   */
  error GranteeNotFound();

  /**
   * @dev Thrown when attempting to add or reference an invalid external link.
   */
  error InvalidExternalLink();

  /**
   * @dev Thrown when an invalid index is provided (e.g., out of bounds for an array).
   */
  error InvalidIndex();

  /**
   * @dev Thrown when a milestone date is invalid (e.g., earlier than the grant's start date).
   */
  error InvalidStartDate();

  /**
   * @dev Thrown when attempting to add a milestone date that is already present.
   */
  error MilestoneDateAlreadyAdded();

  /**
   * @dev Thrown when attempting to remove or reference a milestone date that is not found.
   */
  error MilestoneDateNotFound();

  /**
   * @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.
   */
  event GrantRegistered(
    bytes32 indexed grantId,
    uint256 indexed id,
    uint256 chainid,
    string indexed community,
    address grantManager
  );

  /**
   * @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.
   */
  event OwnershipTransferred(
    bytes32 indexed grantId,
    address indexed newGrantManager
  );

  /**
   * @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.
   */
  event GranteeAdded(bytes32 indexed grantId, address indexed grantee);

  /**
   * @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.
   */
  event GranteeRemoved(bytes32 indexed grantId, address indexed grantee);

  /**
   * @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.
   */
  event StartDateSet(bytes32 indexed grantId, uint256 startDate);

  /**
   * @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.
   */
  event MilestoneDateAdded(bytes32 indexed grantId, uint256 milestoneDate);

  /**
   * @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.
   */
  event MilestoneDateRemoved(bytes32 indexed grantId, uint256 milestoneDate);

  /**
   * @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.
   */
  event DisbursementAdded(
    bytes32 indexed grantId,
    uint256 milestoneDate,
    address indexed fundingToken,
    uint256 fundingAmount
  );

  /**
   * @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.
   */
  event DisbursementRemoved(bytes32 indexed grantId, uint256 milestoneDate);

  /**
   * @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.
   */
  event DisbursementMade(
    bytes32 indexed grantId,
    uint256 milestoneDate,
    bool isDisbursed
  );

  /**
   * @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.
   */
  event ExternalLinkAdded(bytes32 indexed grantId, string link);

  /**
   * @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.
   */
  event ExternalLinkRemoved(bytes32 indexed grantId, string link);

  /**
   * @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.
   */
  function registerGrant(
    uint256 id,
    uint256 chainid,
    string memory community,
    address grantManager
  ) external returns (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.
   */
  function transferOwnership(bytes32 grantId, address newGrantManager) 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.
   */
  function addGrantee(bytes32 grantId, address grantee) 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.
   */
  function removeGrantee(bytes32 grantId, address grantee) 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.
   */
  function setStartDate(bytes32 grantId, uint256 startDate) 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.
   */
  function addMilestoneDate(bytes32 grantId, uint256 milestoneDate) 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.
   */
  function removeMilestoneDate(bytes32 grantId, uint256 milestoneDate) 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.
   */
  function addDisbursement(
    bytes32 grantId,
    uint256 milestoneDate,
    address fundingToken,
    uint256 fundingAmount
  ) 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.
   */
  function removeDisbursement(bytes32 grantId, uint256 milestoneDate) 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.
   */
  function setDisbursementStatus(
    bytes32 grantId,
    uint256 milestoneDate,
    bool isDisbursed
  ) 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.
   */
  function addExternalLink(bytes32 grantId, string memory link) 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.
   */
  function removeExternalLink(bytes32 grantId, uint256 index) 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
   */
  function getGrant(bytes32 grantId) external view returns (Grant memory);

  /**
   * @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
   */
  function getGrantManager(bytes32 grantId) external view returns (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
   */
  function getGrantees(
    bytes32 grantId
  ) external view returns (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
   */
  function getMilestonesDates(
    bytes32 grantId
  ) external view returns (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
   */
  function getDisbursement(
    bytes32 grantId,
    uint256 milestoneDate
  ) external view returns (Disbursements memory);

  /**
   * @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
   */
  function getExternalLinks(
    bytes32 grantId
  ) external view returns (string[] memory);
}

When calling the registerGrant function:

  • The grantManager MUST 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 grantManager MUST be the current grant manager to make changes to the grant.

When adding a milestoneDate:

  • The milestoneDate MUST not exist in the milestonesDates set.

When editing disbursments:

  • The milestoneDate MUST 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:

  1. 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.

  2. 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.

  3. 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
pragma solidity ^0.8.20;

import { IGrantRegistry } from "./IGrantRegistry.sol";
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

contract GrantRegistry is IGrantRegistry {
  using EnumerableSet for EnumerableSet.AddressSet;
  using EnumerableSet for EnumerableSet.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}.
   */
  function registerGrant(
    uint256 id,
    uint256 chainid,
    string memory community,
    address grantManager
  ) external returns (bytes32) {
    bytes32 grantId = keccak256(
      abi.encodePacked(id, chainid, community, block.timestamp)
    );

    if (grantManager == address(0)) revert InvalidGrantManager();
    if (bytes(community).length == 0) revert InvalidCommunityNameLength();
    if (bytes(_grants[grantId].community).length > 0)
      revert GrantAlreadyRegistered();

    _grants[grantId] = Grant(id, chainid, community);
    _participants[grantId].grantManager = grantManager;

    emit GrantRegistered(grantId, id, chainid, community, grantManager);
    return grantId;
  }

  /**
   * @dev See {IGrantRegistry-transferOwnership}.
   */
  function transferOwnership(
    bytes32 grantId,
    address newGrantManager
  ) external {
    _requireManager(grantId);
    if (newGrantManager == address(0)) revert InvalidGrantManager();
    _participants[grantId].grantManager = newGrantManager;
    emit OwnershipTransferred(grantId, newGrantManager);
  }

  /**
   * @dev See {IGrantRegistry-addGrantee}.
   */
  function addGrantee(bytes32 grantId, address grantee) external {
    _requireManager(grantId);
    if (grantee == address(0)) revert InvalidGrantManager();
    bool success = _participants[grantId].grantees.add(grantee);
    if (!success) revert GranteeAlreadyAdded();
    emit GranteeAdded(grantId, grantee);
  }

  /**
   * @dev See {IGrantRegistry-removeGrantee}.
   */
  function removeGrantee(bytes32 grantId, address grantee) external {
    _requireManager(grantId);
    bool success = _participants[grantId].grantees.remove(grantee);
    if (!success) revert GranteeNotFound();
    emit GranteeRemoved(grantId, grantee);
  }

  /**
   * @dev See {IGrantRegistry-setStartDate}.
   */
  function setStartDate(bytes32 grantId, uint256 startDate) external {
    _requireManager(grantId);
    _milestones[grantId].startDate = startDate;
    emit StartDateSet(grantId, startDate);
  }

  /**
   * @dev See {IGrantRegistry-addMilestoneDate}.
   */
  function addMilestoneDate(bytes32 grantId, uint256 milestoneDate) external {
    _requireManager(grantId);
    bool success = _milestones[grantId].milestonesDates.add(milestoneDate);
    if (!success) revert MilestoneDateAlreadyAdded();
    emit MilestoneDateAdded(grantId, milestoneDate);
  }

  /**
   * @dev See {IGrantRegistry-removeMilestoneDate}.
   */
  function removeMilestoneDate(
    bytes32 grantId,
    uint256 milestoneDate
  ) external {
    _requireManager(grantId);
    bool success = _milestones[grantId].milestonesDates.remove(milestoneDate);
    if (!success) revert MilestoneDateNotFound();
    emit MilestoneDateRemoved(grantId, milestoneDate);
  }

  /**
   * @dev See {IGrantRegistry-addDisbursement}.
   */
  function addDisbursement(
    bytes32 grantId,
    uint256 milestoneDate,
    address fundingToken,
    uint256 fundingAmount
  ) external {
    _requireManager(grantId);
    _requireMilestoneDate(grantId, milestoneDate);
    _milestones[grantId].disbursements[milestoneDate] = Disbursements(
      fundingToken,
      fundingAmount,
      false
    );
    emit DisbursementAdded(grantId, milestoneDate, fundingToken, fundingAmount);
  }

  /**
   * @dev See {IGrantRegistry-removeDisbursement}.
   */
  function removeDisbursement(bytes32 grantId, uint256 milestoneDate) external {
    _requireManager(grantId);
    _requireMilestoneDate(grantId, milestoneDate);
    delete _milestones[grantId].disbursements[milestoneDate];
    emit DisbursementRemoved(grantId, milestoneDate);
  }

  /**
   * @dev See {IGrantRegistry-setDisbursementStatus}.
   */
  function setDisbursementStatus(
    bytes32 grantId,
    uint256 milestoneDate,
    bool isDisbursed
  ) external {
    _requireManager(grantId);
    _requireMilestoneDate(grantId, milestoneDate);
    _milestones[grantId].disbursements[milestoneDate].isDisbursed = isDisbursed;
    emit DisbursementMade(grantId, milestoneDate, isDisbursed);
  }

  /**
   * @dev See {IGrantRegistry-addExternalLink}.
   */
  function addExternalLink(bytes32 grantId, string memory link) external {
    _requireManager(grantId);
    if (bytes(link).length == 0) revert InvalidExternalLink();
    _externalLinks[grantId].push(link);
    emit ExternalLinkAdded(grantId, link);
  }

  /**
   * @dev See {IGrantRegistry-removeExternalLink}.
   */
  function removeExternalLink(bytes32 grantId, uint256 index) external {
    _requireManager(grantId);
    if (index >= _externalLinks[grantId].length) revert InvalidIndex();
    string memory link = _externalLinks[grantId][index];
    _externalLinks[grantId][index] = _externalLinks[grantId][
      _externalLinks[grantId].length - 1
    ];
    _externalLinks[grantId].pop();
    emit ExternalLinkRemoved(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(bytes32 grantId) internal view {
    if (msg.sender != _participants[grantId].grantManager)
      revert InvalidGrantManager();
  }

  /**
   * @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(
    bytes32 grantId,
    uint256 milestoneDate
  ) internal view {
    if (!_milestones[grantId].milestonesDates.contains(milestoneDate))
      revert MilestoneDateNotFound();
  }

  /**
   * @dev See {IGrantRegistry-getGrant}.
   */
  function getGrant(bytes32 grantId) external view returns (Grant memory) {
    return _grants[grantId];
  }

  /**
   * @dev See {IGrantRegistry-getGrantManager}.
   */
  function getGrantManager(bytes32 grantId) external view returns (address) {
    return _participants[grantId].grantManager;
  }

  /**
   * @dev See {IGrantRegistry-getGrantees}.
   */
  function getGrantees(
    bytes32 grantId
  ) external view returns (address[] memory) {
    return _participants[grantId].grantees.values();
  }

  /**
   * @dev See {IGrantRegistry-getMilestonesDates}.
   */
  function getMilestonesDates(
    bytes32 grantId
  ) external view returns (uint256, uint256[] memory) {
    return (
      _milestones[grantId].startDate,
      _milestones[grantId].milestonesDates.values()
    );
  }

  /**
   * @dev See {IGrantRegistry-getDisbursement}.
   */
  function getDisbursement(
    bytes32 grantId,
    uint256 milestoneDate
  ) external view returns (Disbursements memory) {
    return _milestones[grantId].disbursements[milestoneDate];
  }

  /**
   * @dev See {IGrantRegistry-getExternalLinks}.
   */
  function getExternalLinks(
    bytes32 grantId
  ) external view returns (string[] memory) {
    return _externalLinks[grantId];
  }
}

Key considerations for this implementation:

  1. 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.

  2. 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.

Security Considerations

No security concerns were found.

Copyright and related rights waived via CC0.