🦄

【ガバナンスやりたい人必見】OpenZeppelinを用いたDAOのスマコン例 ~hardhatによるテストコードを添えて~

2023/01/15に公開

結論

  1. OZの記事を見ろ
  1. 実際に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が出しています。

流れ

  1. Propose (提案)
  2. CastVote (投票)
  3. Queue (実行前待ち行列に追加的な)
  4. Execute (実行)

環境とか

https://github.com/lowzzy/governance-sample

hardhatを使用しています。hardhatの導入方法は調べて見てください。省略します。

実際のコードの構成など(コントラクト)

コントラクトは3つです。

  1. Gov.sol : ガバナンスをするコントラクト(メインのコントラクト)
  2. Token.sol : ガバナンストークンを発行するコントラクト
  3. 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つ

  1. deployGovFixture() : デプロイ ~ 準備
  2. 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 ↓とあるので、そちらの関数使いながら確認してみると良きです。(今回は力尽きたので飛ばします、リクエストあったらやるかも、、、)

その他

ステータスの種類

  1. Pending
  2. Active
  3. Canceled
  4. Defeated
  5. Succeeded
  6. Queued
  7. Expired
  8. Executed

感想

おもろいなーと思ったこと-> proposeした際にpropose内容であるdescriptionやcalldata, targetなどを全てハッシュ化していること。そのまま保持するのではなくて、ハッシュ化してIDにして、proposalsってmappingのindexとしてそのハッシュIDを用いることで提案にアクセスしていること。
多分これは、チェーンにdescriptionなどの詳細データは刻まれているのでコントラクトとしては保持する必要がなく、ミニマムではアクセスして提案の投票状況などが取得できれば良いだけだからと言う理由な気がする。

OpenZeppelinの出しているガバナンスのソースコードから確認できるのでぜひ。

最後に

というわけでDAOには必須のガバナンスコントラクトの紹介をしました。

主に

  • OpenZeppelin
  • hardhat
    を用いました。

Discussion