Dapp 开发实例二
投票 DApp
当你第一次打开 Remix 就会看到一个 Ballot 的合约,这是关于投票的示例,但是并不完善,下面我们来一步步的改进它。
鉴于已经是本书的第二个实例,因此基本概念不会在赘述,进度相对于实例一会快一些。
梳理思路
假设该智能合约里有一个投票项目,分为红方蓝方,每个投票者只能投一票并且要交保证金,当投票截止日期到了时合约 owner 会触发结算方法,公布结果的同时退还保证金。
因此合约里应该有针对红蓝的计数器、可供外部调用的投票方法、可供外部调用的查询票数统计方法、owner 触发投票结束方法、退款方法。整体流程如下图:
锁定编译版本
这样的源代码文件不会使用早于版本 0.4.0 的编译器进行编译,并且它也不适用于从版本 0.5.0 开始的编译器(这个条件是使用^添加的)。要注意版本声明时 “^” 的意思。
// 不建议
pragma solidity ^0.4.0;
// 建议
pragma solidity 0.4.4;
如建议的那一句所示,我们指定了编译版本,因为在这个版本下我们测试最多,能保证质量。
开发智能合约
按照需求,我们如此开发:
pragma solidity 0.4.4;
contract Vote {
//检查是否为合约创建者
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
//检测汇入的钱是否够 1 ether
modifier checkBail(){
require(msg.value >= bail);
_;
}
//检查是否有该投票者
modifier checkVoter(){
require(voters[msg.sender].delegate != 0x0);
_;
}
//检查投票是否结束
modifier isFinish(){
require(!finishFlag);
_;
}
struct Voter {
uint item;
address delegate;
bool isRefund;
}
struct Proposal {
uint voteCount;
}
address public owner;
Proposal red;
Proposal blue;
uint constant bail = 1 ether;
bool finishFlag = false;
Proposal[] proposals;
mapping(address => Voter) voters;
address[] voterList;
function Vote(uint8 _numProposals) public {
owner = msg.sender;
red = Proposal(0);
blue = Proposal(0);
proposals.push(red);
proposals.push(blue);
}
function delegate(uint item) payable external checkBail isFinish{
if(voters[msg.sender].delegate == 0x0){
var sender = Voter(item, msg.sender, false);
voters[msg.sender] = sender;
voterList.push(msg.sender);
proposals[item].voteCount++;
}
}
function getResult() isFinish external returns(uint result) {
result = red.voteCount > blue.voteCount ? 0 : 1;
}
function finish() onlyOwner{
finishFlag = true;
}
function refundAll() public {
for(uint x; x < voterList.length; x++) {
if(!voterList[x].send(bail)) {
throw;
}
}
}
}
既然是红蓝双方我们就使用 Proposal 来创建了,它们之间的不同仅是票数。与此同时,在构造方法里把 red 和 blue 加进了数组,之后再 delegate 投票时直接使用数组索引就代表了投的是哪项。
因此在这里,我们约定 red 标记为 0,blue 标记为 1。相应的使用 modifier 进行辅助判断,在构造方法里初始化红蓝双方和 owner,设定付款即投票的方法 delegate,只能由合约创建者确定的 finishFlag 标志,公共查询投票结果的 getResult 方法,以及最重要的退款 refundAll 方法。
注意:x.transfer(y) 和 if (!x.send(y)) throw; 是等价的,这里只是为了更清楚的解释而已。建议尽可能使用 transfer。
总结与改进
还记得 refundAll 这个给投票者退款的方法么?在这里有一点需特别注意。refundAll 通过数组迭代来向用户支付退款,但前提是每笔交易都成功,但凡有一笔交易失败将导致所有退款操作回滚,这意味着该循环将永远不会完成。
外部调用合约都有可能失败,为了减少这些损失最好把外部调用逻辑和内部逻辑分开:由收款方负责发起调用该方法。比如让用户自己拿回保证金而不是直接发送给他们,千万注意设好撤回资金的额度。
function withdraw() external checkVoter{
var voter = voters[msg.sender];
if(this.balance >= bail && finishFlag && !voter.isRefund){
if(msg.sender.send(bail)){
voter.isRefund = true;
}
}
}
比如在这里,废弃原来的 refundAll 方法改用 withdraw,当发送退款成功时再标记 isRefund 为 true。
至此第二个实例的主要部分就已经分析完成了,因为智能合约的安全非常重要,有必要简单介绍下几个著名的漏洞事件,希望读者以史为鉴。
DAO 事件
DAO 是一个数字分散的自治组织,也是一种由投资者主导的风险投资基金。2016 年 6 月 17 日,黑客在智能合约中发现了一个漏洞,他可以从 DAO 中提出 ether。 在袭击的前几个小时内,360 万 ETH 被盗,相当于当时的 7000 万美元。
在这个漏洞攻击中,攻击者能够在智能合约更新其余额之前多次“请求”智能合约(DAO)转账 ether(re-entry 攻击)。 事实上,当 DAO 智能合约创建时,开发人员没有考虑到恶意递归调用的可能性,和智能合约先发送 ETH 资金,然后更新内部余额的缺陷。
Parity 钱包
由于 Parity 客户端发布的多重签名钱包智能合约存在严重漏洞,攻击者可以立即接管钱包(成为合约 owner)并提取所有资金。原因在于合约代码中的基本漏洞未能正确限制外部调用。 在 Solidity 中,没有修饰符的方法被认为是 public。 这意味着该方法可以从任何来源访问,包括对合同进行的外部交易调用。
黑客先向合约发起了一笔 value 为 0 的交易,里面包括在 msg.data 加上 initWallet 的调用:
function() payable {
// just being sent some cash?
if (msg.value > 0)
Deposit(msg.sender, msg.value);
else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data);
}
而后调用了:
function initWallet(address[] _owners, uint _required, uint _daylimit) {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
重新初始化 owner 地址。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 什么是数字货币钱包以及钱包的分类
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论