結論
- OZの記事を見ろ
- 実際にhardhatで動かしているコードを見ろ👇
https://github.com/lowzzy/governance-sample
序論~DAO、バズワードなのに日本語記事少な過ぎワロた~
ここ半年くらい、特に直近1ヶ月でガバナンスのスマコンを作らなければならなくなりました。Lowzzyです。なんやかんやNFTとか独自トークンとかは作れたのでいけるやろ!と思って着手しましたがめちゃめちゃ手こずったので、記事を書こうと思い筆、もといキーボードを叩いております。
DAOってweb3ではとてもバズワードで日本でも〜〜DAOってよく聞きますよね。
色々なDAOがありますが、僕の理解ではそもそもDAOはスマートコントラクトが真ん中に必要です。スマコンによってガバナンスをとって行動をする組織だと理解しています。ですが実際スマコンは存在していなかったりするイメージでDAOといいつつただのコミュニティであることも多いです。それもそのはず、まじでガバナンスのスマコン実装の記事が少ない。英語でも少ないですが、日本語だともっと少ないです。僕の調査力が低い可能性もありますが。
DAOって何?
DAOとはDecentralized Autonomous Organizationの略で日本語で言うと自立分散型組織です。
有名な例で言うと↓
- AAVE
- Maker
- Decred
- Compound
- Uniswap
- PancakeSwap
- eCash
参考
ちなみに日本で有名なDAOはこの辺らしい↓
- Ninja DAO
- 國光DAO
- 和組DAO
- SUPER SAPIENSS
- MZ CLUB
- HENKAKU Discord Community
参考
OpenZeppelinが実はガバナンスのスマコン出している
よくあるスマコン、トークン作ろう!って思ったら大体OpenZeppelinが出しています。
- NFT
-
独自トークン
そして今回使用したのが - ガバナンスコントラクト
-
ガバナンスセットアップ
この2つです。
流れ
- Propose (提案)
- CastVote (投票)
- Queue (実行前待ち行列に追加的な)
- Execute (実行)
環境とか
hardhatを使用しています。hardhatの導入方法は調べて見てください。省略します。
実際のコードの構成など(コントラクト)
コントラクトは3つです。
- Gov.sol : ガバナンスをするコントラクト(メインのスマートコントラクト)
- Token.sol : ガバナンストークンを発行するスマートコントラクト
- TLC.sol : (タイムロックコントローラー)ガバナンスの決定が実行されるまでに遅延が追加されるためのスマートコントラクト
contracts/Gov.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
contract Gov is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {
constructor(IVotes _token, TimelockController _timelock)
Governor("MyGovernor")
GovernorSettings(4, 50400 , 0)
GovernorVotes(_token)
GovernorVotesQuorumFraction(1)
GovernorTimelockControl(_timelock)
{}
function votingDelay()
public
view
override(IGovernor, GovernorSettings)
returns (uint256)
{
return super.votingDelay();
}
function votingPeriod()
public
view
override(IGovernor, GovernorSettings)
returns (uint256)
{
return super.votingPeriod();
}
function quorum(uint256 blockNumber)
public
view
override(IGovernor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
}
function state(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (ProposalState)
{
return super.state(proposalId);
}
function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
public
override(Governor, IGovernor)
returns (uint256)
{
return super.propose(targets, values, calldatas, description);
}
function proposalThreshold()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.proposalThreshold();
}
function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
internal
override(Governor, GovernorTimelockControl)
{
string memory errorMessage = "Governor: call reverted without message";
(bool success, bytes memory returndata) = payable(targets[0]).call{value: values[0]}("");
Address.verifyCallResult(success, returndata, errorMessage);
}
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, GovernorTimelockControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
receive() override external payable {
}
}
contracts/TLC.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/governance/TimelockController.sol";
contract TLC is TimelockController {
constructor(
uint minDelay,
address[] memory proposers,
address[] memory executors
)TimelockController(minDelay,proposers,executors) {
}
}
contracts/Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
contract Token is ERC20Votes {
constructor(
string memory name,
string memory symbol,
uint256 totalSupply_
) ERC20(name, symbol) ERC20Permit("HowDAOToken") {
_mint(msg.sender, totalSupply_);
}
}
テストコード by hardhat
テストコードの構成
大きく分けて2つ
- deployGovFixture() : デプロイ ~ 準備
- describe('Gov', function () {} : propose ~ execute
test/Gov.js
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { network, ethers } = require('hardhat');
const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000';
let description = 'description';
describe('Gov', function () {
async function deployGovFixture() {
const [owner, otherAccount] = await ethers.getSigners();
// ############################
// ########## deploy ##########
// ############################
const Gov = await ethers.getContractFactory('Gov');
const Token = await ethers.getContractFactory('Token');
const TLC = await ethers.getContractFactory('TLC');
const token = await Token.deploy(
'GovToken',
'GT',
'10000000000000000000000'
);
const minDelay = 1;
const proposers = [otherAccount.address];
const executors = [otherAccount.address];
const tlc = await TLC.deploy(minDelay, proposers, executors);
const TokenAddress = token.address;
const TlcAddress = tlc.address;
const gov = await Gov.deploy(TokenAddress, TlcAddress);
await token.deployed();
await gov.deployed();
await tlc.deployed();
// #############################
// ########## role ############
// ############################
const proposerRole = await tlc.PROPOSER_ROLE();
const executorRole = await tlc.EXECUTOR_ROLE();
const adminRole = await tlc.TIMELOCK_ADMIN_ROLE();
await tlc.grantRole(proposerRole, gov.address);
await tlc.grantRole(executorRole, ADDRESS_ZERO);
await tlc.revokeRole(adminRole, owner.address);
// ################################
// ########### send eth ###########
// ################################
await owner.sendTransaction({
to: gov.address,
value: ethers.utils.parseEther('10.0'),
});
// ################################
// ########## delegate ############
// ################################
await delegate(owner.address, token);
return { token, gov, owner, otherAccount, tlc };
}
async function execute(token, toAddress, gov) {
const value_ = 100;
try {
const des = await generateHash(description);
const ret = await gov.execute([toAddress], [value_], ['0x'], des);
return ret;
} catch (e) {
console.log(e);
}
}
async function generateHash(str) {
return ethers.utils.keccak256(ethers.utils.toUtf8Bytes(str));
}
async function queue(token, toAddress, gov) {
const value_ = 100;
try {
const des = await generateHash(description);
let ret = await gov.queue([toAddress], [value_], ['0x'], des);
return ret;
} catch (e) {
console.log(e);
}
}
async function propose(token, toAddress, gov) {
const value_ = 100;
try {
const propose_ret = await gov.propose(
[toAddress],
[value_],
['0x'],
description
);
const des = await generateHash(description);
ret = await gov.hashProposal([toAddress], [value_], ['0x'], des);
return ret;
} catch (e) {
console.log(e);
}
}
async function proposalVotes(proposalId, gov) {
try {
const ret = await gov.proposalVotes(proposalId);
return ret;
} catch (e) {
console.log(e);
}
}
async function getVotes(account, blockNumber, gov) {
try {
const ret = await gov.getVotes(account, blockNumber);
return ret;
} catch (e) {
console.log(e);
}
}
async function getDeadLine(proposalId, gov) {
try {
const ret = await gov.proposalDeadline(proposalId);
return ret;
} catch (e) {
console.log(e);
}
}
async function getSnapshot(proposalId, gov) {
try {
const ret = await gov.proposalSnapshot(proposalId);
return ret;
} catch (e) {
console.log(e);
}
}
async function hasVoted(proposalId, account, gov) {
try {
const ret = await gov.hasVoted(proposalId, account);
return ret;
} catch (e) {
console.log(e);
}
}
async function getState(proposalId, gov) {
try {
const ret = await gov.state(proposalId);
return ret;
} catch (e) {
console.log(e);
}
}
async function castVote(proposalId, support, gov) {
try {
const ret = await gov.castVote(proposalId, support);
return ret;
} catch (e) {
console.log(e);
}
}
async function delegate(account, token) {
try {
const ret = await token.delegate(account);
return ret;
} catch (e) {
console.log(e);
}
}
describe('Gov', function () {
it('deploy', async function () {
const { token, gov, owner, otherAccount } = await loadFixture(
deployGovFixture
);
// ###############################
// ########### propose ###########
// ###############################
let proposalId = await propose(token, owner.address, gov);
const id_ = proposalId.toString();
ret = await network.provider.send('hardhat_mine', ['0x4']);
// ##########################################
// ############## ここを変える ################
// ##########################################
const support = 1; // 賛成
// const support = 0; // 反対
// ##############################
// ########### castVote #########
// ##############################
await castVote(id_, support, gov);
await network.provider.send('hardhat_mine', ['0x10000']);
// ###########################
// ########### Queue #########
// ###########################
await queue(token, owner.address, gov);
// ##########################
// ######## Execute #########
// ##########################
await execute(token, owner.address, gov);
// ========== util methods ↓ ===========
// console.log('################################');
// console.log('########## hasVoted ############');
// console.log('################################');
// ret = await hasVoted(id_, owner.address, gov);
// console.log(ret);
// console.log('######################################');
// console.log('########## proposal votes ############');
// console.log('######################################');
// ret = await proposalVotes(0, gov);
// console.log(ret);
// console.log('#############################');
// console.log('########## state ############');
// console.log('#############################');
// ret = await getState(id_, gov);
// console.log(ret);
// console.log('###################################');
// console.log('########## block number ###########');
// console.log('###################################');
// blockNumber = await ethers.provider.getBlockNumber();
// console.log(blockNumber);
});
});
});
上記の最後の方にutil methods ↓とあるので、そちらの関数使いながら確認してみると良きです。(今回は力尽きたので飛ばします、リクエストあったらやるかも、、、)
その他
ステータスの種類
- Pending
- Active
- Canceled
- Defeated
- Succeeded
- Queued
- Expired
- Executed
感想
おもろいなーと思ったこと-> proposeした際にpropose内容であるdescriptionやcalldata, targetなどを全てハッシュ化していること。そのまま保持するのではなくて、ハッシュ化してIDにして、proposalsってmappingのindexとしてそのハッシュIDを用いることで提案にアクセスしていること。
多分これは、チェーンにdescriptionなどの詳細データは刻まれているのでコントラクトとしては保持する必要がなく、ミニマムではアクセスして提案の投票状況などが取得できれば良いだけだからと言う理由な気がする。
OpenZeppelinの出しているガバナンスのソースコードから確認できるのでぜひ。
最後に
というわけでDAOには必須のガバナンスのスマートコントラクトの紹介をしました。
主に
- OpenZeppelin
- hardhat
を用いました。