DAO Smart Contract Development
A DAO isn't just a voting contract. It's a set of smart contracts implementing governance lifecycle: proposal creation → voting → timelock → execution. Add treasury management, delegation mechanics, quorum calculations, and upgrade logic — and you get a system that without proper design becomes either too centralized (everything decided by multisig) or dysfunctional (quorum never achieved).
Standards and Frameworks
Before building from scratch, understand what exists.
OpenZeppelin Governor — de facto standard for EVM DAOs. Modular architecture: Governor base contract extended by mixins GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl. Used by Compound, Uniswap, Gitcoin, ENS DAO.
Compound Governor Bravo — older standard, predecessor to OpenZeppelin Governor. Still used in Compound forks. Less flexible, but battle-tested.
Aragon — high-level framework with plugin architecture. Useful when custom governance logic through plugins is needed without touching core contracts.
Zodiac (Gnosis) — pattern set for extending Gnosis Safe through modules. Enables converting multisig into on-chain DAO.
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: await snapshot fixation
1 weeks, // voting period: voting duration
100_000e18 // proposal threshold: minimum tokens to create
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(4) // 4% from total supply for quorum
GovernorTimelockControl(_timelock)
{}
// Mandatory overrides to resolve conflicts between mixins
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 a delay between proposal approval and execution. Gives the community time to react if proposal proves 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 for initial setup, then renounce
);
// After deploy — Governor becomes 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 after delay
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
// Revoke admin rights from deployer!
timelock.revokeRole(timelock.TIMELOCK_ADMIN_ROLE(), deployer);
Last step is critical: if deployer retains admin Timelock, they can bypass governance. Most governance hacks are built on left-over admin rights.
ERC-20 Votes: Governance Token
Voting token must implement IVotes interface. OpenZeppelin ERC20Votes maintains 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 nuance with delegation: in ERC20Votes tokens lack voting power until owner calls delegate(address). Usually delegate(msg.sender) — self-delegation. This requires explicit onboarding. Many DAOs solve through auto-delegation on first transfer.
Delegation for Inactive Holders
Participation problem: most holders are passive. Delegated voting allows transmitting voting power to specialized participants (delegates) without transferring tokens.
// User delegates voting power to another address
governanceToken.delegate(trustedDelegate);
// Now trustedDelegate votes with weight of all who delegated to them
// Tokens remain with owner
// Revoke delegation — reclaim
governanceToken.delegate(msg.sender);
Compound pioneered public delegation framework: delegates publish their positions, argue decisions, participants choose delegates by views. ENS DAO, Gitcoin, Uniswap actively use delegate ecosystem.
Proposal Lifecycle
Pending → Active → (Defeated | Succeeded) → Queued → Executed
↘ Canceled
↗ Expired (not executed within MAX_TIMELOCK)
Creating Proposal
// Proposal = set of transactions executed on approval
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 (usually IPFS), only keccak256(description) is stored on-chain as 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 user must pay gas for voting, participation drops sharply. Relayer assumes gas, user signs EIP-712 message.
Treasury Management
DAO treasury — funds managed by Timelock contract (and through it Governor). No one can spend treasury without passed proposal.
Multisig as Additional Safety Layer
For emergency situations (critical vulnerability when governance cycle lacks time) standard practice — separate 5/9 Guardian multisig with emergency pause right.
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");
_;
}
// Grants disbursement, work financing, 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). 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 — second case gives voting power of 1000 * sqrt(1000) ≈ 31623 vs sqrt(1M) = 1000. Makes splitting profitable. Quadratic voting works with Sybil-resistance (Proof of Humanity, Worldcoin).
Conviction Voting
Conviction voting (Gardens/1Hive) accumulates voting power over time: longer holding vote for proposal, greater weight. Withdrawing vote resets conviction. Good for continuous funding (treasury spending without separate proposals for each 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 recommended to deploy behind UUPS proxy — allows updating logic through governance proposal.
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. Means upgrade passed full governance cycle including Timelock delay.
Common Mistakes in Development
Flash loan governance attacks. Attacker borrows flash loan → gains huge voting power → creates and immediately approves proposal → returns loan. Protection: voting delay (delay between proposal and voting start) + snapshot-based voting (voting counted at snapshot, not current balances).
Low quorum trap. 4% quorum seems reasonable, but if large holder delegates elsewhere — realistically active votes may lack. Calibrate quorum to real voter turnout statistics.
Proposal spam. Without proposalThreshold anyone creates proposals. With low threshold — also problematic. Set threshold high enough for spam to be expensive, but not so high to block real participants.
Short timelock. 24-hour Timelock — too short 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 | Duration |
|---|---|---|
| Design Mechanics | Voting model choice, 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 |
| Tests | Fork tests, voting simulations, attack scenarios | 2–3 weeks |
| Frontend | Proposal UI, voting interface, delegate directory | 3–5 weeks |
| Audit | 3–4 weeks |
Governance smart contracts — one of few areas where independent audits justified. Governor vulnerability can allow attacker to drain entire treasury via one proposal.







