Skip to content
Guide · DAO

Governor DAO 101

Wire ERC20Votes, TimelockController, and OpenZeppelin Governor into a production DAO — with deployment order, roles, and proposal lifecycle explained.

DAO
16 min read

The three-contract architecture

A minimal on-chain DAO is three contracts: a votes token (ERC20Votes), a Governor (proposal + voting logic), and a Timelock (delayed execution). Token holders propose and vote; the Governor counts votes; the Timelock queues successful proposals and executes them after a delay. The Timelock — not the Governor — should own protocol contracts and treasuries so every privileged action passes through the delay.

  • ERC20Votes — checkpoints balances so flash-loan voting is impossible
  • Governor — handles proposal threshold, quorum, voting delay, and voting period
  • TimelockController — enforces minDelay between queue and execute
  • Treasury — ETH and tokens sit in the Timelock; proposals move them via targets + calldata

Step 1 — Deploy the votes token

Use the Token Creator with the Votes extension enabled, or deploy an existing ERC20Votes-compatible token. Holders must call delegate() to activate voting power — raw token balances do not count. The delegatee can be the holder themselves (self-delegate) or a trusted representative.

  • Distribute tokens before governance goes live so quorum is reachable
  • Publish a delegation guide — most holders do not self-delegate by default
  • Use ERC20Permit so delegates can be set gaslessly via signature
GovernanceToken.solsolidity
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes, Ownable {
    constructor(address initialOwner)
        ERC20("DAO Token", "DAO")
        ERC20Permit("DAO Token")
        Ownable(initialOwner)
    {
        _mint(initialOwner, 10_000_000 * 10 ** decimals());
    }

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

Step 2 — Deploy the Timelock

Deploy the Timelock template with a minDelay of at least 2 days for production DAOs (172800 seconds). Proposers and executors are addresses allowed to call schedule and execute on the Timelock. Initially, only the Governor address should be a proposer; executors can include the zero address (anyone) so passed proposals execute without a privileged executor.

  • minDelay — time between queue and execute; longer delays give the community time to react
  • proposers — start with only the Governor contract address
  • executors — include 0x000…000 so any address can trigger execute after the delay
  • admin — the deployer is temporary admin; renounce or transfer to the Timelock itself after setup
deploy-timelock.solsolidity
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";

// minDelay = 172800 (2 days)
// proposers = [governorAddress]
// executors = [address(0)]  // anyone may execute
TimelockController timelock = new TimelockController(
    172800,
    proposers,
    executors,
    msg.sender // temporary admin — renounce after wiring Governor
);

Step 3 — Deploy the Governor

The Governor template accepts your token address, Timelock address, voting delay, voting period, and quorum percentage. Voting delay is measured in blocks — at ~12s per block on Ethereum, 7200 blocks ≈ 1 day. Voting period is how long ballots stay open. Quorum is the minimum participation fraction of total supply required for a proposal to succeed.

  • votingDelay: 7200 blocks (~1 day) — prevents last-minute surprise proposals
  • votingPeriod: 50400 blocks (~1 week) — gives global holders time to vote
  • quorum: 4% of supply — tune up for high-participation communities, down for early stage
  • proposalThreshold — minimum votes to create a proposal; prevents spam
AcmeGovernor.solsolidity
import {Governor} from "@openzeppelin/contracts/governance/Governor.sol";
import {GovernorSettings} from "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import {GovernorCountingSimple} from "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import {GovernorVotes} from "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import {GovernorTimelockControl} from "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";

contract AcmeGovernor is
    Governor,
    GovernorSettings,
    GovernorCountingSimple,
    GovernorVotes,
    GovernorTimelockControl
{
    constructor(IVotes _token, TimelockController _timelock)
        Governor("AcmeGovernor")
        GovernorSettings(7200, 50400, 100_000e18) // delay, period, proposal threshold
        GovernorVotes(_token)
        GovernorTimelockControl(_timelock)
    {}
    // ... required overrides for Timelock integration
}

Wire roles and transfer ownership

After all three contracts are deployed, connect them with role grants. The Governor must be a PROPOSER on the Timelock. The Timelock should be the admin of any Ownable protocol contracts. Revoke the deployer's Timelock admin role last.

  • timelock.grantRole(PROPOSER_ROLE, governorAddress)
  • timelock.grantRole(EXECUTOR_ROLE, address(0))
  • timelock.revokeRole(PROPOSER_ROLE, deployerAddress) — remove deployer backdoor
  • protocolContract.transferOwnership(timelockAddress)
  • timelock.renounceRole(DEFAULT_ADMIN_ROLE, deployerAddress) — only after everything is wired

Proposal lifecycle

Understanding the state machine prevents panic during your first vote. A proposer calls propose(targets, values, calldatas, description). After votingDelay, voting opens for votingPeriod blocks. If quorum is met and votes for exceed votes against, the proposal succeeds. Anyone calls queue() to schedule on the Timelock; after minDelay, execute() runs the batched calls.

  • Pending — submitted, waiting for votingDelay to elapse
  • Active — holders cast For, Against, or Abstain votes
  • Succeeded — quorum met and majority For; call queue() before proposal expires
  • Queued — scheduled on Timelock with an eta timestamp
  • Executed — Timelock ran the calls; check events to confirm state changes

Operations and upgrades

Living DAOs need runbooks. Document how to craft proposal calldata, how to cancel malicious proposals during the voting period, and how to upgrade contracts if you use proxies. Ethereum Toolset's Timelock and Governor templates follow OpenZeppelin's audited patterns — extend them, do not rewrite governance from scratch.

  • Use the ABI Encoder to build proposal calldata for contract upgrades or treasury transfers
  • Monitor ProposalCreated and VoteCast events for community transparency
  • Keep a public forum (Discourse, Snapshot off-chain) for pre-proposal discussion
  • Consider a Guardian role with veto power during early mainnet — remove via governance later