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