How To Build A DAO - Part 2

How To Build A DAO - Part 2

Launching our own NFT Collection to use as DAO membership validator

ยท

17 min read

In the last article, we built a decentralized app to allow our early adopters to whitelist themselves for our NFT sales. You can find it here. Let us now design another smart contract to allow the users to go ahead and actually buy our NFTs.

NFT Collection Dapp

Here's a sneak peek of what we are building:

Screenshot 2022-05-25 at 12.16.27 AM.png

Requirements:

  • There should be no more than ten GeekDev NFTs, each of which should be unique.
  • Users should be able to mint only 1 NFT per transaction.
  • Before the sale opens to the general public, whitelisted customers should enjoy a 5-minute presale period.

Prerequisites:

  • You must have completed our prior tutorial, in which we created a dapp for whitelisting our early users, before proceeding with this article.

We will be utilizing Ownable.sol from Openzeppelin to manage contract ownership. You can learn more about it by reading its documentation.

We will also use an ERC721 extension known as ERC721 Enumerable. This will allow us to maintain track of all tokenIds in the contract as well as those held by a specific address.

Before we begin working on our app, I recommend that you become acquainted with all of the functions provided by each of these contracts.

With that out of the way, let's start with the solidity contract before moving on to the front-end.

Building the Smart Contract

To develop our smart contract, we will use the Hardhat environment, as we did with our last dapp. As we mentioned above, we will need Ownable and ERC721Enumerable contracts in our smart contract, so, let's install the openzeppelin contracts from where we can import both the required contracts later. Use the following code snippet:

npm install @openzeppelin/contracts

Next, we'll need our last deployed Whitelist contract to determine whether or not a certain address is on the whitelist. However, inheriting the entire contract for using only one function will result in a significant gas fee. So, we'll construct an interface for the Whitelist contract with only one function,mapping(address => bool) public whitelistedAddresses;, which will provide us the necessary mapping of whitelisted addresses. So, in the contracts folder, make a new file called Iwhitelist.sol. Enter the following code snippet into your console:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

interface IWhitelist {
    function whitelistedAddresses(address) external view returns (bool);
}

Now, we'll start writing our NFT sale contract.

  • Let's create a new file named GeekDevs.sol in the contracts directory and create a new contract. As previously stated, this contract will inherit the ERC721Enumerable and Ownable contracts. Enter the following code snippet into your console:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./IWhitelist.sol";

contract GeekDevs is ERC721Enumerable, Ownable {

}

Now, we'll declare all the state variables that will be maintaining the state of our smart contract.

- `string _baseTokenURI;` - To store the base URI for all our NFTs.
- `uint256 public _price = 0.01 ether;` - This is the hard-coded price of each NFT.
- `bool public _paused;` - To pause the contract in case of an emergency.
- `uint256 public maxTokenIds = 10;` - To store the maximum number of tokens.
- `uint256 public tokenIds;` - To store the number of tokens that have been minted.
- `IWhitelist whitelist;` - To store an instance of our last deployed Whitelist smart contract.
- `bool public presaleStarted;` - To keep a track of whether the presale has been started or not.
- `uint256 public presaleEnded;` - To store the timestamp of when the presale will end.

Pretty self-explanatory stuff, but we'll go over them further when we'll get to the functional part of our contract.

Next, create a function modifier named onlyWhenNotPaused which will allow another function (with which it's used) to be executed only when the _paused is false in the contract. Enter the following code snippet into your console:

modifier onlyWhenNotPaused {
    require(!_paused, "Contract currently paused");
    _;
}

What's a function modifier in Solidity?

Function Modifiers are used to change how a function behaves. To add a requirement to a function, for example. First, we design a modifier. After inserting the function body, the special sign _; appears in the definition of a modifier, which returns control back to the main function.

We will also use another function modifier called onlyOwner within our contract, which comes from the Ownable contract and ensures that a function is only callable by the contract's owner.

Let's move further and create a constructor for our contract:

constructor (string memory baseURI, address whitelistContract) ERC721("GeekDevs", "GD") {
    _baseTokenURI = baseURI;
    whitelist = IWhitelist(whitelistContract);
}

This constructor will be called automatically while deploying our smart contract and will initialize its state With this, let's move on to the functions that our contract consists of.

Our smart contract contains a number of functions. Let us go over them one by one.

- `startPresale` - As the name suggests, this function starts the presale of our tokens. We are having the function modifier `onlyOwner` attached with this function to ensure that only the owner of the contract is able to start the presale. 
```
function startPresale() public onlyOwner {
     presaleStarted = true;
     presaleEnded = block.timestamp + 5 minutes;
 }
```

Going through the body of the function, we can clearly see that it does the following two things:

    1. Sets the state variable `presaleStarted` to **true**.
    2. Sets the `presaleEnded` to the time 5 minutes later than the current timestamp.

> Noticed the super cool syntax used while setting the presale end time? Learn more about that [here](https://docs.soliditylang.org/en/v0.8.13/units-and-global-variables.html#time-units).

Moving on to the next function.

- `presaleMint` - This function is supposed to be called in order to mint NFTs during the presale period. Here, we will be using our function modifier `onlyWhenNotPaused` to ensure it's not called while the contract is in paused state.
```
function presaleMint() public payable onlyWhenNotPaused {
    require(presaleStarted && block.timestamp < presaleEnded, "Presale is not running");
    require(whitelist.whitelistedAddresses(msg.sender), "You are not whitelisted");
    require(tokenIds < maxTokenIds, "Exceeded maximum GeekDevs supply");
    require(msg.value >= _price, "Ether sent is not correct");
    tokenIds += 1;
    _safeMint(msg.sender, tokenIds);
 }
```

Let's go through this function line by line:

    1. The first line of the function checks if the presale has been started and not ended yet. If not, it throws an error.
    2. The second line checks if the user who called the function is a whitelisted user or not. If he's not, it throws an error.
    3. The third line ensures that the number of tokens already minted is not more than the maximum NFTs allowed to be minted.
    4. The fourth line ensures that the ether sent by the user is equal to or more than the price of one token.
    5. If all the above conditions are satisfied, a new token is minted to the user with a unique id which is 1 more than the previous minted token's id. To keep track of the same, `tokenIds` variable is incremented by 

Moving on to the next function.

- `mint` - This function is supposed to be called in order to mint NFTs after the presale period. Here too, we will be using our function modifier `onlyWhenNotPaused` to ensure it's not called while the contract is in paused state. Enter the following code snippet into your console:
```
function mint() public payable onlyWhenNotPaused {
     require(presaleStarted && block.timestamp >=  presaleEnded, "Presale has not ended yet");
     require(tokenIds < maxTokenIds, "Exceed maximum GeekDevs supply");
     require(msg.value >= _price, "Ether sent is not correct");
     tokenIds += 1;
     _safeMint(msg.sender, tokenIds);
 }
```

Breaking it down, it's a lot similar to the previously explained presaleMint function. The only thing that's different is that the function will run if the timestamp of the block is later than the presaleEnded timestamp. Also, we are not going to check for the whitelisted address in this function for obvious reasons.

Let us take a look at some of these functions:

- `_baseURI` - This is going to be an internal function that will be called by other functions only and it's going to return the base URI of our NFTs.
```
function _baseURI() internal view virtual override returns (string memory) {
     return _baseTokenURI;
}
```
- `setPaused` - In the event of an emergency, this function will be utilised to pause our contract. This privilege can only be granted to the owner, which is why we'll use the `onlyOwner` function modifier with it.
```
function setPaused(bool val) public onlyOwner {
     _paused = val;
}
```

With this, let's move on to our last function.

- `withdraw` - As the declaration suggests, this function will be called by the owner of the contract to withdraw all the funds collected in the contract to his own account. 
```
function withdraw() public onlyOwner  {
     address _owner = owner();
     uint256 amount = address(this).balance;
     (bool sent, ) =  _owner.call{value: amount}("");
     require(sent, "Failed to send Ether");
}
```

That concludes the coding part of our smart contract. Let's go ahead and deploy it. Create a new file deploy.js and copy the following code.

const { ethers } = require("hardhat");
require("dotenv").config({ path: ".env" });
const { WHITELIST_CONTRACT_ADDRESS, METADATA_URL } = require("../constants");

async function main() {
  const whitelistContract = WHITELIST_CONTRACT_ADDRESS;
  const metadataURL = METADATA_URL;

  const geekDevsContract = await ethers.getContractFactory("GeekDevs");

  const deployedGeekDevsContract = await geekDevsContract.deploy(
    metadataURL,
    whitelistContract
  );

  console.log(
    "GeekDevs Contract Address:",
    deployedGeekDevsContract.address
  );
}

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

It's a simple script where we provide the address of our previously deployed Whitelist contract and the URL for our NFTs' metadata as inputs while deploying our contract. If you find anything difficult to grasp, you can refer to our last article.
Also, you can see we are importing both the input constants. So, let's declare those. Create a new file named constants.js in the root directory and export the following constants.

const WHITELIST_CONTRACT_ADDRESS = "address-of-the-whitelist-contract";
const METADATA_URL = "nft-collection-metadata-url";

module.exports = { WHITELIST_CONTRACT_ADDRESS, METADATA_URL };

Go ahead and replace the address-of-the-whitelist-contract with your already deployed whitelist contract address that we deployed in our last article.
As for the nft-collection-metadata-url, here, we will provide the URL of our NFTs' metadata. We will be creating an API later for this while working on our front-end website. So, for now, we can leave this as it is, and will update it later when we get the URL after hosting our website.

What is Metadata URL?

We plan to make our NFTs available on the OpenSea website (essentially on its testnet version). To make an NFT viewable on OpenSea, it needs to hold certain data. This data, we are going to provide via an API that we will develop while working on the front-end part of our dapp. The term metadata URL refers to the same API's URL, which, when invoked with a specific token's id, will return the token's metadata.

  • The final thing to get our deployment environment ready is updating our hardhat configuration. So, open hardhat.config.js file and replace its content with the following lines of code.
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],
    },
  },
};

We are going to leave it here. We will finish deploying our front-end and then return with the metadata URL to update in our contract and deploy it.

Creating the Website

Let's boot up a new NextJS project for our dapp's front-end and equip it with the necessary libraries as well.

npx create-next-app nft-collection
cd nft-collection
npm run dev
// install web3modal and ethers
npm install web3modal
npm install ethers

With that sorted, let's move on to the actual page that the users will visit in order to buy the tokens. Open pages/index.js and replace its contents with the following React component.

import { Contract, providers, utils } from "ethers";
import Head from "next/head";
import React, { useEffect, useRef, useState } from "react";
import Web3Modal from "web3modal";
import { abi, NFT_CONTRACT_ADDRESS } from "../constants";
export default function Home() {

  const [walletConnected, setWalletConnected] = useState(false);
  const [presaleStarted, setPresaleStarted] = useState(false);
  const [presaleEnded, setPresaleEnded] = useState(false);
  const [loading, setLoading] = useState(false);
  const [isOwner, setIsOwner] = useState(false);
  const [tokenIdsMinted, setTokenIdsMinted] = useState("0");
  const web3ModalRef = useRef();

  const presaleMint = async () => {
    try {
      const signer = await getProviderOrSigner(true);
      const nftContract = new Contract(
        NFT_CONTRACT_ADDRESS,
        abi,
        signer
      );
      const tx = await nftContract.presaleMint({
        value: utils.parseEther("0.01"),
      });
      setLoading(true);
      await tx.wait();
      setLoading(false);
      window.alert("You successfully minted a GeekDev token!");
    } catch (err) {
      console.error(err);
    }
  }

  const publicMint = async () => {
    try {
      const signer = await getProviderOrSigner(true);
      const nftContract = new Contract(
        NFT_CONTRACT_ADDRESS,
        abi,
        signer
      );
      const tx = await nftContract.mint({
        value: utils.parseEther("0.01"),
      });
      setLoading(true);
      await tx.wait();
      setLoading(false);
      window.alert("You successfully minted a GeekDev token!");
    } catch (err) {
      console.error(err);
    }
  };

  const connectWallet = async () => {
    try {
      await getProviderOrSigner();
      setWalletConnected(true);
    } catch (err) {
      console.error(err);
    }
  };

  const startPresale = async () => {
    try {
      const signer = await getProviderOrSigner(true);
      const nftContract = new Contract(
        NFT_CONTRACT_ADDRESS,
        abi,
        signer
      );
      const tx = await nftContract.startPresale();
      setLoading(true);
      await tx.wait();
      setLoading(false);
      await checkIfPresaleStarted();
    } catch (err) {
      console.error(err);
    }
  };

  const checkIfPresaleStarted = async () => {
    try {
      const provider = await getProviderOrSigner();
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
      const _presaleStarted = await nftContract.presaleStarted();
      if (!_presaleStarted) {
        await getOwner();
      }
      setPresaleStarted(_presaleStarted);
      return _presaleStarted;
    } catch (err) {
      console.error(err);
      return false;
    }
  };

  const checkIfPresaleEnded = async () => {
    try {
      const provider = await getProviderOrSigner();
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
      const _presaleEnded = await nftContract.presaleEnded();
      const hasEnded = _presaleEnded.lt(Math.floor(Date.now() / 1000));
      if (hasEnded) {
        setPresaleEnded(true);
      } else {
        setPresaleEnded(false);
      }
      return hasEnded;
    } catch (err) {
      console.error(err);
      return false;
    }
  };

  const getOwner = async () => {
    try {
      const provider = await getProviderOrSigner();
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
      const _owner = await nftContract.owner();
      const signer = await getProviderOrSigner(true);
      const address = await signer.getAddress();
      if (address.toLowerCase() === _owner.toLowerCase()) {
        setIsOwner(true);
      }
    } catch (err) {
      console.error(err.message);
    }
  };

  const getTokenIdsMinted = async () => {
    try {
      const provider = await getProviderOrSigner();
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
      const _tokenIds = await nftContract.tokenIds();
      setTokenIdsMinted(_tokenIds.toString());
    } catch (err) {
      console.error(err);
    }
  };

  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;
  };

  useEffect(() => {
    if (!walletConnected) {
      web3ModalRef.current = new Web3Modal({
        network: "rinkeby",
        providerOptions: {},
        disableInjectedProvider: false,
      });
      connectWallet();
      const _presaleStarted = checkIfPresaleStarted();
      if (_presaleStarted) {
        checkIfPresaleEnded();
      }
      getTokenIdsMinted();
      const presaleEndedInterval = setInterval(async function () {
        const _presaleStarted = await checkIfPresaleStarted();
        if (_presaleStarted) {
          const _presaleEnded = await checkIfPresaleEnded();
          if (_presaleEnded) {
            clearInterval(presaleEndedInterval);
          }
        }
      }, 5 * 1000);
      setInterval(async function () {
        await getTokenIdsMinted();
      }, 5 * 1000);
    }
  }, [walletConnected]);

  const renderButton = () => {
    if (!walletConnected) {
      return (
        <button onClick={connectWallet}>
          Connect your wallet
        </button>
      );
    }
    if (loading) {
      return <button>Loading...</button>;
    }
    if (isOwner && !presaleStarted) {
      return (
        <button onClick={startPresale}>
          Start Presale!
        </button>
      );
    }
    if (!presaleStarted) {
      return (
        <div>
          <div>Presale hasn't started!</div>
        </div>
      );
    }
    if (presaleStarted && !presaleEnded) {
      return (
        <div>
          <div>
            Presale has started!!! If your address is whitelisted, Mint a
            GeekDev ๐Ÿฅณ
          </div>
          <button onClick={presaleMint}>
            Presale Mint ๐Ÿš€
          </button>
        </div>
      );
    }
    if (presaleStarted && presaleEnded) {
      return (
        <button onClick={publicMint}>
          Public Mint ๐Ÿš€
        </button>
      );
    }
  };

  return (
    <div>
      <Head>
        <title>Geek Devs</title>
        <meta name="description" content="nft-Dapp" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div>
        <div>
          <h1>Welcome to Geek Devs!</h1>
          <div>
            Its an NFT collection for geeks like us.
          </div>
          <div>
            {tokenIdsMinted}/10 have been minted
          </div>
          {renderButton()}
        </div>
        <div>
          <img src="./geekdevs/0.svg" />
        </div>
      </div>
      <footer>
        Made with &#10084; by GeekDevs
      </footer>
    </div>
  );
}

That was the entire component we needed to allow our users to buy themselves our tokens. All the things here are pretty self-explanatory and you should face no trouble if you are familiar with React application development.

In our component, we are importing the address and abi of our smart contract. So, let's create a new file constants.js in the root of our app and add these values here.

export const abi = []
export const NFT_CONTRACT_ADDRESS = "nft_contract_address"

At this point, we just need to replace these placeholder constant values with the right ones but as we know, we haven't really compiled and deployed our contract yet, so, we don't have these values with us till now. And if you recall why we didn't deploy our contract, it was because we were awaiting the metadata URL of our tokens. So, let's prepare the metadata URL.

To create a metadata URL, first of all, we need to have a domain for our web app.

- Create a new repo for our NextJS app in your Github account and push the whole NextJS project codebase into it.
- Go to [Vercel](https://vercel.com) and log in with the same Github account. 
- Click on the `New Project` button and import the newly created project's repo.
- In the next step, we can choose the project name, framework, and environment variables. We just need to type in a project name, and set the framework to NextJS.
- Keep in mind that this project name is going to form our app's domain. For instance, if you type in ***nft-sale*** as  your project name, after the completion of the hosting process, your domain for the app is going to be ***nft-sale.vercel.app***.

Now that we have got the domain for our web app, let's go back to where we left off our smart contract.

  • Go to your smart contract's directory and replace the nft-collection-metadata-url with our api URL.
  • If your domain was nft-sale.vercel.app, you need to put nft-sale.vercel.app/api/ in place of the nft-collection-metadata-url.

We are going to create this /api/ endpoint in the coming steps.

With that, our smart contract has got the last piece it was waiting for. Time to deploy it on the Rinkeby test net. Similar to the last contract, we just need to run two commands, one for compilation and another one for deployment.

npx hardhat compile
npx hardhat run scripts/deploy.js --network rinkeby

After the compilation and deployment, we'll get the contract's abi as well as its address. We can go ahead and add these to our NextJS app's constants file.

After adding the right constants, push the code to Github and wait for the Vercel to deploy the new code.

Right now, our dapp should work as expected i.e. contract's owner should be able to start the presale and users should be able to mint themselves our GeekDev NFTs. But one last thing still remains. We promised that we will be able to view our NFTs on the Opensea website. So, let's go ahead and get done with the API we need for that.

Making our NFT collection viewable on OpenSea

To make our NFTs viewable on Opensea, we should have a multimedia file associated with our token. In this case, we are going with some SVGs.

  • Create 10 unique SVG files and add them to public/geekdevs/ folder in our NextJS project with their names the same as their expected tokenIds. That means we need to have 10 different SVG files with names such as 0.svg, 1.svg, all the way to 9.svg.
  • Now create a new file named [tokenId].js under the pages/api folder. We are going to write the code for our API here.
  • Add the following code for the handler here.
    export default function handler(req, res) {
    const tokenId = req.query.tokenId;
    const image_url =
      "https://raw.githubusercontent.com/YOUR-GITHUB-USERNAME/PROJECT-REPO-NAME/main/public/geekdevs/";
    res.status(200).json({
      name: "GeekDev #" + tokenId,
      description: "GeekDev is a collection of NFTs for geeks like us",
      image: image_url + tokenId + ".svg",
    });
    }
    
    Here, we have created an API endpoint which will be called by Opensea to retrieve the metadata for the NFTs. As we can see, this metadata contains three data points.
    • name of the token
    • description which will be the same for all the tokens for now.
    • image which will contain the image URL for the particular token.

Push the latest code to GitHub and wait for Vercel to deploy it. And that completes it all. You can now visit the hosted app's URL and start the presale as the owner of the dapp. If you had your address registered in the whitelist, you can go ahead and buy NFTs during the presale or if not, you can wait for the presale to get over in five minutes after which you can buy the tokens.

After minting a token, you can visit here to take a look at your freshly minted NFT. Don't forget to replace your-nft-contract-address and the value 1 with your contract's address and your minted token's id respectively.

Conclusion

And there you have it! As promised in the last part of this series, we have built our own NFT collection and a platform for users to come and buy those NFTs in this part. In the next part of the series, we will take this another step further and create a full-fledged DAO where our NFT holders will be the members of the DAO and will be able to make decisions for it. Till then, be safe and keep exploring. If you enjoyed this article, you can leave a like to show your appreciation :) and follow for future articles.

ย