Appearance
L2-20: OZ Governor DAO(OpenZeppelin Governor 标准治理)
1. 问题
使用 OpenZeppelin 的 Governor 标准合约构建生产级 DAO 治理系统。核心模块:GovernorCountingSimple(For/Against/Abstain 三态投票)、GovernorVotes(基于 ERC20Votes 的代币投票权重)、GovernorTimelockControl(提案通过后延时执行)、GovernorSettings(可配置投票参数)。
这是 Compound Governor Bravo 模式的标准化实现,被数百个 DAO 使用。
2. 原因
从零实现 DAO 治理合约容易出错——投票计数、法定人数计算、时间锁安全检查都是潜在的漏洞点。OpenZeppelin Governor 框架提供了模块化的、经过审计的治理组件:
- CountingSimple:已验证的 For/Against/Abstain 计数逻辑
- Votes 集成:通过 IVotes 接口与 ERC20Votes(带历史快照的投票代币)无缝衔接
- TimelockControl:提案通过后强制执行延迟执行(通常 2 天),给用户时间退出
- Settings 可配置:votingDelay、votingPeriod、proposalThreshold 全部可调
这个挑战教授的不是「从头写治理合约」,而是「如何正确组装和使用经过验证的治理模块」——这是专业 Solidity 开发的核心能力。
3. 方案
架构设计
GovernanceToken (ERC20 + ERC20Permit + ERC20Votes)
│
│ 提供代币 + 投票权重
▼
OZGovernorDAO (Governor + CountingSimple + Votes + Timelock + Settings)
│
│ 管理
▼
TimelockController (延时执行器)
│
│ 执行
▼
目标合约
治理代币
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);
}
// 必须重写: Solidity 要求多重继承的 _update 显式指定
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 合约
solidity
contract OZGovernorDAO is
Governor,
GovernorCountingSimple, // For/Against/Abstain 计数
GovernorVotes, // ERC20Votes 集成
GovernorTimelockControl, // Timelock 延时执行
GovernorSettings // 可配置参数
{
uint256 public constant QUORUM_BPS = 400; // 4% 法定人数
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;
}
// 菱形继承需要的显式重写
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(); }
}
提案生命周期
1. propose(targets, values, calldatas, description)
→ state = Pending (votingDelay 期间)
2. [votingDelay 过后]
→ state = Active (votingPeriod 期间,开始投票)
3. castVote(proposalId, support) // 0=Against, 1=For, 2=Abstain
4. [votingPeriod 结束,forVotes > againstVotes,达到 quorum]
→ state = Succeeded
5. queue(targets, values, calldatas, descriptionHash)
→ 进入 Timelock 队列 (minDelay 期间)
6. [minDelay 过后]
→ execute(targets, values, calldatas, descriptionHash)
部署顺序
- 部署
GovernanceToken("GovToken", "GOV", deployer, 1000000e18) - 部署
TimelockController(minDelay=2days, proposers=[], executors=[], admin=address(0)) - 部署
OZGovernorDAO(token, timelock, votingDelay=1day, votingPeriod=1week, proposalThreshold=1000e18) - 授予 Governor 合约在 Timelock 中的 proposer + executor 角色
- 放弃 Timelock 的 admin 角色(
renounceRole)
4. 遭遇的陷阱
4.1 菱形继承的函数重写冲突
OZGovernorDAO 继承了 5 个合约,其中 votingDelay()、votingPeriod()、proposalThreshold() 在 Governor 和 GovernorSettings 中都有定义。Solidity 要求显式指定使用哪个父合约的实现。不写 override(Governor, GovernorSettings) 会编译失败。
4.2 ERC20Votes 的快照机制
getPastTotalSupply(timepoint) 和 getPastVotes(account, timepoint) 使用「检查点」记录历史余额。这要求 _update 在每次代币转移时写入检查点。如果忘记在 GovernanceToken 中重写 _update(调用 ERC20Votes._update),投票权重查询会失败。
4.3 Timelock 的角色配置
TimelockController 有三个角色:PROPOSER_ROLE(可提交提案到 Timelock 队列)、EXECUTOR_ROLE(可执行队列中的操作)、CANCELLER_ROLE(可取消队列中的操作)。必须将这些角色授予 Governor 合约——否则提案通过后无法执行。
4.4 Timelock minDelay 的设置
minDelay 太短(如 1 小时)→ 用户来不及在恶意提案执行前退出。太长(如 30 天)→ 治理效率极低。标准值是 2 天(172800 秒)。
5. 陷阱的原因
5.1
Solidity 的多重继承使用 C3 线性化来解决菱形问题。当多个父合约定义同一函数时,override(A, B) 告诉编译器你意识到了冲突并选择调用 super.func()(由 C3 顺序决定)。
5.2
ERC20Votes 通过事件溯源追踪历史投票权重。每个代币转移触发 DelegateVotesChanged 事件,写入发送者和接收者的"检查点"(checkpoint)。getPastVotes 从检查点数组中二分查找。
6. 如何解决陷阱
- 仔细遵循 OpenZeppelin 的 Governor 文档,特别是菱形继承所需的显式 override
- 测试完整的提案生命周期:propose → vote → queue → execute
- 在
GovernanceToken中正确实现_updateoverride(调用ERC20Votes._update) - 部署后验证 Timelock 的角色配置: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. 技术要点
| 要点 | 说明 |
|---|---|
| Governor 模块 | CountingSimple + Votes + Timelock + Settings |
| ERC20Votes | 带历史快照的治理代币(IVotes 接口) |
| 3 态投票 | Against(0) / For(1) / Abstain(2) |
| Quorum 计算 | 基于 getPastTotalSupply(timepoint) 的 4% |
| Timelock 延时 | 通过后等待 minDelay 才可执行 |
| 菱形继承 | override(Governor, GovernorSettings) 显式指定 |
| 部署顺序 | Token → Timelock → Governor → 授权角色 |