
上次我們談到了由于solidity智能合約代碼的公開性和行業(yè)現(xiàn)狀,這一領(lǐng)域的安全狀況令人堪憂,所幸目前這種情況正在被重視,有些組織和個人已經(jīng)做了一些有意義的實踐與總結(jié)。下面是solidity社區(qū)總結(jié)出的安全方面的主要關(guān)注點與應(yīng)對思路。
智能合約的安全問題與最佳實踐
1. 重入
如果一個智能合約A調(diào)用了另一個智能合約B,那么控制權(quán)將從A完全傳遞給B,也就是說,如果B再回調(diào)回A也,然后再調(diào)用B,然后再調(diào)用A...這樣循環(huán)下去,A也是沒有辦法的。下面的合約將允許一個攻擊者多次得到退款,因為它使用了 call ,默認(rèn)發(fā)送所有剩余的 gas:
pragma solidity ^0.4.0;// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract Fund {?
?/// Mapping of ether shares of the contract.?
?????mapping(address => uint) shares; /// Withdraw your share.?
?????function withdraw() public {?
?????????if (msg.sender.call.value(shares[msg.sender])())?
?????????????shares[msg.sender] = 0;?
? ? ?}
}
作為改善措施,應(yīng)該使用使用“檢查-生效-交互”(Checks-Effects-Interactions)模式編寫函數(shù)代碼:
第一步,做檢查工作,例如參數(shù)是否合法,發(fā)送者是否合法...,這些檢查工作應(yīng)該首先被完成。
第二步,狀態(tài)變量修改。
第三步,與其他合約交互,這一步在任何合約中都應(yīng)該最后被調(diào)用。
由于對已知合約的調(diào)用反過來也可能導(dǎo)致對未知合約的調(diào)用,所以最好是一直保持使用這個模式編寫代碼。按照這一模式,以上代碼應(yīng)該這么寫:
pragma solidity ^0.4.11;
contract Fund { /// 合約中 |ether| 分成的映射。?
?????mapping(address => uint) shares; /// 提取你的分成。?
?????function withdraw() public {?
?????????var share = shares[msg.sender];?
?????????shares[msg.sender] = 0;?
?????????msg.sender.transfer(share);?
?????}
}
2.address.send(), address.transfer()? vs address.call.value()()
這三個調(diào)用都可以用來向一個智能合約轉(zhuǎn)賬,但仍然有以下區(qū)別:
address.send()和address.transfer()是重入安全(可以防止重入)的. 原因是,這兩種調(diào)用只分配到了2300 gas,這一數(shù)量一般只夠記錄一兩條log。所以,當(dāng)被調(diào)用的地址是一個合約,而且實現(xiàn)了可以被外界執(zhí)行的默認(rèn)函數(shù),那么這個默認(rèn)函數(shù)仍然會被調(diào)用到。address.transfer(y)效果等同于require(address.send(y));.
address.call.value(y)()將會把所有的gas發(fā)送到合約地址上并執(zhí)行默認(rèn)函數(shù). 所以這個默認(rèn)函數(shù)將會有足夠的gas執(zhí)行任何操作,包括重新調(diào)用原合約的接口,因此是重入不安全(不能防止重入)的。
因此,在用這三個接口轉(zhuǎn)賬時,一定要根據(jù)實際情況把安全狀況考慮清楚。一般來說,首選address.send()和 address.transfer()。其次選address.call.value(y)(),在使用address.call.value(y)()的時候最好限定所能使用的最多gas。
3.push模式 vs pull模式
在以上的討論中,盡管你使用address.call.value(y)()的時候限定了所能使用的最多gas,仍然不意味著就沒有問題了,因為也許一個你沒有想到的漏洞所需要的gas本來就低于你的限定量。我們最好將調(diào)用限制到智能運行其本身的transaction,因此,在付款相關(guān)操作中,拉取模式(pull)應(yīng)該優(yōu)先于推送模式(push), 讓我們看看以下例子:
// bad
contract auction {?
????address highestBidder;?
? ? uint highestBid;?
? ? function bid() payable {?
? ? ? ? require(msg.value >= highestBid);?
? ? ? ? if (highestBidder != 0) {?
? ? ? ? ? ? highestBidder.transfer(highestBid); // if this call consistently fails, no one else can bid
? ? ? ? }?
? ? ? ? highestBidder = msg.sender;?
? ? ? ? highestBid = msg.value;?
? ? }
}
以上例子中,退款采用的是push模式,如果有一個攻擊者在fallback中調(diào)用require(0);然后參與以上拍賣之后,其他人再參與拍賣時以上代碼highestBidder.transfer(highestBid);便會失敗。從而達(dá)到鎖定拍賣的目的。而如果退款時采取pull模式,則會避免這一缺陷:
// good
contract auction {?
?????address highestBidder;?
?????uint highestBid;?
?????mapping(address => uint) refunds;?
?????function bid() payable external {?
?????????require(msg.value >= highestBid);?
?????????if (highestBidder != 0) {?
?????????????refunds[highestBidder] += highestBid; // record the refund that this user can claim?
?????????}?
?????????highestBidder = msg.sender;?
?????????highestBid = msg.value;?
?????}?
?????function withdrawRefund() external {?
?????????uint refund = refunds[msg.sender];?
?????????refunds[msg.sender] = 0;?
?????????msg.sender.transfer(refund);
?????}
}
4.合約間調(diào)用處理
Solidity 開放了一些底層調(diào)用函數(shù)如 address.call(),address.callcode(),address.delegatecall()和 andaddress.send(). 這些底層調(diào)用的一個特點是:他們調(diào)用失敗時不會拋出異常,只是會返回失敗,而異常在合約間的調(diào)用中是可以傳遞的,返回的失敗結(jié)果卻不能。因此,當(dāng)調(diào)用這些底層函數(shù)時一定要考慮調(diào)用失敗的情形,例如,我們不能這么寫代碼:
// bad
someAddress.send(55);
someAddress.call.value(55)();?
someAddress.call.value(100)(bytes4(sha3("deposit()")));?
而應(yīng)該這么寫:
// good
if(!someAddress.send(55)) {?
?????// Some failure code
}
ExternalContract(someAddress).deposit.value(100);
5.不要假設(shè)智能合約里沒有eth或者其他代幣

攻擊者甚至可以在你的合約沒有創(chuàng)建之前向你的智能合約轉(zhuǎn)入一筆eth。當(dāng)然也有一些其他辦法巧妙的向你的智能合約存入eth,例如:攻擊者可以創(chuàng)建一個合約,向這個合約中轉(zhuǎn)入少量eth,然后調(diào)用selfdestruct(victimAddress),這個victimAddress填成你的合約地址,這樣eth就轉(zhuǎn)到你的合約里了,這種方法沒有人能阻止。
事實上,除了以上基本準(zhǔn)則,人們還在不斷總結(jié)經(jīng)驗并抽象出一些代碼模式。相信在人們的共同努力下,solidity智能合約會越來越安全。