Skip to content
On this page

L2-20: OZ Governor DAO(OpenZeppelin Governor 標準ガバナンス)

1. 課題

OpenZeppelin の Governor 標準コントラクトを使用して、本番レベルの DAO ガバナンスシステムを構築します。コアモジュール:GovernorCountingSimple(賛成/反対/棄権の 3 状態投票)、GovernorVotes(ERC20Votes に基づくトークン投票重み)、GovernorTimelockControl(提案可決後の遅延実行)、GovernorSettings(設定可能な投票パラメータ)。

これは Compound Governor Bravo パターンの標準化された実装であり、数百の DAO で使用されています。

2. 理由

DAO ガバナンスコントラクトをゼロから実装するとエラーが発生しやすくなります。投票カウント、定足数計算、タイムロックのセキュリティチェックはすべて潜在的な脆弱性ポイントです。OpenZeppelin Governor フレームワークは、モジュール化された監査済みのガバナンスコンポーネントを提供します:

  • CountingSimple:検証済みの賛成/反対/棄権のカウントロジック
  • 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,   // 賛成/反対/棄権のカウント
    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=反対, 1=賛成, 2=棄権
4. [votingPeriod 終了、forVotes > againstVotes、定足数達成]
   → 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 には 3 つのロールがあります: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 イベントを発行し、送信者と受信者の「チェックポイント」を書き込みます。getPastVotes はチェックポイント配列に対してバイナリサーチを実行します。

6. 落とし穴の解決方法

  • OpenZeppelin の Governor ドキュメント、特にダイヤモンド継承に必要な明示的な override を注意深く守る
  • 完全な提案ライフサイクルをテストする:propose → vote → queue → execute
  • GovernanceToken_update のオーバーライドを正しく実装する(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 状態投票反対(0) / 賛成(1) / 棄権(2)
定足数計算getPastTotalSupply(timepoint) に基づく 4%
Timelock 遅延可決後 minDelay 待ってから実行可能
ダイヤモンド継承override(Governor, GovernorSettings) による明示的な指定
デプロイ順序Token → Timelock → Governor → ロール付与

Built with AiAda