Build a DAO With a Treasury and a Governance Token

Build a DAO With a Treasury and a Governance Token - thirdweb Guides

⚠️ Warning: This guide currently uses v4 of the Connect SDK. For v5 (latest) code snippets, please check out our documentation while this guide is being updated. ⚠️

In this guide, we'll show you how to use the vote contract to set up governance and create proposals.

Everything we do in this tutorial has been taken from our project Build your own DAO with just JavaScript on buildspace.

Let's build our DAO

So you want to build a DAO, but... what is a DAO? A DAO is a community of people with a shared bank account.

Decisions around how that bank account is used are made by voting on different proposals that members create. When a proposal gets enough votes, it is executed on-chain!

As a pre-requisite, we need a governance token so users can vote on proposals. If you haven't deployed a token yet, you can follow this guide.

Once we do, we can start working with our Vote contract.

Setting up our local environment

First, we'll need to set up our local environment and install the SDK.

You can use the thirdweb cli to do this:

npx thirdweb@latest create app
Create a new node.js app using npx thirdweb create app

Deploy a governance contract

We want to let people vote on proposals, automatically count the votes, and let any member execute the proposal on-chain.

Create a vote.js file, and make sure you initialize the SDK before you copy and paste the code.

To deploy a vote contract using the SDK, we can make use of the sdk.deployer,
which can create any of our pre-built contracts!

To deploy from your wallet, you'll first need to export your wallet's private key.

Learn how to export your private key from your wallet.

Ensure you store and access your private key securely.

  • Never commit any file that may contain your private key to your source control.

Learn more about securely accessing your private key.

import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import "dotenv/config";

const NETWORK = "mumbai";
// Learn more about securely accessing your private key: https://portal.thirdweb.com/web3-sdk/set-up-the-sdk/securing-your-private-key
const sdk = ThirdwebSDK.fromPrivateKey("<your-private-key-here>", NETWORK);

Now we're ready to deploy the Vote!

Replace <TOKEN_ADDRESS> with the address of the token you just created.

// SDK initialization here

(async () => {
  try {
    const voteContractAddress = await sdk.deployer.deployVote({
      // Give your governance contract a name.
      name: "My amazing DAO",

      // This is the location of our governance token, our ERC-20 contract!
      voting_token_address: "INSERT_TOKEN_ADDRESS",

      // These parameters are specified in number of blocks.
      // Assuming block time of around 13.14 seconds (for Ethereum)

      // After a proposal is created, when can members start voting?
      // For now, we set this to immediately.
      voting_delay_in_blocks: 0,

      // How long do members have to vote on a proposal when it's created?
      // we will set it to 1 day = 6570 blocks
      voting_period_in_blocks: 6570,

      // The minimum % of the total supply that need to vote for
      // the proposal to be valid after the time for the proposal has ended.
      voting_quorum_fraction: 0,

      // What's the minimum # of tokens a user needs to be allowed to create a proposal?
      // I set it to 0. Meaning no tokens are required for a user to be allowed to
      // create a proposal.
      proposal_token_threshold: 0,
    });

    console.log(
      "✅ Successfully deployed vote contract, address:",
      voteContractAddress
    );
  } catch (err) {
    console.error("Failed to deploy vote contract", err);
  }
})();

We can go ahead and execute this file to deploy our Vote contract!

node vote.mjs

This will give this log the following to our terminal:

✅ Successfully deployed vote contract, address: <VOTE_CONTRACT_ADDRESS>

Copy that Vote contract address, we are going to need it! If you lose it, you can always get it from your thirdweb dashboard.

Setup the treasury

Then, create a treasury.mjs file, and make sure the SDK is initialized before you try to use it.

// SDK initialization here

(async () => {
  try {
    // This is our governance contract.
    const vote = await sdk.getContract("INSERT_VOTE_ADDRESS", "vote");
    // This is our ERC-20 contract.
    const token = await sdk.getContract("INSERT_TOKEN_ADDRESS", "token");
    // Give our treasury the power to mint additional token if needed.
    await token.roles.grant("minter", vote.getAddress());

    console.log(
      "Successfully gave vote contract permissions to act on token contract"
    );
  } catch (error) {
    console.error(
      "failed to grant vote contract permissions on token contract",
      error
    );
    process.exit(1);
  }

  try {
    // This is our governance contract.
    const vote = await sdk.getContract("INSERT_VOTE_ADDRESS", "vote");
    // This is our ERC-20 contract.
    const token = await sdk.getContract("INSERT_TOKEN_ADDRESS", "token");
    // Grab our wallet's token balance, remember -- we hold basically the entire supply right now!
    const ownedTokenBalance = await token.balanceOf(process.env.WALLET_ADDRESS);

    // Grab 90% of the supply that we hold.
    const ownedAmount = ownedTokenBalance.displayValue;
    const percent90 = (Number(ownedAmount) / 100) * 90;

    // Transfer 90% of the supply to our voting contract.
    await token.transfer(vote.getAddress(), percent90);

    console.log(
      "✅ Successfully transferred " + percent90 + " tokens to vote contract"
    );
  } catch (err) {
    console.error("failed to transfer tokens to vote contract", err);
  }
})();

What's going on here? We're moving 90% of the total tokens we hold to the Vote contract, basically giving up our dictatorship and letting the DAO decide how those tokens get spent.

Before doing this, we could've done an airdrop to our NFT holders, or distributed tokens in another way, it's up to you how you want to do it!

Let's execute this!

Remember to populate the vote address, token address, and wallet address before running the script.

node treasury.mjs

This will give this log the following to our terminal:

✅ Successfully gave vote contract permissions to act on token contract
✅ Successfully transferred tokens to vote contract

Create your DAO first proposals

This is the fun part.

Create a proposals.mjs file, and make sure the SDK is initialized before you try to use it.

import { ethers } from "ethers";

// SDK initialization here

(async () => {
  try {
    // This is our governance contract.
    const vote = await sdk.getContract("INSERT_VOTE_ADDRESS", "vote");
    // This is our ERC-20 contract.
    const token = await sdk.getContract("INSERT_TOKEN_ADDRESS", "token");
    // Create proposal to mint 420,000 new token to the treasury.
    const amount = 420_000;
    const description =
      "Should the DAO mint an additional " +
      amount +
      " tokens into the treasury?";
    const executions = [
      {
        // Our token contract that actually executes the mint.
        toAddress: token.getAddress(),
        // Our nativeToken is ETH. nativeTokenValue is the amount of ETH we want
        // to send in this proposal. In this case, we're sending 0 ETH.
        // We're just minting new tokens to the treasury. So, set to 0.
        nativeTokenValue: 0,
        // We're doing a mint! And, we're minting to the vote, which is
        // acting as our treasury.
        // in this case, we need to use ethers.js to convert the amount
        // to the correct format. This is because the amount it requires is in wei.
        transactionData: token.encoder.encode("mintTo", [
          vote.getAddress(),
          ethers.utils.parseUnits(amount.toString(), 18),
        ]),
      },
    ];

    await vote.propose(description, executions);

    console.log("✅ Successfully created proposal to mint tokens");
  } catch (error) {
    console.error("failed to create first proposal", error);
    process.exit(1);
  }

  try {
    // This is our governance contract.
    const vote = await sdk.getContract("INSERT_VOTE_ADDRESS", "vote");
    // This is our ERC-20 contract.
    const token = await sdk.getContract("INSERT_TOKEN_ADDRESS", "token");
    // Create proposal to transfer ourselves 6,900 tokens for being awesome.
    const amount = 6_900;
    const description =
      "Should the DAO transfer " +
      amount +
      " tokens from the treasury to " +
      process.env.WALLET_ADDRESS +
      " for being awesome?";
    const executions = [
      {
        // Again, we're sending ourselves 0 ETH. Just sending our own token.
        nativeTokenValue: 0,
        transactionData: token.encoder.encode(
          // We're doing a transfer from the treasury to our wallet.
          "transfer",
          [
            process.env.WALLET_ADDRESS,
            ethers.utils.parseUnits(amount.toString(), 18),
          ]
        ),
        toAddress: token.getAddress(),
      },
    ];

    await vote.propose(description, executions);

    console.log(
      "✅ Successfully created proposal to reward ourselves from the treasury, let's hope people vote for it!"
    );
  } catch (error) {
    console.error("failed to create second proposal", error);
  }
})();

We're actually creating two new proposals for members to vote on:

  1. We're creating a proposal that allows the treasury to mint 420,000 new tokens. You can see we do a "mint" in the code. Maybe the treasury is running low and we want more tokens to award members. Remember, earlier we gave our voting contract the ability to mint new tokens — so this works! It's a democratic treasury. If your members think this proposal is stupid and vote “NO”, this simply won't pass!
  2. We're creating a proposal that transfer 6,900 token to our wallet from the treasury. You can see we do a "transfer" in the code.

Maybe we did something good and want to be rewarded for it! In the real world, you'd usually create proposals to send other people tokens. For example, maybe someone helped code up a new website for the DAO and wants to be rewarded for it. You can transfer them tokens!

I want to make a note on nativeTokenValue. Let's say we wanted to have our proposal say, “We'd like to reward this person for helping us with marketing with 2500 governance token and 0.1 ETH”. This is really cool! It means you can reward people with both ETH and governance token — best of both worlds. Note: That 0.1 ETH would need to be in our treasury if we wanted to send it!

Let's execute this!

Remember to replace the values of <WALLET_TO_TRANSFER_TOKENS_TO>, as well as the vote and token contract addresses.

node proposals.mjs

This will give this log the following to our terminal:

✅ Successfully created proposal to mint tokens
✅ Successfully created proposal to reward ourselves from the treasury

We're done!

And that's it! Now your proposals have been created, then you would need to create a frontend so people can vote on these.

You can see more info on how to do it in this link, in which we built an entire frontend so people could vote on proposals.

If you want lengthier explanations about everything we did in this tutorial, you can check "Build your own DAO with just JavaScript", our collaboration with buildspace, in this link.