Proposal and Voting System Development
If a DAO already exists and voting system needs refinement or building from scratch — need clarity on what we're building: full on-chain Governor, lightweight off-chain mechanism, or hybrid. Choice depends on active participant count, treasury size, and decentralization requirements.
Here — detailed on proposal mechanics and voting, including patterns not obvious from documentation.
Proposal Lifecycle
Creation and Snapshot
Proposal is created by calling governor.propose(). At creation, proposalSnapshot — block number where voting power will be counted — is fixed. This is critical: if snapshot matches current block, attacker can buy tokens in same block and vote.
Therefore votingDelay — blocks/seconds between proposal creation and voting start — must be nonzero. Compound uses 1 day (6570 blocks on Ethereum mainnet); Aave uses 1 day.
// GovernorSettings parameters
uint48 public constant VOTING_DELAY = 1 days; // delay before voting starts
uint32 public constant VOTING_PERIOD = 7 days; // voting duration
uint256 public constant PROPOSAL_THRESHOLD = 100_000e18; // min tokens to create
Voting: Simple vs Weighted
GovernorCountingSimple — standard: FOR, AGAINST, ABSTAIN. Proposal passes if: (1) quorum reached (votes not less than threshold), (2) FOR > AGAINST.
Fractional voting — more advanced: delegate can distribute voting power fractionally among options. Useful when delegate wants expressing constituent positions where they disagree. Implemented via custom GovernorCountingFractional (fork by a16z exists).
Quadratic voting — voting power = sqrt(tokens). Equalizes influence of large and small holders. Hard implementing on-chain honestly due to Sybil: address with 10000 tokens can split into 100 addresses with 100 tokens each, getting 10x more influence. Requires identity verification (Worldcoin, Gitcoin Passport) for Sybil resistance.
Quorum and Calculation
Quorum is minimum votes (FOR + AGAINST + ABSTAIN) for validity. GovernorVotesQuorumFraction counts quorum as percentage of total supply at snapshot moment.
Problem: if vesting gradually unlocks tokens, total supply grows — quorum in absolute numbers too. With high supply growth early proposals passed with lower quorum. getPastTotalSupply(proposalSnapshot) solves this — quorum counted from supply at snapshot, not current.
function quorum(uint256 timepoint) public view override returns (uint256) {
return token.getPastTotalSupply(timepoint) * quorumNumerator(timepoint) / quorumDenominator();
}
Delegation Mechanism
Fluid Delegation
ERC20Votes allows changing delegate anytime. Change takes effect immediately for future votes but not retroactively — for open proposals snapshot is already fixed.
This creates interesting dynamics: before important voting, active participants aggressively collect delegations. Delegate companies (Gauntlet, a16z governance team, Blockchain@UMich) publicly declare positions on each issue, attracting passive holders.
Subdelegation
Standard ERC20Votes doesn't support subdelegation: if A delegated B, B can't further delegate to C (B uses own tokens + delegated together but can't subdelegation). Compound v3 Governor introduced partial delegation and subdelegation via separate mechanism.
For complex governance with delegate hierarchies — need custom extension atop ERC20Votes.
Proposal Types and Mechanics
Single-action Proposals
Simplest: one action — change parameter. Example: change percentage rate in lending protocol:
targets = [address(lendingPool)];
values = [0];
calldatas = [abi.encodeWithSelector(ILendingPool.setInterestRate.selector, newRate)];
Multi-action Proposals (Batched)
Governor supports arrays of targets/values/calldatas — all actions execute atomically. Useful for related changes: update contract implementation AND update parameters in one proposal. If any action reverts — all reverts.
Proposal Cancellation
Creator can cancel proposal before voting starts. Protects from errors (wrong calldata). After voting starts — only Guardian with CANCELLER role in TimelockController.
function cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public returns (uint256) {
uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);
require(
_msgSender() == proposalProposer(proposalId),
"Only proposer can cancel"
);
return _cancel(targets, values, calldatas, descriptionHash);
}
Prevent Late Quorum Extension
Classic attack: large holder waits until voting's last minutes when outcome seems decided, then swings result with one vote. Opponents have no time to react.
GovernorPreventLateQuorum — extension extending voting period if quorum reached in last N blocks before deadline:
function _castVote(...) internal override returns (uint256) {
uint256 result = super._castVote(...);
uint256 deadline = proposalDeadline(proposalId);
if (deadline - block.number < voteExtension && _quorumReached(proposalId)) {
// extend deadline
_extendedDeadlines[proposalId] = block.number + voteExtension;
emit ProposalExtended(proposalId, block.number + voteExtension);
}
return result;
}
Compound Governance uses analogous mechanism. Important for fairness, especially early stage with few active participants.
Frontend and UX
Tally and Boardroom as Ready Solutions
Tally.xyz and Boardroom.io — ready governance UIs. Support OpenZeppelin Governor and Governor Bravo. Free for public DAOs. Provide: proposal lists, voting interface, delegate directory, treasury view.
Minus: branding and customization limited. For embedded governance in own dApp — need custom UI.
Custom Governance UI
Key frontend data:
- Proposal list:
ProposalCreatedevents via The Graph subgraph - User voting power:
token.getVotes(address)for current,token.getPastVotes(address, blockNumber)for snapshot - Delegate info: Tally API or own indexer
wagmi hooks for voting:
const { writeContract } = useWriteContract()
function castVote(proposalId: bigint, support: 0 | 1 | 2) {
writeContract({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'castVoteWithReason',
args: [proposalId, support, 'My reasoning here'],
})
}
Timeline and Scope
Basic proposal and voting system on OpenZeppelin Governor — 1-2 weeks contract development plus 1 week testing. Custom voting mechanics (fractional, quadratic) — +1-2 weeks. Frontend with full governance UI — 3-4 weeks.
Audit mandatory if Governor manages real funds. Typical governance contract audit scope — 1-2 weeks from qualified team.







