TechLead
Lesson 19 of 20
5 min read
Web3 & Blockchain

DAO Governance

Build decentralized autonomous organizations with on-chain governance, token voting, proposal systems, and treasury management

What is a DAO?

A Decentralized Autonomous Organization (DAO) is an organization governed by smart contracts and the collective decisions of its token holders. Instead of a CEO or board of directors, a DAO uses on-chain voting to make decisions about treasury spending, protocol upgrades, partnerships, and more. DAOs represent a new model of organizational governance that is transparent, permissionless, and resistant to censorship.

DAO Components

  • Governance Token: An ERC-20 token with vote delegation (ERC20Votes). Token holders can vote or delegate their voting power.
  • Governor Contract: The core governance logic — proposal creation, voting, and execution. OpenZeppelin Governor is the standard.
  • Timelock Controller: A delay between vote approval and execution, giving members time to exit if they disagree.
  • Treasury: A smart contract (often the Timelock itself) that holds the DAO's funds and can only disburse via governance.

Governance Token with Vote Delegation

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/utils/Nonces.sol";

/// @title GovToken — Governance token with voting and delegation
contract GovToken is ERC20, ERC20Permit, ERC20Votes {
    constructor()
        ERC20("Governance Token", "GOV")
        ERC20Permit("Governance Token")
    {
        _mint(msg.sender, 1_000_000 * 10 ** decimals());
    }

    // Required overrides for ERC20Votes
    function _update(address from, address to, uint256 value)
        internal override(ERC20, ERC20Votes)
    {
        super._update(from, to, value);
    }

    function nonces(address owner)
        public view override(ERC20Permit, Nonces) returns (uint256)
    {
        return super.nonces(owner);
    }
}

// IMPORTANT: Token holders must delegate their votes to themselves
// (or to another address) before they can vote!
// await govToken.delegate(myAddress); // Self-delegate to activate voting

Governor Contract

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

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

contract MyGovernor is
    Governor,
    GovernorSettings,
    GovernorCountingSimple,
    GovernorVotes,
    GovernorVotesQuorumFraction,
    GovernorTimelockControl
{
    constructor(
        IVotes _token,
        TimelockController _timelock
    )
        Governor("My DAO Governor")
        GovernorSettings(
            7200,   // votingDelay: ~1 day in blocks (12s blocks)
            50400,  // votingPeriod: ~1 week in blocks
            100e18  // proposalThreshold: 100 tokens to create a proposal
        )
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4) // 4% of total supply must vote
        GovernorTimelockControl(_timelock)
    {}

    // Required overrides for multiple inheritance
    function votingDelay() public view override(Governor, GovernorSettings) returns (uint256) {
        return super.votingDelay();
    }

    function votingPeriod() public view override(Governor, GovernorSettings) returns (uint256) {
        return super.votingPeriod();
    }

    function quorum(uint256 blockNumber)
        public view override(Governor, GovernorVotesQuorumFraction) returns (uint256)
    {
        return super.quorum(blockNumber);
    }

    function state(uint256 proposalId)
        public view override(Governor, GovernorTimelockControl) returns (ProposalState)
    {
        return super.state(proposalId);
    }

    function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
        return super.proposalThreshold();
    }

    function proposalNeedsQueuing(uint256 proposalId)
        public view override(Governor, GovernorTimelockControl) returns (bool)
    {
        return super.proposalNeedsQueuing(proposalId);
    }

    function _queueOperations(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
        internal override(Governor, GovernorTimelockControl) returns (uint48)
    {
        return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
    }

    function _executeOperations(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
        internal override(Governor, GovernorTimelockControl)
    {
        super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
    }

    function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
        internal override(Governor, GovernorTimelockControl) returns (uint256)
    {
        return super._cancel(targets, values, calldatas, descriptionHash);
    }

    function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
        return super._executor();
    }
}

Governance Frontend Interaction

import { ethers } from 'ethers';

// Creating and voting on a proposal
const governor = new ethers.Contract(governorAddress, governorABI, signer);

// Step 1: Create a proposal to transfer 10 ETH from treasury
const transferCalldata = treasuryContract.interface.encodeFunctionData('transfer', [
  recipientAddress,
  ethers.parseEther('10'),
]);

const proposalTx = await governor.propose(
  [treasuryAddress],                      // targets
  [0],                                     // values (ETH to send with call)
  [transferCalldata],                      // calldatas
  'Proposal #1: Fund marketing team with 10 ETH for Q2 campaign'
);
const receipt = await proposalTx.wait();

// Get proposal ID from event
const proposalId = receipt.logs[0].args.proposalId;
console.log('Proposal ID:', proposalId);

// Step 2: Wait for voting delay, then vote
// 0 = Against, 1 = For, 2 = Abstain
const voteTx = await governor.castVoteWithReason(
  proposalId,
  1,  // For
  'I support funding the marketing team'
);
await voteTx.wait();

// Step 3: After voting period ends, queue for execution
await governor.queue(
  [treasuryAddress], [0], [transferCalldata],
  ethers.id('Proposal #1: Fund marketing team with 10 ETH for Q2 campaign')
);

// Step 4: After timelock delay, execute
await governor.execute(
  [treasuryAddress], [0], [transferCalldata],
  ethers.id('Proposal #1: Fund marketing team with 10 ETH for Q2 campaign')
);

DAO Governance Best Practices

  • Use timelocks: Always add a delay between vote approval and execution so members can react
  • Set appropriate quorum: Too high and nothing passes; too low and a small group can control the DAO
  • Enable delegation: Most token holders will not vote on every proposal — delegation lets active members represent passive ones
  • Off-chain discussion first: Use forums (Discourse) and temperature checks (Snapshot) before on-chain proposals
  • Guard against governance attacks: Flash loan voters and late-quorum attacks can be mitigated with vote delay and quorum extensions

Continue Learning