OpenZeppelin Governor Integration
OpenZeppelin Governor is a modular framework for on-chain governance, implementing a standard compatible with Compound Governor Bravo. Most DeFi protocols with on-chain governance are built on this foundation or compatible with it: Uniswap, Compound, Gitcoin, ENS use Governor-compatible contracts. Therefore, integrating with Governor isn't just adding governance to your protocol — it's achieving compatibility with the broader ecosystem of tools: Tally, Boardroom, Snapshot (for off-chain signaling).
Governor Architecture
Governor isn't a single contract but a set of modules via Solidity multiple inheritance.
contract MyGovernor is
Governor,
GovernorSettings, // voting delay, voting period, proposal threshold
GovernorCountingSimple, // For, Against, Abstain counting
GovernorVotes, // ERC-20Votes token integration
GovernorVotesQuorumFraction, // quorum as % of total supply
GovernorTimelockControl // execution via Timelock
{
constructor(IVotes _token, TimelockController _timelock)
Governor("MyProtocol Governor")
GovernorSettings(
1 days, // voting delay: how long to wait after proposal creation
1 weeks, // voting period: how long voting lasts
10_000e18 // proposal threshold: minimum tokens to create proposal
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(4) // 4% of circulating supply = quorum
GovernorTimelockControl(_timelock)
{}
}
Each module solves a separate task. If custom vote counting logic is needed (e.g., quadratic voting), replace GovernorCountingSimple with your implementation.
Proposal Lifecycle
A proposal goes through strictly defined states:
Pending → Active → Succeeded/Defeated/Canceled → Queued → Executed
Creation → [voting delay] → Voting → [voting period] → Counting →
[timelock delay] → Execution
votingDelay protects against flash loan attacks. If voting starts immediately after creation, an attacker can buy large tokens in the same block, vote, and sell. With a 1-2 day voting delay, this is impossible: tokens must be held until the snapshot block.
GovernorVotes fixes voting power for each address at voting start block via ERC-20Votes checkpoint mechanism. Balance changes after this block don't affect vote weight.
Timelock Integration
TimelockController is a separate contract that owns your protocol contracts. Governor sends transactions to Timelock queue; after delay (typically 48–72 hours) execution becomes possible.
// Deploy TimelockController
TimelockController timelock = new TimelockController(
2 days, // minDelay
proposers, // only Governor can queue
executors, // anyone can execute (or specific address)
admin // after setup admin = address(0), remove admin rights
);
// Governor gets PROPOSER_ROLE
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
// Executor — open (address(0)) or specific address
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
// Revoke admin (important!)
timelock.revokeRole(timelock.DEFAULT_ADMIN_ROLE(), deployer);
Why revoking admin is important: if deployer keeps DEFAULT_ADMIN_ROLE, they can anytime bypass governance and directly grant themselves PROPOSER_ROLE. Final step — revokeRole admin from deployer.
Propose, Vote, Execute
Creating a Proposal
A proposal is one or more function calls executed if voting passes.
// Example: proposal to change protocol fee parameter
address[] memory targets = new address[](1);
targets[0] = address(protocolContract);
uint256[] memory values = new uint256[](1); // ETH value, usually 0
values[0] = 0;
bytes[] memory calldatas = new bytes[](1);
calldatas[0] = abi.encodeWithSignature("setFee(uint256)", 30); // 0.3%
string memory description = "Proposal #1: Set protocol fee to 0.3%";
uint256 proposalId = governor.propose(targets, values, calldatas, description);
proposalId is computed as keccak256(abi.encode(targets, values, calldatas, keccak256(bytes(description)))). This matters: description is part of the id, so changing it creates a different proposal.
Voting
// 0 = Against, 1 = For, 2 = Abstain
governor.castVote(proposalId, 1);
// With reasoning (on-chain, expensive gas)
governor.castVoteWithReason(proposalId, 1, "Increases protocol competitiveness");
// EIP-712 signature for gasless voting via relayer
governor.castVoteBySig(proposalId, 1, v, r, s);
castVoteBySig is critical for UX. Most holders won't pay $5–20 for an on-chain vote. Gasless voting: user signs vote off-chain, relayer (or protocol itself) sends transaction and pays gas.
Execution
// After successful voting and timelock delay
governor.execute(targets, values, calldatas, keccak256(bytes(description)));
Customization: What You Can Change
Voting power source. By default — ERC-20Votes token. Can replace with: ERC-721Votes (NFT governance), custom weighting (locked tokens weigh more), veToken model (Curve style).
Quorum. GovernorVotesQuorumFraction counts quorum from total supply. Problem: if large token portion isn't delegated, quorum never reaches. Solution — count from delegated supply, not total. Requires overriding quorum() function.
Proposal threshold. High threshold (e.g., 1% supply) protects from spam proposals but excludes small holders. Alternative: delegation threshold — any holder can create proposal if collecting enough delegators.
Tally and Tool Ecosystem
Tally is a web3 governance dashboard working with any Governor-compatible contract out of the box. After deploying Governor on Tally it appears automatically via their indexer. This matters: no need to build custom voting UI from scratch.
Snapshot for off-chain signaling uses EIP-712 signatures without gas. Governor + Snapshot integration: Snapshot results serve as signal before on-chain proposal, or through Governor-style Snapshot module enable binding voting with on-chain execution. ENS uses this pattern.
Common Integration Mistakes
Forgetting to transferOwnership to Timelock. Governor + Timelock are useless if your protocol contracts still belong to the deployer wallet. Must explicitly transfer ownership.
Wrong quorum. 4% of total supply with 30% circulation rate means 13% of circulating — hard to achieve. Test against real token distribution data.
Missing ERC-20Votes in token. Standard ERC-20 doesn't support checkpoints. Either token inherits ERC20Votes or use wrapper (like wToken). Retrofit existing token through ERC20VotesWrapper.
Integration timeline: standard Governor integration with existing token — 1–2 weeks development plus 1 week testing. Custom voting mechanics — 3–4 weeks additional.







