本文將幫助您理解 DAO 的概念,並幫助您構建一個基本的 DAO。
什麼是 DAO?
您可以將 DAO 視為基於網際網路的實體(比如企業),由其股東(擁有代幣和比例投票權的成員)共同擁有和管理。在 DAO 中,決策是透過提案做出的,DAO 的成員可以對這些提案進行投票,然後執行它們。
DAO 完全由可公開檢視/可驗證的程式碼管理,沒有一個人(如 CEO)負責決策。
DAO 如何運作?
如前所述,DAO 由程式碼管理,但是如果執行程式碼的機器的人決定關閉機器或編輯程式碼怎麼辦?
所需要的是讓相同的程式碼在由不同實體託管的一組機器上執行,這樣即使其中一個關閉,另一個也可以接管。區塊鏈幫助我們解決了上述問題,基於 EVM 的區塊鏈(如 Ethereum 和 Polygon)允許我們在公共去中心化分類賬上執行智慧合約。部署在這些網路上的智慧合約將傳播到網路上所有可以檢視和驗證它的節點,並且沒有任何一方控制網路。
具有代幣成員資格的 DAO 向其成員發行代幣,代幣代表系統中的投票權。根據所設定的治理,任何人都可以建立 DAO 更改提案,並將其提交給具有法定人數(透過所需的最低百分比/票數)和投票持續時間的投票。成員可以對提案進行檢視和投票,投票權與成員擁有的代幣數量成正比。投票期結束後,我們檢查提案是否透過,如果透過,則執行。
下圖顯示了流程。
讓我們開始建設
我們將在我們的程式碼庫中使用OpenZeppelin合約,我還將使用 Patrick Collins 的DAO 模板中的一些程式碼。
先決條件
您將需要以下內容才能開始。
- Node.js :您可以從 Node.js網站下載最新版本。我在撰寫本文時使用的版本是 16.14.2。
- Yarn:我們將使用 Y arn作為包管理器。
- Hardhat:我們將使用Hardhat作為本地開發環境。
資料庫
我已經編寫程式碼並將其推送到 GitHub,如果您想自己嘗試,可以在此處獲取程式碼,不過我建議您留下來,因為我會解釋程式碼。
場景
我們將構建一個將執行以下操作的 DAO:
場景 1
- 新增初始成員。(讓我們稱他們為創始人)。
- 讓創始人建立一個提案。(提出要在智慧合約上執行的功能)。
- 讓方正對上述提案進行投票,因為方正擁有 100% 的投票份額,因此它將透過。
- 執行提案。(以及智慧合約中的功能)
場景 2
- 新增一個初始成員(我們稱他們為 Founder)。
- 新增另一個成員並向他們發行價值 20% 的創始人份額的新代幣。
- 讓創始人建立一個提案(提出一個要在智慧合約上執行的功能)。
- 讓創始人和新成員對上述提案進行投票。法定人數設定為 90%。
- 執行提案(以及智慧合約中的功能)。
合約
如前所述,我們將使用 OpenZeppelin 的治理合約。合同如下:
- Governor contract: Governor contract決定了一個quorum所需的票數/百分比(例如,如果quorum是4%,那麼只需要4%的選民投票透過該提案),投票週期即多久投票是否開放,投票延遲,即提案建立後多長時間允許成員更改他們擁有的代幣數量。總督還提供建立提案、投票和執行提案的功能。
- TimeLock: TimeLock 合約為不同意決定執行前退出系統的成員提供時間。
- Token: Token合約是一種特殊型別的ERC20合約,實現ERC20Votes擴充套件。這允許將投票權對映到過去餘額的快照而不是當前餘額,這有助於防止成員知道即將提出重要提案並試圖透過購買更多代幣然後在之後拋售來增加他們的投票權投票。
- 目標:這是合約,其程式碼將在提案透過投票後執行。
程式碼
讓我們開始把這一切放在一起。使用 Hardhat 建立一個空的示例專案。在您的終端中執行以下命令。
yarn add — 開發安全帽
接下來,讓我們使用 hardhat 建立我們的資料夾結構。
你應該看到這樣的提示
單擊“建立基本示例專案”。該過程完成後,您應該會看到類似這樣的內容。
合約
讓我們開始新增合約,首先讓我們新增 GovernorContract。我們可以從OpenZeppelin獲得相同的程式碼,或者您可以複製下面的程式碼或從我的倉庫中複製程式碼。我的合約程式碼修復了 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()
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 getVotes(address account, uint256 blockNumber)
public
view
override(Governor, IGovernor)
returns (uint256)
{
return _getVotes(account, blockNumber, _defaultParams());
}
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)
{
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, GovernorTimelockControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
接下來,讓我們新增令牌合約,這在 OpenZeppelin 上也是可用的。我的程式碼有一個額外的“issueToken”函式(稍後會詳細介紹)。
// SPDX-License-Identifier: MIT
pragma 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)
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);
}
function issueToken(address to, uint256 amount) public{
_mint(to, amount);
}
}
最後,讓我們檢查一下 Target 合約,在我們的例子中,我們將使用 Patrick Collins 使用的相同 Box 合約。
// SPDX-License-Identifier: MIT
pragma 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 execute
constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors
) TimelockController(minDelay, proposers, executors) {}
}
測試
現在我們有了合同,我們需要編寫測試。在“test”資料夾下建立一個檔案“sample-test.js”。讓我們開始編寫我們的測試。首先,讓我們使用以下資料建立一個名為“helper.config.js”的配置檔案。
module.exports=
{
MIN_DELAY:3600,
QUORUM_PERCENTAGE:90,
VOTING_PERIOD:5,
VOTING_DELAY:3,
ADDRESS_ZERO :"0x0000000000000000000000000000000000000000"
}
法定人數為 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()
提案建立
接下來,建立提案。我們傳遞將在 Box 合約及其引數上呼叫的函式的編碼值。
propose 函式的輸出是一個包含Proposal 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 合約上觸發儲存功能。
表決
然後我們對該提案進行投票,投票“1”表示同意。
注意:在這種情況下,我們只有一名成員(擁有 100% 的選票)在投票。
const voteWay = 1
const reason = "I vote yes"
let voteTx = await deployedGovernor.castVoteWithReason(proposalId, voteWay, reason)
佇列和執行
接下來,來自 DAO 的任何成員都可以排隊並執行該提案,如果提案透過投票,它將被執行,並且 Box 合約上的儲存函式將被呼叫,值為 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)
執行測試
我們現在可以使用以下命令執行測試
紗線安全帽測試
向新成員發行代幣
我們上面看到的是場景 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 個令牌。
新成員投票
現在我們有了另一個成員,我們可以用他來投票,但是新成員不能投票,除非他首先將自己新增為代幣合約的代表,這樣做是為了讓擁有代幣但不想參與決策的成員不需要花費額外的 gas 成本來維護他們在賬本上的投票權快照。
自委託的程式碼如下。
[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)
透過Github 獲取更多區塊鏈學習資料!