本文へ移動
    hazBasehazBaseDocs
    IMPLEMENTATION PATTERNS

    コミュニティ・ガバナンス

    提案・投票・実行までを、参加者と権限に合わせて設計する。

    実装パターン一覧
    具体例

    共同施設の予算配分を、提案・投票・実行につなぐ

    共同施設の運営コミュニティが、保有する予算トークンから100単位を活動担当者へ配分する例です。提案本文だけでなく、送金先と数量を含む実行データを投票対象にし、可決後に待機期間を経て実行します。

    提案内容と実行内容を一致させ、誰が投票し、いつ可決され、いつ配分が実行されたかを追跡できます。

    システム構成と役割

    1. 提案・投票画面

      対象・数量・期間を入力し、実行データを読める形で表示します。

      Web UI / Wallet
    2. 投票権と集計条件

      投票権のトークン、委任、閾値や定足数の条件を設定します。

      IVotes / Weight strategy
    3. 提案の管理

      提案ID、投票期間、投票結果、状態を管理します。

      GenericGovernorHelper
    4. 待機後の実行

      Timelockが待機期間後に対象コントラクトを呼び出します。

      TimelockController / Target contract

    データモデル例

    業務DBに保存する情報と、チェーンやAPIで管理する情報を対応づけます。フィールド名とサンプル値は、この構成で使うアプリケーション側の設計例です。

    項目・値の例管理する場所役割・対応関係
    proposalIduint256 as stringチェーン+業務DBGovernorが返す提案ID。JavaScriptのnumberへ変換せず文字列で保存します。
    targets / values / calldatastoken / 0 / transfer(...)チェーン同じ添字の要素が1つの呼出しを表します。valuesはネイティブ通貨の数量です。
    descriptionCommunity activity grantチェーン+業務DB提案IDの計算にも使用する本文。実行データと対応づけて保存します。
    startTs / endTsUNIX secondsチェーン投票の開始・終了時刻。ブラウザのミリ秒をそのまま渡しません。
    vote / supportproposalId / 1チェーン0は反対、1は賛成、2は棄権です。投票権は設定された戦略に従います。
    executionHash / operationId0x… / timelock IDチェーン+業務DB提案IDとTimelockの操作ID、最終実行の取引ハッシュを区別します。

    処理の流れ

    1. 投票基盤と予算を用意する

      Governor、Timelock、IVotes対応トークン、Weight Strategyを接続します。予算トークンは実行者であるTimelockが保有し、必要な操作権限を持つ構成にします。

      GenericGovernor / TimelockController / IVotes
    2. 実行する処理を提案に含める

      送金先と100単位をERC-20のtransfer呼出しデータへ変換し、投票期間と本文を添えてproposeします。画面にはエンコードされた値だけでなく、対象トークン・宛先・表示数量を表示します。

      Interface.encodeFunctionData → governor.propose
    3. 投票を受け付ける

      Activeの期間に、投票権を持つ利用者が署名して投票します。ERC20Votesを使う構成では、必要な委任や過去時点の投票権を確認します。保有量がそのまま投票可能数になるとは限りません。

      state(proposalId) / castVote(proposalId, 1)
    4. 可決した提案を待機状態へ送る

      投票終了と可決を確認してqueueします。可決・Queued・実行済みは別の状態です。否決や取消しを含め、状態に応じて操作ボタンを制御します。

      queue(proposalId) → Timelock delay
    5. 待機期間後に実行する

      Timelockの操作がReadyになったら、許可された実行者がexecuteします。取引確定後に配分先の残高と実行イベントを確認し、提案画面へ反映します。

      Timelock.isOperationReady / governor.execute(proposalId)

    コードを利用する前の準備

    • Governor・Timelock・投票トークン・Weight Strategyを配置し、提案・キュー・実行に必要な役割を設定します。
    • 予算トークンはTimelockが保有します。譲渡可能で、Pauseやホワイトリストなどの条件も満たす必要があります。
    • startTs・endTsには対象チェーンの時刻を基準としたUNIX秒を使用します。終了は開始より後で、設定できる期間の上限に合わせます。
    Install
    npm install --save-exact @hazbase/kit@0.9.0 ethers@6.16.0

    以下はアプリケーションから呼び出すTypeScriptの関数です。接続先・権限・対象データを引数として渡します。UI、DBへの保存、処理の再開は、上記の構成に沿って業務側へ組み込めます。

    100単位の配分を提案する

    units: "100"を渡し、予算トークンの小数桁に応じて実行データを生成します。この関数は提案を作成します。投票・queue・executeは各状態に達した後の別操作です。

    pattern-governance.ts
    import { GenericGovernorHelper, FlexibleTokenHelper } from '@hazbase/kit';
    import { Interface, parseUnits } from 'ethers';
    type Signer = Parameters<typeof GenericGovernorHelper.deploy>[1];
    
    export async function proposeCommunityGrant(input: {
      governorAddress: string;
      budgetTokenAddress: string;
      recipient: string;
      units: string;
      description: string;
      startTs: bigint;
      endTs: bigint;
      proposer: Signer;
    }) {
      const governor = GenericGovernorHelper.attach(input.governorAddress, input.proposer);
      const token = FlexibleTokenHelper.attach(input.budgetTokenAddress, input.proposer);
      const amount = parseUnits(input.units, await token.decimals());
      if (amount <= 0n) throw new Error('Grant must be positive.');
      const erc20 = new Interface(['function transfer(address to, uint256 amount)']);
    
      // At execution, Timelock must hold the budget tokens being transferred.
      const proposalId = await governor.propose({
        proposer: await input.proposer.getAddress(),
        targets: [input.budgetTokenAddress],
        values: [0n],
        calldatas: [erc20.encodeFunctionData('transfer', [input.recipient, amount])],
        description: input.description,
        startTs: input.startTs,
        endTs: input.endTs,
      });
    
      // Voting, queueing and execution are separate actions at later states.
      return { proposalId: proposalId.toString(), state: await governor.state(proposalId) };
    }
    
    コード例をダウンロード

    実行結果と、アプリケーションへの反映

    proposalIdを文字列で保存し、stateで進行状況を取得できます。提案の作成時点ではトークンは移転しません。実際の配分は可決と待機期間を経たexecuteで行われます。

    実装で押さえておきたい点

    提案本文と実行内容の不一致

    本文に書かれた数量だけを信頼せず、targets・values・calldatasをデコードして利用者に表示します。宛先やトークンの取り違えを提案前に確認できるようにします。

    予算を持つアカウント

    提案者のウォレットに残高があっても、Timelockからの送金原資にはなりません。実行主体の残高と、対象コントラクトの権限を確認します。

    可決後の実行失敗

    可決後に残高不足、Pause、移転制限が生じると実行が失敗します。可決済みと実行済みを区別し、再実行前に対象操作の状態を確認します。

    用途に合わせた拡張

    予算配分以外の操作

    権限を持つTimelockから実行できる操作なら、契約設定の変更なども提案対象にできます。対象関数ごとに必要な権限と影響範囲を表示します。

    利用者ごとに異なる投票権

    会員資格や役割に応じた投票権は、IVotesとWeight Strategyの組み合わせで設計します。単純な残高投票との違いは、閾値・定足数・基準時点まで含めて定義します。