65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023年10月12日

CPOL

6分钟阅读

viewsIcon

5014

downloadIcon

51

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日:初始版本
© . All rights reserved.