如何在 Solidity 中構建 DAO(去中心化自治組織)?

source : https://blog.blockmagnates.com/how-to-build-a-dao-decentralized-autonomous-organization-in-solidity-af1cf900d95d by pranav kirtani

本文將幫助您理解 DAO 的概念並幫助您構建一個基本的 DAO。

什麼是 DAO?

您可以將 DAO 視為一個基於互聯網的實體(例如企業),由其股東(擁有代幣和比例投票權的成員)集體擁有和管理。在 DAO 中,決策是通過提案做出的,DAO 的成員可以對這些提案進行投票,然後執行它們。

DAO 完全由公開可見/可驗證的代碼管理,沒有一個人(如 CEO)負責決策。

DAOs 是如何工作的?

如前所述,DAO 由代碼管理,但如果運行代碼的機器的人決定關閉機器或編輯代碼怎麼辦?

所需要的是在由不同實體託管的一組機器上運行相同的代碼,這樣即使其中一個關閉,另一個也可以接管。區塊鏈幫助我們解決了上述問題,基於 EVM 的區塊鏈(如 Ethereum 和 Polygon)允許我們在公共去中心化賬本上運行智能合約。部署在這些網絡上的智能合約將傳播到網絡上所有可以查看和驗證的節點,並且沒有任何一方控製網絡。

具有代幣成員資格的 DAO 向其成員發行代幣,代幣代表系統中的投票權。根據設置的治理,任何人都可以創建一個更改 DAO 的提案,並將其提交給具有法定人數(通過所需的最小百分比/票數)和投票持續時間的投票。會員可以查看提案並對其進行投票,投票權與會員擁有的代幣數量成正比。投票期結束後,我們檢查提案是否通過,如果通過,則執行。

DAO 的一些例子是 MakerDAO 和 Aragon

下圖顯示了流程。

接下來,我們將構建我們自己的 DAO。

讓我們開始建造

我們將在我們的代碼庫中使用 OpenZeppelin 合約,我還將使用 Patrick Collins 的 DAO 模板中的一些代碼。

先決條件

您將需要以下內容才能開始。

  1. Node.js:您可以從 Node.js 網站下載最新版本。我在寫這篇文章時使用的版本是 16.14.2。
  2. Yarn:我們將使用 Yarn 作為包管理器。
  3. Hardhat:我們將使用Hardhat 作為我們的本地開發環境。

存儲庫

我已經編寫並推送了代碼到 GitHub,如果你想自己嘗試,你可以在這裡獲取代碼,不過我會建議留下來,因為我會解釋代碼。

場景

我們將構建一個 DAO,它將執行以下操作:

方案 1

  1. 添加初始成員。 (讓我們稱他們為創始人)。
  2. 讓創始人創建一個提案。 (提出要在智能合約上執行的功能)。
  3. 讓創始人對上述提案進行投票,由於創始人擁有 100% 的投票份額,它將通過。
  4. 執行提案。 (以及智能合約內部的功能)

方案 2

  1. 添加一個初始成員(我們稱他們為創始人)。
  2. 添加另一個成員並向他們發行價值 20% 創始人份額的新代幣。
  3. 讓創始人創建一個提案(提出要在智能合約上執行的功能)。
  4. 讓創始人和新成員​​對上述提案進行投票。法定人數設置為 90%。
  5. 執行提案(以及智能合約中的函數)。

合約

如前所述,我們將使用 OpenZeppelin 的治理合約。合同如下:

  1. 州長合約:州長合約決定了一個法定人數所需的票數/百分比(例如,如果法定人數為 4%,則只需 4% 的選民投票即可通過),投票期限,即多長時間投票是否開放,投票延遲,即提案創建後多長時間允許成員更改他們擁有的代幣數量。總督還提供創建提案、投票和執行提案的功能。
  2. TimeLock:TimeLock 合約為在決定執行前不同意退出系統的決定的成員提供時間。
  3. Token:Token合約是一種特殊類型的ERC20合約,實現了ERC20Votes擴展。這允許將投票權映射到過去餘額的快照而不是當前餘額,這有助於防止知道有重要提案即將出現並試圖通過購買更多代幣然後拋售它們來增加他們的投票權的成員投票。
  4. 目標:這是提案通過投票後將執行其代碼的合約。

代碼

讓我們開始把這一切放在一起。使用 Hardhat 創建一個空的示例項目。在終端中運行以下命令。

yarn add — dev hardhat

接下來,讓我們使用安全帽來創建我們的文件夾結構。

yarn hardhat

你應該看到這樣的提示

單擊“創建基本示例項目”。 該過程完成後,您應該會看到類似這樣的內容。

如果您從我的倉庫中克隆了代碼,那麼您可以跳過上述步驟。

合約

讓我們開始添加合約,首先讓我們添加GovernorContract。 我們可以從 OpenZeppelin 獲得相同的代碼,或者您可以復制下面的代碼或從我的 repo 中。 我的合約代碼修復了 OpenZeppelin 版本中的問題,以及投票延遲、法定人數和投票期限的參數化,類似於 Patrick Collins 版本。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
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 GovernorContract is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {constructor(IVotes _token, TimelockController _timelock,uint256 _quorumPercentage,uint256 _votingPeriod,uint256 _votingDelay)Governor(“GovernorContract”)GovernorSettings(_votingDelay,_votingPeriod,0)GovernorVotes(_token)GovernorVotesQuorumFraction(_quorumPercentage)GovernorTimelockControl(_timelock){}// The following functions are overrides required by Solidity.function votingDelay()publicviewoverride(IGovernor, GovernorSettings)returns (uint256)
{return super.votingDelay();}function votingPeriod()publicviewoverride(IGovernor, GovernorSettings)returns (uint256){return super.votingPeriod();}function quorum(uint256 blockNumber)publicviewoverride(IGovernor, GovernorVotesQuorumFraction)returns (uint256){return super.quorum(blockNumber);}function getVotes(address account, uint256 blockNumber)publicviewoverride(Governor, IGovernor)returns (uint256){return _getVotes(account, blockNumber, _defaultParams());}function state(uint256 proposalId)publicviewoverride(Governor, GovernorTimelockControl)returns (ProposalState){return super.state(proposalId);}function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)publicoverride(Governor, IGovernor)returns (uint256){return super.propose(targets, values, calldatas, description);}function proposalThreshold()publicviewoverride(Governor, GovernorSettings)returns (uint256){return super.proposalThreshold();}function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)internaloverride(Governor, GovernorTimelockControl){super._execute(proposalId, targets, values, calldatas, descriptionHash);}function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)internaloverride(Governor, GovernorTimelockControl)returns (uint256){return super._cancel(targets, values, calldatas, descriptionHash);}function _executor()internalviewoverride(Governor, GovernorTimelockControl)returns (address){return super._executor();}function supportsInterface(bytes4 interfaceId)publicviewoverride(Governor, GovernorTimelockControl)returns (bool){return super.supportsInterface(interfaceId);}}

接下來,讓我們添加 Token 合約,這在 OpenZeppelin 上也是可用的。 我的代碼有一個額外的“issueToken”功能(稍後會詳細介紹)。

// SPDX-License-Identifier: MITpragma solidity ^0.8.2;import "@openzeppelin/contracts/token/ERC20/ERC20.sol";import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";contract MyToken is ERC20, ERC20Permit, ERC20Votes {constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {_mint(msg.sender, 1000);}// The functions below are overrides required by Solidity.function _afterTokenTransfer(address from, address to, uint256 amount)internaloverride(ERC20, ERC20Votes){super._afterTokenTransfer(from, to, amount);}function _mint(address to, uint256 amount)internaloverride(ERC20, ERC20Votes){super._mint(to, amount);}function _burn(address account, uint256 amount)internaloverride(ERC20, ERC20Votes){super._burn(account, amount);}function issueToken(address to, uint256 amount) public{_mint(to, amount);}}

接下來是 TimeLock 合約,這是來自 DAO-template 的合約副本

// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/governance/TimelockController.sol";contract TimeLock is TimelockController {// minDelay is how long you have to wait before executing// proposers is the list of addresses that can propose// executors is the list of addresses that can executeconstructor(uint256 minDelay,address[] memory proposers,address[] memory executors) TimelockController(minDelay, proposers, executors) {}}

最後,讓我們檢查一下 Target 合約,在我們的例子中,我們將使用 Patrick Collins 使用的同一個 Box 合約。

// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "@openzeppelin/contracts/access/Ownable.sol";contract Box is Ownable {uint256 private value;// Emitted when the stored value changesevent ValueChanged(uint256 newValue);// Stores a new value in the contractfunction store(uint256 newValue) public onlyOwner {value = newValue;emit ValueChanged(newValue);}// Reads the last stored valuefunction retrieve() public view returns (uint256) {return value;}}

測試

呼!!,那是很多代碼,現在我們有了合約,我們需要編寫我們的測試。 在“test”文件夾下創建一個文件“sample-test.js”。 讓我們開始編寫我們的測試。 首先,讓我們使用以下數據創建一個名為“helper.config.js”的配置文件。

module.exports=
{
MIN_DELAY:3600,
QUORUM_PERCENTAGE:90,
VOTING_PERIOD:5,
VOTING_DELAY:3,
ADDRESS_ZERO :"0x0000000000000000000000000000000000000000"
}

Quorum為 90%,投票週期為 5 個區塊,投票延遲為 3 個區塊。 TimeLock 的最小延遲為 3600 秒。

讓我們編寫代碼以將所有合約部署到本地網絡(Hardhat 在內部進行管理,我們不需要啟動任何進程)

governanceToken = await ethers.getContractFactory("MyToken")deployedToken=await governanceToken.deploy();await deployedToken.deployed();transactionResponse = await deployedToken.delegate(owner.address)await transactionResponse.wait(1)timeLock = await ethers.getContractFactory("TimeLock")deployedTimeLock=await timeLock.deploy(MIN_DELAY,[],[]);await deployedTimeLock.deployed();governor = await ethers.getContractFactory("GovernorContract")deployedGovernor=await governor.deploy(deployedToken.address,deployedTimeLock.address,QUORUM_PERCENTAGE,VOTING_PERIOD,VOTING_DELAY);await deployedGovernor.deployed()box = await ethers.getContractFactory("Box")deployedBox=await box.deploy()await deployedBox.deployed()

接下來,我們需要將已部署的 Target 合約(Box)的所有權轉移給 TimeLock 合約。 這樣做是為了讓 TimeLock 有權對 Box 合約執行操作。

const transferTx = await deployedBox.transferOwnership(deployedTimeLock.address);

接下來,州長合約被授予提議者角色,執行角色被授予“零地址”,這意味著任何人都可以執行提議。

const proposerRole = await deployedTimeLock.PROPOSER_ROLE()const executorRole = await deployedTimeLock.EXECUTOR_ROLE()const adminRole = await deployedTimeLock.TIMELOCK_ADMIN_ROLE()const proposerTx = await deployedTimeLock.grantRole(proposerRole, deployedGovernor.address)await proposerTx.wait(1)const executorTx = await deployedTimeLock.grantRole(executorRole, ADDRESS_ZERO)await executorTx.wait(1)const revokeTx = await deployedTimeLock.revokeRole(adminRole, owner.address)await revokeTx.wait(1)

提案創建

接下來,創建提案。 我們傳遞將在 Box 合約上調用的函數的編碼值及其參數。

提議函數的輸出是一個包含提議 ID 的交易。 這用於跟踪提案。

const proposalDescription="propose this data"let encodedFunctionCall = box.interface.encodeFunctionData("store", [77])const proposeTx = await deployedGovernor.propose([deployedBox.address],[0],[encodedFunctionCall],proposalDescription);

提議是在價值 77 的 Box 合約上觸發 store 功能。

表決

然後我們對該提案進行投票,“1”票表示同意。

注意:在這種情況下,我們只有一名成員(擁有 100% 的選票)正在投票。

const voteWay = 1const reason = "I vote yes"let voteTx = await deployedGovernor.castVoteWithReason(proposalId, voteWay, reason)

隊列和執行

接下來,來自 DAO 的任何成員都可以排隊執行該提案,如果提案通過投票,它將被執行,並調用 Box 合約上的 store 函數,其值為 77。您可能已經註意到諸如 moveTime 和 moveBlocks,這些來自 Patrick Collins DAO 模板,在開發環境中用於模擬時間流逝和塊挖掘,它們幫助我們模擬投票週期的完成、時間鎖定延遲等。

const queueTx = await deployedGovernor.queue([deployedBox.address], [0], [encodedFunctionCall], descriptionHash)await queueTx.wait(1)await moveTime(MIN_DELAY + 1)await moveBlocks(1)console.log("Executing...")const executeTx = await deployedGovernor.execute([deployedBox.address],[0],[encodedFunctionCall],descriptionHash)await executeTx.wait(1)const value = await deployedBox.retrieve();console.log(value)

運行測試

我們現在可以使用以下命令運行測試

yarn hardhat test

向新成員發行代幣

我們在上面看到的是場景 1 的流程。對於場景 2,我們需要向新成員發行新代幣並讓他們對提案進行投票。

發行令牌的代碼如下所示

[owner, addr1, addr2] = await ethers.getSigners();const signer = await ethers.getSigner(addr1.address);const deployedTokenUser2 = await deployedToken.connect(signer)await deployedTokenUser2.issueToken(addr1.address, 200)

函數 getSigners() 返回 Hardhat 開發環境中所有帳戶的列表,然後我們向該地址發放 200 個代幣。

新成員投票

現在我們是另一個成員,我們可以用他來投票,但是新成員不能投票,除非他先將自己添加為 Token 合約的代表,這樣做是為了讓擁有代幣但不想參與決策的成員 不需要花費額外的 gas 成本來維護他們在賬本上的投票權快照。

自我委託的代碼如下。

const voteWay = 1const reason = "I vote yes"const deployedGovernorUser2=await deployedGovernor.connect(signer)voteTx = await deployedGovernorUser2.castVoteWithReason(proposalId, voteWay, reason)