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。當然,如果您知道必須恢復效果,則應該始終寫合同時不要一開始就調用這些函數,但是在某些用例中,事後才知道。