以太坊构建DApps系列教程(六):使用定制代币进行投票

在本系列关于使用以太坊构建DApps教程的第5部分中,我们讨论了如何为Story添加内容,查看如何添加参与者从DAO购买代币的功能以及在Story中添加提交内容。现在是编写DAO最终形式的时候了:投票,黑名单,股息分配和退出。我们将提供一些额外的辅助函数以便进行监测。

如果你对这一切感觉迷失了,那么repo中会提供完整的源代码。

投票和提案

我们将发布Votes并投票。这需要两个新的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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,以便我们可以将它们存储在某个地方,并提供一个计数器来计算有多少提案。

让我们现在构建他们的附属函数,从投票函数开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;
}

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

投票功能做了一些健壮性检查,例如投票权是积极的,选民有足够的代币实际投票等。然后我们从存储中获取提案并确保它既没有过期也没有已经执行。对已经完成的提案进行投票是没有意义的。我们还需要确保这个人还没有投票。我们可以允许改变投票权,但这会让DAO面临一些漏洞,例如人们在最后一刻撤回投票等等。也许是未来版本的候选人?

然后我们在提案中注册一个新的投票,更改当前结果以便于查找分数,最后发出Voted事件。但是什么是token.increaseLockedAmount

这一点逻辑增加了用户的锁定代币数量。该功能只能由代币合约的所有者执行(此时希望是DAO)并且将阻止用户发送超过其帐户注册的锁定金额的令牌数量。提案落实或执行后,此锁定被解除。

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

投票删除和黑名单

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

  • 1.删除条目:通过投票确认后,目标条目将被删除。投票时间:48小时。
  • 2.紧急删除条目[仅限所有者]:只能由所有者触发。通过投票确认后,目标条目将被删除。投票时间:24小时。
  • 3.紧急删除图像[仅限所有者]:仅适用于图像条目。只能由所有者触发。通过投票确认后,目标条目将被删除。投票时间:4小时。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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中的数据进行大的更改,这可能会超过以太坊块的气体限制,从而导致交易失败。构建一个可以由具有明确规则的任何人调用的手动执行功能要容易得多,因此社区可以关注需要执行的提议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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以太。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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属性和一个事件:

1
2
bool public active = true;
event StoryEnded();

然后,让我们构建函数:

1
2
3
4
5
function endStory() storyActive external {
withdrawToOwner();
active = false;
emit StoryEnded();
}

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

1
2
3
4
modifier storyActive() {
require(active == true);
_;
}

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

1
function whitelistAddress(address _add) storyActive public payable {

如果DAO中遗留了任何代币,让我们将它们取回并接管这些代币的所有权,以便以后能够在另一个故事中使用它们:

1
2
3
4
5
6
7
8
9
10
11
12
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函数用于解锁所有锁定的代币,以防某些锁定代币为特定用户锁定。它不应该发生,并且应该通过大量测试来移除此功能。

股息分配和提款

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

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

如果这些股息未在一定时限内撤回,业主可以抓住其余股息:

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

留个家庭作业,考虑重新使用相同部署的智能合约,清除其数据,并将代币保留在底池中并重新启动另一章而无需重新部署是多么容易或困难。尝试自己这样做,并密切关注回购,以便将来更新本系列教程!还要考虑额外的激励机制:也许账户中的代币数量会影响他们从故事结束中获得的红利?你的想象力是极限!

部署问题

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

1
2
3
4
5
6
7
8
9
10
11
12
// ...

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系列教程(六):使用定制代币进行投票