Skip to content
On this page

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)

部署顺序

  1. 部署 GovernanceToken("GovToken", "GOV", deployer, 1000000e18)
  2. 部署 TimelockController(minDelay=2days, proposers=[], executors=[], admin=address(0))
  3. 部署 OZGovernorDAO(token, timelock, votingDelay=1day, votingPeriod=1week, proposalThreshold=1000e18)
  4. 授予 Governor 合约在 Timelock 中的 proposer + executor 角色
  5. 放弃 Timelock 的 admin 角色(renounceRole

4. 遭遇的陷阱

4.1 菱形继承的函数重写冲突

OZGovernorDAO 继承了 5 个合约,其中 votingDelay()votingPeriod()proposalThreshold()GovernorGovernorSettings 中都有定义。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 中正确实现 _update override(调用 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 → 授权角色

Built with AiAda