如何在 Solidity 中构建 DAO?
本文将帮助我们理解DAO的概念,并帮助我们构建基本DAO。
什么是DAO?
我们可以将DAO看作是基于互联网的实体(比如企业),由其股东(持有代币和比例投票权的成员)集体拥有和管理。在DAO中,决策是通过提案做出的,DAO的成员可以对这些提案进行投票,然后执行它们。
DAO完全由公开可见/可验证的代码管理,没有单独的个人(比如CEO)负责决策制定。
DAO如何工作?
如前所述,DAO由代码控制,但如果运行代码机器的人决定关闭机器或编辑代码,该怎么办呢?
我们需要的是让相同的代码运行在由不同实体托管的一组机器上,这样即使其中一个关闭了,另一个也可以接管。区块链帮助我们解决上述问题,基于EVM的区块链,如以太坊和Polygon,允许我们在公共去中心化账本上运行智能合约。部署在这些网络上的智能合约将传播到网络上的所有节点,这些节点可以查看和验证它,没有任何一方控制网络。
DAO向其成员发放代币,代币代表系统中的投票权。根据所设置的治理,任何人都可以创建对DAO进行更改的提案,并将其提交给规定人数(通过所需的最小百分比/投票数),并在投票时间里进行投票。成员可以对提案进行查看和投票,投票权与成员拥有的代币数量成正比。一旦投票期结束,我们检查提案是否通过,如果通过,则执行。
例如MakerDAO和Aragon。
下图显示了该流程。
接下来,我们将构建自己的DAO。
开始构建
我们将在代码库中使用OpenZeppelin合约,我还将使用来自Patrick Collins的DAO模板的一些代码。
先决条件
我们需要以下工具。
- Node.js:可以从Node.js网站下载最新版本。
- Yarn:我们将使用Yarn作为包管理器。
- Hardhat:我们将使用Hardhat作为我们的本地开发环境。
场景
我们将构建一个DAO,它将做以下工作:
场景1
- 添加一个初始成员。(我们称他们为创始人)。
- 让创始人写一份提案。(提出一个在智能合约上执行的功能)。
- 让创始人投票表决,提案将会通过,因为创始人拥有100%的投票权。
- 执行提案。(智能合约中的功能)
场景2
- 添加一个初始成员(我们称他们为创始人)。
- 增加另一名成员,并向他们发行价值占创始人的20%份额的新代币。
- 让创始人创建一个提案(提议一个要在智能合约上执行的功能)。
- 让创始人和新成员投票表决。“Quorum”设置为“90%”。
- 执行提案(以及智能合约中的功能)。
合约
如前所述,我们将使用OpenZeppelin的治理合约。合约内容如下:
- 治理者合约:治理者合约决定了法定人数所需的投票数量/百分比(例如,如果法定人数是4%,那么只有4%的选民需要投票支持提案通过),投票期限,即投票持续多长时间,投票延迟,即提案创建后多长时间允许成员更改他们拥有的代币数量。治理合约还提供创建提案、投票和执行提案的功能。
- TimeLock:TimeLock合约为不同意系统决定的成员提供在执行决定前退出系统的时间。
- Token:Token合约是一种特殊类型的ERC20合约,它实现了ERC20Votes扩展。这允许将投票权映射到过去余额的快照而不是当前余额,这有助于防止知道有重要提案即将出现并试图通过购买更多代币然后抛售它们来增加他们的投票权的成员投票。
- 目标:这是提案通过投票后执行代码的合约。
代码
让我们开始把这些放在一起。使用Hardhat创建一个空的示例项目。在终端中运行如下命令。
Yarn add - dev hardhat
接下来,让我们使用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()
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);
}
}
接下来是TimeLock合约,这是DAO模板中的合约副本
// 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) {}
}
最后,让我们检查Target合约,在我们的例子中,我们将使用Patrick Collins使用的同一个Box合约。
// 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;
}
}
测试
在“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):
接下来,Governor合约被授予提案者角色,执行角色被授予“零地址”,这意味着任何人都可以执行提案。
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 合约上调用的函数的编码值及其参数。
Proposal函数的输出是一个包含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 合约上触发store功能。
投票
我们对提案进行投票,投票“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)
运行测试
我们现在可以使用以下命令运行测试
yarn hardhat test
向新成员发放代币
对于场景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 = 1
const reason = "I vote yes"
const deployedGovernorUser2=await deployedGovernor.connect(signer)
voteTx = await deployedGovernorUser2.castVoteWithReason(proposalId, voteWay, reason)
Source:https://medium.com/block-magnates/how-to-build-a-DAO-decentralized-autonomous-organization-in-solidity-af1cf900d95d
关于
ChinaDeFi - ChinaDeFi.com 是一个研究驱动的DeFi创新组织,同时我们也是区块链开发团队。每天从全球超过500个优质信息源的近900篇内容中,寻找思考更具深度、梳理更为系统的内容,以最快的速度同步到中国市场提供决策辅助材料。
Layer 2道友 - 欢迎对Layer 2感兴趣的区块链技术爱好者、研究分析人与Gavin(微信: chinadefi)联系,共同探讨Layer 2带来的落地机遇。敬请关注我们的微信公众号 “去中心化金融社区”。
|