How To Create a DAO with Smart Contracts
Welcome to the second part of the on-chain DAO tutorial! In this practical guide, you will discover how to create a smart contract for an NFT DAO. If you haven't had a chance to explore the previous article, give it a read to ensure you're up to date with what we're doing.
Estimated time to follow along 20 mins - 25 mins
Prerequisites
In this walkthrough, you’ll implement an on-chain NFT DAO using the OpenZeppelin Governor contract. Be sure you understand how DAOs work and also how to upload NFT images to IPFS. If you don’t, you can check out the articles here and here.
Let's dive right in!
Tutorial: Creating a DAO
By building a DAO on-chain, you’ll learn how to:
Work with the Open Zeppelin Governor contract.
Deploy an NFT DAO smart contract.
Create and execute DAO proposals on Tally.
Step 1: Creating the starter project
Launch the Remix IDE on the Remix website here. Next, on the Remix interface in the top left corner, click on the file icon and create a new file. Here, we created two files: CoVNFTDAO.sol and CovGovernance.sol.
Step 2: Creating the DAO contracts
To implement the contracts, we’ll be making use of Open Zeppelin ERC721 and OpenZeppelin Governor contracts.
OpenZeppelin ERC721: The ERC721 is used as assets or governance tokens within a DAO.
OpenZeppelin Governor: This allows for decentralized governance by allowing token holders to propose, discuss, and vote on DAO-related decisions.
To achieve this, go to Open Zeppelin Wizard and create the contracts as shown.
Step 3: Implementing the functions
Let’s go through the contract.
In the first lines, we're setting the license for the code to MIT, which is a widely used open-source license. The pragma solidity statement specifies the version of the Solidity compiler to use. In this case, it's version 0.8.20.
Imports
ERC721.sol: This import brings in the ERC721 contract from the OpenZeppelin library, which is the standard for creating NFTs.
ERC721Enumerable.sol: This adds support for enumerating tokens within the ERC721 standard, allowing us to list and manage NFTs more efficiently.
ERC721URIStorage.sol: This enables the storage and retrieval of Uniform Resource Identifiers (URIs) associated with our NFTs, often used to link to our NFT metadata.
ERC721Pausable.sol: This provides the ability to pause and unpause certain functions within the ERC721 contract.
AccessControl.sol: This introduces access control features, allowing us to manage roles and permissions within the smart contract. It's particularly useful for defining who can perform specific actions
ERC721Burnable.sol: This adds the capability to burn (destroy) NFTs. In certain cases, burning tokens is often used to reduce the supply of NFTs.
EIP712.sol: This helps with signing and on-chain verification.
ERC721Votes.sol: This import extends the ERC721 standard to include voting capabilities. It allows NFT holders to vote on proposals, making it suitable for our DAO implementation.
Contract Declaration
The contract CovDAO is ERC721, ERC721Enumerable….
inherits its functionalities from the contracts we imported.
State Variables
keccak256("PAUSER_ROLE")
: This line defines a constant variablePAUSER_ROLE
as a hash of the string"PAUSER_ROLE"
. This role is used for pausing the contract.keccak256("MINTER_ROLE")
: Similar to the previous line, this one defines theMINTER_ROLE
for minting new tokens.keccak256("UPDATE_URI_ROLE")
: This line declares theUPDATE_URI_ROLE
for updating the URI of tokens.keccak256("BURN_ROLE")
: Similar to the above lines, this defines theBURN_ROLE
for burning tokens.uint256 private _nextTokenId
: This declares a privatevariable _nextTokenId
to keep track of the next token ID to be minted.
Functions
function pause()
: This function allows for pausing the contract and is accessible only to addresses with thePAUSER_ROLE
. When paused, the contract's functionality can be temporarily halted.function unpause()
: Similar to the pause function, this one allows unpausing the contract and is restricted to addresses with thePAUSER_ROLE
.function safeMint()
: This allows for minting a new token and associates it with the provided address and URI. It can only be called by addresses with theMINTER_ROLE
.function Update_uri()
: This allows for updating the URI associated with a specific token. It takes the tokenId of the token and the token URI as parameters.function burn_member()
. Theburn_member
function is used to burn (destroy) a specific token with the given tokenId. It can only be executed by addresses with theBURN_ROLE
.function _update()
: An internal override function that manages the updating of the token's information.function _increaseBalance()
: An internal override function that increases the balance of an address.function tokenURI()
: A public view function that returns the URI associated with a given tokenId.
Governance Contract
Imports
Governor.sol: This import in the core Governor contract from the OpenZeppelin Contracts library. It serves as the foundation for building governance mechanisms.
GovernorSettings.sol: This adds the
GovernorSettings
extension to the Governor contract. It introduces additional settings and configuration options for the governance process.GovernorCountingSimple.sol: This import includes the
GovernorCountingSimple
extension, which provides simple vote-counting mechanisms for the governance process.GovernorVotes.sol: This integrates the
GovernorVotes
extension, which enhances the governor with voting-related functionality, such as delegation and vote delegation checks.GovernorVotesQuorumFraction.sol: This brings in the
GovernorVotesQuorumFraction
extension, which introduces quorum fraction-related functionality to the governor, aiding in decision-making processes by determining the minimum participation required for a vote to be valid.
Functions
function votingDelay()
: This function retrieves and returns the voting delay. The voting delay is the amount of time that must pass after a proposal is created before it can be voted on.function votingPeriod()
: This function retrieves and returns the voting period. The voting period is the duration during which token holders can cast their votes on a proposal after the voting delay has passed.function quorum()
: This calculates and returns the quorum requirement for a specific block number. Quorum is the minimum number of votes or tokens that must be cast in favour of a proposal for it to be considered valid and passed.function proposalThreshold()
: The function retrieves and returns the proposal threshold. The proposal threshold is the minimum number of votes or tokens that a proposal must have in its favour to be considered for a vote. If a proposal doesn't meet this threshold, it may not progress to the voting stage.
Step 5: Compiling and deploying the smart contract
To compile our contract, navigate to the Compiler tab on the left and click Compile.
Once you've successfully compiled the contract, proceed to the Deploy tab to initiate the deployment. Select Injected Provider-Metamask from the available environments, and configure the constructor value to match your Metamask address.
Once the deployment is successful, you should have something similar to this.
We'll follow the same procedure we used for the DaoNft contract, but for the Governance contract, you'll provide the DaoNft contract address as the constructor argument, as demonstrated below.
Step 6: Minting the DAO NFT
We'll mint an NFT to act as a membership token. For the purposes of this guide, I will demonstrate minting the NFT to my own address. To mint the token, follow these steps:
Click on the deployed contract.
Select Safe Mint.
Paste your wallet address (you can easily copy it from your MetaMask).
Provide the token URI, which, in this case, corresponds to the NFT metadata hosted on IPFS.
Once the minting process is successful, you can view it on OpenSea.
Great! We've successfully minted the membership NFT. Now, let's proceed to create a proposal and conduct on-chain voting. For this part, we'll utilize Tally, and you can find more information about how to use it here.
Step 7: Registering our DAO on Tally
Go to Tally.xyz, and from there, click on Add a DAO.
Click on Next to initiate the creation of your DAO organization. In this step, you'll be required to provide details about your DAO.
Next, you should input the NFT contract and the Governor smart contract addresses, select the network type (for this guide, we're using Sepolia), and specify the block height. To find the block height, go to Sepolia scan and find the deployed contracts.
Governor Contract details
NFT Token Details
After successfully completing these steps, you should see something similar to what we have below.
Step 8: Creating a proposal and voting on-chain
Now, let's create a proposal for voting. Click on Create New Proposal, and you'll be presented with the following screen. Click Continue to proceed.
After clicking Continue, you can enter the name and description of your proposal and then click Continue again to proceed.
Proposal Description
You can add the actions you want to perform. For this guide, we'll choose custom action. Input the covDaoNft address as the target contract address, the function to call (in this case, the safemint
function), and the call data, including the to
address and token URI. Click Add Action and then click on Continue to proceed.
The interface should look similar to the one displayed below:
Now, let’s vote on the proposal. To vote, click on Vote onchain and click Continue. We should then have a page that shows the current vote. For this guide, it’s only one address that votes.
After the voting period, which was set for 5 minutes, has elapsed, you can proceed to execute the proposal if it has passed successfully.
And that’s it! You can check the execution here.
Conclusion
Congratulations on reaching the finish line! You've navigated through creating and interacting with an on-chain NFT DAO. You're now well-prepared to dive into the real-world applications of DAOs. Happy coding!