Create a Tool for DAOs to Manage their Voting and Proposal Processes On Celo

Create a Tool for DAOs to Manage their Voting and Proposal Processes On Celo https://celo.academy/uploads/default/optimized/2X/b/b4175187a20202b00a9ce55b416e269d8b98bca6_2_1024x576.png
none 0.0 0

Introduction

We know that DAOs have gained popularity in recent years due to their potential to offer this new model of organization that can operate in a decentralized, autonomous, and transparent manner. Though thrilling to see that organizations have the ability to thrive without central authorities, it’s very complex building and managing a DAO. There has to be a way to control the decision-making process, consideration and execution of proposals to ensure the progress of the DAO.

Background Knowledge

Platforms like snapshot.org and tally.xyz help on-chain decentralized organizations such as Investment DAO or DEX that use DAO proposals to make significant decisions, to manage their DAO process.

Requirements

Before we begin, make sure to have a package manager installed on your machine. yarn and npm are perfect managers.

Github Code

If you would like to see the implementation of this tutorial, check this github repo here

Project Flow

Let us look this flow that explains a typical DAO operates

The first step to consider when trying to manage a DAO is the Membership.
To become a member of most DAOs, you need to hold a certain number of tokens and be a participant or contribute to the DAO projects As a member, you have the power to vote on proposals and be a part of general decision making.

The next thing we should consider is the Governance. Governance facilitate the decision process. There are several models used for governance. One is token-weighted voting and it allows members vote on proposals based on the amount of tokens they hold. There is also the conviction voting which allows members to vote on proposals based on their level of conviction in a proposal.

A crucial aspect for managing a DAO is the Proposal Submission. Submissions can be of any kind such as requests for funding or a request for a change in policies. This can be done by creating and uploading proposal files that can be stored on decentralized storage platforms like IPFS. The files can be retrieved and all members of the DAO have access to them.

Another important step is Voting. Depending on the model the DAO chooses to use as discussed earlier, members can submit their vote on the proposals submitted. All proposals that have the required number of votes necessary for execution will be approved. Voting can be done on or off-chain.

Smart contract execution is usually the final step. As soon as a proposal is approved, the governance smart contract will execute the proposal automatically. The smart contract has access to all the necessary files stored on IPFS that is used to complete it’s execution.

Now that we have a good overview of DAO processes. Let’s build a tool to simulate this process.

Project Demo

Set-up

Smart Contract development

Start by setting up your project using Celo Composer. In this tutorial, we are using React for our front-end framework, Rainbowkit-celo for our web3 library and Hardhat for the smart-contract framework.

We continue by writing the governance contract. Navigate in this order on your IDE packages/hardhat/contracts. Create a new file called Governance.sol. Your smart contract can look like this

pragma solidity ^0.8.0;

contract Governance {
    struct Proposal {
        uint256 id;
        address creator;
        string title;
        string description;
        uint256 amount;
        uint256 deadline;
        bytes32 ipfsHash;
        uint256 yesVotes;
        uint256 noVotes;
        bool executed;
        bool disputed;
    }

    Proposal[] public proposals;

    mapping(address => uint256) public tokens;

    uint256 public totalTokens;

    constructor() {
        totalTokens = 1000;
        tokens[msg.sender] = totalTokens;
    }

    function submitProposal(string memory _title, string memory _description, uint256 _amount, uint256 _deadline, bytes32 _ipfsHash) public {
        require(tokens[msg.sender] > 0, "Must have tokens to submit a proposal");
        require(_amount <= address(this).balance, "Not enough funds in DAO to fulfill request");
        uint256 proposalId = proposals.length;
        proposals.push(Proposal(proposalId, msg.sender, _title, _description, _amount, _deadline, _ipfsHash, 0, 0, false, false));
        tokens[msg.sender] -= 1;
    }

    function vote(uint256 _proposalId, bool _inSupport) public {
        require(tokens[msg.sender] > 0, "Must have tokens to vote");
        require(proposals[_proposalId].deadline > block.timestamp, "Voting period has ended");
        require(!proposals[_proposalId].executed, "Proposal has already been executed");
        require(!proposals[_proposalId].disputed, "Proposal is currently in dispute");
        if (_inSupport) {
            proposals[_proposalId].yesVotes += tokens[msg.sender];
        } else {
            proposals[_proposalId].noVotes += tokens[msg.sender];
        }
        tokens[msg.sender] = 0;
    }

    function executeProposal(uint256 _proposalId) public {
        require(proposals[_proposalId].yesVotes > proposals[_proposalId].noVotes, "Proposal did not receive enough support");
        require(proposals[_proposalId].deadline < block.timestamp, "Voting period has not ended");
        require(!proposals[_proposalId].executed, "Proposal has already been executed");
        proposals[_proposalId].executed = true;
        payable(proposals[_proposalId].creator).transfer(proposals[_proposalId].amount);
    }

    function disputeProposal(uint256 _proposalId) public {
        require(!proposals[_proposalId].disputed, "Proposal is already in dispute");
        proposals[_proposalId].disputed = true;
        // Trigger dispute resolution process
    }

    function getProposal(uint256 _proposalId) public view returns (Proposal memory) {
        return proposals[_proposalId];
    }

    function getProposalFileHash(
        uint256 proposalId
    ) public view returns (bytes32) {
        return proposals[proposalId].ipfsHash;
    }
}

Note: Feel free to customize this smart contract. Some functions might have to be payable. For example, yours might only have executeProposal function if you wish to do submission and voting off-chain

From this smart contract, a member of the DAO can submit a proposal by creating a new proposal object that contains the following: title, description, proposal type, proposal amount (assuming they are requesting for funds), proposal deadline and the IPFS Hash. Members can vote for or against within the deadline. Proposal receives enough votes in favor and the proposal is executed automatically i.e funds are sent out to the recipient. If there is a dispute, a dispute resolution process is triggered.

Front-end

To have a feel of Celo Composer, you can run the react-app. Navigate to packages >> react-app. Run the following command

yarn
yarn dev

Here is what you will see

Let’s start by writing out the components. You can create a SideBar.tsx component. This can be called from our Layout.tsx component. In here , your code can look like this

import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect, useRef } from "react";
import AboutIcon from "../icons/AboutIcon";
import ExploreIcon from "../icons/ExploreIcon";
import SpacesIcon from "../icons/SpacesIcon";
import MobileSidebar from "./MobileSidebar";

const Sidebar: React.FC = ({}) => {
  const router = useRouter();
  const ref = useRef<HTMLDivElement>(null);

  return (
    <>
      <div className="h-screen flex justify-between shadow-sm scrollbar-change flex-col overflow-y-auto w-60 border-r border-r-[#545252] ml-0">
        <div className="h-full flex flex-col justify-between">
          <div>
            <div className="flex items-center justify-center mx-auto py-4">
              <Link
                href="/"
                className="flex items-center justify-between px-6 py-3 gap-5 bg-[#3F3F3F] rounded-full text-sm text-white mx-auto"
              >
                Create DAO
              </Link>
            </div>

            <div className="nav__inner mt-5">
              <div className="flex flex-col justify-between ">
                <Link
                  href="/explore"
                  className={`nav__link hover:bg-[#3F3F3F] transition-all ease-in duration-200 ${
                    router.asPath === "/explore" ? "bg-[#3F3F3F] active" : ""
                  } `}
                >
                  <div className="flex items-center gap-3 py-4 px-6 mx-auto border-b border-[#545252] text-sm relative">
                    <span>
                      <ExploreIcon />
                    </span>

                    <span className="nav_item">Explore</span>
                    <span className="right_border"></span>
                  </div>
                </Link>
                <Link
                  href="/spaces"
                  className={`nav__link hover:bg-[#3F3F3F] transition-all ease-in duration-200 border-b border-[#545252] ${
                    router.asPath === "/spaces" ||
                    router.pathname.startsWith("/spaces")
                      ? "bg-[#3F3F3F] active"
                      : ""
                  }`}
                >
                  <div className="flex items-center gap-3 py-4 px-6 mx-auto text-sm relative">
                    <span>
                      <SpacesIcon />
                    </span>

                    <span className="nav_item">DAOs</span>

                    <span className="right_border"></span>
                  </div>
                </Link>
              </div>
            </div>
          </div>

          <div className="about_btn hover:bg-[#3F3F3F] transition-all ease-in duration-200 ">
            <Link href="/spaces">
              <div className="flex items-center gap-3 py-4 px-6 mx-auto text-sm">
                <span>
                  <AboutIcon />
                </span>

                <span className="nav_item">About</span>
              </div>
            </Link>
          </div>
        </div>
      </div>
    </>
  );
};

export default Sidebar;

We need to write 3 forms as components that we will be referencing shortly. Taking the first one as the benchmark, create a FormComponents folder in the components folder. Inside the FormComponents folder, you can also create another folder called formsteps folder. Then create a file StepOne.js. Here is how your StepOne.js file could look like.

import React, { useState } from "react";

const StepOne = ({ handleClick, currentStep, steps, data, setData }) => {
  const addTag = (tag) => {
    let tags = data?.categories ? data?.categories.split(", ") : [];
    tags.push(tag);

    if (tag == "clear_all") {
      tags = [];
      setData({ ...data, categories: "" });
    }

    setData({ ...data, categories: tags.join(", ") });

    return false;
  };

  const upload = (e) => {
    const file = e.target.files[0];
    const reader = new FileReader();
    reader.readAsDataURL(file);

    const CLOUDINARY_URL = YOUR_URL;
    const CLOUDINARY_UPLOAD_PRESET = YOUR_PRESET;

    const formData = new FormData();
    formData.append("file", file);
    formData.append("upload_preset", CLOUDINARY_UPLOAD_PRESET);

    fetch(CLOUDINARY_URL, {
      method: "POST",
      body: formData,
    })
      .then((response) => response.json())
      .then((res) => {
        if (res.secure_url !== "") {
          console.log(res);
          setData({
            ...data,
            logo: res.secure_url,
          });
        }
      })
      .catch((err) => {
        console.error(err);
        setData({
          ...data,
          logo: YOUR_LOGO,
        });
      });
  };

  return (
    <>
      <div className="fade-in">
        {/* 1 0f 3 Create a DAO */}

        <form onSubmit={() => handleClick("next", data)}>
          <div className="space-y-7">
            <div className="w-full">
              <label className="text-sm text-gray-300">Name of DAO</label>
              <input
                type="text"
                required
                placeholder="Lets go on"
                onChange={(e) => setData({ ...data, name: e.target.value })}
                value={data?.name}
                className="bg-transparent border w-full text-sm h-12 border-[#545252] focus:outline-none  rounded-3xl p-3 text-[#000000]"
              />
            </div>

            <div className="w-full">
              <label className="text-sm text-gray-300 ">
                Description of DAO
              </label>
              <input
                onChange={(e) =>
                  setData({ ...data, description: e.target.value })
                }
                type="text"
                required
                value={data?.description}
                placeholder="Talk about your DAO. What is it about?"
                className="bg-transparent border w-full h-12 border-[#545252] text-sm rounded-3xl px-4 py-3 text-[#000000] focus:outline-none "
              />
            </div>

            <div>
              <div className="w-full mb-3">
                <label className="text-sm text-gray-300">Select category</label>
                <input
                  type="text"
                  onChange={(e) =>
                    setData({ ...data, category: e.target.value })
                  }
                  value={data?.categories ? data?.categories : "General"}
                  readOnly
                  placeholder="You can type in what category is your DAO?"
                  className="bg-transparent border w-full h-12 border-[#545252] text-sm rounded-3xl p-3 mb-1 focus:outline-none  "
                />
              </div>

              <div className="flex flex-wrap gap-2">
                <button
                  onClick={(e) => {
                    e.preventDefault();
                    addTag("Art");
                  }}
                  className="bg-lyt  py-3 px-6 text-xs rounded-full"
                >
                  Art
                </button>
                <button
                  onClick={(e) => {
                    e.preventDefault();
                    addTag("Protocol");
                  }}
                  className="bg-lyt  py-3 px-6 text-xs rounded-full"
                >
                  Protocol
                </button>
                <button
                  onClick={(e) => {
                    e.preventDefault();
                    addTag("Social");
                  }}
                  className="bg-lyt  py-3 px-6 text-xs rounded-full"
                >
                  Social
                </button>
                <button
                  onClick={(e) => {
                    e.preventDefault();
                    addTag("Investment");
                  }}
                  className="bg-lyt  py-3 px-6 text-xs rounded-full"
                >
                  Investment
                </button>
                <button
                  onClick={(e) => {
                    e.preventDefault();
                    addTag("Services");
                  }}
                  className="bg-lyt  py-3 px-6 text-xs rounded-full"
                >
                  Services
                </button>
                <button
                  onClick={(e) => {
                    e.preventDefault();
                    addTag("clear_all");
                  }}
                  className="bg text-red-700  py-3 px-6 text-xs rounded-full"
                >
                  Clear all x
                </button>
              </div>
            </div>

            <div>
              <h4 className="text-sm ">Your Logo (png,jpg,jpeg)</h4>
              <p className="text-sm text-gray-300 mb-4">
                This will be displayed on your profile.
              </p>
              <div className="flex mb-6">
                <div className="rounded-full mr-6 bg-lyt w-12 h-12 flex items-center justify-center">
                  <img
                    className="border rounded"
                    src={data.logo ? data.logo : "./gallery.svg"}
                  />
                </div>
                <div className="flex gap-3 items-center">
                  <input
                    type="file"
                    accept=".png, .jpg, .jpeg"
                    className="hidden"
                    id="logo"
                    onChange={upload}
                  />
                  <button
                    className="text-purple-800 text-sm py-1 px-4 bg-[#ffffff] rounded-full"
                    onClick={(e) => {
                      e.preventDefault();
                      document.getElementById("logo").click();
                    }}
                  >
                    Upload Logo
                  </button>
                </div>
              </div>
            </div>

            <div className="w-full">
              <label className="text-sm text-gray-300">Website</label>
              <input
                type="text"
                onChange={(e) => setData({ ...data, website: e.target.value })}
                value={data?.website ? data?.website : ""}
                required
                // placeholder="Paste URL of your website if any"
                className="bg-transparent border w-full h-12 border-[#545252]  rounded-3xl px-4 py-3 text-[#000000] text-sm focus:outline-none"
              />
            </div>

            <div className="w-full">
              <label className="text-sm text-gray-300">Terms of service</label>
              <input
                type="text"
                onChange={(e) => setData({ ...data, terms: e.target.value })}
                value={data?.terms ? data?.terms : ""}
                required
                placeholder="Paste URL of your terms of service if any"
                className="bg-transparent border w-full h-12 border-[#545252]  rounded-3xl px-4 py-3 text-[#000000] text-sm focus:outline-none "
              />
            </div>
          </div>

          <div className="border my-7 px-7 py-8 border-[#545252] rounded-md">
            <h4 className="mb-5">Add Socials</h4>
            <div className="space-y-6">
              <div>
                <label className="text-sm text-gray-300">Github</label>
                <br />
                <div className="mt-1 relative rounded-full flex-1 bg-transparent items-center grow flex h-14 w-full  ">
                  <div className="absolute inset-y-0 left-0 px-3 flex items-center pointer-events-none h-full">
                    <span className="text-gray-500 px-3 w-22 h-22">
                      <img src="/github.svg" className="w-full h-full" />
                    </span>
                  </div>
                  <input
                    placeholder="Paste URL"
                    onChange={(e) =>
                      setData({
                        ...data,
                        socials: { ...data.socials, github: e.target.value },
                      })
                    }
                    value={data?.socials ? data?.socials?.github : ""}
                    type="text"
                    name="github_address"
                    id="github_address"
                    className="  py-2 block w-full pl-16 pr-7  sm:text-sm rounded-full h-full focus:outline-none bg-transparent border border-[#545252] transition ease-in duration-200 text-[#000000]"
                  />
                </div>
              </div>

              <div>
                <label className="text-sm text-gray-300">Twitter</label>
                <br />
                <div className="mt-1 relative rounded-full flex-1 bg-transparent items-center grow flex h-14 w-full  ">
                  <div className="absolute inset-y-0 left-0 px-3 flex items-center pointer-events-none h-full">
                    <span className="text-gray-500 px-3 w-22 h-22">
                      <img src="/twitter.svg" className="w-full h-full" />
                    </span>
                  </div>
                  <input
                    placeholder="Paste URL"
                    type="text"
                    onChange={(e) =>
                      setData({
                        ...data,
                        socials: { ...data.socials, twitter: e.target.value },
                      })
                    }
                    value={data?.socials ? data?.socials?.twitter : ""}
                    name="twitter_address"
                    id="twitter_address"
                    className="  py-2 block w-full pl-16 pr-7  text-sm rounded-full h-full focus:outline-none bg-transparent border border-[#545252] transition ease-in duration-200 text-[#000000]"
                  />
                </div>
              </div>

              <div>
                <label className="text-sm text-gray-300">Discord</label>
                <div className="mt-1 relative rounded-full flex-1 bg-transparent items-center grow flex h-14 w-full  ">
                  <div className="absolute inset-y-0 left-0 px-3 flex items-center pointer-events-none h-full">
                    <span className="text-gray-500 px-3 w-22 h-22">
                      <img src="/discord.svg" className="w-full h-full" />
                    </span>
                  </div>
                  <input
                    placeholder="Paste URL"
                    type="text"
                    name="discord_address"
                    onChange={(e) =>
                      setData({
                        ...data,
                        socials: { ...data.socials, discord: e.target.value },
                      })
                    }
                    value={data?.socials ? data?.socials?.discord : ""}
                    id="discord_address"
                    className="  py-2 block w-full pl-16 pr-7  text-sm rounded-full h-full focus:outline-none bg-transparent border border-[#545252] transition ease-in duration-200 text-[#000000]"
                  />
                </div>
              </div>
            </div>
          </div>
          <button
            type="submit"
            className="button1 h-12 w-full m-auto rounded-3xl bg-black text-white"
          >
            {currentStep === steps.length - 1 ? "Confirm" : "Next"}
          </button>
        </form>
      </div>
    </>
  );
};

export default StepOne;

You can decide to tweak it to suit your preferences and other steps (StepTwo, StepThree) can follow this format as well.

Let’s also proceed to edit our index.tsx file. This can be found in the pages folder.

In here, we define several functions and state variables

const [data, setData] = useState<{
  admins: any[];
  authors: any[];
  socials: {
    twitter: string;
    discord: string;
    github: string;
  };
  settings: {
    erc20Balance: {
      symbol: string;
      address: string;
      decimals: number;
    };
  };
}>({
  admins: [],
  authors: [],
  socials: { twitter: "", discord: "", github: "" },
  settings: {
    erc20Balance: {
      symbol: "",
      address: "",
      decimals: 2,
    },
  },
});
const [currentStep, setCurrentStep] = useState(1);
const [user, signer, provider, setUser, login] = useSigner();
const [step, setStep] = useState(1);
const createDAO = async (): Promise<void> => {
  let user: any;
  try {
    user = await signer.login();
  } catch (error: any) {
    alert(error.message);
    return;
  }
  setUser(user);
  setData({
    ...data,
    controller: user?.address,
    admins: [],
    authors: [],
    public_key: user?.publicKey,
    socials: { twitter: "", discord: "", github: "" },
    settings: {
      erc20Balance: {
        symbol: "",
        address: "",
        decimals: 2,
      },
    },
  });

  setStartForm(true);
};

const [startForm, setStartForm] = useState(false);
const steps = [1, 2, 3];

const displayStep = (step: any) => {
  switch (step) {
    case 1:
      return (
        <StepOne
          key={1}
          handleClick={handleClick}
          currentStep={currentStep}
          steps={steps}
          data={data}
          setData={setData}
        />
      );
    case 2:
      return (
        <StepTwo
          key={2}
          handleClick={handleClick}
          currentStep={currentStep}
          steps={steps}
          data={data}
          setData={setData}
        />
      );
    case 3:
      return (
        <StepThree
          key={3}
          handleClick={handleClick}
          currentStep={currentStep}
          steps={steps}
          data={data}
          setData={setData}
          signer={signer}
        />
      );
    default:
  }
};

const [formData, setFormData] = useState({
  title: "",
  description: "",
  budget: "",
});

const handleClick = (direction: any, formData = {}) => {
  console.log("handle click: passed", formData);
  console.log("handle click: before", data);
  let newStep = currentStep;

  setData({ ...data, ...formData });

  console.log("handle click: after", data);

  direction === "next" ? newStep++ : newStep--;
  // check if steps are within bounds
  newStep > 0 && newStep <= steps.length && setCurrentStep(newStep);
};

The useState hook is used to define these state variables : data, currentStep, user, signer, provider, startForm, and formData.

The data state variable holds information about the DAO, such as the admins, authors, socials, and settings.

The currentStep state variable holds the current step of the form. There are 3 steps in the form.

The user, signer, and provider state variables are used to interact with the Ethereum blockchain.

The startForm state variable is used to determine whether the form should be displayed.

The formData state variable holds the values of the form fields.

The createDAO function is called when the user clicks a button to create the DAO. The function logs the user in using signer, sets the user state variable, updates the data state variable with the user’s address

The handleClick function is called when the user clicks a button to move to the next or previous step of the form.

The displayStep function returns a specific step of the form based on the currentStep state variable. The StepOne, StepTwo, and StepThree components are rendered depending on the value of currentStep.

Your div can now look similar to this

return (
  <>
    {!startForm && (
      <div className="mx-auto max-w-screen-lg mt-10">
        <div className=" pl-0 lg:pl-10">
          <h2 className="text-2xl font-bold my-4">Create a DAO</h2>
          <p className="text-sm font-thin text-black mb-4">
            Create your own DAO right away and begin making choices!
          </p>
          <div className="m-auto text-centers">
            <button
              className="button1 h-12 w-4/5 m-autos rounded-3xl bg-black text-white"
              onClick={createDAO}
            >
              Create DAO
            </button>
          </div>
        </div>
      </div>
    )}

    {startForm && (
      <div>
        <div className=" px-3 lg:px-10 py-6">
          <h2 className="text-2xl font-bold my-4">Create a DAO</h2>

          <UseContextProvider>{displayStep(currentStep)}</UseContextProvider>
        </div>
      </div>
    )}

    {startForm && (
      <div className="flex items-center justify-between my-5 py-3 transition duration-200 ease-in-out">
        <div>
          {currentStep === 1 ? (
            ""
          ) : (
            <button
              className="flex items-center bg-[#3F3F3F] gap-2 px-6 py-4 rounded-full"
              onClick={() => handleClick("back", {})}
            >
              <ReturnIcon /> Back
            </button>
          )}
        </div>
        <div className="w-4/12 transition duration-200 ease-in-out mr-5">
          <FormStepper steps={steps} currentStep={currentStep} />
        </div>
      </div>
    )}
  </>
);

Run your code to see what you have so far. In the react-app, open your integrated terminal and run the following command

npm run dev

For this tutorial, this is what we get

Click on next

Click on next

After a successful submission, you can display a success modal

You can now add an explore page that will display all the DAOs that has been created by the address connected.

When you click on any of them, you can display a form that has the description of the dao, category and other information and also a button to create a new proposal.

Let’s look at the proposals page.

You can create a create-proposals.js file and have this div and form that carries details about proposals

return (
  <div>
    {space && (
      <form onSubmit={createProposal}>
        <div className="ml-20">
          <div className="flex items-center justify-between mb-6">
            <div>
              <button
                className="flex items-center bg-[#3F3F3F] gap-2 px-6 py-4 rounded-full"
                onClick={goBack}
              >
                <ReturnIcon /> Back
              </button>
            </div>
            <div className="flex items-center flex-row gap-3">
              <div className="h-14 w-14">
                <img
                  src={space.logo}
                  className="w-full object-cover rounded-full  "
                />
              </div>

              <div>
                <h4 className="mb-1">{space.name}</h4>
              </div>
            </div>
          </div>

          <div className=" w-full mt-3 md:mt-0  relative h-full px-2 lg:px-5 ">
            <div className="flex-1 w-full mb-5">
              <h3 className="section__header">Create Proposals</h3>
            </div>

            <div className="mt-5 w-full lg:w-8/12">
              <div className="space-y-5 py-6 ">
                <div className="flex flex-col gap-2">
                  <label className="text-sm text-black">
                    Title of Proposal
                  </label>
                  <div className="space-x-4">
                    <input
                      type="text"
                      required
                      onChange={(e) =>
                        setProposal({ ...proposal, title: e.target.value })
                      }
                      value={proposal?.title}
                      className="w-full h-12 rounded-full bg-transparent text-[#8F8F8F] border border-[#3F3F3F] px-4 focus:border-[#8F8F8F] active:border-[#8F8F8F] focus:outline-none transition duration-150 ease-in-out"
                    />
                  </div>
                </div>

                <div>
                  <div className="flex items-center justify-between mb-3">
                    <label className="flex flex-col gap-3 text-sm text-black">
                      Description of space
                    </label>

                    <span className="text-sm text-black">
                      0/2000 characters
                    </span>
                  </div>

                  <textarea
                    className="w-full border border-[#545252] focus:outline-none active:outline-none p-3  placeholder-text-primary bg-transparent rounded-lg textarea placeholder-text-sm text-[#8F8F8F] scrollbar-change"
                    aria-placeholder="Leave a note for your client"
                    rows="6"
                    required
                    onChange={(e) =>
                      setProposal({ ...proposal, body: e.target.value })
                    }
                    value={proposal?.body}
                    id="body"
                    defaultValue=""
                  ></textarea>
                </div>

                <div className="flex flex-col gap-2">
                  <label className="text-sm text-black">
                    Input Discussion URL (Optional)
                  </label>
                  <div className="space-x-4">
                    <input
                      type="text"
                      required
                      onChange={(e) =>
                        setProposal({
                          ...proposal,
                          discussion: e.target.value,
                        })
                      }
                      value={proposal?.discussion}
                      className="w-full h-12 rounded-full bg-transparent text-[#8F8F8F] border border-[#545252] px-4 focus:border-[#8F8F8F] active:border-[#8F8F8F] focus:outline-none transition duration-150 ease-in-out"
                    />
                  </div>
                </div>

                <div className="flex flex-col gap-2 py-4">
                  <h3 className="mb-5 text-sm text-black">Voting Strategy</h3>

                  <div className="space-y-8  w-full gap-5">
                    <div>
                      <h4 className="mb-4 text-sm  text-black">Type</h4>
                      <div className="lg:space-x-4 flex flex-col md:flex-row w-full gap-5">
                        <div className="w-full lg:w-1/2">
                          <div className="">
                            <label className="px-4 py-6 border border-[#545252] text-white text-sm font-medium rounded-md flex items-start justify-start form-check gap-2 cursor-pointer">
                              <input
                                type="radio"
                                name="Weighted"
                                onChange={(e) =>
                                  setProposal({
                                    ...proposal,
                                    type: e.target.value,
                                  })
                                }
                                checked={true}
                                className="w-8 h-8 text-red-600 checked:bg-blue-600 checked:border-blue-600 focus:outline-none transition duration-200"
                              />

                              <span className="flex items-start justify-start flex-col space-y-2">
                                <h5 className=" text-black text-sm">
                                  Weighted voting
                                </h5>
                                <p className="text-xs text-[#8F8F8F]">
                                  Each voter has the ability to cast their votes
                                  across any number of options.
                                </p>
                              </span>
                            </label>
                          </div>
                        </div>
                      </div>
                    </div>

                    <div>
                      <h4 className="mb-4 text-sm text-black">Set Options</h4>
                      <div className="space-y-4">
                        <div className="flex flex-row items-center gap-2">
                          <label className="w-4/12 lg:w-2/12 text-sm text-[#8F8F8F]">
                            Option 1
                          </label>
                          <div className="space-x-4 w-full lg:w-7/12 ">
                            <input
                              type="text"
                              required
                              onChange={(e) =>
                                setProposal({
                                  ...proposal,
                                  choices: [
                                    e.target.value,
                                    proposal.choices[1],
                                    proposal.choices[2],
                                  ],
                                })
                              }
                              value={proposal?.choices[0] || "Yes"}
                              className="w-full h-12 rounded-full bg-transparent text-[#8F8F8F] border border-[#545252] px-4 focus:border-[#8F8F8F] active:border-[#8F8F8F] focus:outline-none transition duration-150 ease-in-out"
                            />
                          </div>
                        </div>
                        <div className="flex flex-row items-center gap-2">
                          <label className="w-4/12 lg:w-2/12 text-sm text-[#8F8F8F]">
                            Option 2
                          </label>
                          <div className="space-x-4 w-full lg:w-7/12">
                            <input
                              type="text"
                              required
                              onChange={(e) =>
                                setProposal({
                                  ...proposal,
                                  choices: [
                                    proposal.choices[0],
                                    e.target.value,
                                    proposal.choices[2],
                                  ],
                                })
                              }
                              value={proposal?.choices[1] || "No"}
                              className="w-full h-12 rounded-full bg-transparent text-[#8F8F8F] border border-[#545252] px-4 focus:border-[#8F8F8F] active:border-[#8F8F8F] focus:outline-none transition duration-150 ease-in-out"
                            />
                          </div>
                        </div>
                        <div className="flex flex-row items-center gap-2">
                          <label className="w-4/12 lg:w-2/12 text-sm text-[#8F8F8F]">
                            Option 3
                          </label>
                          <div className="space-x-4  w-full lg:w-7/12">
                            <input
                              type="text"
                              required
                              onChange={(e) =>
                                setProposal({
                                  ...proposal,
                                  choices: [
                                    proposal.choices[0],
                                    proposal.choices[2],
                                    e.target.value,
                                  ],
                                })
                              }
                              value={proposal?.choices[2] || "Abstain"}
                              className="w-full h-12 rounded-full bg-transparent text-[#8F8F8F] border border-[#545252] px-4 focus:border-[#8F8F8F] active:border-[#8F8F8F] focus:outline-none transition duration-150 ease-in-out"
                            />
                          </div>
                        </div>
                      </div>
                    </div>
                    <div>
                      <h4 className="mb-4 text-sm  text-black">
                        Voting Duration
                      </h4>
                      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 gap-5">
                        <div className="flex flex-row items-center gap-2">
                          <label className="w-3/12 text-sm text-[#8F8F8F]">
                            Begins
                          </label>
                          <div className="space-x-4  w-full lg:w-7/12">
                            <input
                              type="date"
                              required
                              onChange={(e) =>
                                setProposal({
                                  ...proposal,
                                  start: e.target.value,
                                })
                              }
                              value={proposal?.start}
                              className="w-full h-12 rounded-full bg-transparent text-[#8F8F8F] border border-[#545252] px-4 focus:border-[#8F8F8F] active:border-[#8F8F8F] focus:outline-none transition duration-150 ease-in-out"
                            />
                          </div>
                        </div>
                        <div className="flex flex-row items-center gap-4">
                          <label className="w-3/12 text-sm lg:text-right text-[#8F8F8F]">
                            Time
                          </label>
                          <div className="space-x-4  w-full lg:w-7/12">
                            <input
                              type="date"
                              required
                              onChange={(e) =>
                                setProposal({
                                  ...proposal,
                                  end: e.target.value,
                                })
                              }
                              value={proposal?.end}
                              className="w-full h-12 rounded-full bg-transparent text-[#8F8F8F] border border-[#545252] px-4 focus:border-[#8F8F8F] active:border-[#8F8F8F] focus:outline-none transition duration-150 ease-in-out"
                            />
                          </div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>

              <div className="w-full my-5 py-7">
                <button
                  // type="submit"
                  onClick={SignMessage}
                  className="button1 px-7 py-3 flex justify-center items-center text-center gap-5 rounded-full w-full bg-black text-white"
                >
                  Create Proposal
                </button>
              </div>
            </div>
          </div>
        </div>
      </form>
    )}
  </div>
);

Updating our front-end will give a UI that looks like this

Upload docs to IPFS

You can create an adapter to handle this. But we first need to create a data transfer object folder where you define objects. In the dto folder, you can create objects that will later be saved to IPFS

type DbDAOData = {
  name: string
  slug: string
  description?: string
  categories: string
  controller: string
  logo?: string
  website?: string
  terms: string
  socials?: {[keys: string]: string}
  admins: string[]
  authors: string[]
}

export type DbDAO = {
  data: DbDAOData
  proposals: { [key: string]: DbProposal }
  settings: {
    voting_period: string,
    voting_delay: string,
    quorum: string,
    threshold: string,
    voting_token: string,
    erc20Balance: {
      network: Chains
      symbol: string
      address: string
      decimals: number
    }
  }
  members: string[]
  signature: SignatureVerifierOptions
  metadata: {
    ipfsHash: string
    updatedAt: number
  }
}

export type DAO = DbDAOData & {
  proposals: Proposal[]
  members: string[]
}

export type DAOListItem = {
  name: string
  slug: string
  controller: string
  logo: string
  membersCount: number
  categories: string
  description: string
}

export function dbDAOToDAO(space: DbDAO): DAO {
  const proposals = Object.values<DbProposal>(dao.proposals).map((proposal) => dbProposalToProposal(proposal))
  return { …dao.data, proposals, members: dao.members }
}

You can create an adapter folder and have an adapter file that will handle the upload.

export class PinatadaoAdapter implements daoWriterDb, daoReaderDb {
    private readonly pinata;

    constructor(pinata: PinataClient) {
      this.pinata = pinata;
    }

    async list(): Promise<Dbdao[]> {
      const response = await this.pinata.pinList({
        status: "pinned",
        metadata: {
          keyvalues: {
            kind: {
              value: "dao",
              op: "eq",
            },
          },
        },
      });

      const fetchPromises = response.rows.map((row) =>
        loadFilesByHash(row.ipfs_pin_hash)
      );
      return Promise.all(fetchPromises);
    }

    async find(slug: string): Promise<Dbdao | null> {
      const response = await this.findBySlug(slug);

      if (response.rows.length === 0) {
        return null;
      }

      const pin = response.rows[0];
      const dao: Dbdao | null = await loadFilesByHash(pin.ipfs_pin_hash);
      if (!dao) return null;

      dao.metadata = {
        ipfsHash: pin.ipfs_pin_hash,
        // @ts-ignore
        updatedAt: pin.metadata?.keyvalues?.updatedAt,
      };

      return dao;
    }

    private findBySlug(slug: string) {
      return this.pinata.pinList({
        status: "pinned",
        metadata: {
          keyvalues: {
            slug: {
              value: slug,
              op: "eq",
            },
            kind: {
              value: "dao",
              op: "eq",
            },
          },
        },
      });
    }

    async update(dao: Dbdao): Promise<Dbdao> {
      const hash = dao.metadata.ipfsHash;
      await this.save(dao);
      await this.pinata.unpin(hash);

      return dao;
    }

    async save(dao: Dbdao): Promise<Dbdao> {
      const response = await this.pinata.pinJSONToIPFS(dao, {
        pinataMetadata: {
          name: dao.data.slug as string,
        },
      });

      const updatedAt = Date.now();
      const metadata = {
        name: dao.data.slug,
        keyvalues: {
          slug: dao.data.slug,
          kind: "dao",
          updatedAt: updatedAt,
        },
      };

      // @ts-ignore
      await this.pinata.hashMetadata(response.IpfsHash, metadata);

      return {
        ...dao,
        metadata: {
          ipfsHash: response.IpfsHash,
          updatedAt: updatedAt,
        },
      };
    }

    async generateSlug(name: string) {
      let text = name
        .toString()
        .normalize("NFD")
        .replace(/[\u0300-\u036f]/g, "")
        .toLowerCase()
        .trim()
        .replace(/\s+/g, "-")
        .replace(/[^\w-]+/g, "")
        .replace(/--+/g, "-");

      const listResponse = await this.pinata.pinList({
        metadata: { name: text, keyvalues: {} },
      });
      const length = listResponse?.rows?.length;
      if (length > 0) {
        text += length;
      }

      return text;
    }

    static makeFromPinataSdk(): PinatadaoAdapter {
      return new this(new pinataSDK(PINATA_API_KEY, PINATA_API_SECRET_KEY))
    }
  }

After creating a proposal and uploading to IPFS, you will see your proposal in the list of proposals.

Clicking on this proposal, you can create a form that will give you more details about it.

Conclusion

Managing a DAO can be a complex task. The tool we built in this tutorial will handle things such as decision-making, governance, treasury management, and community engagement.

About Author

Oluwafemi Alofe is a Blockchain developer with 3 years of applicable knowledge and 8 years of total experience as a Software engineer. He has written and deployed vulnerability-free smart contracts as a blockchain developer on a variety of blockchains, including but not limited to Ethereum, BCS, and Celo. Oluwafemi has a strong interest in developing distributed systems, and continues to learn new things every day to stay current with technological advancements.

He is has two Udemy courses on solidity development with over 6,300 student enrolled and also a book on Amazon KDP for PHP and Laravel Developers.

He’s currently a platform owner at myco.io, first world, watch to earn platform with over 1 Million users.

Connect with Oluwafemi on Twitter or LinkedIn.

3 Likes

Great illustrations and great work put in here too.

4 Likes

Nice work man

3 Likes

Nice piece :+1:

3 Likes

good piece,keep up the good work! :smiling_face_with_three_hearts:

4 Likes

Liked how you included a video tutorial just the best to do it. Nice work

3 Likes

This is top notch , well done brother.

2 Likes

changed Linkedin to LinkedIn

2 Likes

One of most comprehensive contents so far.

3 Likes

Great tutorial. Quite indepth. Thanks

2 Likes

This is a great piece and it is very insightful. I recommend :clinking_glasses:

2 Likes