DAO。简洁明了。让我们创建自己的。





5.00/5 (1投票)
DAO 概念介绍和创建自己的 DAO 工作坊
什么是DAO?
DAO - 去中心化自治组织。虽然名称中说这是一个组织,但您需要明白,实际上DAO并不是整个组织,而只是其中的一部分,它主要负责投票和决策。事实上,DAO的目的是通过投票做出决策。
关于谁可以投票,甚至他们的投票有多大的权重,存在不同的算法。其中最流行的一种是:权重直接与某人存入指定钱包的代币数量成正比。所以,关于每个人的公平机会就这么多。别忘了,一切都需要钱,并且是为钱而建立的。因此,DAO的主要思想是一个黑箱,它允许您做出集体决策。它有自己的逻辑和进入这群人范围的门槛。
在哪里使用
- 为组织或初创公司分配资金的投票
- 在某个自动货币兑换器提高佣金的投票。在这里,顺便说一句,您可以立即将DAO附加到兑换器的合约上。例如,对于后者,创建一个更改佣金的函数,该函数只能由DAO合约调用
- 集体所有权。例如,一群人购买了一首歌曲的版权。现在,他们通过DAO决定如何管理它。您的投票权重取决于您在这首歌上投入了多少钱。如果您投入了很多,那么您的投票就是决定性的
- 以及更多可能的解决方案和应用。也许,这些方案可以更简单、更便宜地实现,但同样不那么有趣
与任何区块链解决方案一样,DAO具有巨大的优势——透明性和不可篡改性。投票前,您可以熟悉智能合约代码,并很快理解它。每个人都知道Solidity语言,真的 :) 并且知道这段代码不会改变。让我们暂时忘记代理合约以及开发人员以“我们需要以某种方式修复错误并发布新版本,一切变化如此之快”的名义提出的一些“改变”合约的方法。因此,由于透明性和不可篡改性,DAO成为需要最大透明度的决策过程的流行机制。
让我们创建自己的
首先,对我们将要使用的工具进行简短的介绍。IDE:您可以使用记事本,但更智能的工具更好。例如,VisualCode或WebStorm。主要组件将是hardhat,因为我们需要编写部署脚本、调用合约的一些任务,当然还有测试,所有这些都在hardhat中。
现在我将稍微描述一下我们将要创建的内容。一个智能合约,它可以
- 从用户那里收取代币以增加其投票权重。用户指的是钱包,而代币指的是ERC20代币
- 提供添加提案的机会
- 提供投票的机会。如果有人投票,则他的投票数量等于他存入合约账户的代币数量
- 提供完成提案的机会。提案有一个持续时间,只有在投票时间结束后才能将其视为完成。如果成功,我们将调用另一个智能合约的函数。
- 提取代币的可能性
这是我在第一轮迭代中得到的结果。
pragma solidity ^0.8.20;
contract DAO {
constructor(
address _voteToken){
}
/// @notice Deposit vote tokens
/// @param amount Amount tokens which you want to send
function deposit(uint256 amount) public {
}
/// @notice Withdrawal vote tokens
/// @param amount Amount tokens which you want to get
function withdrawal(uint256 amount) public {
}
/// @notice Start new proposal
/// @param callData Signature call recipient method
/// @param recipient Contract which we will call
/// @param debatingPeriodDuration Voting duration
/// @param description Proposal description
function addProposal(bytes memory callData, address recipient,
uint256 debatingPeriodDuration, string memory description)
public returns (bytes32){
bytes32 proposalId = keccak256(abi.encodePacked
(recipient, description, <code>callData</code>, block.timestamp));
return proposalId;
}
/// @notice Vote to some proposal
/// @param proposalId Proposal id
/// @param decision Your decision
function vote(bytes32 proposalId, bool decision) public{
}
/// @notice Finish proposal
/// @param proposalId Proposal id
function finishProposal(bytes32 proposalId) public{
}
}
也许只有addProposal
函数需要解释。通过它,任何人都可以创建投票提案。投票时长由debatingPeriodDuration
参数指定。在此期间之后,如果决定是积极的,则将使用callData
中的数据调用接收方。
好的,我们已经确定了方法。
是时候进行测试了
存入
- 如果用户未授权我们的合约提取资金,应检查交易是否会因错误而失败
- 应将用户钱包中的代币转移请求的金额
- 应将请求的金额存入我们的合约钱包
提取
- 如果用户参与投票,则不应允许提取
- 不应允许用户提取超过其余额的金额
- 应将请求的金额从合约钱包转移
- 应将请求的金额存入用户的钱包
addProposal
- 检查重复提案。用户不能创建两个具有相同描述和
callData
的提案 - 当然,我也可以做一个获取活动投票列表的方法,并检查我们的提案是否出现在那里,但我不想这样做。如果您有其他想法,请在文章的评论中写下。
- 检查重复提案。用户不能创建两个具有相同描述和
投票
- 仅适用于余额为正的账户
- 仅适用于现有投票
- 重试投票时应返回错误
- 投票时间过期时应返回错误
finishProposal
- 如果不存在投票,则应返回错误
- 投票分配的时间尚未过期,应返回错误
- 在决定积极的情况下,应调用接收方方法
- 在决定消极的情况下,不应调用接收方方法
我们将投票以铸造代币。投票权重将由相同的代币决定。这意味着,为了测试,我们将需要一个模拟的ERC20代币合约,该合约可以进行铸造。我们将以openzeppelin
中的ERC20合约为基础,并将其扩展到我们需要的那个方法。
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ERC20Mint is ERC20 {
constructor() ERC20("ERC20Mint", "ERC20Mint"){
}
function mint(address account, uint256 amount) public {
_mint(account, amount);
}
}
现在关于npm包。我将使用TypeScript编写,所以我们需要一个用于TypeScript的包。当然,我们需要hardhat,@nomicfoundation/hardhat-toolbox
,@nomicfoundation/hardhat-ethers
来编译和生成我们智能合约的ts类。我们还需要chai
和@types/chai
来进行漂亮的测试,以及openzeppelin
的合约包@openzeppelin/contracts token
。
这是我做的测试。现在它们都会因错误而失败,但这很正常,我们还没有实现任何东西。
import { expect } from "chai";
import { ethers } from "hardhat";
import {HardhatEthersSigner} from "@nomicfoundation/hardhat-ethers/signers";
import {DAO, ERC20Mint} from "../typechain-types";
import {ContractFactory} from "ethers";
describe("Dao contract", () => {
let accounts : HardhatEthersSigner[];
let daoOwner : HardhatEthersSigner;
let voteToken : ERC20Mint;
let voteTokenAddress : string;
let recipient : ERC20Mint;
let recipientAddress : string;
let dao : DAO;
let daoAddress : string;
let proposalDuration : number;
let callData : string;
let proposalDescription : string;
let proposalTokenRecipient : HardhatEthersSigner;
let proposalMintAmount: number;
beforeEach(async () =>{
accounts = await ethers.getSigners();
[proposalTokenRecipient] = await ethers.getSigners();
proposalDuration = 100;
const erc20Factory : ContractFactory =
await ethers.getContractFactory("ERC20Mint");
voteToken = (await erc20Factory.deploy()) as ERC20Mint;
voteTokenAddress = await voteToken.getAddress();
recipient = (await erc20Factory.deploy()) as ERC20Mint;
recipientAddress = await recipient.getAddress();
const daoFactory : ContractFactory = await ethers.getContractFactory("DAO");
dao = (await daoFactory.deploy(voteTokenAddress)) as DAO;
daoAddress = await dao.getAddress();
proposalMintAmount = 200;
callData = recipient.interface.encodeFunctionData
("mint", [proposalTokenRecipient.address, proposalMintAmount]);
proposalDescription = "proposal description";
});
async function getProposalId(recipient : string,
description: string, callData: string) : Promise<string> {
let blockNumber : number = await ethers.provider.getBlockNumber();
let block = await ethers.provider.getBlock(blockNumber);
return ethers.solidityPackedKeccak256(["address", "string", "bytes"],
[recipient, description, callData]);
}
describe("deposit", () => {
it("should require allowance", async () => {
const account: HardhatEthersSigner = accounts[2];
const amount : number = 100;
await expect(dao.connect(account).deposit(amount))
.to.be.revertedWith("InsufficientAllowance");
});
it("should change balance on dao", async () => {
const account: HardhatEthersSigner = accounts[2];
const amount : number = 100;
await voteToken.mint(account.address, amount);
await voteToken.connect(account).approve(daoAddress, amount);
await dao.connect(account).deposit(amount);
expect(await voteToken.balanceOf(daoAddress))
.to.be.equal(amount);
});
it("should change token balance", async () => {
const account: HardhatEthersSigner = accounts[2];
const amount : number = 100;
await voteToken.mint(account.address, amount);
await voteToken.connect(account).approve(daoAddress, amount);
await dao.connect(account).deposit(amount);
expect(await voteToken.balanceOf(account.address))
.to.be.equal(0);
});
});
describe("withdrawal", () => {
it("should not be possible when all balances are frozen", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
const withdrawalAmount : number = voteTokenAmount;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
await dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription);
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await dao.connect(account).vote(proposalId, true);
await expect(dao.connect(account).withdrawal(withdrawalAmount))
.to.be.revertedWith("FrozenBalance");
});
it("should be possible with a partially frozen balance", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount1 : number = 100;
const voteTokenAmount2 : number = 100;
const withdrawalAmount : number = voteTokenAmount2;
await voteToken.mint(account.address, voteTokenAmount1 + voteTokenAmount2);
await voteToken.connect(account).approve
(daoAddress, voteTokenAmount1 + voteTokenAmount2);
await dao.connect(account).deposit(voteTokenAmount1);
await dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription);
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await dao.connect(account).vote(proposalId, true);
await dao.connect(account).deposit(voteTokenAmount2);
await dao.connect(account).withdrawal(withdrawalAmount);
expect(await voteToken.balanceOf(account.address))
.to.be.equal(withdrawalAmount);
});
it("shouldn't be possible with withdrawal amount more then balance", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
const withdrawalAmount : number = voteTokenAmount + 1;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
await expect(dao.connect(account).withdrawal(withdrawalAmount))
.to.be.revertedWith("FrozenBalance");
});
it("should change account balance", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
const withdrawalAmount : number = voteTokenAmount - 1;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
await dao.connect(account).withdrawal(withdrawalAmount);
expect(await voteToken.balanceOf(account.address))
.to.be.equal(withdrawalAmount);
});
it("should change dao balance", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
const withdrawalAmount : number = voteTokenAmount - 1;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
await dao.connect(account).withdrawal(withdrawalAmount);
expect(await voteToken.balanceOf(daoAddress))
.to.be.equal(voteTokenAmount - withdrawalAmount);
});
});
describe("addProposal", () => {
it("should not be possible with duplicate proposal", async () => {
const account: HardhatEthersSigner = accounts[5];
await dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription);
await expect(dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription))
.to.be.revertedWith("DoubleProposal");
});
});
describe("vote", () => {
it("should be able for account with balance only", async () => {
const account : HardhatEthersSigner = accounts[5];
await dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription);
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await expect(dao.connect(account).vote(proposalId, true))
.to.be.revertedWith("InsufficientFounds");
});
it("shouldn't be able if proposal isn't exist", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await expect(dao.connect(account).vote(proposalId, true))
.to.be.revertedWith("NotFoundProposal");
});
it("shouldn't be able double vote", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
await dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription);
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await dao.connect(account).vote(proposalId, true);
await expect(dao.connect(account).vote(proposalId, true))
.to.be.revertedWith("DoubleVote");
});
it("shouldn't be able after proposal duration", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
await dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription);
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await ethers.provider.send('evm_increaseTime', [proposalDuration]);
await expect(dao.connect(account).vote(proposalId, true))
.to.be.revertedWith("ExpiredVotingTime");
});
});
describe("finishProposal", () => {
it("shouldn't be able if proposal isn't exist", async () => {
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await expect(dao.finishProposal(proposalId))
.to.be.revertedWith("NotFoundProposal");
});
it("shouldn't be able if proposal period isn't closed", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
await dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription);
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await dao.connect(account).vote(proposalId, true);
await ethers.provider.send('evm_increaseTime', [proposalDuration-2]);
await expect(dao.finishProposal(proposalId))
.to.be.revertedWith("NotExpiredVotingTime");
});
it("shouldn't call recipient when cons", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
await dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription);
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await dao.connect(account).vote(proposalId, false);
await ethers.provider.send('evm_increaseTime', [proposalDuration]);
await dao.finishProposal(proposalId);
expect(await recipient.balanceOf(proposalTokenRecipient.address))
.to.be.equal(0);
});
it("should call recipient when pons", async () => {
const account : HardhatEthersSigner = accounts[5];
const voteTokenAmount : number = 100;
await voteToken.mint(account.address, voteTokenAmount);
await voteToken.connect(account).approve(daoAddress, voteTokenAmount);
await dao.connect(account).deposit(voteTokenAmount);
await dao.addProposal(callData, recipientAddress,
proposalDuration, proposalDescription);
let proposalId : string = await getProposalId
(recipientAddress, proposalDescription, callData);
await dao.connect(account).vote(proposalId, true);
await ethers.provider.send('evm_increaseTime', [proposalDuration]);
await dao.finishProposal(proposalId);
expect(await recipient.balanceOf(proposalTokenRecipient.address))
.to.be.equal(proposalMintAmount);
});
});
});
开始实现
首先,我们来决定将在我们的合约中存储什么
- 投票代币地址,这样我们就可以从中提取资金。它只是一个类型为address的字段
- 我们用户的余额,以便了解投票权重以及用户可以提取多少。我们将它们存储在一个映射中,其中键是地址,值是用户的余额
- 投票本身以及投票数量。这有点复杂。很明显,这将是一个以投票ID为键的映射,但您需要为提案创建一个特殊的结构。这里需要投票者来防止重复投票。
mapping(bytes32 => Proposal) private _proposals; struct Proposal{ uint256 startDate; uint256 endDate; bytes callData; address recipient; string description; uint256 pros; uint256 cons; mapping(address => uint256) voters; address[] votersAddresses; }
- 我们还需要一个地板,这将帮助我们了解投票中有多少资金。这对于计算用户可提取金额是必需的。这里的一切都与存储余额相似。
在这个阶段,我最喜欢的是测试的逐步成功执行。这是我得到的结果。
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract DAO {
/// @notice Token which you need to deposit for can vote
address public voteToken;
/// @notice Balances of users
mapping(address => uint256) public balances;
/// @notice Frozen balances of users
mapping(address => uint256) public frozenBalances;
mapping(bytes32 => Proposal) private _proposals;
struct Proposal{
uint256 startDate;
uint256 endDate;
bytes callData;
address recipient;
string description;
uint256 pros;
uint256 cons;
mapping(address => uint256) voters;
address[] votersAddresses;
}
constructor(
address _voteToken){
voteToken = _voteToken;
}
/// @notice Deposit vote tokens
/// @param amount Amount tokens which you want to send
function deposit(uint256 amount) public {
require(IERC20(voteToken).allowance(msg.sender,
address(this)) >= amount, "InsufficientAllowance");
balances[msg.sender] += amount;
SafeERC20.safeTransferFrom(IERC20(voteToken), msg.sender, address(this), amount);
}
/// @notice Withdrawal vote tokens
/// @param amount Amount tokens which you want to get
function withdrawal(uint256 amount) public {
require(amount > 0 && balances[msg.sender] -
frozenBalances[msg.sender] >= amount, "FrozenBalance");
balances[msg.sender] -= amount;
SafeERC20.safeTransfer(IERC20(voteToken), msg.sender, amount);
}
/// @notice Start new proposal
/// @param callData Signature call recipient method
/// @param recipient Contract which we will call
/// @param debatingPeriodDuration Voting duration
/// @param description Proposal description
function addProposal(bytes memory callData, address recipient,
uint256 debatingPeriodDuration, string memory description) public{
bytes32 proposalId = keccak256(abi.encodePacked(recipient, description, callData));
require(_proposals[proposalId].startDate == 0, "DoubleProposal");
_proposals[proposalId].startDate = block.timestamp;
_proposals[proposalId].endDate =
_proposals[proposalId].startDate + debatingPeriodDuration;
_proposals[proposalId].recipient = recipient;
_proposals[proposalId].callData = callData;
_proposals[proposalId].description = description;
}
/// @notice Vote to some proposal
/// @param proposalId Proposal id
/// @param decision Your decision
function vote(bytes32 proposalId, bool decision) public{
require(balances[msg.sender] > 0, "InsufficientFounds");
require(_proposals[proposalId].startDate >0, "NotFoundProposal");
require(balances[msg.sender] > _proposals[proposalId].voters[msg.sender],
"DoubleVote");
require(_proposals[proposalId].endDate > block.timestamp, "ExpiredVotingTime");
decision ? _proposals[proposalId].pros+=balances[msg.sender] -
_proposals[proposalId].voters[msg.sender] :
_proposals[proposalId].cons+=balances[msg.sender] -
_proposals[proposalId].voters[msg.sender];
_proposals[proposalId].voters[msg.sender] = balances[msg.sender];
frozenBalances[msg.sender] += balances[msg.sender];
_proposals[proposalId].votersAddresses.push(msg.sender);
}
/// @notice Finish proposal
/// @param proposalId Proposal id
function finishProposal(bytes32 proposalId) public{
require(_proposals[proposalId].startDate >0, "NotFoundProposal");
require(_proposals[proposalId].endDate <= block.timestamp, "NotExpiredVotingTime");
for (uint i = 0; i < _proposals[proposalId].votersAddresses.length; i++) {
frozenBalances[_proposals[proposalId].votersAddresses[i]] -=
_proposals[proposalId].voters[_proposals[proposalId].votersAddresses[i]];
}
bool decision = _proposals[proposalId].pros > _proposals[proposalId].cons;
if (decision) callRecipient(_proposals[proposalId].recipient,
_proposals[proposalId].callData);
delete _proposals[proposalId];
}
function callRecipient(address recipient, bytes memory signature) private {
(bool success, ) = recipient.call{value: 0}(signature);
require(success, "CallRecipientError");
}
}
好了,我们完成了。所有测试都通过了。在这篇文章中,您了解了DAO是什么以及它可以在哪里使用。我们成功地编写了我们的DAO,并完全用测试覆盖了它。希望这篇文章对您有所帮助。
历史
- 2023年10月12日:初始版本