Building a Governance DAO on Celo - Part 1

Building a Governance DAO on Celo - Part 1 https://celo.academy/uploads/default/optimized/2X/7/72ebba11900d153bb50374a138e6aeb176718b62_2_1024x576.jpeg
none 0.0 0

Introduction

Hey there! This tutorial will delve into the creation of a decentralized autonomous organization (DAO) on the Celo blockchain. Part 1 of this tutorial will provide step-by-step guidance on creating the DAO with the help of smart contracts, and deploying the smart contract.

This tutorial is designed for developers who wish to gain practical experience in the web3 space on the Celo blockchain. we’ll be using Solidity, a programming language, to write the smart contract By the end of this tutorial , you will have a comprehensive understanding of building a DAO on Celo, enabling you to explore more advanced applications on the Celo blockchain.

Prerequisite

Before we dive into building the DAO, there are some prerequisites that you need to have:

  • Basic knowledge of Hardhat
  • Familiarity with the Solidity
  • A code editor

Requirements

  • Node js
  • Solidity
  • Hardhat

Writing the Smart Contracts

In this step, we will be creating our token and dao contracts respectively, To begin, let’s create a new file in the hardhat folder called Token.sol and we can add the following code

  • Token.sol

This contract creates a new ERC20 token called “Token” with the symbol “TOKEN” and mints 1,000,000 tokens based on the ERC20 standard extended from the OpenZeppelin library.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
 
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

// SPDX-License-Identifier: MIT specifies the license under which the code is released.

The pragma solidity ^0.8.17; statement specifies the version of the Solidity compiler to be used.

The import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; statement imports the ERC20.sol file from the OpenZeppelin library. This file contains the implementation of the ERC20 token standard.

contract Token is ERC20 {
   
    constructor() ERC20("Token", "TOKEN") {
        _mint(msg.sender, 1000000);
    }
 
}

The constructor() ERC20("Token", "TOKEN") { ... } is the constructor function for the Token contract. It is executed only once when the contract is deployed. Within the constructor:

The ERC20("Token", "TOKEN") calls the constructor of the ERC20 contract and sets the token name to Token and the symbol to TOKEN.

The _mint(msg.sender, 1000000) statement mints 1,000,000 tokens and assigns them to the address that deployed the contract (msg.sender).

Your final code should look like this

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
 
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
 
contract Token is ERC20 {
   
    constructor() ERC20("Token", "TOKEN") {
        _mint(msg.sender, 1000000);
    }
 
}

For the Dao contract, create a new file called Dao.sol

  • Dao.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
 
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
contract DAO {
    IERC20 public token;
    uint256 public proposalIndex;


    event winner(uint256 _index, bytes32 proposal, Vote winningVote);
    mapping(uint256 => Proposal) public proposals;

Here, we declared our Dao contract , the IERC20 public token is a public variable, representing an ERC20 token contract. It will be used to interact with the token within the DAO.

uint256 public proposalIndex is a public variable representing the index of the current proposal. It will be incremented for each new proposal added to the DAO.

event winner(uint256 _index, bytes32 proposal, Vote winningVote): emits the declaration of an event that will be triggered when a proposal is determined.

    constructor(address _token) {
        token = IERC20(_token);
    }

The constructor includes an address parameter _token, which signifies the address of an ERC20 token contract.

    struct Proposal {
        uint256 accept;
        uint256 reject;
        uint256 abstain;
        uint256 deadline;
        bytes32 title;
        mapping(address => bool) voted;
    }

The struct allows the DAO contract to manage and track the details of each proposal, including vote counts, deadlines, titles, and voter participation.

mapping(address => bool) voted maintains a record of whether an address has participated in voting for the proposal. It associates each voter’s address with a boolean value that signifies whether they have already cast their vote or not.

    enum Vote {
        accept,
        reject,
        abstain
    }

Here, we define an enum called Vote which has three possible values
accept, reject and abstain.

     modifier onlyTokenHolders() {
        require(
            token.balanceOf(msg.sender) > 0,
            "NOT_A_TOKEN_HOLDER"
        );
        _;
    } 

The modifier verifies that only addresses possessing a balance greater than zero are permitted to interact with other sections of the smart contract. Otherwise, it will display the error message “NOT_A_TOKEN_HOLDER.”

    function createProposal(bytes32 _proposal) public onlyTokenHolders returns(uint256) {
        Proposal storage proposal = proposals[proposalIndex];
       
        proposal.title = _proposal;
        proposal.deadline = block.timestamp + 1 days;
 
        proposalIndex++;
 
        return proposalIndex - 1;
    }

The createProposal function allows token holders to create proposals by providing a title and setting a deadline for the proposal. The proposals are stored in the mapping (proposals) we created earlier on.

    function voteOnProposal(uint256 _index, Vote _vote) public onlyTokenHolders {
        Proposal storage proposal = proposals[_index];
        require (proposal.deadline > block.timestamp, "Voting has ended");
        require(proposal.voted[msg.sender] == false, "you have voted");

        proposal.voted[msg.sender] = true;

        if (_vote == Vote.accept) {
            proposal.accept += token.balanceOf(msg.sender);
        } else if (_vote == Vote.reject) {
            proposal.reject += token.balanceOf(msg.sender);
        } else if (_vote == Vote.abstain) {
            proposal.abstain += token.balanceOf(msg.sender);
        }

    }

The voteOnProposal function enables token holders to cast their votes on a particular proposal within the smart contract. Voters have the option to select one of the three available votes: accept, reject, or abstain. The function then updates the respective vote count within the storage of the proposal.

        function executeProposal(uint256 _index)  public {
 
        Proposal storage proposal = proposals[_index];
 
        require(block.timestamp > proposal.deadline, "ACTIVE_PROPOSAL");
 
        if (proposal.accept >= proposal.reject) {
            if (proposal.accept >= proposal.abstain) {
                emit winner(_index, proposal.title, Vote.accept);
            } else {
                emit winner(_index, proposal.title, Vote.abstain);
            }
        }
        else {
            if (proposal.reject >= proposal.abstain){
                emit winner(_index, proposal.title, Vote.reject);
            } else{
                emit winner(_index, proposal.title, Vote.abstain);
            }
        }
    }
}

The executeProposal function allows the smart contract to execute a proposal after its deadline has passed. Once the winning vote is determined, the function emits an event winner with the proposal’s index, title, and the winning vote as parameters.

Now, your final code should be looking like this

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
 
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
contract DAO {
    IERC20 public token;
    uint256 public proposalIndex;

    event winner(uint256 _index, bytes32 proposal, Vote winningVote);
    mapping(uint256 => Proposal) public proposals;
    
 
    constructor(address _token) {
        token = IERC20(_token);
    }

    struct Proposal {
        uint256 accept;
        uint256 reject;
        uint256 abstain;
        uint256 deadline;
        bytes32 title;
        mapping(address => bool) voted;
    }
    enum Vote {
        accept,
        reject,
        abstain
    }

     modifier onlyTokenHolders() {
        require(
            token.balanceOf(msg.sender) > 0,
            "NOT_A_TOKEN_HOLDER"
        );
        _;
    }

    function createProposal(bytes32 _proposal) public onlyTokenHolders returns(uint256) {
        Proposal storage proposal = proposals[proposalIndex];
       
        proposal.title = _proposal;
        proposal.deadline = block.timestamp + 1 days;
 
        proposalIndex++;
 
        return proposalIndex - 1;
    }
    function voteOnProposal(uint256 _index, Vote _vote) public onlyTokenHolders {
        Proposal storage proposal = proposals[_index];
        require (proposal.deadline > block.timestamp, "Voting has ended");
        require(proposal.voted[msg.sender] == false, "you have voted");

        proposal.voted[msg.sender] = true;

        if (_vote == Vote.accept) {
            proposal.accept += token.balanceOf(msg.sender);
        } else if (_vote == Vote.reject) {
            proposal.reject += token.balanceOf(msg.sender);
        } else if (_vote == Vote.abstain) {
            proposal.abstain += token.balanceOf(msg.sender);
        }

    }

        function executeProposal(uint256 _index)  public {
 
        Proposal storage proposal = proposals[_index];
 
        require(block.timestamp > proposal.deadline, "ACTIVE_PROPOSAL");
 
        if (proposal.accept >= proposal.reject) {
            if (proposal.accept >= proposal.abstain) {
                emit winner(_index, proposal.title, Vote.accept);
            } else {
                emit winner(_index, proposal.title, Vote.abstain);
            }
        }
        else {
            if (proposal.reject >= proposal.abstain){
                emit winner(_index, proposal.title, Vote.reject);
            } else{
                emit winner(_index, proposal.title, Vote.abstain);
            }
        }
    }
}

Deployment

To deploy our smart contract, we need to download the celo extension wallet on your web browser.

Next, we need to fund our wallet by requesting tokens on celo alfajores faucet

You can now fund your wallet and deploy your contract using the celo plugin in the Remix IDE.

Conclusion

In this tutorial, we have created a decentralized autonomous organization (DAO) contract on the Celo blockchain. The DAO allows token holders to create proposals and vote on them. After a specified deadline, the winning vote is determined based on the highest number of accepted votes.

Author

Kyrian Chigozie is a Technical Writer with a passion for web3 technology. Connect with me on Twitter.

Reference

1 Like

I will do a review For you

@Kyrian I want to ask, is this tutorial using Remix IDE or Hardhat?

The tutorial is done using Hardhat

is this what you mean

Hardhat — Remix - Ethereum IDE 1 documentation?

but overall your writing is good, all you need to change is the Twitter link in the author section so it will look neater

@Kyrian It looks like you’ve made changes, you can move your tutorial to publish