泰嶽聯盟鏈,智能合約中的安全

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章