Decentralized Autonomous Organization (在以太坊上,如何创建DAO-去中心化自治组织?)

原文:https://www.ethereum.org/dao

翻译:terryc007

版本:v1.0 纯中文

时间: 2018.4.13

“在区块链世界,没有人知道你是一个冰箱”

  • Richard Brown

到目前为止,我们所列出的合约都属于由人类控制的账号所拥有,执行。 但是在以太坊生态中,机器人,跟人类并不会被区别对待。合约可以做任何其他账号能做的事情。合约可以拥有token,参与众筹,并且还能成为其他合约的投票成员。

在本节,我们将在区块链上,创建一个单独地去中心化民主组织,它能做简单账号能做的任何事情。这个组织有个中心管理者,它决定谁可是成为成员,制定投票规则,但是我们会看到,这些也是也可以更改的。

这个民主组织是是以这种方式来运作的:它有一个拥有者,就像一个管理员,CEO, 或总统那样工作。 拥有者能添加,删除投票成员。任何成员可以提出一个提议,这个提议以以太坊交易的形式,来发送ETH或执行合约,其他成员可以投票支持或反对。一旦达到预先设定的时间,一定票数,这个提议就会被执行: 合约会计算票数,如果有足够的票数,它就执行指定的交易。

区块链国会

代码

pragma solidity ^0.4.16;

contract owned {
  address public owner;

  function owned() public {
    owner = msg.sender;
  }

  modifier onlyOwner {
    require(msg.sender == owner);
    _;
  }

  function transferOwnership(address newOwner) onlyOwner public {
    owner = newOwner;
  }
}

contract tokenRecipient {

  event ReceivedEther(address sender, uint amount);
  event ReceivedTokens(address _from, uint256 _value, address _token, bytes _extraData);

  function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public {
    Token t = Token(_token);
    require(t.transferFrom(_from, this, _value));
    emit ReceivedTokens(_from, _value, _token, _extraData);
  }

  function () payable public{
    emit ReceivedEther(msg.sender, msg.value);
  }
}

interface Token {
  function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);
}

contract Congress is owned, tokenRecipient {
  // Contract Variables and events
  uint public minimumQuorum;
  uint public debatingPeriodInMinutes;
  int public majorityMargin;
  Proposal[] public proposals;
  uint public numProposals;
  mapping (address => uint) public memberId;
  Member[] public members;

  event ProposalAdded(uint proposalID, address recipient, uint amount, string description);
  event Voted(uint proposalID, bool position, address voter, string justification);
  event ProposalTallied(uint proposalID, int result, uint quorum, bool active);
  event MembershipChanged(address member, bool isMember);
  event ChangeOfRules(uint newMinimumQuorum, uint newDebatingPeriodInMinutes, int newMajorityMargin);

  struct Proposal{
    address recipient;
    uint amount;
    string description;
    uint minExecutionDate;
    bool executed;
    bool proposalPassed;
    uint numberOfVotes;
    int currentResult;
    bytes32 proposalHash;
    Vote[] votes;
    mapping (address => bool) voted;
  }

  struct Member{
    address member;
    string name;
    uint memberSince;
  }

  struct Vote{
    bool inSupport;
    address voter;
    string justification;
  }

  // Modifer that allows only sharholders to vote and craete new proposals
  modifier onlyMembers{
    require(memberId[msg.sender] != 0);
    _;
  }

  /**
   * Construct function
  **/
  function Congress(
    uint minimumQuorumForProposals,
    uint minutesForDebate,
    int marginOfVotesForMajority
  ) public {
    changeVotingRules(minimumQuorumForProposals, minutesForDebate, marginOfVotesForMajority);
    // It's necessary to add an empty first member
    addMember(0, "");
    // and let's add the founder , to save a step later
    addMember(owner, 'founder');
  }

  /**
   * Add member
   *
   * Make 'targetMember' a member named 'memberName'
   *
   * @param targetMember ethereum address to be added
   * @param memberName public name for that member
  **/
  function addMember(address targetMember, string memberName) onlyOwner public {
    uint id = memberId[targetMember];
    if (id == 0)
    {
      memberId[targetMember] = members.length;
      id = members.length++;
    }

    members[id] = Member({member: targetMember, memberSince: now, name: memberName});
    emit MembershipChanged(targetMember, true);
  }

  /**
  *  Remove member
  *
  * @notice Remove membership from 'targetMember'
  *
  * @param targetMember ethereum address to be removed
  **/
  function removeMember(address targetMember) onlyOwner public {
    require(memberId[targetMember] != 0);

    for(uint i = memberId[targetMember]; i < members.length-1; i++)
    {
      members[i] = members[i+1];
    }
    delete members[members.length-1];
    members.length--;
  }

  /**
   * Change voting rules
   *
   * Make so that proposals need to be discussed for at least 'minituesForDebate/60' hours,
   * have at least 'minimumQuorumForProposals' votes, and have 50%+ 'marginOfVotesForMajority' votes
   *
   * @param minimumQuorumForProposals How many members must vote on a proposal for it to be executed
   * @param minutesForDebate The minimum amount of delay between when a proposal is mad and when it can be executed
   * @param marginOfVotesForMajority The proposal needs to have 50% plus this number
  **/
  function changeVotingRules(
    uint minimumQuorumForProposals,
    uint minutesForDebate,
    int marginOfVotesForMajority
    ) onlyOwner public {
       minimumQuorum = minimumQuorumForProposals;
       debatingPeriodInMinutes = minutesForDebate;
       majorityMargin = marginOfVotesForMajority;

       emit ChangeOfRules(minimumQuorum, debatingPeriodInMinutes, majorityMargin);
    }

    /**
    *  Add Proposal
    *
    * Propose to send 'weiAmount / 1e18 ' ether to 'beneficiary' for 'jobDescription'.
    * 'transactionBytecode ? Contains : Does not contain' code.
    *
    * @param beneficiary Who to send the ether to
    * @param weiAmount Amount of ether to send , in wei
    * @param jobDescription Description of job
    * @param transactionBytecode bytecode of transaction
    **/
    function newProposal(
      address beneficiary,
      uint weiAmount,
      string jobDescription,
      bytes transactionBytecode
      ) onlyMembers public returns (uint proposalID)
      {
        proposalID = proposals.length++;
        Proposal storage p = proposals[proposalID];
        p.recipient = beneficiary;
        p.amount = weiAmount;
        p.description = jobDescription;
        p.proposalHash = keccak256(beneficiary, weiAmount, transactionBytecode);
        p.minExecutionDate = now + debatingPeriodInMinutes * 1 minutes;
        p.executed = false;
        p.proposalPassed = false;
        p.numberOfVotes = 0;
        emit ProposalAdded(proposalID, beneficiary, weiAmount, jobDescription);
        numProposals = proposalID + 1;

        return proposalID;
      }

      /**
       * Add proposal in Ether
       *
       * Propose to send 'etherAmount' ether to 'beneficiary' for 'jobDescription'. 'transactionBytecode ? Contains: Does not contain' code
       * This is a convenience function to use if the amount to be given is in round number of either units
       *
       * @param beneficiary Who to send the ether to
       * @param etherAmount Amount of ether to send
       * @param jobDescription Description of job
       * @param transactionBytecode Bytecode of transaction
      **/
      function newProposalInEther(
        address beneficiary,
        uint etherAmount,
        string jobDescription,
        bytes transactionBytecode
        ) onlyMembers public returns (uint proposalID)
        {
          return newProposal(beneficiary, etherAmount * 1 ether, jobDescription, transactionBytecode);
        }

        /**
         * Check if a proposal code matchs
         *
         * @param proposalNumber ID number of the proposal to query
         * @param beneficiary Who to send the ether to
         * @param weiAmount Amount of ether to send
         * @param transactionBytecode Bytecode of transaction
        **/
        function checkProposalCode(
          uint proposalNumber,
          address beneficiary,
          uint weiAmount,
          bytes transactionBytecode
          ) constant public  returns (bool codeChecksOut)
          {
            Proposal storage p = proposals[proposalNumber];
            return p.proposalHash == keccak256(beneficiary, weiAmount, transactionBytecode);
          }

          /**
           * Log a vote for a proposal
           *
           * Vote 'supportsProposal? in support of : against' proposal #'proposalNumber'
           *
           * @param proposalNumber Number of proposal
           * @param supportsProposal Either in favor or against it
           *@param justificationText Optional justification text
          **/
          function vote(
            uint proposalNumber,
            bool supportsProposal,
            string justificationText
            ) onlyMembers public returns (uint voteID)
            {

              Proposal storage p = proposals[proposalNumber]; // Get the proposal
              require(!p.voted[msg.sender]);                  // If has already voted, cancel
              p.voted[msg.sender] = true;                     // Set this voter as having voted
              p.numberOfVotes++;                             // Increase the number of votes
              if (supportsProposal)                           // If they support the proposal
              {                                               // Increase score
                p.currentResult++;
              }
              else{
                p.currentResult--;                            // If the don't, decrease the score
              }

              // Create a log fo this event
              emit Voted(proposalNumber, supportsProposal, msg.sender, justificationText);
              return p.numberOfVotes;
            }

            /**
            * Finish vote
            *
            * Count the votes proposal #'proposalNumber' and excute it if approved
            *
            * @param proposalNumber Proposal number
            * @param transactionBytecode Optional : if the transaction contained a bytecode, you need to send it
            **/
            function executeProposal(uint proposalNumber, bytes transactionBytecode) public {
              Proposal storage p = proposals[proposalNumber];

              require(now > p.minExecutionDate                                                 // If it is past the voting deadline
                   && !p.executed                                                             // and it has not already been executed
                   && p.proposalHash == keccak256(p.recipient, p.amount, transactionBytecode) // and the supplied code matchs the proposal
                   && p.numberOfVotes >= minimumQuorum);                                      // and a minimum quorum has been reached, then execute result
              if (p.currentResult > majorityMargin)
              {
                // Proposal passed; execute the transaction

                p.executed = true; // Avoid recursive calling
                require(p.recipient.call.value(p.amount)(transactionBytecode));

                p.proposalPassed = true;
              }
              else
              {
                // Proposal failed
                p.proposalPassed = false;
              }

              // Fire Events
              emit ProposalTallied(proposalNumber, p.currentResult, p.numberOfVotes, p.proposalPassed);
            }
}

如何部署

打开以太坊钱包(如果仅是测试,去菜单 develop > network > Pinkeby - Test Nextwork), 再到Contracts标签页,然后点击deploy contract, 在solidity code区域,粘贴上面的代码。在合约选取器,选择Congress,你会看到一些需要设置的变量。

  • 提议最少成员数: 提议被执行最少需要的票数。
  • 辩论分钟数:在提议能被执行前,最少需要经过的时间(按分钟算)
  • 额外票数比例: 提议必须有50%以上的票数,再加上额外票数比例,才能通过。 最简单就设置为0,要获得100%的票,那就设置为1。
Decentralized Autonomous Organization (在以太坊上,如何创建DAO-去中心化自治组织?)_第1张图片
DAO Setup

这些参数你可以以后再修改。一开始,可以选择5分钟辩论时间,其他的都保留为0. 在这个页面底部,你会看到部署这个合约大约需要花费多少ETH。 如果你想省点钱,你可以试着调低这个价格,不过这可能会让合约的创建时间会更长。 点击Deploy,输入你的密码,然后等待合约部署成功。

几秒后,会自动跳到钱包主页,滚到到底部,你能看到你刚才创建的交易。1分钟左右,交易成功,并会创建一个新的icon。 点击这个合约名字去查看下(你可以在Contract页面,随时查看它)。

DAO Just created

分享给其他人

如果你想分享你的DAO给其他人,他们需要DAO的合约地址,以及它的接口文件 — 它是一串很小的字符,就像这个合约的说明手册,告诉区块链这个合约是如何工作的。 点击copy address复制Dao合约地址,点击show interface显示接口字符串。

在别的电脑,打开以太坊钱包,去Contracts标签页面,然后点击watch contract,添加个合约地址,接口字符串,然后点击OK

Decentralized Autonomous Organization (在以太坊上,如何创建DAO-去中心化自治组织?)_第2张图片
Add Contract

跟合约交互

Read from contract,你能看到合约中所有的,可免费执行的函数,因为他们只是从区块链读取信息。比如,在这里,你能看到合约的拥有者(部署合约的账号)。

Write to contract,有一个函数列表,他们会进行一些计算,并会在区块链上写入数据,因此执行这些函数,需要花费ETH. 选择"New Proposal", 会显示这个函数所有的参数。

在跟合约交互前,你需要在DAO中,加入新的成员。在Select function选择器,选择“Add member”. 加入会员账号地址(要删除会员,选Remove Member函数)。在execute from,确保是Dao合约的拥有者账号,因为只有管理员才能执行这个操作。 点击execute按钮,等待几秒后,等待下个区块完成确认。

这没有显示成员列表,但是在Read from contract栏目,你可以在Members函数拿,输入一个账号地址来核对他们是否成员。

同时,如果你想让合约拥有一些钱,你需要给它冲一些ETH(或其他token),否则,那就是一个毫无用处的组织而已。点击顶部右上角的,Transfer Ether & Tokens来充值。

新增一个简单提议: 发送ETH

现在我们给合约新增第一个提议。在函数选择器那,选择New Proposal.

在"beneficiary", 输入你想要接收ETH人的账号地址,在“Ether amount”,输入要发送的数量,按wei来算,且必须是一个整数, Wei是ETH最小的单位,1ETH = 10 18 wei,。 比如,如果你想发送1ETH, 那就输入1000000000000000000(18个0)。 最后,输入你发起这个提议的原由。 "Transaction bytecode"设为空。点击“execute”, 输入密码。 几秒后,提议的数量会增加一个,第一个提议,它的序号是0,会在左边栏目显示出来。当你加了很多提议后,你可以在“proposals”区域,简单的输入他们的编号,就可以查询到它们的信息。

给提议投票也非常简单。 在函数选择器那,选择“Vote”,在第一个输入框,输入提议的编号,如果你同意这个提议,就选中“Yes”,否则就,不用去点选,点击“execute”来发送你的投票。

Decentralized Autonomous Organization (在以太坊上,如何创建DAO-去中心化自治组织?)_第3张图片
Add new proposal

当投票时间已过,你可以选择“executeProposal”。如果这个提议只是简单的发送ETH, 你可以让transactionBytecode区域为空。 点击“execute”后,在输入你的密码前,请留意屏幕上出现的东西。

如果有一个“消耗费用估算”失败警告,这就意味着这个函数不会被执行,并会被终止。有很多原因会导致这个问题,但是基于这个合约的上下文,在这个合约截止时间前,当你尝试去执行另外一个合约的时候,或者用户试图发送一个跟提议不一样的bytecode的时,会出现这个警告。出于安全考虑,如果发生这些情况,合约执行会被立即终止,用户如果试图执行这些非法交易,他们会损失掉已支付的交易费用。

如果交易被正确执行,几秒后,你可以看到"executed"的结果变为:true。相应的ETH会从合约中扣除,并发送到接受者的地址上去。

添加一个复杂提议: 拥有另外的token

你可以使用这种民主,在以太坊上执行任何交易,只要你能算出这个交易生成的bytecode。好在你可以使用钱包来精确地完成这件事。

在这个例子中,我们使用一个token来展示这个合约,它不仅可以持有ETH,也可以用其他基于以太坊的资产来进行交易。首先,用你的普通账号,创建一个token。 在“Contract”页面,点击“Transfer Ether & Tokens”,给Dao合约发送一些token(为了简单点,给你的Dao合约发送的token数量,不要超过50%)。完成后,我们模拟你想要执行的一些操作,如果你想提议Dao给一个人发送500mg的黄金token,来作为支付。从你的一个账号执行这个交易,点击“Send”,但是当弹出确认对话时,不要输入你的密码。

Decentralized Autonomous Organization (在以太坊上,如何创建DAO-去中心化自治组织?)_第4张图片
Select the bytecode

而是点击“SHOW RAW DATA”链接,然后复制RAW DATA区域的代码,保存到一个文本文件或notepad上。取消这个交易。你还需要一个合约地址,用它来执行这个操作,在这里,就是这个token的合约地址。你可以在“Contracts”标签找到它,并把它保存起来。

现在回到DAO合约,新增一个提议:

  • beneficiary,填你的token合约地址(注意它的icon是否一致)
  • Ether amount ,留空白
  • Job description,写一个简短描叙:你想做什么
  • Transaction Bytecode,粘贴前一步保存的bytecode。
Decentralized Autonomous Organization (在以太坊上,如何创建DAO-去中心化自治组织?)_第5张图片
New proposal

几秒后,你应该会看到该提议的详细信息。你会注意到,交易的bytecode并没有显示,而是一个交易哈希。跟其他区域不同,Bytecode可以很长,因此存储在区块链上会很费钱,因此,并没用对bytecode进行归档,而是在以后,由执行这个调用的人,提供这些bytecode。

但是,这会导致一个安全漏洞: 在没有实际代码时,如何让一个提议可以被投票?在投票开启后,又有什么办法防止用户执行不同的代码?这就引入了交易哈希。在“read from contract”函数列表,往下滚动一点,会看到一个提议核对器函数,任何人可以输入函数所有的参数,来核对是否跟正在投票的提议匹配。这就保证提议不会被执行,除非bytecode的哈希值跟提供的哈希值是一样的。

Decentralized Autonomous Organization (在以太坊上,如何创建DAO-去中心化自治组织?)_第6张图片
It's an older code, but it checks out

任何人可以非常容易地核对提议。通过使用同一种步骤获取正确的bytecode,提议的编号,以及其他参数,然后输入到 Read from contract区域底部的 Check proposal code函数就可以实现。

后面的投票流程是一样的: 所有的成员能投票,过了截止时间,一些人可以执行这个提议。唯一的不同在于,你需要提供相同的bytecode。要留意确定对话窗口的警告:如果提示说不能执行你的代码,要检查下是否已经过了截止时间,是否有足够的票数,是否你的交易bytecode检验通过。

优化

当前的DAO有一些缺点,作为练习留给读者来做:

  • 你能让成员列表公开,并能被索引吗?
  • 你能允许成员修改他们的投票吗(在投完后,提议结算前)?
  • 当前的投票消息只能在日志中看到,你能写一个函数来显示所有的投票消息吗?

股东协会

在前面的章节,我们创建的合约,看起来像一个邀请俱乐部,董事可以随时邀请或禁止成员。但是这有一些缺点: 如果一些人想改他们的地址怎么办? 如果一些成员有更大的权重怎么办? 如果你想在公开市场上,买卖你成员关系,股份怎么办? 如果你想让你的组织,通过股东,像一个恒定的决策机器一样工作怎么办?

我们将对合约做一点修改,让它跟一个特殊的token连接起来,token起到持有这个合约股份的作用。 首先,我们需要创建这个token: 按token教程,创建一个简单的token,发行量 100份,小数位为0,符合为%。 如果你想能够在买卖的时候带有小数的百分数,可以把发行量乘以100或1000倍,然后把小数位设置成相应的个数。 部署这个合约,把它的地址保存在一个文本文件。

下面是股东协会代码

pragma solidity ^0.4.16;

contract owned{
   address public owner;

   function owned() public {
     owner = msg.sender;
   }

   modifier onlyOwner {
     require(msg.sender == owner);
     _;
   }

   function transferOwnership(address newOwner) onlyOwner public {
     owner = newOwner;
   }
}

contract tokenRecipient {
    event ReceiveEther(address sender, uint amount);
    event ReceiveTokens(address _frome, uint256 _value, address _token, bytes _extraData);

    function recevieApproval(address _from, uint256 _value, address _token, bytes _extraData) public {
      Token t = Token(_token);
      require(t.transferFrom(_from, this, _value));
      emit ReceiveTokens(_frome, _value, _token, _extraData)
    };

    function () payable public {
      emit ReceiveEther(msg.sender, msg.value);
    }
}

contract Token {
    mapping (address => uint256) public balanceOf;
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success);
}

/**
 * The shareholder association contract itself
**/
contract Assoication is owned, tokenRecipient {

  uint public minimumQuorum;
  uint public debatingPeriodInMinutes;
  Proposal[] public proposals;
  uint public numProposals;
  Token public sharesTokenAddress;

  event ProposalAdded(uint proposalID, address recipient, uint amount, string description);
  event Voted(uint proposalID, bool position, address voter);
  event ProposalTallied(uint proposalID, uint result, uint quorum, bool active);
  event ChangeOfRules(uint newMinimumQuorum, uint newDebatingPeriodInMinutes, address newSharesTokenAddress);

  struct Proposal {
          address recipient;
          uint amount;
          string description;
          uint minExecutionDate;
          bool executed;
          bool proposalPassed;
          uint numberOfVotes;
          bytes32 proposalHash;
          Vote[] votes;
          mapping (address => bool) voted;
      }

   struct Vote {
          bool inSupport;
          address voter;
      }

   // Modifier that allows only shareholder to vote and create new proposal
   modifier onlyShareholders {
     requrie(sharesTokenAddress.balanceOf(msg.sender) > 0);
     _;
   }

   /**
     * Constructor function
     *
     * First time setup
     */
    function Association(Token sharesAddress, uint minimumSharesToPassAVote, uint minutesForDebate) payable public {
        changeVotingRules(sharesAddress, minimumSharesToPassAVote, minutesForDebate);
    }

    /**
    * Change voting rules
    *
    * Make so that proposals need to be discussed for at least `minutesForDebate/60` hours
    * and all voters combined must own more than `minimumSharesToPassAVote` shares of token `sharesAddress` to be executed
    *
    * @param sharesAddress token address
    * @param minimumSharesToPassAVote proposal can vote only if the sum of shares held by all voters exceed this number
    * @param minutesForDebate the minimum amount of delay between when a proposal is made and when it can be executed
    */
   function changeVotingRules(Token sharesAddress, uint minimumSharesToPassAVote, uint minutesForDebate) onlyOwner public {
       sharesTokenAddress = Token(sharesAddress);
       if (minimumSharesToPassAVote == 0 ) minimumSharesToPassAVote = 1;
       minimumQuorum = minimumSharesToPassAVote;
       debatingPeriodInMinutes = minutesForDebate;
       emit ChangeOfRules(minimumQuorum, debatingPeriodInMinutes, sharesTokenAddress);
   }

   /**
     * Add Proposal
     *
     * Propose to send `weiAmount / 1e18` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code.
     *
     * @param beneficiary who to send the ether to
     * @param weiAmount amount of ether to send, in wei
     * @param jobDescription Description of job
     * @param transactionBytecode bytecode of transaction
     */
    function newProposal(
        address beneficiary,
        uint weiAmount,
        string jobDescription,
        bytes transactionBytecode
    )
        onlyShareholders public
        returns (uint proposalID)
    {
        proposalID = proposals.length++;
        Proposal storage p = proposals[proposalID];
        p.recipient = beneficiary;
        p.amount = weiAmount;
        p.description = jobDescription;
        p.proposalHash = keccak256(beneficiary, weiAmount, transactionBytecode);
        p.minExecutionDate = now + debatingPeriodInMinutes * 1 minutes;
        p.executed = false;
        p.proposalPassed = false;
        p.numberOfVotes = 0;
        emit ProposalAdded(proposalID, beneficiary, weiAmount, jobDescription);
        numProposals = proposalID+1;

        return proposalID;
    }

    /**
     * Add proposal in Ether
     *
     * Propose to send `etherAmount` ether to `beneficiary` for `jobDescription`. `transactionBytecode ? Contains : Does not contain` code.
     * This is a convenience function to use if the amount to be given is in round number of ether units.
     *
     * @param beneficiary who to send the ether to
     * @param etherAmount amount of ether to send
     * @param jobDescription Description of job
     * @param transactionBytecode bytecode of transaction
     */
    function newProposalInEther(
        address beneficiary,
        uint etherAmount,
        string jobDescription,
        bytes transactionBytecode
    )
        onlyShareholders public
        returns (uint proposalID)
    {
        return newProposal(beneficiary, etherAmount * 1 ether, jobDescription, transactionBytecode);
    }

    /**
    * Check if a proposal code matches
    *
    * @param proposalNumber ID number of the proposal to query
    * @param beneficiary who to send the ether to
    * @param weiAmount amount of ether to send
    * @param transactionBytecode bytecode of transaction
    */
   function checkProposalCode(
       uint proposalNumber,
       address beneficiary,
       uint weiAmount,
       bytes transactionBytecode
   )
       constant public
       returns (bool codeChecksOut)
   {
       Proposal storage p = proposals[proposalNumber];
       return p.proposalHash == keccak256(beneficiary, weiAmount, transactionBytecode);
   }

   /**
     * Log a vote for a proposal
     *
     * Vote `supportsProposal? in support of : against` proposal #`proposalNumber`
     *
     * @param proposalNumber number of proposal
     * @param supportsProposal either in favor or against it
     */
    function vote(
        uint proposalNumber,
        bool supportsProposal
    )
        onlyShareholders public
        returns (uint voteID)
    {
        Proposal storage p = proposals[proposalNumber];
        require(p.voted[msg.sender] != true);

        voteID = p.votes.length++;
        p.votes[voteID] = Vote({inSupport: supportsProposal, voter: msg.sender});
        p.voted[msg.sender] = true;
        p.numberOfVotes = voteID +1;
        emit Voted(proposalNumber,  supportsProposal, msg.sender);
        return voteID;
    }


    /**
     * Finish vote
     *
     * Count the votes proposal #`proposalNumber` and execute it if approved
     *
     * @param proposalNumber proposal number
     * @param transactionBytecode optional: if the transaction contained a bytecode, you need to send it
     */
    function executeProposal(uint proposalNumber, bytes transactionBytecode) public {
        Proposal storage p = proposals[proposalNumber];

        require(now > p.minExecutionDate                                             // If it is past the voting deadline
            && !p.executed                                                          // and it has not already been executed
            && p.proposalHash == keccak256(p.recipient, p.amount, transactionBytecode)); // and the supplied code matches the proposal...


        // ...then tally the results
        uint quorum = 0;
        uint yea = 0;
        uint nay = 0;

        for (uint i = 0; i <  p.votes.length; ++i) {
            Vote storage v = p.votes[i];
            uint voteWeight = sharesTokenAddress.balanceOf(v.voter);
            quorum += voteWeight;
            if (v.inSupport) {
                yea += voteWeight;
            } else {
                nay += voteWeight;
            }
        }

        require(quorum >= minimumQuorum); // Check if a minimum quorum has been reached

        if (yea > nay ) {
            // Proposal passed; execute the transaction

            p.executed = true;
            require(p.recipient.call.value(p.amount)(transactionBytecode));

            p.proposalPassed = true;
        } else {
            // Proposal failed
            p.proposalPassed = false;
        }

        // Fire Events
        emit ProposalTallied(proposalNumber, yea - nay, quorum, p.proposalPassed);
    }

}

部署和使用

代码的部署跟之前的代码基本一致,但这你还需要提供一个股份token的地址,股份token有投票权利。

注意这行代码: 在新的合约里面,我们描述了这个token合约,因为在新的合约里面,我只用到balanceOf函数,所以我们只需加这一行就可以。

contract Token { mapping (address => uint256) public balanceOf; }

然后,我们在新的合约里面,定义一个token类型的变量,这就是这变量会继承token所有的函数。最后需要把这个变量指向区块链上的一个地址,这样这个变量就能使用它来获取在线信息。这是以太坊上,最简单的方式实现合约之间的通信。

contract Association {
    token public sharesTokenAddress;
// ...
function Association(token sharesAddress, uint minimumSharesForVoting, uint minutesForDebate) {
        sharesTokenAddress = token(sharesAddress);

这个association合约会碰到一个congress合约没有的问题: 因为任何人持有这个token的人都能投票,且他们的余额能很快的变动,这样了导致当股东投票时,提议的分数无法计算,否则有些人通过把他的股份发送不同的地址,就可以进行多次投票。因此在这个合约,只能记录投票立场情况,然后在execute proposal阶段,在对真实的分数进行统计。

uint quorum = 0;
uint yea = 0;
uint nay = 0;

for (uint i = 0; i < p.votes.length, ++i) {
  Vote v = p.votes[i];
  uint voteWeight = sharesTokenAddress.balanceOf(v.voter);
  quorum += voteWeight;
  if (v.inSupport) 
  {
    yea += voteWeight;
  }
  else
  {
    nay += voteWeight;
  }
}

可以用另外一个方法来统计投票权重,创建一个有符号整数来保存投票分数,在最后的时候,在检查是正还是负,但是你必须使用 int sorce = int(voteWeight) 把非符号的整数voteWeight转成有符合的整数。

跟之前一样用DAO: 成员创建新的提议,给提议投票,等到过了截止时间,然后任何人能计算票数,再执行提议。

Decentralized Autonomous Organization (在以太坊上,如何创建DAO-去中心化自治组织?)_第7张图片
Association example

如何限制拥有者的权利?

在这个合约,被设置为owner的地址有一些特殊的权利: 他们能按自己意愿新增,禁止成员,为了获胜,修改所需的额外票数,改变辩论时间,通过提议所需的法定人数量。但是可以使用另外一个权力来解决这个问题: 修改合约的拥有权。

拥有者可以把拥有权转移给0x0000....。这可以保证所有的规则永远无法更改,但是这是一个不可逆操作。拥有者也可以把所有权转移给合约自己: 只需点击“copy address”,然后把它加入到"new owner"输入框。这可以通过创建提议来行使拥有者所有的权利。

如果你愿意,你也可以把一个合约设置成另外一个合约的拥有者: 假设你想一个这么一个公司架构: 你需要一个终身总裁,它有权利任命董事成员,它可以发行更多的股份,最后通过这些股份来投票决定怎么花预算。你可以创建一个Association合约,它使用以个mintable token合约,而这个mintable token合约被一个congress合约拥有。最后只有一个账号拥有这个Association合约。

但如果你想要不同的投票规则怎么办? 或许要想修改规则,你需要80%的共识,或者这些成员是不同的。在这里,你可以创建另外一个相同的DAO或使用相同的源码,把这个设置成第一个DAO的拥有者。

流动的民主

投票需要花钱,以及执行合约上需要发时间,且要求用户用常常处于活跃状态,注意通知信息,花精力关注投票。 一个有趣的办法,就是选出一个指定的账号,让她控制这个合约,然后它可以快速做决策。

我们将会实现一个通常被称之为“流动民主”的版本, 它是一个更加灵活的代表民主。在这种民主中,任何投票者都有可能成为代表。取代你给候选人投票的方式,你只需要选一个你信任的投票者帮你来做决定。你把你的投票权重委托给他们,他们可以轮流,委托另外一个他们信任的投票者,以此类推。 最终结果是获得最多选票的账号跟最多数量的投票者建立起信任关系。

代码

pragma solidity ^0.4.16;

contract token {
  mapping (address => uint256) public balanceOf;
}

contract LiquidDemocracy  {
  token public votingToken;
  bool underExecution;
  address public appointee;
  mapping (address => uint) public voterId;
  mapping (address => uint256) public voteWeight;

  uint public delegatedPercent;
  uint public lastWeightCalculation;
  uint public numberOfDelegationRounds;

  uint public numberOfVotes;
  DelegatedVote[] public delegatedVotes;
  string public forbiddenFunction;


  struct DelegatedVote {
    address nominee;
    address voter;
  }

  /**
   *  Construct function
  **/
  function LiquidDemocracy (
      address votingWeightToken,
      string forbiddenFunctionCall,
      uint percentLossInEachRound
    ) public {
      votingToken = token(votingWeightToken);
      delegatedVotes.length++;
      delegatedVotes[0] = DelegatedVote({nominee: 0, voter: 0});
      forbiddenFunction = forbiddenFunctionCall;
      delegatedPercent = 100 - percentLossInEachRound;
      if (delegatedPercent > 100) delegatedPercent = 100;
    }

    /**
     * Vote for an address
     *
     * Send your vote weight to another address
     *
     * @param nominatedAddress The destination address receiving the sender's vote
    **/
    function vote(address nominatedAddress) public returns (uint voteIndex) {
       if (voterId[msg.sender] == 0) {
         voterId[msg.sender] = delegatedVotes.length;
         numberOfVotes++;
         voteIndex = delegatedVotes.length++;
         numberOfVotes = voteIndex;
       }
       else {
         voteIndex = voterId[msg.sender];
       }
       delegatedVotes[voteIndex] = DelegatedVote({nominee: nominatedAddress, voter: msg.sender});
       return voteIndex;
    }

    /**
     *  Perform Executive Action
     *
     * @param target The destination address to interact with
     * @param valueInWei The amount of ether to send along with the transaction
     * @param bytecode The data bytecode for the transaction
    **/
    function execute(address target, uint valueInWei, bytes32 bytecode) public {
      require(msg.sender == appointee                           // If caller is the current appointee,
          && !underExecution                                    // if the call is being executed,
          && bytes4(bytecode) != bytes4(keccak256(forbiddenFunction)) //and it's not trying to do the forbidden function
          && numberOfDelegationRounds >= 4);                     //and delegation has been calculated enough

      underExecution = true;
      assert(target.call.value(valueInWei)(bytecode)); // Then execute the command
      underExecution = false;
    }

    /**
     * Calculate Votes
     *
     * Go through all the delegated vote logs and tally up each address's total rank
    **/
    function caculateVotes()  public returns (address winner){
      address currentWinner = appointee;
      uint currentMax = 0;
      uint weight = 0;
      DelegatedVote storage v = delegatedVotes[0];
      if ( now  > lastWeightCalculation + 90 minutes)
      {
        numberOfDelegationRounds = 0;
        lastWeightCalculation = now;

        // Distribute the initial weight
        for (uint i = 1; i < delegatedVotes.length; i++)
        {
            voteWeight[delegatedVotes[i].nominee] = 0;
        }
        for ( i = 1; i < delegatedVotes.length; i++)
        {
            voteWeight[delegatedVotes[i].voter] = votingToken.balanceOf(delegatedVotes[i].voter);
        }
      }
      else
      {
          numberOfDelegationRounds++;
          uint lossRatio = 100 * delegatedPercent ** numberOfDelegationRounds / 100 ** numberOfDelegationRounds;
          if (lossRatio > 0)
          {
            for (i = 1; i < delegatedVotes.length; i++)
            {
                v = delegatedVotes[i];

                if ( v.nominee != v.voter && voteWeight[v.voter] > 0)
                {
                    weight = voteWeight[v.voter] * lossRatio / 100;
                    voteWeight[v.voter] -= weight;
                    voteWeight[v.nominee] += weight;
                }

                if (numberOfDelegationRounds > 3 && voteWeight[v.nominee] > currentMax)
                {
                    currentWinner = v.nominee;
                    currentMax = voteWeight[v.nominee];
                }
            }
          }
      }

      return currentWinner;
    }
}

部署

首先,你需要一个token,如果你之前按照Shareholder association教程创建过一个token,那就用这样token,否则,就部署一个新的token合约到同一个账号。复制token地址。

部署民主合约,同时在Voting weight token填token合约地址,在Percent loss in each round填75, 在把transferOwnership(address) 设为forbidden function.

选择代表

现在部署流动民主合约,然后去这个合约页面。首先选出他们信任的人来代表他们,在这个合约上做决定。如果你自己想成为最终的决策者,也可以给自己投票,或者如果你想弃权,就把代表的地址出填上0。

有足够多的人投完票后,你可以执行Calculate Votes函数,这样它就会计算出每个人的投票权重。这个函数需要运行多次,第一次运行,只是把每个人的权重按token的余额来计算,在下一轮,投票权重会转移给你指定的候选人,再在下一轮,这个权重会给到你选定的候选人所选定的候选人,以此类推。这防止投票委托无限循环,每进行一论投票,每个投票人就会失去一点权力,这个是在合约启动时,用percentLossInEachRound来设定。因此如果丢失率设置为75%, 它意味着你投票的人将会获取你100%的投票权重,但是如果他们把投票委托给其他人, 他们75%的投票权重会转移给别人。那这些人只能获得你的56% (75%*%75% = 56.25%)的投票权重,以此类推。 如果这个比率低于100%,在有限的时间内,会不断的重新计算投票委托,直到再也无法改变结果,但是如果比率是100%,那就意味这个投票权重会计算潜在可能的循环。

如果从调用 Calculate votes 算起,经过了多达1个半小时,所有的权重会被重置,并重新基于初始token的余额来重新计算,因此如果最近你收到了更多的token,你应再执行一次这个函数。

众议院

投票委托有哪些好处? 一个是,在Association合约中,你可以用它了替代token权重。 首先,获得一份shareholder association合约代码,把里面描述token的合约,如下:

contract Token {
    mapping (address => uint256) public balanceOf;
    function transferFrom(address _from, address _to, uint256 _value) returns (bool success);
}

替换成:

contract Token {
   mapping (address => uint256) public voteWeight;
   uint public numberOfDelegationRounds;

   function balanceOf(address member) constant returns (uint256 balance){
      if (numberOfDelegationRounds < 3)
        return 0;
      else
        return this.voteWeight(member);
   }
   
   function transferFrom(address _from, address _to, uint256 _value) return (bool success);
} 

当你写多个合约时,你可以的主合约可以使用多个其他合约。有一些函数,变量可能在目标合约中已经定义了,比如voteWeightnumberOfDelegationRounds。但要注意,balanceOf是一个新函数,它即不存在Liquid Democracy合约里面,也不在Association合约里面,我们现在在token合约里面定义了这个函数,当至少经过三轮委托计算后,才会返回voteWeight

Liquid democracy 当做Token地址,来替换原始的Token,然后跟平常一样,进行部署shareholder association合约。跟之前一样,用户可以创建新的提议,并投票,但是现在,不用token余额多少来代表投票权利大小,而是使用一个委托流程。所以如果你token持有者,你不再需要自己时刻关注所有相关事务,你只需要选择一些你信任的让你,然后委托他们帮你做决定,同时他们也可以选择他们所信任的人:结果将会是,你的代表不再被局限跟你地理位置相近的人群中,他们可以会来自,跟你的社会属性相近的人群中。

这也就意味着,你可以随时改变你的投票: 如果你的代表投票反对一些你感兴趣的提议,在提议结束前,你可以改变你的候选人,或你可以自己来投票。

行政部门

委任式民主是一种非棒的选举代表方式,但是对于一些重要的,简单的决策,在个别提议上,进行投票可能非常慢。这就是为什么很多民主政府有一个行政部门,这个部门有一个委托人,他有权利代表这个州。

经过4论委托后,拥有更多的权重的地址会被设置成任命者。如果有很多的委托投票,可能会需要更多论的Calculate Votes来算出最终的任命地址。

任命者是唯一能执行Execute函数的地址,它几乎能够代表整个民主组织执行任何函数。如果Liquid democracy合约里面有ETH或其他token,任命者会被允许把他们转移到任何地方。

如果你已经按照我们的例子,创建了一个 Shareholder association合约, 并把这个liquid democracy 合约当做一个token, 然后你可以用一个有趣的方式来使用这个行政部门: 去主Association合约页面,并执行Transfer Ownershipf函数, 把Association的拥有者转移给这个liquid democracy合约。

一旦完成转移,切换到Change Voting Rules函数。它可以允许你修改一些重要的投票规则,比如最少投票人数,提议截止时间。试着改变这些设置,点击execute: 当确认窗口弹出时,它会告诉你这个交易不能被执行。这个肯定会发生,因为只有Shareholder association合约的Owner才能更改这些设置,所以这个合约会拒绝这个交易。因此,不要输入你的密码, 而是复制SHOW RAW DATA中的数据,把它保存到一个文本文件。 点击Cancel, 滚动到页面到顶部,点击copy address, 然后把地址保存到一个文本文件。

现在我去Liquid democracy合约页面,选择execute, 在target处,输入Shareholders association合约的地址,让 ether amount处保持为0, 粘贴之前复制的数据(执行Change Voting Rules交易时的raw data)到bytecode data输入框。 确保执行这个函数的账号是任命者,然后点击execute

一旦交易被处理, Liquid democracy 合约会给association合约传递修改投票规则命令,新的投票规则将会被采纳。任命者拥有绝对的权利来做任何在Liquid democracy合约中可以做的事情。你也可以使用同样的技术来创建一个Mintable Token,让后把拥有者设置成Liquid democracy。 然后允许任命者来发币或冻结账号。

为防止权利滥用,你可以设置一个Forbidden function,甚至任命者也不可以调用这个函数。如过你做过我们的例子,这个禁止函数是:transferOwnership(address), 它防止任命者把association合约的拥有者关系转移给他们自己(在政治上,当一个总统使用它的行政权利把一些属于总统职位的东西转移给他们自己, 这叫做政变或贪污)。

时间锁定多重签名

有时候,时间也可以作为一种非常好的安全机制。下面的代码是基于congress Dao合约,但是有一个不同的地方。 对于每个动作需要X个成员的批准的方式,取而代之,任何交易可以被单个成员发起,但是交易在被执行之前,需要一个最少的时间延迟,这延迟时间取决于支持这个交易的人数。认可提议的人越多,它就会越早的被执行。成员可以给交易投反对票,这就意味着它会取消另外一个人的认可签名。

这意味着,如果你没有紧急的事情,要执行一个任何一个交易,你或许只需要1,2个签名就可以。但是如果一个密钥不签名支持,那么其他的密钥就您给把这个交易延迟多个月或上年,甚至阻止交易被执行。

它是如何工作的

一个交易被所有的密钥认可,10分钟后(这个时间是可以配置的),它便可被执行。所需要的时间,随着每%5的成员投反对票(如果他们经常的投反对票,那就4倍),会翻倍。如果它是一个简单的ETH交易,只要到了所需时间,交易就会被执行。但是一个更为复杂的交易需要使用正确的bytecode,来手动执行。下面是一些默认的时间,但是在创建合约的时,可以设置不同的值。

赞成交易成员数: 近似延迟时间

  • 100% approval: 10 minutes (minimum default)
  • 90% approval: 40 minutes
  • 80%: 2hr 40min
  • 50%: about a week
  • 40%: 1 month
  • 30%: 4 months
  • 20%: Over a year
  • 10% or less: 5 years or never

一旦经过最少时间,任何人能执行这个交易(更为完整的内容,请查看"Congress")。这是故意这样设计的,因为它允许一些人安排一个交易或雇别人来执行交易。

代码

pragma solidity ^0.4.16;

contract owned {
    address public owner;

    function owned() public {
        owner = msg.sender;
    }

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    function transferOwnership(address newOwner) onlyOwner public {
        owner = newOwner;
    }
}

contract tokenRecipient {
    event ReceivedEther(address sender, uint amount);
    event ReceivedTokens(address _from, uint256 _value, address _token, bytes _extraData);

    function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public {
        Token t = Token(_token);
        require(t.transferFrom(_from, this, _value));
        emit ReceivedTokens(_from, _value, _token, _extraData);
    }

    function () payable  public {
        emit ReceivedEther(msg.sender, msg.value);
    }
}

interface Token {
    function transferFrom(address _from, address _to, uint256 _value)   external returns  (bool success);
}

contract TimeLockMultisig is owned, tokenRecipient {

    Proposal[] public proposals;
    uint public numProposals;
    mapping (address => uint) public memberId;
    Member[] public members;
    uint minimumTime = 10;

    event ProposalAdded(uint proposalID, address recipient, uint amount, string description);
    event Voted(uint proposalID, bool position, address voter, string justification);
    event ProposalExecuted(uint proposalID, int result, uint deadline);
    event MembershipChanged(address member, bool isMember);

    struct Proposal {
        address recipient;
        uint amount;
        string description;
        bool executed;
        int currentResult;
        bytes32 proposalHash;
        uint creationDate;
        Vote[] votes;
        mapping (address => bool) voted;
    }

    struct Member {
        address member;
        string name;
        uint memberSince;
    }

    struct Vote {
        bool inSupport;
        address voter;
        string justification;
    }

    // Modifier that allows only shareholders to vote and create new proposals
    modifier onlyMembers {
        require(memberId[msg.sender] != 0);
        _;
    }

    /**
     * Constructor function
     *
     * First time setup
     */
    function TimeLockMultisig(address founder, address[] initialMembers, uint minimumAmountOfMinutes)  public payable {
        if (founder != 0) owner = founder;
        if (minimumAmountOfMinutes !=0) minimumTime = minimumAmountOfMinutes;
        // It’s necessary to add an empty first member
        addMember(0, '');
        // and let's add the founder, to save a step later
        addMember(owner, 'founder');
        changeMembers(initialMembers, true);
    }

    /**
     * Add member
     *
     * @param targetMember address to add as a member
     * @param memberName label to give this member address
     */
    function addMember(address targetMember, string memberName) onlyOwner  public {
        uint id;
        if (memberId[targetMember] == 0) {
            memberId[targetMember] = members.length;
            id = members.length++;
        } else {
            id = memberId[targetMember];
        }

        members[id] = Member({member: targetMember, memberSince: now, name: memberName});
        emit MembershipChanged(targetMember, true);
    }

    /**
     * Remove member
     *
     * @param targetMember the member to remove
     */
    function removeMember(address targetMember) onlyOwner  public {
        require(memberId[targetMember] != 0);

        for (uint i = memberId[targetMember]; i proposalDeadline(proposalNumber)
            && p.currentResult > 0
            && p.proposalHash == keccak256(p.recipient, p.amount, '')
            && supportsProposal) {
            executeProposal(proposalNumber, '');
        }
    }

    function proposalDeadline(uint proposalNumber) constant  public returns(uint deadline)  {
        Proposal storage p = proposals[proposalNumber];
        uint factor = calculateFactor(uint(p.currentResult), (members.length - 1));
        return p.creationDate + uint(factor * minimumTime *  1 minutes);
    }

    function calculateFactor(uint a, uint b)  internal pure returns (uint factor) {
        return 2**(20 - (20 * a)/b);
    }

    /**
     * Finish vote
     *
     * Count the votes proposal #`proposalNumber` and execute it if approved
     *
     * @param proposalNumber proposal number
     * @param transactionBytecode optional: if the transaction contained a bytecode, you need to send it
     */
    function executeProposal(uint proposalNumber, bytes transactionBytecode)  public {
        Proposal storage p = proposals[proposalNumber];

        require(now >= proposalDeadline(proposalNumber)                                         // If it is past the voting deadline
            && p.currentResult > 0                                                              // and a minimum quorum has been reached
            && !p.executed                                                                      // and it is not currently being executed
            && checkProposalCode(proposalNumber, p.recipient, p.amount, transactionBytecode));  // and the supplied code matches the proposal...


        p.executed = true;
        assert(p.recipient.call.value(p.amount)(transactionBytecode));

        // Fire Events
        emit ProposalExecuted(proposalNumber, p.currentResult, proposalDeadline(proposalNumber));
    }
}

部署和使用

在开始本教程前,跟之前一样部署好代码。在设置参数时, 把最少时间设置30分钟,如果你想更快点,可以设置为1分钟。完成上传后,执行Add Members函数,加一些新的成员到组里,他们可以是一些你知道的人,或者不同计算机上的账号,或者是离线账号。

被设置“owner”的账号它可以自由的增加,删减成员,它的权利非常大。因此,等你添加完主要的成员后,我们建议你通过执行Transfer Membership,把“owner”设置为别的账号。如果你想通过投票来决定增加,删除成员,就跟其他任何交易一样,那么你可以把owner设置为multisig合约自己。另外一种办法就是把owner设置成一个多重签名的钱包。 如果你就想永远不再加减成员,就设置成0x000。记住,这个合约里面的资金跟”owner“账号同样安全。

根据上面其他DAO合约一样,这个合约也能存ETH, 任何基于以太坊的token,可以执行任何合约。 要想做这些,请查看在congress DAO上,如何执行复杂提议。

警告和改进

出于简单考虑,给提议投一个反对票只是简单统计为少了一票支持。如果你愿意,你可以考虑下这个想法: 反对票的权重会更大,但这就意味着只需要一小部分成员就可以达到否决任何提议交易。

你有还有别的方法改进这个合约吗?

让我们一起探索!

你已经到了教程的结尾,但是这仅仅是伟大冒险的开始。回头看看,你完成了多少事情: 创建一个在线的,会说话的机器人,你拥有自己的加密货币,通过免信任的众筹来筹集资金,并使用它发起你个人的民主组织。

接下来会发生什么?

  • 你可以在去中心化交易所卖掉你的token,或用它买东西,服务。 来进一步开发第一个合约,发展组织。
  • 这个组织,不仅持有ETH, 还有可以持有任何在以太坊上创建的币,包括那些跟bitcion或者dollar锚定的资产。
  • 通过对DAO编程,来允许一个带有多个交易的提议,有些可以安排在以后执行。它也可以拥有其他DAO组织的股份,这意味着它在一个更大的组织进行投票或者成为另外DAO的一个联邦。
  • 通过对Token合约重新编程,让它可以持有ETH,或其他token,把他们分发给token的持有者。 这会把其他有价值的资产跟token的价值连接起来,这样对于支付利息,可以通过简单得把token转移到token地址就能完成。

这个就意味着,你创建的小社会可以发展,从第三方获得资金,支付工资,拥有任何类型的加密资产,甚至使用众筹来为活动筹集资金。所有的这些都是透明的,完全可问责的,完全免受人类干预。合约一直会在以太坊网络,它会精确的执行所创建的代码,永远没有异常。

那么你的合约会成为什么? 成为一个国家,一个公司,一个非盈利组织? 你的代码会做什么?

这完全取决于你!

你可能感兴趣的:(Decentralized Autonomous Organization (在以太坊上,如何创建DAO-去中心化自治组织?))