如何寫安全的智能合約?

Solidity was started in October 2014 when neither the Ethereum network nor the virtual machine had any real-world testing, the gas costs at that time were even drastically different from what they are now. Furthermore, some of the early design decisions were taken over from Serpent. During the last couple of months, examples and patterns that were initially considered best-practice were exposed to reality and some of them actually turned out to be anti-patterns. Due to that, we recently updated some of the Solidity documentation, but as most people probably do not follow the stream of github commits to that repository, I would like to highlight some of the findings here.

Solidity自2014年10月份開始叁温,不論是以太坊的網(wǎng)絡(luò)還是虛擬機(jī)都經(jīng)歷了真實(shí)世界的考驗(yàn)再悼,現(xiàn)在gas的消耗已經(jīng)和當(dāng)初有非常大的變化。而且膝但,一些早期的設(shè)計(jì)決定已經(jīng)從Serpent中被替換掉冲九。在過去的幾個(gè)月,一些最初被認(rèn)為是最佳實(shí)踐的例子和模式锰镀,已經(jīng)被驗(yàn)證可行娘侍,而有些被證實(shí)為反模式。因?yàn)樯鲜鲈蛴韭覀冏罱铝艘恍?a target="_blank" rel="nofollow">Solidity的文檔憾筏,但是大多數(shù)人并沒有一直關(guān)注git的提交,我會(huì)在這里重點(diǎn)描述一些結(jié)果花鹅。

I will not talk about the minor issues here, please read up on them in the documentation.

我不會(huì)在這里討論小的問題氧腰,大家可以閱讀文檔來了解它們。

Sending Ether
Sending Ether is supposed to be one of the simplest things in Solidity, but it turns out to have some subtleties most people do not realise.
It is important that at best, the recipient of the ether initiates the payout. The following is a BAD example of an auction contract:

發(fā)送Ether
發(fā)送ether應(yīng)該是Solidity里最簡單的事情之一,但是它有一些微妙之處多數(shù)人都沒有意識(shí)到古拴。重要的是箩帚,最好的辦法是,由ether的收款人發(fā)起支付黄痪。以下是一個(gè)關(guān)于拍賣的不好的例子紧帕。

// THIS IS A NEGATIVE EXAMPLE! DO NOT USE!
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; 
  }
}

Because of the maximal stack depth of 1024 the new bidder can always increase the stack size to 1023 and then call bid()
which will cause the send(highestBid)
call to silently fail (i.e. the previous bidder will not receive the refund), but the new bidder will still be highest bidder. One way to check whether send
was successful is to check its return value:

因?yàn)樽畲蟮恼{(diào)用棧深度是1024,一個(gè)新的投標(biāo)者可以一直增加調(diào)用棧到1023桅打,然后調(diào)用bid()是嗜,這樣就會(huì)導(dǎo)致send(highestBid)調(diào)用被悄悄地失敗(也就是前一個(gè)投標(biāo)者沒有收到返回金額)挺尾,但是現(xiàn)在新的投標(biāo)者仍然是最高的投標(biāo)者鹅搪,檢查send是否成功的一個(gè)方法是,檢查它的返回值:

/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE!
if (highestBidder != 0) 
  if (!highestBidder.send(highestBid)) throw;

The throw statement causes the current call to be reverted. This is abad idea, because the recipient, e.g. by implementing the fallback function as function() { throw; }
can always force the Ether transfer to fail and this would have the effect that nobody can overbid her.

throw語句引起當(dāng)前的調(diào)用回滾遭铺。這是一個(gè)糟糕的主意丽柿,因?yàn)榻邮芊剑绻麑?shí)現(xiàn)了 fallback 函數(shù)function() { throw; }總是能強(qiáng)制ether轉(zhuǎn)移失敗魂挂,然后就會(huì)導(dǎo)致沒有其它人可以報(bào)價(jià)高于它甫题。

The only way to prevent both situations is to convert the sending pattern into a withdrawing pattern by giving the recipient control over the transfer:

唯一的防止這兩種情況的辦法是,轉(zhuǎn)換發(fā)送模式為提款模式锰蓬,使收款方控制以太幣轉(zhuǎn)移:

/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE!
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;
  }
} 

Why does it still say “negative example” above the contract? Because of gas mechanics, the contract is actually fine, but it is still not a good example. The reason is that it is impossible to prevent code execution at the recipient as part of a send. This means that while the send function is still in progress, the recipient can call back into withdrawRefund. At that point, the refund amount is still the same and thus they would get the amount again and so on. In this specific example, it does not work, because the recipient only gets the gas stipend (2100 gas) and it is impossible to perform another send with this amount of gas. The following code, though, is vulnerable to this attack:msg.sender.call.value(refunds[msg.sender])()
.

為什么說上面的合約依然是“負(fù)面的例子”幔睬?因?yàn)間as機(jī)制,合約實(shí)際上是沒問題的芹扭,但是它依然不是一個(gè)好的例子麻顶。因?yàn)樗荒茏柚勾a執(zhí)行,在收款方參與send時(shí)舱卡。這意味著辅肾,當(dāng)send函數(shù)在進(jìn)行時(shí),收款方可以返回調(diào)用withdrawRefund轮锥。在這時(shí)矫钓,返還金額仍然是一樣的,因此他們可以再次獲得金額舍杜。在這個(gè)特殊的例子里新娜,它不能如此,因?yàn)槭湛罘街挥幸欢ǖ膅as額度(2100 gas)既绩,它不可能用這么多gas來執(zhí)行另外一次send概龄。但是以下的代碼就可以被攻擊:msg.sender.call.value(refunds[msg.sender])()

Having considered all this, the following code should be fine (of course it is still not a complete example of an auction contract):

經(jīng)過考慮到上面的情況饲握,下面的代碼應(yīng)該是沒有問題的(當(dāng)然它仍然不是一個(gè)完整的拍賣合約的例子):

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;
  }
}

Note that we did not use throw on a failed send because we are able to revert all state changes manually and not using throw has a lot less side-effects.

注意我們不使用throw在失敗的send函數(shù)上私杜,因?yàn)槲覀兛梢允謩?dòng)的回滾所有的狀態(tài)變化蚕键,而不需要使用throw導(dǎo)致很多副作用。

Using Throw
The throw statement is often quite convenient to revert any changes made to the state as part of the call (or whole transaction depending on how the function is called). You have to be aware, though, that it also causes all gas to be spent and is thus expensive and will potentially stall calls into the current function. Because of that, I would like to recommend to use it only in the following situations:

使用Throw
Throw字句可以經(jīng)常十分方便地回滾任何狀態(tài)上的變化作為方法調(diào)用的一部分(或許整個(gè)交易都依賴于這個(gè)函數(shù)如何調(diào)用)衰粹。盡管如此锣光,你必須明白,它也可以導(dǎo)致所有的gas被消耗铝耻,因此它很昂貴誊爹,而且會(huì)停止調(diào)用當(dāng)前的函數(shù)。因此瓢捉,我推薦只在下面這幾種情況下使用only替废。

1. Revert Ether transfer to the current function
If a function is not meant to receive Ether or not in the current state or with the current arguments, you should use throw to reject the Ether. Using throw is the only way to reliably send back Ether because of gas and stack depth issues: The recipient might have an error in the fallback function that takes too much gas and thus cannot receive the Ether or the function might have been called in a malicious context with too high stack depth (perhaps even preceding the calling function).

1. 回滾發(fā)送到當(dāng)前函數(shù)的ether
如果一個(gè)函數(shù)不是為了接受ether或者不在當(dāng)前狀態(tài)或者不是當(dāng)前參數(shù),你應(yīng)該使用throw來拒絕ether泊柬。使用throw是唯一的可靠的辦法來返還ether,因?yàn)間as和調(diào)用棧深度的問題:收款方可能在fallback函數(shù)里存在錯(cuò)誤诈火,造成消耗太多gas而無法收到ether或者在函數(shù)調(diào)用時(shí)兽赁,在一個(gè)充滿惡意的包含很深調(diào)用棧的上下文中(或許甚至在執(zhí)行這個(gè)函數(shù)之前)。

Note that accidentally sending Ether to a contract is not always a UX failure: You can never predict in which order or at which time transactions are added to a block. If the contract is written to only accept the first transaction, the Ether included in the other transactions has to be rejected.

記住偶然發(fā)送ether到一個(gè)合約失敗并不總是用戶體驗(yàn)錯(cuò)誤:你無法預(yù)測(cè)在哪種順序下或者在何時(shí)transaction會(huì)被加到block中冷守。如果合約被寫成只接受第一個(gè)transaction刀崖,包含在其它transactions里的ether必須被拒絕。

2. Revert effects of called functions
If you call functions on other contracts, you can never know how they are implemented. This means that the effects of these calls are also not know and thus the only way to revert these effects is to use throw. Of course you should always write your contract to not call these functions in the first place, if you know you will have to revert the effects, but there are some use-cases where you only know that after the fact.

2. 回滾已經(jīng)調(diào)用過的函數(shù)結(jié)果
如果你調(diào)用函數(shù)在其它合約上拍摇,你永遠(yuǎn)不知道他們是如何執(zhí)行的亮钦。這意味著,這些調(diào)用的結(jié)果也無法知道充活,因此唯一回滾這些結(jié)果的辦法是throw蜂莉。當(dāng)然你也可以使你的合約不在第一時(shí)間調(diào)用這些函數(shù),如果你知道必須要回滾這些結(jié)果混卵,但是有一些例子說明你只有在這些事實(shí)發(fā)生之后才能知道這些結(jié)果映穗。

Loops and the Block Gas Limit
There is a limit of how much gas can be spent in a single block. This limit is flexible, but it is quite hard to increase it. This means that every single function in your contract should stay below a certain amount of gas in all (reasonable) situations. The following is a BAD example of a voting contract:

循環(huán)和塊gas限制
在一個(gè)塊里使用gas是有一個(gè)限制的。這個(gè)限制是動(dòng)態(tài)的幕随,但是很難去增長它蚁滋。這意味著在你的合約里的每一個(gè)函數(shù)調(diào)用,在所有(合理的)情況下應(yīng)該保持在低于某一個(gè)特定的gas數(shù)量赘淮。以下是一個(gè)關(guān)于投票的糟糕例子:

/// THIS IS STILL A NEGATIVE EXAMPLE! DO NOT USE!
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);
  }
}

The contract actually has several issues, but the one I would like to highlight here is the problem of the loop: Assume that vote weights are transferrable and splittable like tokens (think of the DAO tokens as an example). This means that you can create an arbitrary number of clones of yourself. Creating such clones will increase the length of the loop in the tallyVotes function until it takes more gas than is available inside a single block.

這個(gè)合約實(shí)際上有幾個(gè)問題辕录,但是我想在這里強(qiáng)調(diào)的是關(guān)于循環(huán)的問題:假設(shè)投票的權(quán)重是可以轉(zhuǎn)移和分割的,就像tokens(就像DAO tokens那樣)梢卸。這意味著走诞,你可以創(chuàng)建任意多個(gè)你自己的克隆。創(chuàng)建這樣的克隆會(huì)增加tallyVotes函數(shù)里的循環(huán)低剔,至到它消耗超過在一個(gè)單獨(dú)塊里可用gas額度速梗。

This applies to anything that uses loops, also where loops are not explicitly visible in the contract, for example when you copy arrays or strings inside storage. Again, it is fine to have arbitrary-length loops if the length of the loop is controlled by the caller, for example if you iterate over an array that was passed as a function argument. But never create a situation where the loop length is controlled by a party that would not be the only one suffering from its failure.

這種情況適用于所有使用循環(huán)的情況肮塞,同樣包括那些隱晦的循環(huán),比如說當(dāng)你拷貝storage里一個(gè)數(shù)組或者字符串時(shí)姻锁。另外枕赵,如果循環(huán)的長度被調(diào)用者控制,有任意長度的循環(huán)也是沒有問題的位隶,例如你遍歷一個(gè)被當(dāng)參數(shù)傳遞進(jìn)來的數(shù)組拷窜。但是永遠(yuǎn)不要造成這種情況,讓遍歷被某一方控制涧黄,但是他又不能承受遍歷失敗篮昧。

As a side note, this was one reason why we now have the concept of blocked accounts inside the DAO contract: Vote weight is counted at the point where the vote is cast, to prevent the fact that the loop gets stuck, and if the vote weight would not be fixed until the end of the voting period, you could cast a second vote by just transferring your tokens and then voting again.

另外,這也是為什么我們?cè)贒AO合約里有凍結(jié)帳戶的概念:投票權(quán)重在投票進(jìn)行時(shí)就進(jìn)行了計(jì)算笋妥,為了防止循環(huán)被卡住懊昨,如果在投票結(jié)束后,投票權(quán)重沒有被滿足春宣,我們可以進(jìn)行第二輪投票酵颁,只需要轉(zhuǎn)移你的tokens然后再次投票即可。

Receiving Ether / the fallback function
If you want your contract to receive Ether, you have to make its fallback function cheap. It can only use 2300, gas which neither allows any storage write nor function calls that send along Ether. Basically the only thing you should do inside the fallback function is log an event so that external processes can react on the fact.

接受ether/fallback函數(shù)
如果你想要你的合約接受ether月帝,你必須使fallback函數(shù)便宜躏惋。它只能使用2300gas,既不允許任何storage寫入也不允許function調(diào)用任何其它ether發(fā)送嚷辅〔疽蹋基本上,你只需要在fallback函數(shù)里log下event簸搞,這樣外部的調(diào)用可以被反應(yīng)到事實(shí)上扁位。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市攘乒,隨后出現(xiàn)的幾起案子贤牛,更是在濱河造成了極大的恐慌,老刑警劉巖则酝,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件殉簸,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡沽讹,警方通過查閱死者的電腦和手機(jī)般卑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爽雄,“玉大人蝠检,你說我怎么就攤上這事≈课粒” “怎么了叹谁?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵饲梭,是天一觀的道長。 經(jīng)常有香客問我焰檩,道長憔涉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任析苫,我火速辦了婚禮兜叨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衩侥。我一直安慰自己国旷,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布茫死。 她就那樣靜靜地躺著跪但,像睡著了一般。 火紅的嫁衣襯著肌膚如雪峦萎。 梳的紋絲不亂的頭發(fā)上特漩,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音骨杂,去河邊找鬼。 笑死雄卷,一個(gè)胖子當(dāng)著我的面吹牛搓蚪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丁鹉,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼妒潭,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了揣钦?” 一聲冷哼從身側(cè)響起雳灾,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎冯凹,沒想到半個(gè)月后谎亩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宇姚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年匈庭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浑劳。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阱持,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出魔熏,到底是詐尸還是另有隱情衷咽,我是刑警寧澤鸽扁,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站镶骗,受9級(jí)特大地震影響桶现,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜卖词,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一巩那、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧此蜈,春花似錦即横、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至战授,卻和暖如春页藻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背植兰。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工份帐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人楣导。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓废境,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筒繁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子噩凹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容