在本系列关于使用以太坊构建DApps教程的第5部分中,咱们讨论了如何为Story添加内容,查看如何添加参与者从DAO购买代币的功能以及在Story中添加提交内容。如今是编写DAO最终形式的时候了:投票,黑名单,股息分配和退出。咱们将提供一些额外的辅助函数以便进行监测。php
若是你对这一切感受迷失了,那么repo中会提供完整的源代码。前端
咱们将发布Votes并投票。这须要两个新的结构:java
struct Proposal { string description; bool executed; int256 currentResult; uint8 typeFlag; // 1 = delete bytes32 target; // ID of the proposal target. I.e. flag 1, target XXXXXX (hash) means proposal to delete submissions[hash] uint256 creationDate; uint256 deadline; mapping (address => bool) voters; Vote[] votes; address submitter; } Proposal[] public proposals; uint256 proposalCount = 0; event ProposalAdded(uint256 id, uint8 typeFlag, bytes32 hash, string description, address submitter); event ProposalExecuted(uint256 id); event Voted(address voter, bool vote, uint256 power, string justification); struct Vote { bool inSupport; address voter; string justification; uint256 power; }
提案将对选民进行映射,以防止人们对提案进行两次投票,以及其余一些应该不言自明的元数据。投票将是一个是或否投票,并将记住选民以及他们以某种方式投票的理由,以及投票权——他们但愿投入该投票的代币数量。咱们还添加了一系列Proposals
,以便咱们能够将它们存储在某个地方,并提供一个计数器来计算有多少提案。node
让咱们如今构建他们的附属函数,从投票函数开始:python
modifier tokenHoldersOnly() { require(token.balanceOf(msg.sender) >= 10**token.decimals()); _; } function vote(uint256 _proposalId, bool _vote, string _description, uint256 _votePower) tokenHoldersOnly public returns (int256) { require(_votePower > 0, "At least some power must be given to the vote."); require(uint256(_votePower) <= token.balanceOf(msg.sender), "Voter must have enough tokens to cover the power cost."); Proposal storage p = proposals[_proposalId]; require(p.executed == false, "Proposal must not have been executed already."); require(p.deadline > now, "Proposal must not have expired."); require(p.voters[msg.sender] == false, "User must not have already voted."); uint256 voteid = p.votes.length++; Vote storage pvote = p.votes[voteid]; pvote.inSupport = _vote; pvote.justification = _description; pvote.voter = msg.sender; pvote.power = _votePower; p.voters[msg.sender] = true; p.currentResult = (_vote) ? p.currentResult + int256(_votePower) : p.currentResult - int256(_votePower); token.increaseLockedAmount(msg.sender, _votePower); emit Voted(msg.sender, _vote, _votePower, _description); return p.currentResult; }
注意函数修饰符:经过将该修饰符添加到咱们的合约中,咱们能够将它附加到任何未来的函数,并确保只有令牌持有者才能执行该函数。这是一个可重复使用的安全检查!android
投票功能作了一些健壮性检查,例如投票权是积极的,选民有足够的代币实际投票等。而后咱们从存储中获取提案并确保它既没有过时也没有已经执行。对已经完成的提案进行投票是没有意义的。咱们还须要确保这我的尚未投票。咱们能够容许改变投票权,但这会让DAO面临一些漏洞,例如人们在最后一刻撤回投票等等。也许是将来版本的候选人?git
而后咱们在提案中注册一个新的投票,更改当前结果以便于查找分数,最后发出Voted事件。可是什么是token.increaseLockedAmount
?程序员
这一点逻辑增长了用户的锁定代币数量。该功能只能由代币合约的全部者执行(此时但愿是DAO)而且将阻止用户发送超过其账户注册的锁定金额的令牌数量。提案落实或执行后,此锁定被解除。github
让咱们编写如今提议删除条目的函数。web
如本系列第1部分所述 ,咱们计划了三个条目删除功能:
单个地址条目的五个删除致使黑名单。
让咱们看看咱们如今该怎么作。首先,删除功能:
modifier memberOnly() { require(whitelist[msg.sender]); require(!blacklist[msg.sender]); _; } function proposeDeletion(bytes32 _hash, string _description) memberOnly public { require(submissionExists(_hash), "Submission must exist to be deletable"); uint256 proposalId = proposals.length++; Proposal storage p = proposals[proposalId]; p.description = _description; p.executed = false; p.creationDate = now; p.submitter = msg.sender; p.typeFlag = 1; p.target = _hash; p.deadline = now + 2 days; emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender); proposalCount = proposalId + 1; } function proposeDeletionUrgent(bytes32 _hash, string _description) onlyOwner public { require(submissionExists(_hash), "Submission must exist to be deletable"); uint256 proposalId = proposals.length++; Proposal storage p = proposals[proposalId]; p.description = _description; p.executed = false; p.creationDate = now; p.submitter = msg.sender; p.typeFlag = 1; p.target = _hash; p.deadline = now + 12 hours; emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender); proposalCount = proposalId + 1; } function proposeDeletionUrgentImage(bytes32 _hash, string _description) onlyOwner public { require(submissions[_hash].image == true, "Submission must be existing image"); uint256 proposalId = proposals.length++; Proposal storage p = proposals[proposalId]; p.description = _description; p.executed = false; p.creationDate = now; p.submitter = msg.sender; p.typeFlag = 1; p.target = _hash; p.deadline = now + 4 hours; emit ProposalAdded(proposalId, 1, _hash, _description, msg.sender); proposalCount = proposalId + 1; }
一旦提出,建议书就会被添加到提案列表中,并记录条目哈希所针对的条目。保存说明并添加一些默认值,并根据提案类型计算截止日期。该提案添加了事件,而且提案总数增长了。
接下来让咱们看看如何执行提案。为了可执行,提案必须有足够的票数,而且必须超过其截止日期。执行功能将接受要执行的提议的ID。没有简单的方法可让EVM当即执行全部待处理的提案。可能有太多人要等待执行,而且他们会对DAO中的数据进行大的更改,这可能会超过以太坊块的气体限制,从而致使交易失败。构建一个能够由具备明确规则的任何人调用的手动执行功能要容易得多,所以社区能够关注须要执行的提议。
function executeProposal(uint256 _id) public { Proposal storage p = proposals[_id]; require(now >= p.deadline && !p.executed); if (p.typeFlag == 1 && p.currentResult > 0) { assert(deleteSubmission(p.target)); } uint256 len = p.votes.length; for (uint i = 0; i < len; i++) { token.decreaseLockedAmount(p.votes[i].voter, p.votes[i].power); } p.executed = true; emit ProposalExecuted(_id); }
咱们经过其ID获取提案,检查它是否符合未执行的要求和截止日期过时,而后若是提案的类型是删除提案且投票结果是确定的,咱们使用已经写入的删除功能,最后发出了咱们添加的新事件(将其添加到合约的顶部)。assert
调用与require
语句具备相同的用途:断言一般在“断言”结果为真时使用。要求用于先决条件。在功能上它们是相同的,assert
语句的差别在它们失败时没法接受消息参数。该功能经过为该一个提案中的全部投票解锁代币而结束。
咱们可使用相同的方法添加其余类型的提案,但首先,让咱们更新deleteSubmission
函数以禁止在其账户上有五个或更多删除的用户:这意味着他们一直在提交社区投票反对的内容。让咱们更新deleteSubmission
函数:
function deleteSubmission(bytes32 hash) internal returns (bool) { require(submissionExists(hash), "Submission must exist to be deletable."); Submission storage sub = submissions[hash]; sub.exists = false; deletions[submissions[hash].submitter] += 1; if (deletions[submissions[hash].submitter] >= 5) { blacklistAddress(submissions[hash].submitter); } emit SubmissionDeleted( sub.index, sub.content, sub.image, sub.submitter ); nonDeletedSubmissions -= 1; return true; }
那更好。自动将五个删除列入黑名单。可是,若是不给黑名单地址提供赎回的机会,那是不公平的。咱们还须要定义黑名单功能自己。让咱们作这两件事并将不合理的费用设置为例如0.05以太。
function blacklistAddress(address _offender) internal { require(blacklist[_offender] == false, "Can't blacklist a blacklisted user :/"); blacklist[_offender] == true; token.increaseLockedAmount(_offender, token.getUnlockedAmount(_offender)); emit Blacklisted(_offender, true); } function unblacklistMe() payable public { unblacklistAddress(msg.sender); } function unblacklistAddress(address _offender) payable public { require(msg.value >= 0.05 ether, "Unblacklisting fee"); require(blacklist[_offender] == true, "Can't unblacklist a non-blacklisted user :/"); require(notVoting(_offender), "Offender must not be involved in a vote."); withdrawableByOwner = withdrawableByOwner.add(msg.value); blacklist[_offender] = false; token.decreaseLockedAmount(_offender, token.balanceOf(_offender)); emit Blacklisted(_offender, false); } function notVoting(address _voter) internal view returns (bool) { for (uint256 i = 0; i < proposalCount; i++) { if (proposals[i].executed == false && proposals[i].voters[_voter] == true) { return false; } } return true; }
请注意,列入黑名单的账户的令牌会被锁定,直到他们发送不合格的费用为止。
使用咱们上面写的函数的灵感,尝试编写其余提议。对于剧透,请查看项目的GitHub仓库并从那里复制最终代码。为简洁起见,让咱们继续讨论DAO中剩下的其余功能。
一旦达到故事的时间或章节限制,就应该结束故事了。任何人均可以在容许提取股息的日期以后调用结束函数。首先,咱们须要一个新的StoryDAO属性和一个事件:
bool public active = true; event StoryEnded();
而后,让咱们构建函数:
function endStory() storyActive external { withdrawToOwner(); active = false; emit StoryEnded(); }
简单:它将收集的费用发送给全部者并发出事件后停用故事。但实际上,这并无真正改变整个DAO中的任何内容:其余功能对它的结束没有反应。那么让咱们构建另外一个修饰符:
modifier storyActive() { require(active == true); _; }
而后,咱们将此修饰符添加到除withdrawToOwner
以外的全部函数中,以下所示:
function whitelistAddress(address _add) storyActive public payable {
若是DAO中遗留了任何代币,让咱们将它们取回并接管这些代币的全部权,以便之后可以在另外一个故事中使用它们:
function withdrawLeftoverTokens() external onlyOwner { require(active == false); token.transfer(msg.sender, token.balanceOf(address(this))); token.transferOwnership(msg.sender); } function unlockMyTokens() external { require(active == false); require(token.getLockedAmount(msg.sender) > 0); token.decreaseLockedAmount(msg.sender, token.getLockedAmount(msg.sender)); }
unlockMyTokens
函数用于解锁全部锁定的代币,以防某些锁定代币为特定用户锁定。它不该该发生,而且应该经过大量测试来移除此功能。
如今故事已经结束,收集的费用须要分配给全部代币持有者。咱们能够从新使用咱们的白名单来标记全部取消费用的人:
function withdrawDividend() memberOnly external { require(active == false); uint256 owed = address(this).balance.div(whitelistedNumber); msg.sender.transfer(owed); whitelist[msg.sender] = false; whitelistedNumber--; }
若是这些股息未在必定时限内撤回,业主能够抓住其他股息:
function withdrawEverythingPostDeadline() external onlyOwner { require(active == false); require(now > deadline + 14 days); owner.transfer(address(this).balance); }
留个家庭做业,考虑从新使用相同部署的智能合约,清除其数据,并将代币保留在底池中并从新启动另外一章而无需从新部署是多么容易或困难。尝试本身这样作,并密切关注回购,以便未来更新本系列教程!还要考虑额外的激励机制:也许帐户中的代币数量会影响他们从故事结束中得到的红利?你的想象力是极限!
鉴于咱们的合约如今很是大,部署和/或测试它可能会超过以太坊区块的gas限制。这是限制大型应用程序部署在以太坊网络上的缘由。不管如何要部署它,在编译期间尝试使用代码优化器,方法是更改truffle.js
文件以包含用于优化的solc设置,以下所示:
// ... module.exports = { solc: { optimizer: { enabled: true, runs: 200 } }, networks: { development: { // ...
这将在代码中运行优化器200次以查找在部署以前能够缩小,移除或抽象的区域,这将显着下降部署成本。
这就是咱们详尽的DAO开发——但课程尚未结束!咱们仍然须要为这个故事构建和部署UI。幸运的是,后端彻底托管在区块链上,构建前端的复杂程度要低得多。让咱们看看这个系列的倒数第二部分。
======================================================================
分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:
- java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
- python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
- php以太坊,主要是介绍使用php进行智能合约开发交互,进行帐号建立、交易、转帐、代币开发以及过滤器和交易等内容。
- 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
- 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
- C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括帐户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
- EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、帐户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
- java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如建立地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
- php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如建立地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
- tendermint区块链开发详解,本课程适合但愿使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。
汇智网原创翻译,转载请标明出处。这里是原文以太坊构建DApps系列教程(六):使用定制代币进行投票