Build an on-chain DAO for your NFT holders on Ethereum using Solidity, Next.js, ethers.js, Web3Modal



What’s a DAO?

DAO stands for Decentralized Autonomous Organization. You may consider DAOs as analogous to corporations in the actual world. Primarily, DAOs permit for members to create and vote on governance choices.

In conventional corporations, when a choice must be made, the board of administrators or executives of the corporate are in command of making that call. In a DAO, nevertheless, this course of is democratized, and any member can create a proposal, and all different members can vote on it. Every proposal created has a deadline for voting, and after the deadline the choice is made in favour of the voting consequence (YES or NO).

Membership in DAOs is usually restricted both by possession of ERC20 tokens, or by possession of NFTs. Examples of DAOs the place membership and voting energy is proportional to what number of tokens you personal embrace Uniswap and ENS. Examples of DAOs the place they’re based mostly on NFTs embrace Meebits DAO.



Constructing our DAO

You wish to launch a DAO for holders of your CryptoDevs NFTs. From the ETH that was gained by way of the ICO, you constructed up a DAO Treasury. The DAO now has loads of ETH, however presently does nothing with it.

You wish to permit your NFT holders to create and vote on proposals to make use of that ETH for buying different NFTs from an NFT market, and speculate on worth. Possibly sooner or later if you promote the NFT again, you break up the income amongst all members of the DAO.



Necessities

  • Anybody with a CryptoDevs NFT can create a proposal to buy a distinct NFT from an NFT market
  • Everybody with a CryptoDevs NFT can vote for or towards the energetic proposals
  • Every NFT counts as one vote for every proposal
  • Voter can not vote a number of instances on the identical proposal with the identical NFT
  • If majority of the voters vote for the proposal by the deadline, the NFT buy is routinely executed



What we’ll make

  • To have the ability to buy NFTs routinely when a proposal is handed, you want an on-chain NFT market you can name a buy() perform on. There exist loads of NFT marketplaces on the market, however to keep away from overcomplicating issues, we’ll create a simplified pretend NFT market for this tutorial as the main target is on the DAO.
  • We may even make the precise DAO sensible contract utilizing Hardhat.
  • We’ll make the web site utilizing Subsequent.js to permit customers to create and vote on proposals



Conditions



BUIDL IT



Sensible Contract Growth

We’ll begin off with first creating the sensible contracts. We can be making two sensible contracts:

  • FakeNFTMarketplace.sol
  • CryptoDevsDAO.sol

To take action, we’ll use the Hardhat growth framework we have now been utilizing for the previous few tutorials.

  • Create a folder for this undertaking named DAO-Tutorial, and open up a Terminal window in that folder.
  • Setup a brand new hardhat undertaking by operating the next instructions in your terminal:
  mkdir hardhat-tutorial
  cd hardhat-tutorial
  npm init --yes
  npm set up --save-dev hardhat
Enter fullscreen mode

Exit fullscreen mode

Now that you’ve got put in Hardhat, we are able to setup a undertaking. Execute the next command in your terminal.

  • In the identical listing the place you put in Hardhat run:
  npx hardhat
Enter fullscreen mode

Exit fullscreen mode

  • Choose Create a primary pattern undertaking
  • Press enter for the already specified Hardhat Challenge root
  • Press enter for the query on if you wish to add a .gitignore
  • Press enter for Do you wish to set up this pattern undertaking's dependencies with npm (@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)?

    Now you might have a hardhat undertaking able to go!

    In case you are not on mac, please do that additional step and set up these libraries as effectively πŸ™‚

    npm set up --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
    

and press Enter for all of the questions (Select the Create a primary pattern undertaking) possibility.

  npm set up @openzeppelin/contracts
Enter fullscreen mode

Exit fullscreen mode

  • First, let’s make a easy Faux NFT Market. Create a file named FakeNFTMarketplace.sol underneath the contracts listing inside hardhat-tutorial, and add the next code.
  // SPDX-License-Identifier: MIT
  pragma solidity ^0.8.0;

  contract FakeNFTMarketplace {
      /// @dev Keep a mapping of Faux TokenID to Proprietor addresses
      mapping(uint256 => tackle) public tokens;
      /// @dev Set the acquisition worth for every Faux NFT
      uint256 nftPrice = 0.1 ether;

      /// @dev buy() accepts ETH and marks the proprietor of the given tokenId because the caller tackle
      /// @param _tokenId - the pretend NFT token Id to buy
      perform buy(uint256 _tokenId) exterior payable {
          require(msg.worth == nftPrice, "This NFT prices 0.1 ether");
          tokens[_tokenId] = msg.sender;
      }

      /// @dev getPrice() returns the value of 1 NFT
      perform getPrice() exterior view returns (uint256) {
          return nftPrice;
      }

      /// @dev accessible() checks whether or not the given tokenId has already been offered or not
      /// @param _tokenId - the tokenId to verify for
      perform accessible(uint256 _tokenId) exterior view returns (bool) {
          // tackle(0) = 0x0000000000000000000000000000000000000000
          // That is the default worth for addresses in Solidity
          if (tokens[_tokenId] == tackle(0)) {
              return true;
          }
          return false;
      }
  }
Enter fullscreen mode

Exit fullscreen mode

  • The FakeNFTMarketplace exposes some primary features that we are going to be utilizing from the DAO contract to buy NFTs if a proposal is handed. An actual NFT market could be extra difficult – as not all NFTs have the identical worth.
  • Let’s be sure that every thing compiles earlier than we begin writing the DAO Contract. Run the next command contained in the hardhat-tutorial folder out of your Terminal.
  npx hardhat compile
Enter fullscreen mode

Exit fullscreen mode

and ensure there aren’t any compilation errors.

  • Now, we’ll begin writing the CryptoDevsDAO contract. Since that is principally a totally customized contract, and comparatively extra difficult than what we have now performed up to now, we’ll clarify this one bit-by-bit.
  • First, let’s write the boilerplate code for the contract. Create a brand new file named CryptoDevsDAO.sol underneath the contracts listing in hardhat-tutorial and add the next code to it.
  // SPDX-License-Identifier: MIT
  pragma solidity ^0.8.0;

  import "@openzeppelin/contracts/entry/Ownable.sol";

  // We'll add the Interfaces right here

  contract CryptoDevsDAO is Ownable {
      // We'll write contract code right here
  }
Enter fullscreen mode

Exit fullscreen mode

  • Now, we might want to name features on the FakeNFTMarketplace contract, and your beforehand deployed CryptoDevs NFT contract. Recall from the Superior Solidity Subjects tutorial that we have to present an interface for these contracts, so this contract is aware of which features can be found to name and what they take as parameters and what they return.
  • Add the next two interfaces to your code by including the next code
  /**
   * Interface for the FakeNFTMarketplace
   */
  interface IFakeNFTMarketplace {
      /// @dev getPrice() returns the value of an NFT from the FakeNFTMarketplace
      /// @return Returns the value in Wei for an NFT
      perform getPrice() exterior view returns (uint256);

      /// @dev accessible() returns whether or not or not the given _tokenId has already been bought
      /// @return Returns a boolean worth - true if accessible, false if not
      perform accessible(uint256 _tokenId) exterior view returns (bool);

      /// @dev buy() purchases an NFT from the FakeNFTMarketplace
      /// @param _tokenId - the pretend NFT tokenID to buy
      perform buy(uint256 _tokenId) exterior payable;
  }

  /**
   * Minimal interface for CryptoDevsNFT containing solely two features
   * that we're concerned with
   */
  interface ICryptoDevsNFT {
      /// @dev balanceOf returns the variety of NFTs owned by the given tackle
      /// @param proprietor - tackle to fetch variety of NFTs for
      /// @return Returns the variety of NFTs owned
      perform balanceOf(tackle proprietor) exterior view returns (uint256);

      /// @dev tokenOfOwnerByIndex returns a tokenID at given index for proprietor
      /// @param proprietor - tackle to fetch the NFT TokenID for
      /// @param index - index of NFT in owned tokens array to fetch
      /// @return Returns the TokenID of the NFT
      perform tokenOfOwnerByIndex(tackle proprietor, uint256 index)
          exterior
          view
          returns (uint256);
  }
Enter fullscreen mode

Exit fullscreen mode

  • Now, let’s take into consideration what performance we want within the DAO contract.
    • Retailer created proposals in contract state
    • Enable holders of the CryptoDevs NFT to create new proposals
    • Enable holders of the CryptoDevs NFT to vote on proposals, given they have not already voted, and that the proposal hasn’t handed it is deadline but
    • Enable holders of the CryptoDevs NFT to execute a proposal after it is deadline has been exceeded, triggering an NFT buy in case it handed
  • Let’s begin off by making a struct representing a Proposal. In your contract, add the next code:
  // Create a struct named Proposal containing all related info
  struct Proposal {
      // nftTokenId - the tokenID of the NFT to buy from FakeNFTMarketplace if the proposal passes
      uint256 nftTokenId;
      // deadline - the UNIX timestamp till which this proposal is energetic. Proposal may be executed after the deadline has been exceeded.
      uint256 deadline;
      // yayVotes - variety of yay votes for this proposal
      uint256 yayVotes;
      // nayVotes - variety of nay votes for this proposal
      uint256 nayVotes;
      // executed - whether or not or not this proposal has been executed but. Can't be executed earlier than the deadline has been exceeded.
      bool executed;
      // voters - a mapping of CryptoDevsNFT tokenIDs to booleans indicating whether or not that NFT has already been used to solid a vote or not
      mapping(uint256 => bool) voters;
  }
Enter fullscreen mode

Exit fullscreen mode

  • Let’s additionally create a mapping from Proposal IDs to Proposals to carry all created proposals, and a counter to rely the variety of proposals that exist.
  // Create a mapping of ID to Proposal
  mapping(uint256 => Proposal) public proposals;
  // Variety of proposals which have been created
  uint256 public numProposals;
Enter fullscreen mode

Exit fullscreen mode

  • Now, since we can be calling features on the FakeNFTMarketplace and CryptoDevsNFT contract, let’s initialize variables for these contracts.
  IFakeNFTMarketplace nftMarketplace;
  ICryptoDevsNFT cryptoDevsNFT;
Enter fullscreen mode

Exit fullscreen mode

  • Create a constructor perform that can initialize these contract variables, and likewise settle for an ETH deposit from the deployer to fill the DAO ETH treasury. (Within the background, since we imported the Ownable contract, this may even set the contract deployer because the proprietor of this contract)
  // Create a payable constructor which initializes the contract
  // cases for FakeNFTMarketplace and CryptoDevsNFT
  // The payable permits this constructor to simply accept an ETH deposit when it's being deployed
  constructor(tackle _nftMarketplace, tackle _cryptoDevsNFT) payable {
      nftMarketplace = IFakeNFTMarketplace(_nftMarketplace);
      cryptoDevsNFT = ICryptoDevsNFT(_cryptoDevsNFT);
  }
Enter fullscreen mode

Exit fullscreen mode

  • Now, since we wish just about all of our different features to solely be referred to as by those that personal NFTs from the CryptoDevs NFT contract, we’ll create a modifier to keep away from duplicating code.
  // Create a modifier which solely permits a perform to be
  // referred to as by somebody who owns not less than 1 CryptoDevsNFT
  modifier nftHolderOnly() {
      require(cryptoDevsNFT.balanceOf(msg.sender) > 0, "NOT_A_DAO_MEMBER");
      _;
  }
Enter fullscreen mode

Exit fullscreen mode

  • We now have sufficient to jot down our createProposal perform, which is able to permit members to create new proposals.
  /// @dev createProposal permits a CryptoDevsNFT holder to create a brand new proposal within the DAO
  /// @param _nftTokenId - the tokenID of the NFT to be bought from FakeNFTMarketplace if this proposal passes
  /// @return Returns the proposal index for the newly created proposal
  perform createProposal(uint256 _nftTokenId)
      exterior
      nftHolderOnly
      returns (uint256)
  {
      require(nftMarketplace.accessible(_nftTokenId), "NFT_NOT_FOR_SALE");
      Proposal storage proposal = proposals[numProposals];
      proposal.nftTokenId = _nftTokenId;
      // Set the proposal's voting deadline to be (present time + 5 minutes)
      proposal.deadline = block.timestamp + 5 minutes;

      numProposals++;

      return numProposals - 1;
  }
Enter fullscreen mode

Exit fullscreen mode

  • Now, to vote on a proposal, we wish to add a further restriction that the proposal being voted on should not have had it is deadline exceeded. To take action, we’ll create a second modifier.
  // Create a modifier which solely permits a perform to be
  // referred to as if the given proposal's deadline has not been exceeded but
  modifier activeProposalOnly(uint256 proposalIndex) {
      require(
          proposals[proposalIndex].deadline > block.timestamp,
          "DEADLINE_EXCEEDED"
      );
      _;
  }
Enter fullscreen mode

Exit fullscreen mode

Be aware how this modifier takes a parameter!

  • Moreover, since a vote can solely be certainly one of two values (YAY or NAY) – we are able to create an enum representing attainable choices.
  // Create an enum named Vote containing attainable choices for a vote
  enum Vote {
      YAY, // YAY = 0
      NAY // NAY = 1
  }
Enter fullscreen mode

Exit fullscreen mode

  • Let’s write the voteOnProposal perform
  /// @dev voteOnProposal permits a CryptoDevsNFT holder to solid their vote on an energetic proposal
  /// @param proposalIndex - the index of the proposal to vote on within the proposals array
  /// @param vote - the kind of vote they wish to solid
  perform voteOnProposal(uint256 proposalIndex, Vote vote)
      exterior
      nftHolderOnly
      activeProposalOnly(proposalIndex)
  {
      Proposal storage proposal = proposals[proposalIndex];

      uint256 voterNFTBalance = cryptoDevsNFT.balanceOf(msg.sender);
      uint256 numVotes = 0;

      // Calculate what number of NFTs are owned by the voter
      // that have not already been used for voting on this proposal
      for (uint256 i = 0; i < voterNFTBalance; i++) {
          uint256 tokenId = cryptoDevsNFT.tokenOfOwnerByIndex(msg.sender, i);
          if (proposal.voters[tokenId] == false) {
              numVotes++;
              proposal.voters[tokenId] = true;
          }
      }
      require(numVotes > 0, "ALREADY_VOTED");

      if (vote == Vote.YAY) {
          proposal.yayVotes += numVotes;
      } else {
          proposal.nayVotes += numVotes;
      }
  }
Enter fullscreen mode

Exit fullscreen mode

  • We’re nearly performed! To execute a proposal whose deadline has exceeded, we’ll create our remaining modifier.
  // Create a modifier which solely permits a perform to be
  // referred to as if the given proposals' deadline HAS been exceeded
  // and if the proposal has not but been executed
  modifier inactiveProposalOnly(uint256 proposalIndex) {
      require(
          proposals[proposalIndex].deadline <= block.timestamp,
          "DEADLINE_NOT_EXCEEDED"
      );
      require(
          proposals[proposalIndex].executed == false,
          "PROPOSAL_ALREADY_EXECUTED"
      );
      _;
  }
Enter fullscreen mode

Exit fullscreen mode

Be aware this modifier additionally takes a parameter!

  • Let’s write the code for executeProposal
  /// @dev executeProposal permits any CryptoDevsNFT holder to execute a proposal after it is deadline has been exceeded
  /// @param proposalIndex - the index of the proposal to execute within the proposals array
  perform executeProposal(uint256 proposalIndex)
      exterior
      nftHolderOnly
      inactiveProposalOnly(proposalIndex)
  {
      Proposal storage proposal = proposals[proposalIndex];

      // If the proposal has extra YAY votes than NAY votes
      // buy the NFT from the FakeNFTMarketplace
      if (proposal.yayVotes > proposal.nayVotes) {
          uint256 nftPrice = nftMarketplace.getPrice();
          require(tackle(this).stability >= nftPrice, "NOT_ENOUGH_FUNDS");
          nftMarketplace.buy{worth: nftPrice}(proposal.nftTokenId);
      }
      proposal.executed = true;
  }
Enter fullscreen mode

Exit fullscreen mode

  • Now we have at this level carried out all of the core performance. Nevertheless, there are a few further options we might and may implement.
    • Enable the contract proprietor to withdraw the ETH from the DAO if wanted
    • Enable the contract to simply accept additional ETH deposits
  • The Ownable contract we inherit from incorporates a modifier onlyOwner which restricts a perform to solely be capable of be referred to as by the contract proprietor. Let’s implement withdrawEther utilizing that modifier.
  /// @dev withdrawEther permits the contract proprietor (deployer) to withdraw the ETH from the contract
  perform withdrawEther() exterior onlyOwner {
      payable(proprietor()).switch(tackle(this).stability);
  }
Enter fullscreen mode

Exit fullscreen mode

It will switch the complete ETH stability of the contract to the proprietor tackle

  • Lastly, to permit for including extra ETH deposits to the DAO treasury, we have to add some particular features. Usually, contract addresses can not settle for ETH despatched to them, except it was by way of a payable perform. However we do not need customers to name features simply to deposit cash, they need to be capable of tranfer ETH straight from their pockets. For that, let’s add these two features:
  // The next two features permit the contract to simply accept ETH deposits
  // straight from a pockets with out calling a perform
  obtain() exterior payable {}

  fallback() exterior payable {}
Enter fullscreen mode

Exit fullscreen mode



Sensible Contract Deployment

Now that we have now written each our contracts, let’s deploy them to the Rinkeby Testnet. Guarantee you might have some ETH on the Rinkeby Testnet.

  • Set up the dotenv bundle from NPM to have the ability to use atmosphere variables laid out in .env recordsdata within the hardhat.config.js. Execute the next command in your Terminal within the hardhat-tutorial listing.
  npm set up dotenv
Enter fullscreen mode

Exit fullscreen mode

  • Now create a .env file within the hardhat-tutorial listing and set the next two atmosphere variables. Observe the directions to get their values. Make certain the Rinkeby non-public key you utilize is has ETH on the Rinkeby Testnet.
  // Go to https://www.alchemyapi.io, enroll, create
  // a brand new App in its dashboard and choose the community as Rinkeby, and exchange "add-the-alchemy-key-url-here" with its key url
  ALCHEMY_API_KEY_URL="add-the-alchemy-key-url-here"

  // Change this non-public key together with your RINKEBY account non-public key
  // To export your non-public key from Metamask, open Metamask and
  // go to Account Particulars > Export Non-public Key
  // Pay attention to NEVER placing actual Ether into testing accounts
  RINKEBY_PRIVATE_KEY="add-the-rinkeby-private-key-here"
Enter fullscreen mode

Exit fullscreen mode

  • Now, let’s write a deployment script to routinely deploy each our contracts for us. Create a brand new file named deploy.js underneath hardhat-tutorial/scripts, and add the next code:
  const { ethers } = require("hardhat");
  const { CRYPTODEVS_NFT_CONTRACT_ADDRESS } = require("../constants");

  async perform major() {
    // Deploy the FakeNFTMarketplace contract first
    const FakeNFTMarketplace = await ethers.getContractFactory(
      "FakeNFTMarketplace"
    );
    const fakeNftMarketplace = await FakeNFTMarketplace.deploy();
    await fakeNftMarketplace.deployed();

    console.log("FakeNFTMarketplace deployed to: ", fakeNftMarketplace.tackle);

    // Now deploy the CryptoDevsDAO contract
    const CryptoDevsDAO = await ethers.getContractFactory("CryptoDevsDAO");
    const cryptoDevsDAO = await CryptoDevsDAO.deploy(
      fakeNftMarketplace.tackle,
      CRYPTODEVS_NFT_CONTRACT_ADDRESS,
      {
        // This assumes your account has not less than 1 ETH in it is account
        // Change this worth as you need
        worth: ethers.utils.parseEther("1"),
      }
    );
    await cryptoDevsDAO.deployed();

    console.log("CryptoDevsDAO deployed to: ", cryptoDevsDAO.tackle);
  }

  major()
    .then(() => course of.exit(0))
    .catch((error) => {
      console.error(error);
      course of.exit(1);
    });
Enter fullscreen mode

Exit fullscreen mode

  • As you might have observed, deploy.js imports a variable referred to as CRYPTODEVS_NFT_CONTRACT_ADDRESS from a file named constants. Let’s make that. Create a brand new file named constants.js within the hardhat-tutorial listing.
  // Change the worth together with your NFT contract tackle
  const CRYPTODEVS_NFT_CONTRACT_ADDRESS =
    "YOUR_CRYPTODEVS_NFT_CONTRACT_ADDRESS_HERE";

  module.exports = { CRYPTODEVS_NFT_CONTRACT_ADDRESS };
Enter fullscreen mode

Exit fullscreen mode

  • Now, let’s add the Rinkeby Community to your Hardhat Config so we are able to deploy to Rinkeby. Open your hardhat.config.js file and exchange it with the next:
  require("@nomiclabs/hardhat-waffle");
  require("dotenv").config({ path: ".env" });

  const ALCHEMY_API_KEY_URL = course of.env.ALCHEMY_API_KEY_URL;

  const RINKEBY_PRIVATE_KEY = course of.env.RINKEBY_PRIVATE_KEY;

  module.exports = {
    solidity: "0.8.4",
    networks: {
      rinkeby: {
        url: ALCHEMY_API_KEY_URL,
        accounts: [RINKEBY_PRIVATE_KEY],
      },
    },
  };
Enter fullscreen mode

Exit fullscreen mode

  • Let’s be sure that every thing compiles earlier than continuing. Execute the next command out of your Terminal inside the hardhat-tutorial folder.
  npx hardhat compile
Enter fullscreen mode

Exit fullscreen mode

and ensure there aren’t any compilation errors.
In the event you do face compilation errors, attempt evaluating your code towards the final version present here

  • Let’s deploy! Execute the next command in your Terminal from the hardhat-tutorial listing
  npx hardhat run scripts/deploy.js --network rinkeby
Enter fullscreen mode

Exit fullscreen mode

  • Save the FakeNFTMarketplace and CryptoDevsDAO contract addresses that get printed in your Terminal. You’ll need these later.



Frontend Growth

Whew! A lot coding!

We have efficiently developed and deployed our contracts to the Rinkeby Testnet. Now, it is time to construct the Frontend interface so customers can create and vote on proposals from the web site.

To develop the web site, we can be utilizing Next.js as we have now up to now, which is a meta-framework constructed on prime of React.

  • Let’s get began by creating a brand new subsequent app. Your folder construction ought to appear to be this after establishing the subsequent app:
  - DAO-Tutorial
      - hardhat-tutorial
      - my-app
Enter fullscreen mode

Exit fullscreen mode

  • To create my-app, execute the next command in your Terminal inside the DAO-Tutorial listing
  npx create-next-app@newest
Enter fullscreen mode

Exit fullscreen mode

and press Enter for all of the query prompts. This could create the my-app folder and setup a primary Subsequent.js undertaking.

  • Let’s examine if every thing works. Run the next in your Terminal
  cd my-app
  npm run dev
Enter fullscreen mode

Exit fullscreen mode

  • Your web site needs to be up and operating at http://localhost:3000. Nevertheless, this can be a primary starter Subsequent.js undertaking and we have to add code for it to do what we wish.
  • Let’s set up the web3modal and ethers library. Web3Modal will permit us to help connecting to wallets within the browser, and Ethers can be used to work together with the blockchain. Run this in your Terminal from the my-app listing.
  npm set up web3modal ethers
Enter fullscreen mode

Exit fullscreen mode

  • Obtain and save the next file as 0.svg in my-app/public/cryptodevs. We’ll show this picture on the webpage. NOTE: You’ll want to create the cryptodevs folder inside public.
    Download Image
  • Add the next CSS kinds in my-app/kinds/House.modules.css
  .major {
    min-height: 90vh;
    show: flex;
    flex-direction: row;
    justify-content: middle;
    align-items: middle;
    font-family: "Courier New", Courier, monospace;
  }

  .footer {
    show: flex;
    padding: 2rem 0;
    border-top: 1px strong #eaeaea;
    justify-content: middle;
    align-items: middle;
  }

  .picture {
    width: 70%;
    top: 50%;
    margin-left: 20%;
  }

  .title {
    font-size: 2rem;
    margin: 2rem 0;
  }

  .description {
    line-height: 1;
    margin: 2rem 0;
    font-size: 1.2rem;
  }

  .button {
    border-radius: 4px;
    background-color: blue;
    border: none;
    colour: #ffffff;
    font-size: 15px;
    padding: 10px;
    width: 200px;
    cursor: pointer;
    margin-right: 2%;
  }

  .button2 {
    border-radius: 4px;
    background-color: indigo;
    border: none;
    colour: #ffffff;
    font-size: 15px;
    padding: 10px;
    cursor: pointer;
    margin-right: 2%;
    margin-top: 1rem;
  }

  .proposalCard {
    width: fit-content;
    margin-top: 0.25rem;
    border: black 2px strong;
    flex: 1;
    flex-direction: column;
  }

  .container {
    margin-top: 2rem;
  }

  .flex {
    flex: 1;
    justify-content: space-between;
  }

  @media (max-width: 1000px) {
    .major {
      width: 100%;
      flex-direction: column;
      justify-content: middle;
      align-items: middle;
    }
  }
Enter fullscreen mode

Exit fullscreen mode

  • The web site additionally must learn/write information from two sensible contracts – CryptoDevsDAO and CryptoDevsNFT. Let’s retailer their contract addresses and ABIs in a constants file. Create a constants.js file within the my-app listing.
  export const CRYPTODEVS_DAO_CONTRACT_ADDRESS = "";
  export const CRYPTODEVS_NFT_CONTRACT_ADDRESS = "";

  export const CRYPTODEVS_DAO_ABI = [];
  export const CRYPTODEVS_NFT_ABI = [];
Enter fullscreen mode

Exit fullscreen mode

  • Change the contract tackle and ABI values together with your related contract addresses and ABIs.
  • Now for the precise cool web site code. Open up my-app/pages/index.js and write the next code. Clarification of the code may be discovered within the feedback.
  import { Contract, suppliers } from "ethers";
  import { formatEther } from "ethers/lib/utils";
  import Head from "subsequent/head";
  import { useEffect, useRef, useState } from "react";
  import Web3Modal from "web3modal";
  import {
    CRYPTODEVS_DAO_ABI,
    CRYPTODEVS_DAO_CONTRACT_ADDRESS,
    CRYPTODEVS_NFT_ABI,
    CRYPTODEVS_NFT_CONTRACT_ADDRESS,
  } from "../constants";
  import kinds from "../kinds/House.module.css";

  export default perform House() {
    // ETH Steadiness of the DAO contract
    const [treasuryBalance, setTreasuryBalance] = useState("0");
    // Variety of proposals created within the DAO
    const [numProposals, setNumProposals] = useState("0");
    // Array of all proposals created within the DAO
    const [proposals, setProposals] = useState([]);
    // Person's stability of CryptoDevs NFTs
    const [nftBalance, setNftBalance] = useState(0);
    // Faux NFT Token ID to buy. Used when making a proposal.
    const [fakeNftTokenId, setFakeNftTokenId] = useState("");
    // One among "Create Proposal" or "View Proposals"
    const [selectedTab, setSelectedTab] = useState("");
    // True if ready for a transaction to be mined, false in any other case.
    const [loading, setLoading] = useState(false);
    // True if consumer has related their pockets, false in any other case
    const [walletConnected, setWalletConnected] = useState(false);
    const web3ModalRef = useRef();

    // Helper perform to attach pockets
    const connectWallet = async () => {
      attempt {
        await getProviderOrSigner();
        setWalletConnected(true);
      } catch (error) {
        console.error(error);
      }
    };

    // Reads the ETH stability of the DAO contract and units the `treasuryBalance` state variable
    const getDAOTreasuryBalance = async () => {
      attempt {
        const supplier = await getProviderOrSigner();
        const stability = await supplier.getBalance(
          CRYPTODEVS_DAO_CONTRACT_ADDRESS
        );
        setTreasuryBalance(stability.toString());
      } catch (error) {
        console.error(error);
      }
    };

    // Reads the variety of proposals within the DAO contract and units the `numProposals` state variable
    const getNumProposalsInDAO = async () => {
      attempt {
        const supplier = await getProviderOrSigner();
        const contract = getDaoContractInstance(supplier);
        const daoNumProposals = await contract.numProposals();
        setNumProposals(daoNumProposals.toString());
      } catch (error) {
        console.error(error);
      }
    };

    // Reads the stability of the consumer's CryptoDevs NFTs and units the `nftBalance` state variable
    const getUserNFTBalance = async () => {
      attempt {
        const signer = await getProviderOrSigner(true);
        const nftContract = getCryptodevsNFTContractInstance(signer);
        const stability = await nftContract.balanceOf(signer.getAddress());
        setNftBalance(parseInt(stability.toString()));
      } catch (error) {
        console.error(error);
      }
    };

    // Calls the `createProposal` perform within the contract, utilizing the tokenId from `fakeNftTokenId`
    const createProposal = async () => {
      attempt {
        const signer = await getProviderOrSigner(true);
        const daoContract = getDaoContractInstance(signer);
        const txn = await daoContract.createProposal(fakeNftTokenId);
        setLoading(true);
        await txn.wait();
        await getNumProposalsInDAO();
        setLoading(false);
      } catch (error) {
        console.error(error);
        window.alert(error.information.message);
      }
    };

    // Helper perform to fetch and parse one proposal from the DAO contract
    // Given the Proposal ID
    // and converts the returned information right into a Javascript object with values we are able to use
    const fetchProposalById = async (id) => {
      attempt {
        const supplier = await getProviderOrSigner();
        const daoContract = getDaoContractInstance(supplier);
        const proposal = await daoContract.proposals(id);
        const parsedProposal = {
          proposalId: id,
          nftTokenId: proposal.nftTokenId.toString(),
          deadline: new Date(parseInt(proposal.deadline.toString()) * 1000),
          yayVotes: proposal.yayVotes.toString(),
          nayVotes: proposal.nayVotes.toString(),
          executed: proposal.executed,
        };
        return parsedProposal;
      } catch (error) {
        console.error(error);
      }
    };

    // Runs a loop `numProposals` instances to fetch all proposals within the DAO
    // and units the `proposals` state variable
    const fetchAllProposals = async () => {
      attempt {
        const proposals = [];
        for (let i = 0; i < numProposals; i++) {
          const proposal = await fetchProposalById(i);
          proposals.push(proposal);
        }
        setProposals(proposals);
        return proposals;
      } catch (error) {
        console.error(error);
      }
    };

    // Calls the `voteOnProposal` perform within the contract, utilizing the handed
    // proposal ID and Vote
    const voteOnProposal = async (proposalId, _vote) => {
      attempt {
        const signer = await getProviderOrSigner(true);
        const daoContract = getDaoContractInstance(signer);

        let vote = _vote === "YAY" ? 0 : 1;
        const txn = await daoContract.voteOnProposal(proposalId, vote);
        setLoading(true);
        await txn.wait();
        setLoading(false);
        await fetchAllProposals();
      } catch (error) {
        console.error(error);
        window.alert(error.information.message);
      }
    };

    // Calls the `executeProposal` perform within the contract, utilizing
    // the handed proposal ID
    const executeProposal = async (proposalId) => {
      attempt {
        const signer = await getProviderOrSigner(true);
        const daoContract = getDaoContractInstance(signer);
        const txn = await daoContract.executeProposal(proposalId);
        setLoading(true);
        await txn.wait();
        setLoading(false);
        await fetchAllProposals();
      } catch (error) {
        console.error(error);
        window.alert(error.information.message);
      }
    };

    // Helper perform to fetch a Supplier/Signer occasion from Metamask
    const getProviderOrSigner = async (needSigner = false) => {
      const supplier = await web3ModalRef.present.join();
      const web3Provider = new suppliers.Web3Provider(supplier);

      const { chainId } = await web3Provider.getNetwork();
      if (chainId !== 4) {
        window.alert("Please change to the Rinkeby community!");
        throw new Error("Please change to the Rinkeby community");
      }

      if (needSigner) {
        const signer = web3Provider.getSigner();
        return signer;
      }
      return web3Provider;
    };

    // Helper perform to return a DAO Contract occasion
    // given a Supplier/Signer
    const getDaoContractInstance = (providerOrSigner) => {
      return new Contract(
        CRYPTODEVS_DAO_CONTRACT_ADDRESS,
        CRYPTODEVS_DAO_ABI,
        providerOrSigner
      );
    };

    // Helper perform to return a CryptoDevs NFT Contract occasion
    // given a Supplier/Signer
    const getCryptodevsNFTContractInstance = (providerOrSigner) => {
      return new Contract(
        CRYPTODEVS_NFT_CONTRACT_ADDRESS,
        CRYPTODEVS_NFT_ABI,
        providerOrSigner
      );
    };

    // piece of code that runs everytime the worth of `walletConnected` modifications
    // so when a pockets connects or disconnects
    // Prompts consumer to attach pockets if not related
    // after which calls helper features to fetch the
    // DAO Treasury Steadiness, Person NFT Steadiness, and Variety of Proposals within the DAO
    useEffect(() => {
      if (!walletConnected) {
        web3ModalRef.present = new Web3Modal({
          community: "rinkeby",
          providerOptions: {},
          disableInjectedProvider: false,
        });

        connectWallet().then(() => {
          getDAOTreasuryBalance();
          getUserNFTBalance();
          getNumProposalsInDAO();
        });
      }
    }, [walletConnected]);

    // Piece of code that runs everytime the worth of `selectedTab` modifications
    // Used to re-fetch all proposals within the DAO when consumer switches
    // to the 'View Proposals' tab
    useEffect(() => {
      if (selectedTab === "View Proposals") {
        fetchAllProposals();
      }
    }, [selectedTab]);

    // Render the contents of the suitable tab based mostly on `selectedTab`
    perform renderTabs() {
      if (selectedTab === "Create Proposal") {
        return renderCreateProposalTab();
      } else if (selectedTab === "View Proposals") {
        return renderViewProposalsTab();
      }
      return null;
    }

    // Renders the 'Create Proposal' tab content material
    perform renderCreateProposalTab() {
      if (loading) {
        return (
          <div className={kinds.description}>
            Loading... Ready for transaction...
          </div>
        );
      } else if (nftBalance === 0) {
        return (
          <div className={kinds.description}>
            You do not personal any CryptoDevs NFTs. <br />
            <b>You can not create or vote on proposals</b>
          </div>
        );
      } else {
        return (
          <div className={kinds.container}>
            <label>Faux NFT Token ID to Buy: </label>
            <enter
              placeholder="0"
              kind="quantity"
              onChange={(e) => setFakeNftTokenId(e.goal.worth)}
            />
            <button className={kinds.button2} onClick={createProposal}>
              Create
            </button>
          </div>
        );
      }
    }

    // Renders the 'View Proposals' tab content material
    perform renderViewProposalsTab() {
      if (loading) {
        return (
          <div className={kinds.description}>
            Loading... Ready for transaction...
          </div>
        );
      } else if (proposals.size === 0) {
        return (
          <div className={kinds.description}>
            No proposals have been created
          </div>
        );
      } else {
        return (
          <div>
            {proposals.map((p, index) => (
              <div key={index} className={kinds.proposalCard}>
                <p>Proposal ID: {p.proposalId}</p>
                <p>Faux NFT to Buy: {p.nftTokenId}</p>
                <p>Deadline: {p.deadline.toLocaleString()}</p>
                <p>Yay Votes: {p.yayVotes}</p>
                <p>Nay Votes: {p.nayVotes}</p>
                <p>Executed?: {p.executed.toString()}</p>
                {p.deadline.getTime() > Date.now() && !p.executed ? (
                  <div className={kinds.flex}>
                    <button
                      className={kinds.button2}
                      onClick={() => voteOnProposal(p.proposalId, "YAY")}
                    >
                      Vote YAY
                    </button>
                    <button
                      className={kinds.button2}
                      onClick={() => voteOnProposal(p.proposalId, "NAY")}
                    >
                      Vote NAY
                    </button>
                  </div>
                ) : p.deadline.getTime() < Date.now() && !p.executed ? (
                  <div className={kinds.flex}>
                    <button
                      className={kinds.button2}
                      onClick={() => executeProposal(p.proposalId)}
                    >
                      Execute Proposal{" "}
                      {p.yayVotes > p.nayVotes ? "(YAY)" : "(NAY)"}
                    </button>
                  </div>
                ) : (
                  <div className={kinds.description}>Proposal Executed</div>
                )}
              </div>
            ))}
          </div>
        );
      }
    }

    return (
      <div>
        <Head>
          <title>CryptoDevs DAO</title>
          <meta identify="description" content material="CryptoDevs DAO" />
          <hyperlink rel="icon" href="/favicon.ico" />
        </Head>

        <div className={kinds.major}>
          <div>
            <h1 className={kinds.title}>Welcome to Crypto Devs!</h1>
            <div className={kinds.description}>Welcome to the DAO!</div>
            <div className={kinds.description}>
              Your CryptoDevs NFT Steadiness: {nftBalance}
              <br />
              Treasury Steadiness: {formatEther(treasuryBalance)} ETH
              <br />
              Whole Quantity of Proposals: {numProposals}
            </div>
            <div className={kinds.flex}>
              <button
                className={kinds.button}
                onClick={() => setSelectedTab("Create Proposal")}
              >
                Create Proposal
              </button>
              <button
                className={kinds.button}
                onClick={() => setSelectedTab("View Proposals")}
              >
                View Proposals
              </button>
            </div>
            {renderTabs()}
          </div>
          <div>
            <img className={kinds.picture} src="/cryptodevs/0.svg" />
          </div>
        </div>

        <footer className={kinds.footer}>
          Made with &#10084; by Crypto Devs
        </footer>
      </div>
    );
  }
Enter fullscreen mode

Exit fullscreen mode

  • Let’s run it! In your terminal, from the my-app listing, execute:
  npm run dev
Enter fullscreen mode

Exit fullscreen mode

to see your web site in motion. It ought to appear to be the screenshot at first of this tutorial.

Congratulations! Your CryptoDevs DAO web site ought to now be working.



Testing

  • Create a few proposals
  • Strive voting YAY on one, and NAY on the opposite
  • Wait for five minutes for his or her deadlines to cross
  • Execute each of them.
  • Watch the stability of the DAO Treasury go down by 0.1 ETH as a result of proposal which handed because it purchased an NFT upon execution.



Push to Github

Make certain to push all this code to Github earlier than continuing to the following step.



Web site Deployment

What good is an internet site for those who can not share it with others? Let’s work on deploying your dApp to the world so you possibly can share it with all of your LearnWeb3DAO frens.

  • Go to Vercel Dashboard and sign up together with your GitHub account.
  • Click on on the New Challenge button and choose your DAO-Tutorial repo.
  • When configuring your new undertaking, Vercel will assist you to customise your Root Listing
  • Since our Subsequent.js utility is inside a subfolder of the repo, we have to modify it.
  • Click on Edit subsequent to Root Listing and set it to my-app.
  • Choose the framework as Subsequent.js
  • Click on Deploy

  • Now you possibly can see your deployed web site by going to your Vercel Dashboard, deciding on your undertaking, and copying the area from there!



CONGRATULATIONS! You are all performed!

Hopefully you loved this tutorial. Do not forget to share your DAO web site within the #showcase channel on Discord πŸ˜€


Image description

This text is dropped at you by LearnWeb3 DAO. A free, complete A to Z blockchain coaching program for builders throughout the globe.

Every thing from “What’s a Blockchain” to “Hacking sensible contracts”β€Š-β€Šand every thing in between, but in addition rather more!
Be part of us now to start out buidling with 25,000+ builders.

Website
Discord
Twitter



Add a Comment

Your email address will not be published. Required fields are marked *