以太坊dapp_构建以太坊DApp:使用自定义令牌进行投票

以太坊dapp

In part 5 of this tutorial series on building DApps with Ethereum, we dealt with adding content to the story, looking at how to add the ability for participants to buy tokens from the DAO and to add submissions into the story. It’s now time for the DAO’s final form: voting, blacklisting/unblacklisting, and dividend distribution and withdrawal. We’ll throw in some additional helper functions for good measure.

在本教程系列中的第5部分中 ,我们介绍了如何在故事中添加内容,探讨如何增加参与者从DAO购买代币以及向故事中添加提交内容的能力。 现在是DAO最终形式的时候了:投票,将黑名单/取消黑名单以及股息分配和提款。 我们将添加一些额外的辅助函数以很好地解决问题。

If you get lost in all this, the full source code is available in the the repo.

如果您迷失了所有这些,则完整的源代码可在repo中找到

投票和提案 (Votes and Proposals)

We’ll be issuing Proposals and voting with Votes. We need two new structs:

我们将发布提案并通过投票进行投票。 我们需要两个新的结构:

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;
}

A Proposal will have a mapping of voters to prevent people from voting on a proposal twice, and some other metadata which should be self-explanatory. The Vote will either be a yes or no vote, and will remember the voter along with their justification for voting a certain way, and the voting power — the number of tokens they want to devote to voting for this proposal. We also added an array of Proposals so we can store them somewhere, and a counter for counting how many proposals there are.

投标书将包含选民的映射,以防止人们对投标书进行两次投票,而其他一些元数据则应该是不言自明的。 投票将是“是”或“否”表决,并且将记住投票者及其以某种方式投票的理由,以及投票权-他们想要为该提案投票的代币数量。 我们还添加了一组提案,以便我们可以将它们存储在某个地方,以及一个用于计数有多少个提案的计数器。

Let’s build their accompanying functions now, starting with the voting function:

让我们现在从表决功能开始构建它们的附带功能:

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;
}

Notice the function modifier: by adding that modifier into our contract, we can attach it to any future function and make sure only token holders can execute that function. It’s a reusable security check!

注意函数修饰符:通过将修饰符添加到我们的合约中,我们可以将其附加到任何将来的函数上,并确保只有令牌持有者才能执行该函数。 这是可重复使用的安全检查!

The vote function does some sanity checks like the voting power being positive, the voter having enough tokens to actually vote etc. Then we fetch the proposal from storage and make sure it’s neither expired nor already executed. It wouldn’t make sense to vote on a proposal that’s already done. We also need to make sure this person hasn’t yet voted. We could allow changing the vote power, but this opens the DAO to some vulnerabilities like people withdrawing their votes at the last minute etc. Perhaps a candidate for a future version?

投票功能会进行一些健全性检查,例如投票权是否为正,投票者是否具有足够的令牌进行实际投票等。然后,我们从存储中提取提案,并确保提案未过期或尚未执行。 对已经完成的提案进行投票是没有意义的。 我们还需要确保此人尚未投票。 我们可以允许更改投票权,但这会使DAO面临一些漏洞,例如人们在最后一刻撤回投票等。也许是未来版本的候选人?

Then we register a new Vote into the proposal, change the current result for easy lookup of scores, and finally emit the Voted event. But what’s token.increaseLockedAmount?

然后,我们将新的投票注册到提案中,更改当前结果以轻松查找分数,最后发出Voted事件。 但是token.increaseLockedAmount什么?

This bit of logic increases the amount of locked tokens for a user. The function is only executable by the owner of the token contract (by this point that’s hopefully the DAO) and will prevent the user from sending an amount of tokens that exceeds the locked amount registered to their account. This lock is lifted after the proposal falls through or executes.

此逻辑位增加了用户的锁定令牌数量。 该功能仅可由令牌合约的所有者执行(至此,希望是DAO),并且将阻止用户发送超过注册到其帐户的锁定数量的令牌数量。 提案失败或执行后,此锁将解除。

Let’s write the functions for proposing the deletion of an entry now.

让我们现在编写建议删除条目的函数。

投票删除和列入黑名单 (Voting to Delete and Blacklist)

As established in part 1 in this series, we have three entry deletion functions planned:

如本系列第1部分中所述,我们计划了三个条目删除功能:

  1. Remove entry: when confirmed by vote, the target entry is removed. Voting time: 48 hours.

    删除条目 :经投票确认后,将删除目标条目。 投票时间: 48小时

  2. Emergency remove entry [Only Owner]: can only be triggered by Owner. When confirmed by vote, the target entry is removed. Voting time: 24 hours.

    紧急删除条目[仅所有者] :只能由所有者触发。 经投票确认后,将删除目标条目。 投票时间: 24小时

  3. Emergency remove image [Only Owner]: only applies to image entries. Can only be triggered by Owner. When confirmed by vote, the target entry is removed. Voting time: 4 hours.

    紧急删除图像[仅所有者] :仅适用于图像条目。 只能由所有者触发。 经投票确认后,将删除目标条目。 投票时间: 4小时

Five deletions of a single address’ entries lead to a blacklisting.

对单个地址条目的五个删除会导致黑名单。

Let’s see how we can do that now. First up, the deletion functions:

让我们看看我们现在该如何做。 首先,删除功能:

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;
}

Once proposed, a Proposal is added to the list of proposals and notes which entry is being targeted by the entry hash. The description is saved and some defaults added, and a deadline is calculated depending on proposal type. The proposal added event gets emitted and the total number of proposals is increased.

一旦提议,就将提议添加到提议列表,并注意条目哈希所针对的条目。 将保存描述并添加一些默认值,并根据投标类型计算截止日期。 发出添加提案的事件,并且提案总数增加。

Next let’s see how to execute a proposal. To be executable, a proposal must have enough votes, and must be past its deadline. The execution function will accept the ID of the proposal to execute. There’s no easy way to make the EVM execute all pending proposals at once. It’s possible that too many would be pending to execute and that they would make big changes to the data in the DAO, which might exceed the gas limit of Ethereum blocks, thereby failing the transaction. It’s much easier to build a manual execution function callable by anyone with well defined rules, so the community can keep an eye on the proposals that need executing.

接下来,让我们看看如何执行提案。 要执行,提案必须具有足够的投票权,并且必须超过其截止日期。 执行功能将接受要执行的提案的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);
}

We grab the proposal by its ID, check that it meets requirements of not having been executed and deadline being expired, and then if the type of the proposal is a deletion proposal and the voting result is positive, we use the already written deletion function, finally emitting a new event we added (add it to the top of the contract). The assert call is there serving the same purpose as the require statement: assert is generally used when you “assert” that a result is true. Require is used for prerequisites. Functionally they’re identical, with the difference of assert statements not being able to accept message parameters for cases when they fail. The function ends by unlocking the tokens for all the votes in that one proposal.

我们通过提案ID来获取提案,检查其是否符合未执行且截止日期已过期的要求,然后,如果提案的类型为删除提案且投票结果为肯定,则使用已经写入的删除功能,最后发出一个我们添加的新事件(将其添加到合同的顶部)。 assert调用在那里与require语句具有相同的目的:断言通常在您“断言”结果为真时使用。 Require用于先决条件。 从功能上讲,它们是相同的,不同之处在于assert语句在失败时无法接受消息参数。 该功能通过解锁该提案中所有投票的令牌来结束。

We can use this same approach to add other types of proposals, but first, let’s update the deleteSubmission function to ban the users that have five or more deletions on their account: it means they’ve been consistently submitting content the community voted against. Let’s update the deleteSubmission function:

我们可以使用相同的方法来添加其他类型的建议,但是首先,让我们更新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;
}

That’s better. Auto-blacklisting on five deletes. It wouldn’t be fair not to give the blacklisted addresses a chance to redeem themselves, though. We also need to define the blacklisting function itself. Let’s do both of those things and set the unblacklisting fee to, for example, 0.05 ether.

这样更好 在五个删除后自动列入黑名单。 但是,不给黑名单中的地址一个赎回自己的机会是不公平的。 我们还需要定义黑名单功能本身。 让我们做这两个事情,并将取消列入黑名单的费用设置为例如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;
}

Notice that a blacklisted account’s tokens get locked up until they send in the unblacklisting fee.

请注意,列入黑名单的帐户的令牌将被锁定,直到他们发送取消列入黑名单的费用为止。

其他类型的投票 (Other Types of Votes)

Using the inspiration from the functions we wrote above, try writing the other proposals. For spoilers, check out the GitHub repo of the project and copy the final code from there. For brevity, let’s move on to the other functions we still have left in the DAO.

利用我们上面编写的功能的启发,尝试编写其他建议。 对于破坏者,请查看项目的GitHub存储库 ,然后从那里复制最终代码。 为简便起见,让我们继续我们仍留在DAO中的其他功能。

章末 (Chapter End)

Once the time or chapter limit of the story is reached, it’s time to bring the story to an end. Anyone can call the ending function after the date which will allow withdrawals of dividends. First, we need a new StoryDAO attribute and an event:

一旦达到故事的时间或篇章限制,就可以结束故事了。 任何人都可以在允许提取股息的日期之后调用结束函数。 首先,我们需要一个新的StoryDAO属性和一个事件:

bool public active = true;
event StoryEnded();

Then, let’s build the function:

然后,让我们构建函数:

function endStory() storyActive external {
    withdrawToOwner();
    active = false;
    emit StoryEnded();
}

Simple: it deactivates the story after sending the collected fees to the owner and emits the event. But in actuality, this doesn’t really change anything in the DAO as a whole: the other functions don’t react to it being over. So let’s build another modifier:

简单:在将收取的费用发送给所有者并发出事件后,它便停用了故事。 但是实际上,这实际上并没有真正改变整个DAO中的任何内容:其他功能对其结束没有任何React。 因此,让我们构建另一个修饰符:

modifier storyActive() {
    require(active == true);
    _;
}

Then, we add this modifier to all the functions except withdrawToOwner, like so:

然后,将此修饰符添加到除withdrawToOwner之外的所有函数中,如下所示:

function whitelistAddress(address _add) storyActive public payable {

In case any tokens are left over in the DAO, let’s take them back and take over ownership of these tokens in order to be able to use them on another story later on:

如果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));
}

The unlockMyTokens function is there to unlock all locked tokens in case some stayed locked for a specific user. It shouldn’t happen, and this function should be removed by a good amount of tests.

如果某些锁对于特定用户保持锁定状态,那么unlockMyTokens函数可用于解锁所有锁定的令牌。 这不应该发生,并且应该通过大量测试来删除此功能。

股利分配和提取 (Dividend Distribution and Withdrawals)

Now that the story has ended, the fees collected for submissions need to be distributed to all token holders. We can re-use our whitelist to mark everyone who’s made the withdrawal of the fees:

现在故事已经结束,提交的费用需要分配给所有代币持有者。 我们可以重新使用白名单来标记所有撤回费用的人:

function withdrawDividend() memberOnly external {
    require(active == false);
    uint256 owed = address(this).balance.div(whitelistedNumber);
    msg.sender.transfer(owed);
    whitelist[msg.sender] = false;
    whitelistedNumber--;
}

If these dividends aren’t withdrawn within a certain time limit, the owner can grab the rest:

如果未在一定期限内提取这些股息,则所有者可以收取其余款项:

function withdrawEverythingPostDeadline() external onlyOwner {
    require(active == false);
    require(now > deadline + 14 days);
    owner.transfer(address(this).balance);
}

For homework, consider how easy or hard it would be to re-use this same deployed smart contract, clear its data, and keep the tokens in the pot and restart another chapter from this without re-deploying. Try doing this yourself and keep an eye on the repo for future updates to the tutorial series covering this! Also think about additional incentive mechanics: maybe the amount of tokens in an account influences the dividend they get from the story’s end? Your imagination is the limit!

对于家庭作业,请考虑重用相同的已部署智能合约,清除其数据并将令牌保留在锅中并从中重新开始下一章而无需重新部署会多么容易或困难。 尝试自己执行此操作,并注意回购,以获取有关此内容的教程系列的将来更新! 还要考虑其他激励机制:也许一个账户中的代币数量会影响他们从故事的结尾获得的股息? 您的想象力是极限!

部署问题 (Deployment Issues)

Given that our contract is quite large now, deploying and/or testing it might exceed the gas limit of Ethereum blocks. This is what limits large applications from being deployed on the Ethereum network. To get it deployed anyway, try using the code optimizer during compilation by changing the truffle.js file to include solc settings for optimization, like so:

鉴于我们的合同现在很大,部署和/或测试它可能超出以太坊区块的天然气限制。 这就是限制大型应用程序部署在以太坊网络上的原因。 要无论如何部署它,请尝试在编译期间使用代码优化器,方法是将truffle.js文件更改为包括用于优化的solc设置,如下所示:

// ...

module.exports = {
  solc: {
    optimizer: {
      enabled: true,
      runs: 200
    }
  },
  networks: {
    development: {
// ...

This will run the optimizer across the code 200 times to find areas that can be minified, removed or abstracted before deployment, which should reduce the deployment cost significantly.

这将使优化程序在整个代码中运行200次,以发现可以在部署之前缩小,删除或抽象的区域,这将大大降低部署成本。

结论 (Conclusion)

This concludes our exhaustive DAO development — but the course isn’t over yet! We still have to build and deploy the UI for this story. Luckily, with the back end completely hosted on the blockchain, building the front end is much less complicated. Let’s look at that in our penultimate part of this series.

这结束了我们详尽的DAO开发-但课程尚未结束! 我们仍然需要构建和部署该故事的UI。 幸运的是,由于后端完全托管在区块链上,因此构建前端要简单得多。 让我们在本系列的倒数第二部分中看一下。

翻译自: https://www.sitepoint.com/building-ethereum-dapps-voting-custom-tokens/

以太坊dapp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值