call调用
contract auction {
address highestBidder;
uint highestBid;
function bid() {
if (msg.value < highestBid) throw;
if (highestBidder != 0)
highestBidder.send(highestBid); // refund previous bidder
highestBidder = msg.sender;
highestBid = msg.value;
}
}
由于最大堆栈深度为1024,因此新投标人可以始终将堆栈大小增加到1023,然后再进行呼叫bid()
,这将导致send(highestBid)
呼叫默默失败(即,先前的投标人将不会获得退款),但是新投标人仍将是最高的投标人。检查是否send成功的一种方法是检查其返回值:
if (highestBidder != 0)
if (!highestBidder.send(highestBid))
throw;
防止这两种情况的唯一方法是通过让接收者控制转移,将发送模式转换为撤回模式:
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() {
if (msg.value < highestBid) throw;
if (highestBidder != 0)
refunds[highestBidder] += highestBid;
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() {
if (msg.sender.send(refunds[msg.sender]))
refunds[msg.sender] = 0;
}
}
为什么在合同上方仍然说“负面例子”?由于天然气技术的原因,合同实际上是可以的,但它仍然不是一个很好的例子。原因是不可能阻止作为发送的一部分在接收者处执行代码。这意味着在发送功能仍在进行中时,收件人可以回拨到withdrawRefund。那时,退款金额仍然相同,因此他们将再次获得退款,依此类推。在此特定示例中,它不起作用,因为接收者仅获得汽油津贴(2100gas),并且无法用此数量的gas执行另一次发送。下面的代码,虽然是容易受到这种攻击:
msg.sender.call.value(refunds[msg.sender])()
以下代码可以解决
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() {
if (msg.value < highestBid) throw;
if (highestBidder != 0)
refunds[highestBidder] += highestBid;
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
if (!msg.sender.send(refund))
refunds[msg.sender] = refund;
}
}
Gas的限制
一个区块中可以消耗多少天然气是有限制的。这个限制是灵活的,但是很难增加它。这意味着在所有(合理)情况下,合同中的每个功能都应保持在一定量的gas以下。以下是投票合同的不良示例:
contract Voting {
mapping(address => uint) voteWeight;
address[] yesVotes;
uint requiredWeight;
address beneficiary;
uint amount;
function voteYes() { yesVotes.push(msg.sender); }
function tallyVotes() {
uint yesVotes;
for (uint i = 0; i < yesVotes.length; ++i)
yesVotes += voteWeight[yesVotes[i]];
if (yesVotes > requiredWeight)
beneficiary.send(amount);
}
}
合同实际上有几个问题,但是我想在这里强调的是循环问题:假设投票权重像令牌一样是可转让和可拆分的(以DAO令牌为例)。这意味着您可以创建任意数量的自己的克隆。创建此类克隆将增加tallyVotes函数中循环的长度,直到消耗的gas超过单个块中可用的gas为止。
这适用于使用循环的任何内容,也适用于合同中未明确显示循环的情况,例如,在存储内部复制数组或字符串时。同样,如果循环的长度是由调用者控制的,例如,如果您遍历作为函数参数传递的数组,则可以使用任意长度的循环。但是,切勿创建这样一种情况:环路长度受一方控制,而一方不是唯一遭受失败的一方。
附带说明一下,这就是为什么我们现在在DAO合同中拥有冻结帐户的概念的原因之一:投票权重是在投票开始时计算的,以防止循环陷入困境以及是否投票在投票期结束之前,权重是固定的,您可以通过只转移令牌然后再次投票来进行第二次投票。
Throw操作
row语句通常非常方便,可以在调用过程中恢复对状态所做的任何更改(或整个事务,具体取决于调用函数的方式)。但是,您必须知道,它还会导致所有gas都被消耗掉,因此很昂贵,并且有可能使对当前函数的调用停止。因此,我建议仅在以下情况下建议使用它:
1. send
如果某个函数不是要以当前状态或当前参数接收以太币,则应使用throw拒绝。由于gas和堆栈深度问题,使用throw是可靠地发送的唯一方法:接收者的回退功能可能有错误,该功能占用了太多的gas,因此无法接收以太坊,或者该功能可能是在恶意软件中调用的堆栈深度过高的上下文(可能甚至在调用函数之前)。
2.恢复调用函数的效果
如果在其他合同上调用函数,则永远无法知道它们是如何实现的。这意味着这些调用的效果也不知道,因此还原这些效果的唯一方法是使用throw。当然,如果您知道必须恢复效果,则应该始终写合同时不要一开始就调用这些函数,但是在某些用例中,事后才知道。