DAO Smart Contracts Development
A DAO isn't just a contract with voting. It's a set of smart contracts implementing the governance lifecycle: proposal creation → voting → timelock → execution. Add treasury management, delegation mechanics, quorum calculations, and upgrade logic — you get a system that, if poorly designed, becomes either too centralized (everything decided by multisig) or dysfunctional (quorum never reached).
Standards and Frameworks
Before building anything from scratch, understand what already exists.
OpenZeppelin Governor — de facto standard for EVM DAOs. Modular architecture: Governor (base contract) extended via mixins GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl. Used by Compound, Uniswap, Gitcoin, ENS DAO.
Compound Governor Bravo — older standard, from which OpenZeppelin Governor evolved. Still used in Compound forks. Less flexible, but battle-tested.
Aragon — high-level framework with plugin architecture. Useful when you need custom governance logic through plugins without modifying core contracts.
Zodiac (Gnosis) — set of patterns for extending Gnosis Safe through modules. Allows turning a multisig into a DAO with on-chain voting.
OpenZeppelin Governor Architecture
Minimal Assembly
// 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 MyDAO is
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(
IVotes _token,
TimelockController _timelock
)
Governor("MyDAO")
GovernorSettings(
1 days, // voting delay: how long to wait after proposal before voting starts
1 weeks, // voting period: duration of voting
100_000e18 // proposal threshold: minimum tokens to create proposal
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(4) // 4% of total supply for quorum
GovernorTimelockControl(_timelock)
{}
// Mandatory overrides to resolve mixin conflicts
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 _execute(uint256 proposalId, address[] memory targets, uint256[] memory values,
bytes[] memory calldatas, bytes32 descriptionHash)
internal override(Governor, GovernorTimelockControl) {
super._execute(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(); }
function supportsInterface(bytes4 interfaceId)
public view override(Governor, GovernorTimelockControl) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
TimelockController — Critical Component
Timelock is the delay between when a proposal passes voting and when it can be executed. This gives the community time to react if a proposal turns out malicious.
// Deploy TimelockController
TimelockController timelock = new TimelockController(
2 days, // minDelay: minimum delay
proposers, // who can queue (usually Governor)
executors, // who can execute (address(0) = anyone)
admin // admin (usually address(0) after setup)
);
// After deploy — Governor should be PROPOSER and CANCELLER
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
timelock.grantRole(timelock.CANCELLER_ROLE(), address(governor));
// EXECUTOR_ROLE — address(0) means anyone can execute passed proposal
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
// Revoke admin rights from deployer!
timelock.revokeRole(timelock.TIMELOCK_ADMIN_ROLE(), deployer);
The last step is critical: if the deployer keeps admin Timelock rights, they can bypass governance. Most governance exploits are built precisely on leftover admin rights.
ERC-20 Votes: Governance Token
The voting token must implement IVotes interface. OpenZeppelin ERC20Votes stores checkpoint history of balances for snapshot-based voting.
contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes {
constructor(address initialHolder)
ERC20("MyDAO Token", "MDT")
ERC20Permit("MyDAO Token")
{
_mint(initialHolder, 10_000_000e18);
}
// ERC20Votes requires explicit delegation
// New token recipients must call delegate(self) to activate voting power
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);
}
}
Important delegation nuance: in ERC20Votes tokens have no voting power until the owner calls delegate(address). Usually delegate(msg.sender) — self-delegation. This isn't obvious to new users and requires explicit onboarding. Many DAOs solve this through automatic self-delegation on first transfer.
Delegation for Inactive Holders
Participation problem: most holders are passive. Delegated voting lets them transfer voting power to specialized participants (delegates) without transferring tokens.
// User delegates their voting power to another address
governanceToken.delegate(trustedDelegate);
// Now trustedDelegate votes with the weight of all delegators
// The tokens remain with the original owner
// Revoke delegation — take back
governanceToken.delegate(msg.sender);
Compound established a framework for public delegation: delegates publish their positions, argue their decisions, participants choose delegates by alignment. ENS DAO, Gitcoin, Uniswap actively use delegate ecosystems.
Proposal Lifecycle
Pending → Active → (Defeated | Succeeded) → Queued → Executed
↘ Canceled
↗ Expired (not executed within MAX_TIMELOCK)
Creating a Proposal
// Proposal = set of transactions to execute if passed
address[] memory targets = new address[](1);
uint256[] memory values = new uint256[](1);
bytes[] memory calldatas = new bytes[](1);
// Example: change protocol parameter
targets[0] = address(protocol);
values[0] = 0;
calldatas[0] = abi.encodeWithSignature(
"setFeeRate(uint256)",
500 // new fee rate: 5%
);
uint256 proposalId = governor.propose(
targets,
values,
calldatas,
"# Proposal: Update fee rate\n\nPropose changing fee rate from 3% to 5%..."
);
Description is stored off-chain (typically IPFS); on-chain stores only keccak256(description) via descriptionHash.
Voting with Reason
// Three options: 0 = Against, 1 = For, 2 = Abstain
governor.castVote(proposalId, 1); // for
// With reasoning (stored in event)
governor.castVoteWithReason(proposalId, 1, "This fee increase is necessary for protocol sustainability");
// Via meta-transaction (gasless voting through EIP-712 signature)
governor.castVoteBySig(proposalId, 1, v, r, s);
Gasless voting through castVoteBySig is critical for participation — if users must pay gas to vote, participation drops sharply. A relayer covers gas; the user signs an EIP-712 message.
Treasury Management
DAO treasury is funds managed by the Timelock contract (and through it, Governor). No one can spend treasury without a passed proposal.
Multisig as Additional Protection Layer
For emergency situations (critical vulnerability with no time for governance cycle), best practice is a separate 5/9 Guardian multisig with emergency pause rights.
contract DAOTreasury {
address public immutable governor; // only Governor can spend
address public immutable guardian; // Guardian can pause on threat
bool public paused;
modifier onlyGovernance() {
require(msg.sender == governor, "Only governance");
_;
}
modifier whenNotPaused() {
require(!paused, "Treasury paused");
_;
}
// Grant disbursements, work funding, investments
function transfer(address token, address recipient, uint256 amount)
external onlyGovernance whenNotPaused
{
IERC20(token).safeTransfer(recipient, amount);
emit Transfer(token, recipient, amount);
}
// Guardian can only pause, not spend
function pause() external {
require(msg.sender == guardian, "Only guardian");
paused = true;
}
// Unpause — only through governance
function unpause() external onlyGovernance {
paused = false;
}
}
Custom Voting Mechanisms
Quadratic Voting
Quadratic voting reduces whale influence: voting power = sqrt(token balance). A whale with 1M tokens gets voting power 1000, not 1M.
function _getVotes(
address account,
uint256 blockNumber,
bytes memory /*params*/
) internal view virtual override returns (uint256) {
uint256 balance = token.getPastVotes(account, blockNumber);
// Square root via Babylonian method
return _sqrt(balance);
}
function _sqrt(uint256 x) internal pure returns (uint256 y) {
if (x == 0) return 0;
uint256 z = (x + 1) / 2;
y = x;
while (z < y) {
y = z;
z = (x / z + z) / 2;
}
}
Quadratic voting problem: Sybil attacks. One address with 1M tokens vs. 1000 addresses with 1000 tokens each — the second case gets total voting power 1000 * sqrt(1000) ≈ 31623 versus sqrt(1M) = 1000. This makes splitting profitable. Quadratic voting only works paired with Sybil-resistance (Proof of Humanity, Worldcoin).
Conviction Voting
Conviction voting (used by Gardens/1Hive) accumulates voting power over time: the longer you hold a vote for a proposal, the higher weight it gets. Withdrawing your vote resets conviction. Good for continuous funding (treasury spending without separate proposals for each small payment).
struct ProposalConviction {
uint256 stakedTokens;
uint256 lastConviction; // conviction value at last update
uint256 lastTimestamp;
}
// conviction = stakedTokens * (1 - alpha^timePassed) / (1 - alpha)
// alpha = decay rate (e.g. 0.9 per day)
function calculateConviction(
ProposalConviction storage p,
uint256 currentTime
) internal view returns (uint256) {
uint256 timePassed = currentTime - p.lastTimestamp;
// Simplified integer version with decay factor
uint256 decayFactor = DECAY_PRECISION - (DECAY_RATE * timePassed);
return (p.lastConviction * decayFactor / DECAY_PRECISION) + p.stakedTokens;
}
Governor Upgrade Mechanism
Governor contracts should be deployed behind UUPS proxy — this allows updating logic through governance proposals.
contract UpgradeableGovernor is Governor, UUPSUpgradeable {
function _authorizeUpgrade(address newImplementation)
internal override onlyGovernance {}
// onlyGovernance modifier — only through passed proposal
modifier onlyGovernance() {
require(msg.sender == address(this), "Only governance can upgrade");
_;
}
}
Upgrade via self-call: proposal calls upgradeTo(newImplementation) on Governor itself. This means the upgrade completed full governance cycle including Timelock delay.
Common Mistakes in Development
Flash loan governance attacks. Attacker borrows flash loan → gets huge voting power → creates and immediately passes proposal → repays loan. Defense: voting delay (delay between proposal and voting start) plus snapshot-based voting (votes counted at snapshot moment, not current balances).
Low quorum trap. 4% quorum seems reasonable, but if a large holder delegates — actually active votes might be insufficient. Calibrate quorum against real voter turnout statistics.
Proposal spam. Without proposalThreshold, anyone can create proposals. With low threshold, too many proposals. Set threshold high enough to make spam expensive but not so high as to block real participants.
Short timelock. 24-hour Timelock — too little for DeFi protocol. Standard: 48–72 hours minimum for production. For major upgrades — 7 days.
Default Parameters for Different DAO Types
| Parameter | Small Community DAO | DeFi Protocol DAO | Treasury DAO |
|---|---|---|---|
| Voting Delay | 1 day | 2 days | 1 day |
| Voting Period | 5 days | 7 days | 7 days |
| Timelock | 24 hours | 48–72 hours | 48 hours |
| Quorum | 10% | 4% | 5% |
| Proposal Threshold | 1% total supply | 0.25% | 0.5% |
Development Process
| Phase | Content | Timeline |
|---|---|---|
| Mechanics Design | Choose voting model, parameters, treasury policy | 1–2 weeks |
| Governance Token | ERC20Votes + distribution mechanics | 2–3 weeks |
| Governor + Timelock | Assembly from OZ modules + customization | 2–3 weeks |
| Treasury Contract | Asset management, emergency pause | 1–2 weeks |
| Testing | Fork tests, voting simulation, attack scenarios | 2–3 weeks |
| Frontend | Proposal UI, voting interface, delegate directory | 3–5 weeks |
| Audit | 3–4 weeks |
Governance smart contracts are one of the few areas where two-team audits are justified. A vulnerability in Governor can allow an attacker to drain the entire treasury with one proposal.







