Appearance
L2-20: OZ Governor DAO (OpenZeppelin Governor Standard Governance)
1. Problem
Build a production-grade DAO governance system using OpenZeppelin's Governor standard contracts. Core modules: GovernorCountingSimple (For/Against/Abstain three-state voting), GovernorVotes (token voting weight via ERC20Votes), GovernorTimelockControl (delayed execution after proposal passes), GovernorSettings (configurable voting parameters).
This is the standardized implementation of the Compound Governor Bravo pattern, used by hundreds of DAOs.
2. Why
Implementing DAO governance contracts from scratch is error-prone -- vote counting, quorum calculation, and timelock security checks are all potential vulnerability points. The OpenZeppelin Governor framework provides modular, audited governance components:
- CountingSimple: verified For/Against/Abstain counting logic
- Votes integration: seamless integration with ERC20Votes (voting tokens with historical snapshots) via the IVotes interface
- TimelockControl: enforces a mandatory delay (typically 2 days) after proposal approval, giving users time to exit
- Configurable Settings: votingDelay, votingPeriod, and proposalThreshold are all adjustable
This challenge teaches not "how to write a governance contract from scratch" but "how to correctly assemble and use proven governance modules" -- a core skill of professional Solidity development.
3. Solution
Architecture Design
GovernanceToken (ERC20 + ERC20Permit + ERC20Votes)
│
│ provides tokens + voting weight
▼
OZGovernorDAO (Governor + CountingSimple + Votes + Timelock + Settings)
│
│ manages
▼
TimelockController (delayed executor)
│
│ executes
▼
Target Contract
Governance Token
solidity
contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes {
constructor(string memory name_, string memory symbol_,
address initialHolder, uint256 initialSupply)
ERC20(name_, symbol_)
ERC20Permit(name_)
{
_mint(initialHolder, initialSupply);
}
// Must override: Solidity requires explicit _update resolution in multiple inheritance
function _update(address from, address to, uint256 value)
internal override(ERC20, ERC20Votes) {
super._update(from, to, value);
}
function nonces(address owner)
public view override(ERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
}
Governor Contract
solidity
contract OZGovernorDAO is
Governor,
GovernorCountingSimple, // For/Against/Abstain counting
GovernorVotes, // ERC20Votes integration
GovernorTimelockControl, // Timelock delayed execution
GovernorSettings // Configurable parameters
{
uint256 public constant QUORUM_BPS = 400; // 4% quorum
constructor(IVotes _token, TimelockController _timelock,
uint48 _votingDelay, uint32 _votingPeriod,
uint256 _proposalThreshold)
Governor("OZGovernorDAO")
GovernorVotes(_token)
GovernorTimelockControl(_timelock)
GovernorSettings(_votingDelay, _votingPeriod, _proposalThreshold)
{}
function quorum(uint256 timepoint) public view override returns (uint256) {
uint256 totalSupply = token().getPastTotalSupply(timepoint);
return (totalSupply * QUORUM_BPS) / 10_000;
}
// Explicit overrides required for diamond inheritance
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 proposalThreshold() public view override(Governor, GovernorSettings)
returns (uint256) { return super.proposalThreshold(); }
function _executor() internal view override(Governor, GovernorTimelockControl)
returns (address) { return super._executor(); }
}
Proposal Lifecycle
1. propose(targets, values, calldatas, description)
→ state = Pending (during votingDelay)
2. [after votingDelay]
→ state = Active (during votingPeriod, voting begins)
3. castVote(proposalId, support) // 0=Against, 1=For, 2=Abstain
4. [votingPeriod ends, forVotes > againstVotes, quorum met]
→ state = Succeeded
5. queue(targets, values, calldatas, descriptionHash)
→ enters Timelock queue (during minDelay)
6. [after minDelay]
→ execute(targets, values, calldatas, descriptionHash)
Deployment Order
- Deploy
GovernanceToken("GovToken", "GOV", deployer, 1000000e18) - Deploy
TimelockController(minDelay=2days, proposers=[], executors=[], admin=address(0)) - Deploy
OZGovernorDAO(token, timelock, votingDelay=1day, votingPeriod=1week, proposalThreshold=1000e18) - Grant the Governor contract proposer + executor roles in the Timelock
- Renounce the Timelock admin role (
renounceRole)
4. Pitfalls Encountered
4.1 Diamond Inheritance Function Override Conflicts
OZGovernorDAO inherits from 5 contracts, and votingDelay(), votingPeriod(), and proposalThreshold() are defined in both Governor and GovernorSettings. Solidity requires explicitly specifying which parent contract's implementation to use. Failing to write override(Governor, GovernorSettings) causes a compilation error.
4.2 ERC20Votes Snapshot Mechanism
getPastTotalSupply(timepoint) and getPastVotes(account, timepoint) use "checkpoints" to record historical balances. This requires _update to write a checkpoint on every token transfer. Forgetting to override _update in GovernanceToken (calling ERC20Votes._update) will cause voting weight queries to fail.
4.3 Timelock Role Configuration
TimelockController has three roles: PROPOSER_ROLE (can submit proposals to the Timelock queue), EXECUTOR_ROLE (can execute operations in the queue), CANCELLER_ROLE (can cancel queued operations). These roles must be granted to the Governor contract -- otherwise proposals cannot be executed after passing.
4.4 Timelock minDelay Setting
minDelay too short (e.g., 1 hour) → users have no time to exit before a malicious proposal executes. Too long (e.g., 30 days) → governance is extremely inefficient. The standard value is 2 days (172800 seconds).
5. Why the Pitfalls Happen
5.1
Solidity's multiple inheritance uses C3 linearization to resolve the diamond problem. When multiple parent contracts define the same function, override(A, B) tells the compiler you are aware of the conflict and choose to call super.func() (whose resolution follows the C3 order).
5.2
ERC20Votes tracks historical voting weight through event sourcing. Each token transfer emits a DelegateVotesChanged event and writes a "checkpoint" for both sender and receiver. getPastVotes performs a binary search over the checkpoint array.
6. How to Resolve the Pitfalls
- Carefully follow OpenZeppelin's Governor documentation, especially the explicit overrides required for diamond inheritance
- Test the full proposal lifecycle: propose → vote → queue → execute
- Correctly implement the
_updateoverride inGovernanceToken(callERC20Votes._update) - Verify Timelock role configuration after deployment:solidity
timelock.grantRole(PROPOSER_ROLE, address(governor)); timelock.grantRole(EXECUTOR_ROLE, address(governor)); timelock.grantRole(CANCELLER_ROLE, address(governor)); timelock.renounceRole(TIMELOCK_ADMIN_ROLE, address(deployer));
7. Technical Highlights
| Key Point | Description |
|---|---|
| Governor modules | CountingSimple + Votes + Timelock + Settings |
| ERC20Votes | Governance token with historical snapshots (IVotes interface) |
| 3-state voting | Against(0) / For(1) / Abstain(2) |
| Quorum calculation | 4% based on getPastTotalSupply(timepoint) |
| Timelock delay | Wait for minDelay after passing before execution |
| Diamond inheritance | override(Governor, GovernorSettings) explicit specification |
| Deployment order | Token → Timelock → Governor → grant roles |