まいにちDapps11日目はオンチェーンガバナンスについて見ていきます。筆者はDAOについてあまり知見が多いほうではないのですが、さすがに基本的な仕組みは理解しておこうということでやっていきます。
OpenZeppelinのGovernorコントラクトを作成し、ProposalやVoteがどのように機能するかを確認します。以下の公式のウォークスルーに沿ってやっていきます!
How to set up on-chain governance
DeFi等の分散型プロトコルにおいて、初期は開発チームが意思決定を担いますが、徐々にトークンホルダーのコミュニティに移譲していきます。例えばパラメータの調整、コントラクトのアップグレード、トレジャリーの管理、他プロトコルとのアライアンス、grantの配布などが意思決定の対象です。
このガバナンスの仕組みは一般的にGovernorと呼ばれるコントラクトで実装されます。Compoundによって設計されたGovernorAlphaとGovernorBravoというコントラクトを元として、他プロトコルが採用しやすいように一般化したものをOpenZeppelinが用意してくれています。
hardhatのプロジェクトを作成します。
ガバナンストークンのコードを作成します(ウォークスルーからのコピペです)。ERC20とERC20Voteを継承したコントラクトです。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
contract MyToken is ERC20, ERC20Permit, ERC20Votes {
constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {}
// The functions below are overrides required by Solidity.
function _afterTokenTransfer(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._afterTokenTransfer(from, to, amount);
}
function _mint(address to, uint256 amount) internal override(ERC20, ERC20Votes) {
super._mint(to, amount);
}
function _burn(address account, uint256 amount) internal override(ERC20, ERC20Votes) {
super._burn(account, amount);
}
}
ここからは僕も詳しくない領域なのでOpenZeppelinの和訳中心になります。
コアのロジックはGovernorコントラクトによって提供されますが、まだ以下を選択する必要があります:1)投票力の決定方法、2)quorumに必要な投票数、3)投票時の選択肢とその投票の集計方法、4)投票に使用するトークンの種類。これらは、独自のモジュールを作成するか、より簡単にはOpenZeppelin Contractsから選択することによってカスタマイズ可能です。
1)投票力の決定方法にはGovernorVotesモジュールを使用します。このモジュールは、提案がアクティブになった時点でのトークン残高に基づいてアカウントの投票力を決定するために、IVotesインスタンスにフックします。このモジュールは、トークンのアドレスをコンストラクタパラメータとして必要とします。また、このモジュールはトークンが使用するクロックモード(ERC6372)を検出し、Governorに適用します。
2)Quorumに必要な投票数を定義するために、GovernorVotesQuorumFractionを使用します。これはERC20Votesと連携して、提案の投票力が取得されたブロックの総供給量の割合としてQuorumを定義します。このモジュールは、パーセンテージを設定するためのコンストラクタパラメータが必要です。現在の多くのGovernorは4%を使用しているため、パラメータ4でモジュールを初期化します(これはパーセンテージを示し、4%の結果になります)。
3)投票者には3つの選択肢(For、Against、Abstain)を提供するGovernorCountingSimpleを使用します。また、法定議決に対してはForとAbstainの投票のみがカウントされます。
これらを取り入れたものが以下のコードです。また別途Timelockコントラクトをデプロイしておく必要があります。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
contract MyGovernor is
Governor,
GovernorCompatibilityBravo,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(
IVotes _token,
TimelockController _timelock
) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {}
function votingDelay() public pure override returns (uint256) {
return 7200; // 1 day
}
function votingPeriod() public pure override returns (uint256) {
return 50400; // 1 week
}
function proposalThreshold() public pure override returns (uint256) {
return 0;
}
// The functions below are overrides required by Solidity.
function state(
uint256 proposalId
) public view override(Governor, IGovernor, GovernorTimelockControl) returns (ProposalState) {
return super.state(proposalId);
}
function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) {
return super.propose(targets, values, calldatas, description);
}
function cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) {
return super.cancel(targets, values, calldatas, descriptionHash);
}
function _execute(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) {
super._execute(proposalId, targets, values, calldatas, descriptionHash);
}
function _cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
return super._cancel(targets, values, calldatas, descriptionHash);
}
function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
return super._executor();
}
function supportsInterface(
bytes4 interfaceId
) public view override(Governor, IERC165, GovernorTimelockControl) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
デプロイして、デプロイしたコントラクトをTallyというサービスに登録してフロントエンドから触れるようにしていきます。
https://www.tally.xyz/add-a-dao/governor
https://www.tally.xyz/gov/dapps-everyday-dao
proposalを作成してみます。
https://www.tally.xyz/gov/dapps-everyday-dao/proposal/create
ウォレット接続されている状態で、TitleとDescriptionを入れます。add actionsはOptionalのようですが、proposalが可決された際に実行されるfuncitonを設定できるみたいです。なので例えばgrantでこのアドレスにこのトークンをいくらtransferするという設定をする、という使い方が考えられます。
submit on-chainでtransactionを起こしてsubmitします。
ちょっとここで自分のミスがあって、そもそもガバナンストークンにmint機能を付け忘れて誰もVoting Powerを持っていないということになってしまったので😅 他のプロジェクトの画面を参照させてもらいます。
ビジュアライゼーションもされていて面白いですね。
↓否決されているものもありましたw
https://www.tally.xyz/gov/nounsdao/proposal/352
まとめ
Onchainガバナンスを実現するための手順について調べてまとめました。Governorコントラクトの設計とTallyについて学びました。これでクライアント企業さんから「DAO作りたいんだけど!」って言われても対応できますね!ちなみにCharmverseというTallyと似たようなサービスもあるらしいです。こちらの記事が詳しかったのでご覧ください
弊社Pontechはweb3に関わる開発を得意とするテック企業です。サービス開発に関するご相談はこちらのフォームからお願いいたします。
また、受託開発案件に共に取り組むメンバーを募集しています!ご興味のある方はぜひお話させてください!