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í)上扁位。