Welcome to a world where funding for innovative projects is fluid and controlled by a community of token holders, not just a select few. This is the value proposition of a decentralized grants program. In this guide, you will build such a program using the ExecutorDAO protocol on the Stacks blockchain.
Key features of this project include:
Decentralized Governance: Anyone holding a membership-token can vote on grant proposals.
Open Proposal Submission: Anyone can propose a grant, encouraging a wide range of ideas and projects.
Smart Contract Automation: All aspects of the grants program, from proposal submission to voting and fund distribution, are automated through smart contracts, ensuring transparency and tamper-proof processes.
This Hack walks you through the basics of building a decentralized grants program. Over the course of this hack, you will deploy your own functioning grants program.
There are also optional challenges at the end to further stretch your skills.
Now it's time to hack. First, we'll cover the basics of the core functionalities of our grants program and look at 4 contracts. Let's dive in.
ExecutorDAO is a powerful and flexible protocol that allows for the creation of decentralized autonomous organizations (DAOs) with a high degree of modularity and customization. ExecutorDAO operates on three core tenets:
Proposals are smart contracts:
Proposals in ExecutorDAO are expressed as smart contracts, allowing for precise, logical descriptions of the operations, duties, and members of the DAO. In our case, each grant application is a proposal expressed as a smart contract.
The core executes, the extensions give form:
ExecutorDAO starts with a single core contract whose sole purpose is to execute proposals and keep a list of authorized extensions. Extensions are contracts that can be enabled or disabled by proposals and add specific features to the DAO - like proposing grants, voting on grants, distributing funds, and more.
Ownership control happens via sending context:
ExecutorDAO follows a single-address ownership model. The core contract is the de facto owner of external ownable contracts. This allows any proposal or extension to act upon it, like the membership-token we will build out in the sections below.
Start by setting up your development environment. We've prepared a repository that includes an initialized Clarinet project and a React frontend with some boilerplate code and all the required packages.
To clone the repository, open your terminal and run the following command:
Before creating your core contract, you need to create trait contracts that you'll be implementing for your grants program.
Traits in Clarity define a set of functions that a contract must implement. In this case, any contract that wants to be a proposal or an extension must implement the functions defined in the proposal-trait and extension-trait respectively.
In your project's directory, run the following command:
Terminal
$
clarinet contract new extension-trait && clarinet contract new proposal-trait
Now in your contracts, respectively, add the following code:
extension-trait.clar
(define-traitextension-trait
(
(callback(principal(buff34)) (responsebool uint))
)
)
proposal-trait.clar
(define-traitproposal-trait
(
(execute(principal) (responsebool uint))
)
)
Now that you've defined how your set of functions must be implemented, you can begin to create your core contract. First run the following command:
Terminal
$
clarinet contract new core
This will create a new contract in the contracts directory called core.clar.
Inside your core.clar contract, add the two trait contracts you've just created from the steps above:
Next, you need to define some basic error handling and variables for managing your contracts:
core.clar
(define-constantERR_UNAUTHORIZED (erru1000))
(define-constantERR_ALREADY_EXECUTED (erru1001))
(define-constantERR_INVALID_EXTENSION (erru1002))
(define-data-varexecutiveprincipaltx-sender)
(define-mapexecutedProposalsprincipal uint)
(define-mapextensionsprincipal bool)
These constants represent error codes that the contract can return. The variables store the executive principal (the owner of the grants program), a map of executed proposals, and a map of authorized extensions.
This function checks if the caller is authorized, inserts the proposal into the executedProposals map, and then calls the execute function of the proposal contract.
In this section, you will create your first extension, a non-transferable membership token, which will be used to grant voting rights on proposals. The token will be initially distributed to certain addresses during the bootstrapping process. However, new minting (distribution) and burning (removal) of tokens can be managed through proposals.
To create your membership token, navigate to your project's directory and run the following command:
Terminal
$
clarinet contract new membership-token
This will create a new contract in the contracts directory called membership-token.clar.
Let's walk through the key components of this contract.
These constants represent error codes that the contract can return. The variables store the token name, symbol, URI, and decimals. The define-fungible-token function is used to define our sGrant token.
The is-dao-or-extension, function is a private function that checks if the caller of a function is the core contract itself or an authorized extension:
These functions check if the caller is authorized and then mint or burn the specified amount of sGrant tokens. And as you can see, these functions must be executed either through an approved grant proposal or an enabled extension (more on this later).
The contract provides functions to get the token's name (get-name), symbol (get-symbol), decimals (get-decimals), balance (get-balance), total supply (get-total-supply), and URI (get-token-uri):
membership-token.clar
(define-read-only(get-name)
(ok(var-gettokenName))
)
(define-read-only(get-symbol)
(ok(var-gettokenSymbol))
)
(define-read-only(get-decimals)
(ok(var-gettokenDecimals))
)
(define-read-only(get-balance(whoprincipal))
(ok(ft-get-balancesGrant who))
)
(define-read-only(get-total-supply)
(ok(ft-get-supplysGrant))
)
(define-read-only(get-token-uri)
(ok(var-gettokenUri))
)
These functions return the corresponding information about the sGrant token.
These are the key components of the sGrant token contract. Understanding these will help you in managing the distribution and burning of tokens through proposals.
In this section, you will create your second extension, a proposal submission contract. This contract will allow anyone to propose a grant, which will then be voted on by the token holders.
To create your proposal submission contract, navigate to your project's directory and run the following command:
Terminal
$
clarinet contract new proposal-submission
This will create a new contract in the contracts directory called proposal-submission.clar.
Let's walk through the key components of this contract.
First, define a map to store the parameters of your contract:
proposal-submission.clar
(define-mapparameters (string-ascii34)uint)
Set the proposal-duration parameter to a default value. This value represents the duration of a proposal in blocks. For example, if a block is mined approximately every 10 minutes, a proposal-duration of 1440 would be approximately 10 days.
proposal-submission.clar
(map-setparameters"proposal-duration"u1440);; ~10 days based on a ~10 minute block time.
This function calls the add-proposal function of the proposal-voting contract, passing the proposal contract, the current block height as the start block height, the current block height plus the proposal-duration as the end block height, the sender as the proposer, and the title and description of the proposal.
In this section, you will create your third extension, a proposal voting contract. This contract will allow token holders to vote on the proposed grants.
To create your proposal voting contract, navigate to your project's directory and run the following command:
Terminal
$
clarinet contract new proposal-voting
This will create a new contract in the contracts directory called proposal-voting.clar.
Let's walk through the key components of this contract.
This function concludes a proposal. It first retrieves the proposal data and checks if the proposal has more votes for than against. It then asserts that the proposal has not already been concluded and that the current block height is greater than or equal to the end block height of the proposal. If these conditions are met, it sets the concluded and passed fields of the proposal data and prints an event. If the proposal passed, it also tries to execute the proposal. The function returns whether the proposal passed.
The following challenges are additional features you can implement to continue building and sharpening your skills.
starter
Initialize your grants program: Now that you have your core extension contracts, you can initialize the project. The way you do this is through the construct function you wrote inside your core.clar contract. Create your first proposal enabling your extensions (membership-token, proposal-submission, proposal-voting) and distribute the initial token allocation to addresses responsible for voting on grants. If you need a little more guidance, check out the example here.
starter
Create grant proposals: After initializing your grants program, the next step is to create grant proposals. This involves using the propose function in the proposal-submission contract. This function allows anyone to propose a grant, which will then be voted on by the token holders. The proposal includes details such as the title, description, and the proposal contract. Once a proposal is submitted, it can be voted on during the voting period defined by the proposal-duration parameter.
intermediate
Implement milestone-based funding: To implement milestone-based funding, you'll need to create a new extension contract that tracks the progress of each grant proposal. This extension will manage the milestones for each grant, allowing funds to be released as each milestone is achieved. To enable this extension, you'll need to create a proposal using the propose function in the proposal-submission contract. Once enabled, the milestone-based funding extension will provide a more structured and accountable way to distribute funds, ensuring that the grant recipients are making progress before they receive their next round of funding.
advanced
UI integration: Using the provided starter template, integrate your contracts using Stacks.js. This will allow users to submit proposals, vote on them, and view the status of their proposals directly from the UI.