How To Build A DAO - Part 1

How To Build A DAO - Part 1

The first of a three series guide on creating a simple Decentralized Autonomous Organization

·

17 min read

If you've been following the evolution of Ethereum's ecosystem, including Web3 apps and Defi (decentralized finance) projects, you've probably heard of the term "DAO." If you haven't, let me bring you up to speed!

What's a DAO?

A DAO, or Decentralized Autonomous Organization, is essentially an online-only company that operates according to rules written in a piece of code known as a smart contract. Think of it as a traditional company minus the hierarchical control structure. DAOs operate on a flattened hierarchy, which means that everyone has a stake and no single person owns or controls the entire entity, as a traditional CEO would. DAOs are entirely online and use blockchain technology as a ledger to record what happens in the group, whether it's money-changing hands or decisions being made. Memberships in a DAO is typically limited to those who own the DAO's tokens or NFTs. We'll talk more about this when we start building one.

What's a smart contract?

A smart contract is a digital agreement between two or more people. It runs on the blockchain so it is stored on a public database and cannot be changed. Transactions in a smart contract are processed by the blockchain, allowing them to be sent automatically and without the involvement of a third party.

Now that we understand how a DAO works, let's get started and create a basic DAO.

Disclaimer - If you are a complete beginner in the Web3 world, I would not recommend beginning your journey with this article. It would be extremely beneficial if you had some prior knowledge of what ERC20 and ERC721 tokens are and how they function.

So, here's how we'll proceed from here. We are going to build our DAO in three steps:

  • In the first step, we will write a simple smart contract to whitelist a small number of users who will be prioritized when the distribution of our DAO NFTs begins. This article focuses solely on the first step.
  • In the second step, the distribution of our DAO NFTs actually starts. We will compose a contract in which our whitelisted candidates will be granted access to the sale before the sale opens to the general public. One becomes a member of the DAO by owning a DAO NFT.
  • In the third and final step, we will code the smart contract for our actual DAO, where some decisions will be made based on voting by DAO members.

Let's begin with the very first step, which is to create a Dapp for users to visit in order to be whitelisted for an NFT sale.

Whitelist Dapp

We'll be launching an NFT collection called GeekDevs soon, and we'd like to give our early supporters access to a whitelist for our collection so we're making a whitelisted decentralized app for GeekDevs. Here are some requirements we'll be considering as we build our decentralized app.

Sneak peek of what we are building:

Screenshot 2022-04-12 at 11.19.35 PM.png

Requirements:

  • Only the first ten users should have access to the whitelist.
  • There should be a website where people can sign up to be included on the whitelist.

Prerequisites

We will do our best to understand everything along the way, but it would be preferable if you had some prior experience with:

Let's get started now that we've got everything in order!

Building a Smart Contract

We'll start with the smart contract and then move on to the web app. We will be using Hardhat to build our smart contract.

What's hardhat?

Hardhat is a development environment for Ethereum-based dApps that allows developers to test, compile, deploy, and debug them. As a result, it makes it easier for coders and developers to manage many of the chores that come with creating dApps and smart contracts.

  • First, we will create a new folder named whitelist-dapp to set up our hardhat environment. Open up the terminal and execute these commands.
    mkdir whitelist-dapp
    cd whitelist-dapp
    
  • Tnitialize npm and install hardhat, run the following code:
    npm init --yes
    npm install --save-dev hardhat
    
  • In the same directory, run:
    npx hardhat
    
  • Select Create a basic sample project.
  • Press enter for the already specified Hardhat Project root
  • Press enter for the question on adding a .gitignore.
  • Press enter for Do you want to install this sample project's dependencies with npm (@nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers)?
  • If you are not using a mac, you will have to install these dependencies yourself by running:
    npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
    

That concludes the setup of the hardhat environment. Let's get our smart contract started.

Disclaimer: We will create our smart contract using Solidity. If you have no prior knowledge of Solidity programming, I recommend that you learn it first. CryptoZombies is a fantastic free resource for learning about Solidity data types and other concepts.

  • We'll start by creating a new file inside the contracts directory called Whitelist.sol and create a contract named Whitelist as shown below:
    //SPDX-License-Identifier: Unlicense
    pragma solidity ^0.8.0;
    contract Whitelist {
    }
    
  • Inside the contract, create a public variable for storing maximum number of whitelisted addresses allowed by running the following code:
    uint8 public maxWhitelistedAddresses;
    
  • Create a public mapping for storing all the whitelisted addresses.
    mapping(address => bool) public whitelistedAddresses;
    

Mappings in Solidity are similar to the hash map concept in Java or the dictionary concept in C and Python.

  • Create another public variable to store the number of addresses that have been whitelisted.
    uint8 public numAddressesWhitelisted;
    

    All these variables constitute the state of our contract.

  • Now, we'll write a constructor to specify the maximum number of whitelisted addresses allowed that we'll provide when we deploy our smart contract using the following code snippet:
    constructor(uint8 _maxWhitelistedAddresses) {
      maxWhitelistedAddresses =  _maxWhitelistedAddresses;
    }
    

A constructor in Solidity is just like a constructor in any other language such as Java or JavaScript. It runs only once when the contract is created and it is used to initialize the contract state.

  • Now we'll get to the meat of the matter. We'll make a public function called addAddressToWhitelist, which the client will use to add himself to the whitelist. Run the following snippet:
    function addAddressToWhitelist() public {
      require(!whitelistedAddresses[msg.sender], "Sender has already been whitelisted");
      require(numAddressesWhitelisted < maxWhitelistedAddresses, "More addresses cant be added, limit reached");
      whitelistedAddresses[msg.sender] = true;
      numAddressesWhitelisted += 1;
    }
    
    Okay, that was a lot! Let's break down what we did in this function line by line for you to understand this better.
  • In the very first line, we are checking if the user (client who is calling the function) already exists in the whitelist or not. If he does, we throw an error Sender has already been whitelisted, otherwise we move on to the second line.
  • In the second line, we are checking if the number of addresses whitelisted is less than the total number of whitelist addresses allowed. If it's not, we will throw an error.
  • If both of these conditions pass, we move on to add the client's address to the whitelistedAddresses mapping and in the last line of the function, we increase the numAddressesWhitelisted by 1 to keep track of the number of addresses already whitelisted. And that concludes our smart contract. but we are not done yet, not even close. Now we will deploy our smart contract on the *Rinkeby* test network.

    What's Rinkeby test network?

    An Ethereum test net is a network of nodes used to validate the Ethereum protocol. Before being released on the Ethereum main net, smart contracts are tested on these testnets to ensure that they work as expected. Rinkeby is one such test network.

We are going to create a file named deploy.js inside the scripts folder in our working directory and write the following few lines of javascript to deploy our contract:

const { ethers } = require("hardhat");

async function main() {
    const whitelistContract = await ethers.getContractFactory("Whitelist");
    const deployedWhitelistContract = await whitelistContract.deploy(10);
    await deployedWhitelistContract.deployed();
    console.log(
        "Whitelist Contract Address:",
        deployedWhitelistContract.address
    );
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

So, let's break down what's happening here!

  • Here, we have a main function in which we are using ethers.getContractFactory to get our contract. A ContractFactory is an abstraction that is used to deploy new smart contracts so whitelistContract here is a factory for instances of our Whitelist contract.
  • In the second line of the function, we pass the parameters (that our constructor is waiting for) to the deploy method and wait for the contract to get deployed in the next line.
  • When we run the deploy script, we will retrieve the address of our freshly deployed contract from the console.log statement in our function.

But wait, we can't deploy yet! We still need to add some environment variables and make some changes to the hardhat configuration.

  • To do this, we will create a .env file in our working directory and add the following lines:
    ALCHEMY_API_KEY_URL="add-the-alchemy-key-url-here"
    RINKEBY_PRIVATE_KEY="add-the-rinkeby-private-key-here"
    
  • We are going to use Alchemy API to deploy our contract. Sign up at alchemyapi.io, create a new App in its dashboard, pick Rinkeby as the network, and replace "add-the-alchemy-key-url-here" in our .env file with the key url you'll get from Alchemy to proceed.
  • We also need to provide our RINKEBY account a private key. This will be used for signing the transaction as the owner while deploying our smart contract. You can go to Metamask, select Rinkeby test network, and open Account details to get your private key and replace "add-the-rinkeby-private-key-here" with this key in our .env file.
  • We will need these environment variables in our hardhat configuration file, so let's install dotenv package for the same by running the following command:
    npm install dotenv
    
  • Now open the hardhat.config.js file and replace all the content with the following lines:
    require("@nomiclabs/hardhat-waffle");
    require("dotenv").config({ path: ".env" });
    const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL;
    const RINKEBY_PRIVATE_KEY = process.env.RINKEBY_PRIVATE_KEY;
    module.exports = {
    solidity: "0.8.4",
    networks: {
      rinkeby: {
        url: ALCHEMY_API_KEY_URL,
        accounts: [RINKEBY_PRIVATE_KEY],
      },
    },
    };
    
    So, what we've done here is configure our hardhat environment, which essentially comprises information about the network on which we're deploying our contract and the private key of the account with which the transaction has to be signed.
  • That was all the stuff needed to make our contract ready for deployment. Let's quickly compile the contract and then deploy it. Run the following command for compilation:
    npx hardhat compile
    
  • After successful compiling, we will get a new folder named artifacts. This folder contains all the information that is necessary to deploy and interact with the contract. We will visit this folder later while integrating our deployed contract with our website. Now, we may proceed to deploy our contract. We will do this by running our deploy.js script. Run the following command in your terminal.
    npx hardhat run scripts/deploy.js --network rinkeby
    

Important

Please note that deploying a smart contract (or doing any other transaction on the Ethereum network) requires the user to pay gas fees. For paying this gas fee, we need to have some ethers in our account. But don't worry, we won't need to spend real money. As we are deploying on the Rinkeby test network, test network being the keyword here, we'll need only test ethers which we can get for free from faucets like this one.

And that was it, we have successfully deployed our Whitelist smart contract on the Rinkeby network. In your terminal, you will see the address of the deployed contract. Save this address somewhere for later use in the website section.

Let's move on to the website section now.

Creating the Website

As per our requirement, we need a website that users can visit in order to get them whitelisted. So, now we'll build a simple website with React and NextJS, and we'll integrate our smart contract with it. Don't worry, it's almost the same as integrating an API.

  • Let's initialize a new NextJS app by running the following commands:

    npx create-next-app whitelist-website
    
  • This will create a new folder named whitelist-website and we can initialize a NextJS app inside it. Now run the following commands:

    cd whitelist-website
    npm run dev
    
  • Now go to http://localhost:3000, and your app should be running.

  • Because we want our app to be decentralized, it must be able to connect to different crypto wallets. For this, we will make use of the Web3Modal library. Install web3modal and ethers to allow our app to connect to cryptocurrency wallets as well as our deployed contract. Run the following commands.
    npm install web3modal
    npm install ethers
    

What's Web3Modal?

Web3Modal is a simple library that allows developers to add support for different providers (you may read "wallet" here) such as Metamask, Coinbase Wallet, and others, making it simple for users to connect their wallets in order to engage with your Dapp.

  • Let's get started on creating a React component for our website. Replace the contents of the pages/index.js file with the following code:

    import Head from "next/head";
    import Web3Modal from "web3modal";
    import { providers, Contract } from "ethers";
    import { useEffect, useRef, useState } from "react";
    import { WHITELIST_CONTRACT_ADDRESS, abi } from "../constants";
    export default function Home() {
    
    const [walletConnected, setWalletConnected] = useState(false);
    const [joinedWhitelist, setJoinedWhitelist] = useState(false);
    const [loading, setLoading] = useState(false);
    const [numberOfWhitelisted, setNumberOfWhitelisted] = useState(0);
    const web3ModalRef = useRef();
    
    const getProviderOrSigner = async (needSigner = false) => {
      const provider = await web3ModalRef.current.connect();
      const web3Provider = new providers.Web3Provider(provider);
      const { chainId } = await web3Provider.getNetwork();
      if (chainId !== 4) {
        window.alert("Change the network to Rinkeby");
        throw new Error("Change network to Rinkeby");
      }
      if (needSigner) {
        const signer = web3Provider.getSigner();
        return signer;
      }
      return web3Provider;
    };
    
    const addAddressToWhitelist = async () => {
      try {
        const signer = await getProviderOrSigner(true);
        const whitelistContract = new Contract(
          WHITELIST_CONTRACT_ADDRESS,
          abi,
          signer
        );
        const tx = await whitelistContract.addAddressToWhitelist();
        setLoading(true);
        await tx.wait();
        setLoading(false);
        await getNumberOfWhitelisted();
        setJoinedWhitelist(true);
      } catch (err) {
        console.error(err);
      }
    };
    
    const getNumberOfWhitelisted = async () => {
      try {
        const provider = await getProviderOrSigner();
        const whitelistContract = new Contract(
          WHITELIST_CONTRACT_ADDRESS,
          abi,
          provider
        );
        const _numberOfWhitelisted = await whitelistContract.numAddressesWhitelisted();
        setNumberOfWhitelisted(_numberOfWhitelisted);
      } catch (err) {
        console.error(err);
      }
    };
    
    const checkIfAddressInWhitelist = async () => {
      try {
        const signer = await getProviderOrSigner(true);
        const whitelistContract = new Contract(
          WHITELIST_CONTRACT_ADDRESS,
          abi,
          signer
        );
        const address = await signer.getAddress();
        const _joinedWhitelist = await whitelistContract.whitelistedAddresses(
          address
        );
        setJoinedWhitelist(_joinedWhitelist);
      } catch (err) {
        console.error(err);
      }
    };
    
    const connectWallet = async () => {
      try {
        await getProviderOrSigner();
        setWalletConnected(true);
        checkIfAddressInWhitelist();
        getNumberOfWhitelisted();
      } catch (err) {
        console.error(err);
      }
    };
    
    const renderButton = () => {
      if (walletConnected) {
        if (joinedWhitelist) {
          return (
            <div className={styles.description}>
              Thanks for joining the Whitelist!
            </div>
          );
        } else if (loading) {
          return <button className={styles.button}>Loading...</button>;
        } else {
          return (
            <button onClick={addAddressToWhitelist} className={styles.button}>
              Join the Whitelist
            </button>
          );
        }
      } else {
        return (
          <button onClick={connectWallet} className={styles.button}>
            Connect your wallet
          </button>
        );
      }
    };
    
    useEffect(() => {
      if (!walletConnected) {
        web3ModalRef.current = new Web3Modal({
          network: "rinkeby",
          providerOptions: {},
          disableInjectedProvider: false,
        });
        connectWallet();
      }
    }, [walletConnected]);
    
    return (
      <div>
        <Head>
          <title>Whitelist Dapp</title>
          <meta name="description" content="Whitelist-Dapp" />
          <link rel="icon" href="/favicon.ico" />
        </Head>
        <div>
          <div>
            <h1>Welcome to GeekDevs!</h1>
            <div>
              Its an NFT collection for geeks like us.
            </div>
            <div>
              {numberOfWhitelisted} have already joined the Whitelist
            </div>
            {renderButton()}
          </div>
        </div>
        <footer>
          Made with &#10084; by GeekDevs
        </footer>
      </div>
    );
    }
    
  • That was the entire component we required to allow our users to engage with our smart contract. Let's have a look at it now.
  • First of all, we are managing a number of states to store and view information about the following things.
    • walletConnected - tells us if the user's wallet is connected with our dapp or not.
    • joinedWhitelist - tells us if the user's address is in the whitelist or not.
    • loading - To manage the loading state of the component while creating asynchronous requests.
    • numberOfWhitelisted - To store the current number of whitelisted users.
  • Those were all the states we are maintaining in our React component. Let's move on to the functions now.

    • useEffect - If you're familiar with React, you'll understand how useEffect works. It basically executes the logic contained within the function whenever a component is re-rendered or one of its dependencies changes. So, what we're doing here is checking if the user's wallet is linked to our dapp. If it isn't, we create a new instance of Web3Modal (with the network set to 'Rinkeby'), store it in a reference, and then call the connectWallet function.
    • connectWallet() - This function, as the name implies, connects the user's wallet to our dapp. For this, we use the getProviderOrSigner function, which returns either a signer or a provider depending on the arguments supplied to it. We'll go through them in greater detail when we describe that function. As soon as we acquire one of them, we'll set the walletConnected status to true. We now have access to the provider (or signer). So, we can now perform function calls to our smart contract. We will use the checkIfAddressInWhitelist and getNumberOfWhitelisted functions to update data on the client-side.
    • getProviderOrSigner() - In this function, we extract the provider (or signer, provided the relevant inputs are passed) from the user's metamask wallet. Once we have the provider object, we check to see if the user has chosen the network rinkeby (4 as chainId) on his metamask. If it is set to a different chain, we tell the user. Finally, the provider (or signer) is returned to the asking function.

      What is the difference between a Provider and a Signer?

      Provider is a class in Ethers.js that gives abstract read-only access to the Ethereum blockchain and its status. Signer is an Ethers.js class that has access to your private key. This class is in charge of signing messages and authorizing transactions, such as charging Ether from your account to accomplish operations. So, if we need to call a read-only function on the contract, we can do so with a provider alone, however we will need a signer if our function call changes the state of our contract.

    • checkIfAddressInWhitelist() - Again, as the name suggests, in this function, we are going to query our smart contract for checking if the user's address is in the whitelist or not. For this, we will directly query the public mapping whitelistedAddress that we created in our smart contract. While creating a call to the contract, we need to create a new object of the Contract class provided by ethers.js. While creating this object, we need to pass the following three things as arguments:

      • Address of the deployed contract.
      • abiof the contract.
      • Provider or Signer as per the requirement.

What's ABI?

ABI is an abbreviation for Application Binary Interface. It is a huge array that contains all of the information about our contract that is required for its integration, such as a description of all variables and functions as well as the parameters required to call them. We can locate the ABI inside the artifacts/contracts/Whitelist.sol/Whitelist.json file that we got after compiling the contract. We'll update the state of the component as per the response we get from the contract. Here are some important functions:

- `getNumberOfWhitelisted()` - Once again, as the name implies, this function queries the public variable `numAddressesWhitelisted` from our contract and updates the state of our component as per the response. <br/>

And with that, we move on to our main function, with which the user is going to add himself to the whitelist.

- `addAddressToWhitelist()` - By calling this function, the user will be able to add himself to the whitelist. As the function we are querying here will result in a state change in our contract, we will need a `signer` to make the call. We have also used proper error handling in case the user is already in the whitelist and the contract throws an Error. After adding the user to the whitelist, we are again calling the `getNumberOfWhitelisted` function to update the data on the client-side.

So that was the end of the functions we defined in our component. We're not going to go over the render methods because they're very self-explanatory if you're familiar with React.

- Let's finish our Dapp by adding all the required constants. Create a new folder named `constants` in your working directory. In the constants folder, create a new file named `index.js` and from this file, we will export the `abi` and the `contract address` for use by our component as shown:
```
export const abi = YOUR_ABI;
export const WHITELIST_CONTRACT_ADDRESS=YOUR_WHITELIST_CONTRACT_ADDRESS;
```

Replace YOUR_ABI and YOUR_WHITLIST_CONTRACT_ADDRESS with your ABI and the address of your deployed contract respectively.

And with these constants in place, our application is ready to roll. You can visit localhost:3000 and add yourself to the whitelist now!

Conclusion

In this article, we learned how to create a decentralized application that allows users to whitelist themselves for the upcoming NFT launch. In the following articles, we will take this a step further by launching the NFT collection and creating an actual DAO. If you enjoyed this article, please leave a like and follow for future articles.