用hardhat、TypeScript、solidity和Openzeppelin從頭開始建立一個去中心化的治理模式。這個DAO(去中心化自治組織)使用具有投票權的ERC20代币來進行決策
簡介
我們将學習如何使用hardhat、solidity、typescript和Openzeppelin建構一個鍊上DAO。對于那些不知道的人來說,DAO是一個去中心化的自治組織,通常由區塊鍊驅動。
我們将直接跳到代碼中。我們将使用100%的鍊上治理模式,使用ERC20代币對提議的修改進行投票。一旦工具改善了鍊外投票(使用Chainlink OCR[1]模型和像IPFS[2]這樣的去中心化資料庫),我們可能會有一個教程來實踐這個,以節省氣體。
快速開始
最快的方法是執行以下操作。
git clone https://github.com/PatrickAlphaC/dao-template
cd dao-template
yarn
yarn hardhat test
然後轟隆一聲! 你将通過模拟提議投票、投票、排隊投票、然後執行的測試來運作!
以下是該測試套件的工作内容。
- 1. 我們部署了一個ERC20代币,我們将用它來管理我們的DAO。
- 2. 我們部署一個Timelock合約,用來在執行提案之間提供一個緩沖區。
注:Timelock是處理所有資金、所有權等的合同。
- 1. 我們部署我們的治理合同
注意:治理者合同是負責提案等的,但Timelock負責執行
- 1. 我們部署一個Box 合同的例子,它将被我們的治理過程所擁有! (也就是我們的Timelock合約)。
- 2. 我們提出一個新值,添加到我們的Box合同中。
- 3. 然後我們對該提議進行投票。
- 4. 然後我們排隊執行該提案。
- 5. 然後,我們執行它!
但是,讓我們把它分解給你看......
入門
建議你在進行這裡的工作之前,先浏覽一下hardhat的入門文檔[3]。
要求
- • git[4]:如果你能運作git --version并看到git版本x.x.x這樣的響應,你就知道你做對了。
- • Nodejs[5]:如果你能運作node --version,并得到類似:vx.x.x的輸出,你就知道你已經正确安裝了nodejs。
- • Yarn[6]而不是npm:如果你能運作: yarn --version并得到一個類似于:x.x.x的輸出,你就知道你已經正确地安裝了yarn。
我們正在建構的東西
我們将建立一個DAO,使用ERC20代币對我們的基本Box.sol合約進行投票,它看起來像這樣。
contract.box.sol [7]
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/Ownable.sol";
contract Box is Ownable {
uint256 private value;
// Emitted when the stored value changes
event ValueChanged(uint256 newValue);
// Stores a new value in the contract
function store(uint256 newValue) public onlyOwner {
value = newValue;
emit ValueChanged(newValue);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return value;
}
}
這一切運作方式的美妙之處在于,治理是子產品化的,并且可以 "粘 "到任何合同上。這裡的關鍵是,我們的合同是 "可擁有的",這意味着隻有所有者可以調用存儲功能。而這個合約的所有者就是我們的DAO!
建立它
為了開始工作,建立一個TypeScript hardhat項目。
mkdir dao-template
cd dao-template
yarn add hardhat
yarn hardhat
并選擇TypeScript選項。這将在你的目錄中建立一些檔案夾和檔案以供使用。
在你的contracts檔案夾中,建立一個名為Box.sol的檔案。并添加你在上面看到的Box代碼,這将是我們 "進行 "治理的合同。
在我們的hardhat.config.ts中,我們要把solidity的版本更新到0.8.12或任何高于0.8.4的版本。
我們需要添加openzeppelin合約,然後嘗試用編譯。
yarn add @openzeppelin/contracts
yarn hardhat compile
它應該能編譯成功 我們有一個box...現在怎麼辦?
治理令牌
我們的治理令牌會有點特别,在你的contracts檔案夾中建立一個名為GovernanceToken.sol的新檔案。它應該看起來像這樣。
governancetoken.sol [8]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
contract GovernanceToken is ERC20Votes {
uint256 public s_maxSupply = 1000000000000000000000000;
constructor() ERC20("GovernanceToken", "GT") ERC20Permit("GovernanceToken") {
_mint(msg.sender, s_maxSupply);
}
// The functions below are overrides required by Solidity.
function _afterTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20Votes) {
super._afterTokenTransfer(from, to, amount);
}
function _mint(address to, uint256 amount) internal override(ERC20Votes) {
super._mint(to, amount);
}
function _burn(address account, uint256 amount) internal override(ERC20Votes) {
super._burn(account, amount);
}
}
你會發現這不是一個 "普通"的ERC20代币,這是因為我們需要跟蹤 "快照"。每當有人提議投票時,我們要確定使用人們在X個區塊之前的餘額,而不是在提議提出的時候。這将減少人們在認為他們想參與的投票即将到來時購買和出售投票代币,并確定投票數保持一緻。
一旦 "檢查點 "或人們的代币餘額的 "快照 "被計算為一個投票期,,就是這樣!提出投票後,您無法購買更多代币并獲得更多選票!您必須已經持有令牌。
我們可以確定這是用編譯的。
yarn hardhat compile
Governance合同
不要與州長混淆
現在,讓我們在我們的contracts檔案夾中建立一個檔案夾,名為governance_standard 。在未來,我想添加一個governance_offchain檔案夾,但在Chainlink的那部分加入之前,這就是我們得到的東西
我們将建立一個名為 GovernorContract.sol 的合約,看起來像。
governorcontract.sol[9]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/governance/Governor.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,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
uint256 public s_votingDelay;
uint256 public s_votingPeriod;
constructor(
ERC20Votes _token,
TimelockController _timelock,
uint256 _quorumPercentage,
uint256 _votingPeriod,
uint256 _votingDelay
)
Governor("GovernorContract")
GovernorVotes(_token)
GovernorVotesQuorumFraction(_quorumPercentage)
GovernorTimelockControl(_timelock)
{
s_votingDelay = _votingDelay;
s_votingPeriod = _votingPeriod;
}
function votingDelay() public view override returns (uint256) {
return s_votingDelay; // 1 = 1 block
}
function votingPeriod() public view override returns (uint256) {
return s_votingPeriod; // 45818 = 1 week
}
// The following functions are overrides required by Solidity.
function quorum(uint256 blockNumber)
public
view
override(IGovernor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
}
function getVotes(address account, uint256 blockNumber)
public
view
override(IGovernor, GovernorVotes)
returns (uint256)
{
return super.getVotes(account, 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 _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);
}
}
這是促進我們的 GovernorToken 投票的合同。下面是我們看的主要功能。
propose : 提出一個交易。propose函數是子產品化的,因為它允許你在任何合同上調用任何交易。其參數為:
- • targets : 一個你想調用某個函數的位址清單。
- • values : 一個你想調用某個函數的位址清單。你想在交易中向每個位址發送的ETH(或第一層加密貨币)的清單。
- • calldatas : 你想在每個位址上調用的每個函數的編碼函數和參數的清單。
- • description : 你正在使用的提案的描述。
這個函數的好處是它允許你在一個交易中對許多位址做幾乎任何事情。
castVote : 我們如何投票。
queue : 一旦投票通過,我們就将其排隊執行。
execute : 在時間鎖定結束後,我們執行該提案。
你會注意到,一旦投票通過,它不會立即生效,這是故意的。我們希望給人們一些時間,如果他們不喜歡所做的改變,可以 "退出 "協定。這種 "時間鎖定 "或 "退出 "是由我們的 "時間鎖定 "合同來執行的......也我們接下來要做的事
時間鎖
在與我們的治理合同相同的檔案夾中建立一個新的檔案,名為TimeLock.sol 。這是一個将 "擁有 "盒子的合同。
注意:是的,你沒看錯。TimeLock擁有一切。這是因為每當我們的治理者通過某些東西時,我們要確定在執行該功能之前,我們要等待一個最小的延遲。時間鎖執行了這一點。治理者合約将是唯一能夠要求時間鎖合約 "做 "事情的合約。而且,隻有在投票通過的情況下,它才可以要求。
timelock.sol [10]
// 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) {}
}
我們的時間鎖有幾個參數。
- 1. minDelay : 在投票通過和執行之間我們應該等待多長時間。
- 2. proposers : 誰可以向TimeLock合約提議交易(我們将設定為隻有治理合約可以)。
- 3. executors:我們設定了這個,是以任何人都可以執行一個已經通過并等待了時間的函數。然而,這将是一個完美的時間來添加Chainlink Keepers[11],以確定執行是去中心化的!
而這就是你所需要的所有穩固性! 運作yarn hardhat compile來編譯所有的東西!
腳本和測試
現在,我不想把更多的代碼添加到這篇文章,但這是它的要點。你可以在我的dao-template github repo[12]中看到這些腳本和測試,其中有名為vote、proposal和queueAndExecute的腳本,它們的作用與它們的名字完全一樣。
你可以看到代碼,并确切地看到如何做一些先進的solidity/hardhat概念,如:
- 1. 在etherscan上自動驗證你的智能合約
- 2. 本地網絡上的快進時間
- 3. 在本地網絡上快進區塊
- 4. 如何将函數及其參數編碼為位元組
- 5. 在一個指令行中,用所有你喜歡的合同來啟動本地hardhat節點
- 6. 先進的氣體報告
- 7. 類型鍊的使用
還有更多!
了解更多
如果你想繼續獲得最新的智能合約/區塊鍊/Web3開發者内容,請務必我,以保持最新的資訊。
靈感
- • Openzeppelin治理演練[13]
- • Openzeppelin治理Github[14]
- • Vitalik談DAO[15]
- • Vitalik談鍊上治理[16]
- • Vitalik關于一般治理的觀點[17]
引用連結
[1] Chainlink OCR: https://chain.link/
[2] IPFS: https://ipfs.io/
[3] hardhat的入門文檔: https://hardhat.org/getting-started/
[4] git: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
[5] Nodejs: https://nodejs.org/en/
[6] Yarn: https://classic.yarnpkg.com/lang/en/docs/install/
[7] contract.box.sol : https://gist.github.com/BetterProgramming/0288018fe3f8dae91ccb281f50d4dc5d#file-contract-box-sol
[8] governancetoken.sol : https://gist.github.com/BetterProgramming/f422799c4ed41cc9e55eccbb2aa8afb8#file-governancetoken-sol
[9] governorcontract.sol: https://gist.github.com/BetterProgramming/4bfe00e196e53804583170f3c985e124#file-governorcontract-sol
[10] timelock.sol : https://gist.github.com/BetterProgramming/f737a1d6b67a92868f1f819e56253661#file-timelock-sol
[11] Chainlink Keepers: https://docs.chain.link/docs/chainlink-keepers/introduction/
[12] dao-template github repo: https://github.com/PatrickAlphaC/dao-template
[13] Openzeppelin治理演練: https://docs.openzeppelin.com/contracts/4.x/governance
[14] Openzeppelin治理Github: https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/governance
[15] Vitalik談DAO: https://blog.ethereum.org/2014/05/06/daos-dacs-das-and-more-an-incomplete-terminology-guide/
[16] Vitalik談鍊上治理: https://vitalik.ca/general/2021/08/16/voting3.html
[17] Vitalik關于一般治理的觀點: https://vitalik.ca/general/2017/12/17/voting.html