Development of Governance Voting Contracts
A governance contract is not just a voting mechanism. It's a system that manages a protocol with a treasury worth millions of dollars. Errors in governance lead to real losses: the 2022 flash loan attack on Beanstalk exploited voting manipulation to extract $182M. Proper governance architecture is a balance between security and community participation.
OpenZeppelin Governor: Basic Architecture
The de facto standard for on-chain governance is the OpenZeppelin Governor framework. It implements a Governor Bravo compatible interface (compatible with Tally, Boardroom, Snapshot).
System components:
- Governor: core, manages proposal lifecycle
- GovernorSettings: settings (voting delay, voting period, proposal threshold)
- GovernorCountingSimple: vote counting (For/Against/Abstain)
- GovernorVotes: integration with ERC20Votes or ERC721Votes token
- GovernorTimelockControl: mandatory timelock between approval and execution
// 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 DAOGovernor is
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(
IVotes _token,
TimelockController _timelock
)
Governor("DAO Governor")
GovernorSettings(
7200, // voting delay: ~1 day on Ethereum (12s/block)
50400, // voting period: ~7 days
100000e18 // proposal threshold: 100k tokens
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(4) // 4% quorum from circulating supply
GovernorTimelockControl(_timelock)
{}
// Mandatory overrides to resolve conflicts between extensions
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 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 Token: ERC20Votes
Voting power comes from checkpoint history of the token. ERC20Votes stores balance snapshots at each block — preventing manipulation through token purchases immediately before voting.
Critically important: users must delegate (at least to themselves) for their voting power to be counted. This is a frequent point of confusion — token exists, but voting is impossible without delegation.
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
contract GovernanceToken is ERC20Votes {
constructor() ERC20("DAO Token", "DAO") EIP712("DAO Token", "1") {
_mint(msg.sender, 10_000_000e18);
}
// Users call delegate(address(self)) to activate voting power
}
TimelockController: Mandatory Component
Timelock is a buffer between approved voting and execution. Gives the community time to notice malicious proposals and exit the protocol.
// Deploy TimelockController
TimelockController timelock = new TimelockController(
2 days, // minDelay: minimum 2 days between queue and execute
proposers, // only Governor can queue
executors, // anyone can execute (after delay)
admin // temporary admin, then transfer to timelock itself
);
// Governor must have PROPOSER_ROLE
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
// Anyone can execute
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
// Revoke admin from deployer
timelock.revokeRole(timelock.DEFAULT_ADMIN_ROLE(), deployer);
Minimum delay for protocols with TVL > $10M — 48 hours. For critical parameters (upgrades, fee changes) — 7 days.
Protection Against Attacks
Flash loan attacks: attacker borrows flash loan tokens, creates proposal and votes in single transaction. Protection — votingDelay > 0. Checkpoint snapshot is taken at proposal creation block, not voting block. ERC20Votes stores history — balance at snapshot block, not current.
Proposal spam: without proposalThreshold anyone can spam proposals. 100k tokens — reasonable threshold for medium protocols. For small DAOs — sufficient 1-5% of supply.
Quorum gaming: at low turnout, small amount of tokens suffice for passage. GovernorVotesQuorumFraction counts quorum as % of token.getPastTotalSupply() — correct approach, quorum tied to circulating supply, not absolute number.
Parameters for Different DAO Types
| Parameter | Small DAO | Medium Protocol | Large Protocol |
|---|---|---|---|
| Voting delay | 1 day | 2 days | 2 days |
| Voting period | 3 days | 5 days | 7 days |
| Quorum | 4% | 4% | 10% |
| Timelock | 1 day | 2 days | 7 days |
| Proposal threshold | 0.1% supply | 0.5% supply | 1% supply |
Delegated Voting and Gasless Signatures
Most token holders won't vote directly — gas is expensive, process is complex. Solutions:
Delegation: holder delegates voting power to another address (delegate). Delegate votes on behalf of multiple holders. Used in Compound, Uniswap.
EIP-712 gasless vote: castVoteBySig() allows signing vote off-chain (via Snapshot or Tally) and submitting on-chain through relayer. User doesn't pay gas.
// Voting via signature — relayer pays gas
function castVoteBySig(
uint256 proposalId,
uint8 support,
address voter,
bytes memory signature
) public returns (uint256 weight) {
// EIP-712 signature verification of voter
// ...
return _castVote(proposalId, voter, support, "", "");
}
Governance is a living system. Parameters need review as DAO grows: quorum that worked at 10k holders may be unachievable at 500k holders with low turnout.







