How to Build a DAO with Truffle

Alvin LeeAlvin Lee
11 min read

Since Satoshi Nakomoto introduced Bitcoin and the blockchain in his seminal 2009 whitepaper, the number of innovations brought to life has been staggering. Today, we have cryptocurrencies, NFTs, and other digital assets that can be created, owned, and transferred without intermediaries.

However, perhaps one of the most significant advancements achieved by blockchain and smart contracts is governance and the future of work. The aforementioned technologies make decentralized autonomous organizations, also referred to as DAOs possible.

DAOs are community-led entities with no central authority. Think corporation, but global, controlled and owned by each member, and with smart contracts to enforce rules on-chain. DAOs allow people from around the world to come together and achieve a common goal, and even get paid in the process for their contributions.

DAOs have been used to maintain and govern some of the most exciting Web3 projects of our time. Some examples include Compound DAO, which decides on how its namesake lending protocol operates; KlimaDAO, which tackles climate change through a carbon-assets backed token; and FWB, which aims at shaping the cultural landscape of web3.

In this article, we’ll look at how DAOs work, the types of DAOs that exist, and the components of a DAO. Then, we’ll build our own fully functional, on-chain governance DAO using Ganache and the Truffle Suite.

How do DAOs work?

As stated earlier, DAOs are a mechanism that allow multiple people to come together and work towards a common goal. DAO members achieve this by using a governance token to vote and conduct transactions on the blockchain.

Under the hood, DAOs are nothing but a collection of smart contracts. Even the simplest DAOs typically have the following three contracts:

  1. The Governance Token
    An ERC-20 or an ERC-721 token with voting power. In most cases, each token counts for one vote. The more tokens you have, the more influence you can exert on a particular decision.

  2. The Governor
    A smart contract responsible for creating proposals, allowing members to vote on proposals using their tokens, and executing proposals based on voting results.

  3. The Main DAO or Treasury
    The smart contract holding money and data of the DAO modified by the Governor based on voting results.

Broadly speaking, DAOs come in two flavors: on-chain governance and off-chain governance.

The former typically conducts voting on-chain and requires participants to pay gas in order to cast their votes. The latter conducts voting off-chain and eliminates gas fees for its participants — but introduces non-chain elements that now must be trusted.

Building a DAO with on-chain governance

Now that we know all about DAOs, let’s build our own!

In this tutorial, we will build a simple but fully-functional DAO using the Truffle Suite. We will do so by building the following:

  1. A smart contract that implements an ERC-20 governance token

  2. A governor that imparts voting powers to the aforementioned token, and allows the creation, voting, and execution of proposals.

  3. A DAO contract that holds the state and funds of the DAO

  4. A simulation script that airdrops governance tokens, creates a proposal, conducts voting, and executes voting results. Here, we will create a proposal to send several DAO tokens to a contributor as a reward.

Step 1: Install NPM and Node

We will build our project using node and npm. In case you don’t have these installed on your local machine, you can do so here.

To ensure everything is working correctly, run the following command:

If all goes well, you should see a version number for node.

Step 2: Create a Node Project and Install Dependencies

Let’s set up an empty project repository by running the following commands:

$ mkdir truffle-dao && cd truffle-dao
$ npm init -y

We will be using Truffle, a world-class development environment and testing framework for EVM smart contracts, to build and deploy our smart contract. It will give us all the tools we need to complete our tutorial.

Install Truffle by running:

$ npm install -g truffle

We can now create a barebones Truffle project by running the following command:

$ truffle init

To check if everything works properly, run:

$ truffle - version

We now have Truffle successfully configured. Next, let’s install the OpenZeppelin contracts package. This is a collection of battle-tested smart contracts that will give us access to the ERC-20 governance token and an on-chain Governor base implementation, on top of which we will build our component contracts.

$ npm install -s @openzeppelin/contracts

Remember, smart contracts are immutable. Once they are deployed on mainnet, they can’t be modified. So it’s important to make sure they work before deploying. To get around this, we will test the functionalities of our DAO on a local instance of the Ethereum blockchain. This is made possible by Truffle’s Ganache — a one-click way to create a personal and local Ethereum blockchain.

Install it globally by running:

$ npm install -g ganache

Step 3: Create the ERC-20 Governance Token

We’re ready to start coding!

Open the truffle project in your favorite code editor (such as VS Code). In the contracts folder, create a new file and call it GovToken.sol.

We will write a simple ERC-20 contract that implements voting functionality. We’ll also allow the owner of the contract to mint new tokens without any restrictions on supply.

Ideally, we should transfer the ownership of this token contract to the main DAO contract which we will create in a later step and allow DAO members to vote on new token creation. In a typical DAO, it is not a good practice for a single person/wallet to have all the control. However, to keep things simple and fast, we will stick to assigning ownership to the deploying wallet.

Add the following code to GovToken.sol :

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

contract GovToken is ERC20, Ownable, ERC20Permit, ERC20Votes {
  constructor() ERC20("GovToken", "GT") ERC20Permit("GovToken") {}

  function mint(address to, uint256 amount) public onlyOwner {
    _mint(to, amount);
  }

  // The following functions are overrides required by Solidity.
  function _afterTokenTransfer(address from, address to, uint256 amount)
    internal
    override(ERC20, ERC20Votes)
  {
    super._afterTokenTransfer(from, to, amount);
  }

  function _mint(address to, uint256 amount)
    internal
    override(ERC20, ERC20Votes)
  {
  super._mint(to, amount);
  }

  function _burn(address account, uint256 amount)
    internal
    override(ERC20, ERC20Votes)
  {
  super._burn(account, amount);
  }
}

Make sure the contract is compiling correctly by running:

$ truffle compile

Step 4: Create the Governor

In the contracts folder, create a new file and call it DaoGovernor.sol.

We will create a Governor contract that implements OpenZeppelin’s on-chain governance which is inspired by Compound’s governance model. This contract will allow us to create proposals, vote on proposals, and execute any piece of code on the blockchain that it resides in.

Add the following code to DaoGovernor.sol.

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

import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";

contract DaoGovernor is Governor, 
         GovernorCountingSimple, 
         GovernorVotes,
         GovernorVotesQuorumFraction {

  constructor(IVotes _token)
    Governor("DaoGovernor")
    GovernorVotes(_token)
    GovernorVotesQuorumFraction(4)
  {}

  function votingDelay() public pure override returns (uint256) {
    return 1; // 1 block
  }

  function votingPeriod() public pure override returns (uint256) {
    return 5; // 5 blocks
  }

  // The following functions are overrides required by Solidity.
  function quorum(uint256 blockNumber)
    public
    view
    override(IGovernor, GovernorVotesQuorumFraction)
    returns (uint256)
  {
    return super.quorum(blockNumber);
  }
}

Here are a few things to note about our Governor:

  1. The constructor takes in a token as an argument. We will pass the governance token created in the previous step here to give it voting powers in this Governor.

  2. We have set the votingDelay to one block. This means that voting for a proposal will begin one block after the block in which it was created.

  3. We have set the votingPeriod to five blocks. This means that voting will be open only for a duration of five blocks (~1 minute). In real-world DAOs, this figure is typically set to one week.

  4. The quorumFraction has been set to 4%. This means at least 4% of all tokens must vote yes for a proposal to pass.

Feel free to experiment with these numbers, and modify them according to your use case.

Once again, make sure the contract is compiling correctly by running:

$ truffle compile

Step 5: Create the Main DAO Contract

The final contract we need to create represents the main DAO and typically holds the state and/or funds of the DAO. This DAO is also usually owned by the Governor contract, and functions that transfer funds or modify state are marked onlyOwner.

We will keep things simple by storing just a single number in our DAO contract, and writing a function that modifies its value.

Create a new file in the contracts folder named Dao.sol and add the following code:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.9;

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

contract Dao is Ownable {
  uint public daoVal;

  constructor(address _govContract) {
    transferOwnership(_govContract);
  }

  function updateValue(uint256 _newVal) public onlyOwner {
    daoVal = _newVal;
  }
}

Notice we transfer the ownership of the contract to our Governor contract when deploying.

As always, make sure the contract is compiling by running:

$ truffle compile

Step 6: Update the Truffle Config file

In order to deploy and run our contracts on Ganache, we need to update the truffle.config.js file with the following:

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*"
    },
  },
  compilers: {
    solc: {
      version: "0.8.13",
    }
  }
};

Step 7: Simulate the DAO

We’re all set to deploy the DAO, and execute proposals through it.

Normally, it would be hard to test a DAO on a testnet like Goerli because its core functionalities usually involved multiple parties collaborating and working together over an extended period of time. Fortunately, with Ganache, we can simulate time travel and multiple wallets by spinning up an Ethereum instance locally.

Spin up a ganache instance by opening a new terminal and running:

$ ganache

In the migrations folder, create a new file 1_simulation.js. These are the steps we will simulate:

  1. Create three wallets: the owner, voter 1, and voter 2.

  2. Deploy the ERC-20 contract.

  3. Mint 100 tokens to three wallets, and self-delegate the voting power of each token.

  4. Deploy the Governor contract with the ERC-20 contract as the voting token.

  5. Deploy the DAO contract with ownership assigned to the Governor.

  6. Owner creates a proposal to change the DAO value to 42.

  7. Voter 1 and Voter 2 votes in favor.

  8. Owner executes the proposal after it succeeds.

There is a lot going on here! Add the following code to the aforementioned file:

// Import necessary libraries
const ethers = require('ethers');

// Proposal states as defined by OpenZeppelin/Compound
oz_states = ['Pending', 'Active', 'Canceled', 'Defeated',
             'Succeeded', 'Queued', 'Expired', 'Executed'];

// Ganache localhost URL
const url = "http://localhost:8545";
const provider = new ethers.providers.JsonRpcProvider(url);

// Helper function to fast forward blocks
const ff_blocks = async (n) => {
  for (let i = 0; i < n; i++) {
    await provider.send('evm_mine');
  }
  console.log("\nMoved", n, "blocks\n");
}

// Helper function to get current block
const getBlock = async () => {
  const blockNum = await provider.getBlockNumber();
  console.log("Block:", blockNum);
  return blockNum;
}

// Helper function to get proposal state
const getProposalState = async (gov, proposalId) => {
  let propState = await gov.state(proposalId);
  console.log("Proposal State:", oz_states[propState.toNumber()]);
}

// Getting three wallets from Ganache provider
const owner = provider.getSigner(0);
const voter1 = provider.getSigner(1);
const voter2 = provider.getSigner(2);

// Get instances of all three contracts
const tokenContract = artifacts.require("GovToken");
const govContract = artifacts.require("DaoGovernor");
const daoContract = artifacts.require("Dao");

module.exports = async function (deployer) {
  // Deploy the governance token contract
  await deployer.deploy(tokenContract);
  const token = await tokenContract.deployed();
  console.log("Token Contract deployed to:", token.address);

  // Get addresses of all three accounts
  const ow = await owner.getAddress();
  const v1 = await voter1.getAddress();
  const v2 = await voter2.getAddress();

  // Mint 100 tokens to owner, voter1, and voter2
  await token.mint(ow, 100);
  await token.mint(v1, 100);
  await token.mint(v2, 100);
  console.log("Minted 100 tokens to owner, voter1, and voter2", "\n");

  // Delegate voting power to themselves
  await token.delegate(ow, { from: ow });
  await token.delegate(v1, { from: v1 });
  await token.delegate(v2, { from: v2 });

  // Deploy the governor contract
  await deployer.deploy(govContract, token.address);
  const gov = await govContract.deployed();
  console.log("Governor contract deployed to:", gov.address, "\n");

  // Deploy the dao contract
  await deployer.deploy(daoContract, gov.address);
  const dao = await daoContract.deployed();
  console.log("DAO contract deployed to:", dao.address, "\n");

  // Owner creates a proposal to change value to 42
  let proposalFunc = dao.contract.methods.updateValue(42).encodeABI();
  let proposalDesc = "Updating DAO value to 42";
  console.log("Proposing:", proposalDesc);
  let proposalTrxn = await gov.propose(
    [dao.address],
    [0],
    [proposalFunc],
    proposalDesc,
  );

  // Move past voting delay
  await ff_blocks(1)

  // Get proposal ID and make proposal active
  let proposalId = proposalTrxn.logs[0].args.proposalId;
  console.log("Proposal ID:", proposalId.toString());
  await getProposalState(gov, proposalId);

  // Voter 1 and voter 2 vote in favor
  let vote = await gov.castVote(proposalId, 1, { from: v1 });
  console.log("V1 has voted in favor.")
  vote = await gov.castVote(proposalId, 1, { from: v2 });
  console.log("V2 has voted in favor.")

  // Move 5 blocks
  await ff_blocks(5);

  // Get final result
  console.log("Final Result");
  await getProposalState(gov, proposalId);

  // Execute task
  let desHash = ethers.utils.id(proposalDesc);
  execute = await gov.execute(
    [dao.address],
    [0],
    [proposalFunc],
    desHash
  );
  console.log("\nExecuting proposal on DAO")

  // Check if value on dao has changed
  const daoVal = await dao.daoVal.call();
  console.log("daoVal:", daoVal.toNumber());
};

Let’s now run this simulation using the following command:

$ truffle migrate --quiet

If all goes well, you should see output that looks something like this:

Token Contract deployed to: 0x21d530ED509D0b44F94b5896Fb63BDa2d8943E4e
Minted 100 tokens to owner, voter1, and voter2

Governor contract deployed to: 0x1C63D60Dcb50015d3D9598c45F48C7De16E0f925

DAO contract deployed to: 0xFC952D50576064C882bE9E724C5136F774B8f66e

Proposing: Updating DAO value to 42

Moved 1 blocks

Proposal ID: 89287417867871262838116386769359148381852036852071370084586471483495168348362
Proposal State: Pending
V1 has voted in favor.
V2 has voted in favor.

Moved 5 blocks

Final Result
Proposal State: Succeeded

Executing proposal on DAO
daoVal: 42

Notice that upon successful passing and execution of the proposal, the DAO value changed to 42.

Conclusion

DAOs are one of the greatest innovations to come out of blockchains. They have the potential to play a huge role in shaping the future of work. Their ability to bring strangers together, and pay those strangers for their contributions regardless of geographical location, gives them a distinct advantage over their traditional counterparts.

In this tutorial, you have learned what DAOs are and how they work. You then deployed a fully-functional DAO, and executed the entire lifecycle of a proposal. Why not try building your own?

3
Subscribe to my newsletter

Read articles from Alvin Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alvin Lee
Alvin Lee