前不久,轟動一時的BEC事件在幣圈和鏈圈掀起了不小的反響,智能合約安全一度成為焦點話題。
通過一個小小的整數(shù)溢出漏洞,黑客盜取了巨量的BEC token,迫使交易所不得不暫時停止交易。一時間BEC價格呈現(xiàn)斷崖式下跌。隨后,SMT token 的智能合約也因為相同的問題遭到了攻擊。兩次事件都給項目發(fā)行方和token持有者造成了巨額的損失。
據(jù)統(tǒng)計,截止2018年5月12日為止,以太坊上部署的合約總量共計1628059個,其中ERC20合約數(shù)量為71233?個,占比4.3%。這些合約所代表的經(jīng)濟價值更是難以估量。
上圖反映了自以太坊平臺誕生以來,ERC20代幣合約的創(chuàng)建情況,整體呈現(xiàn)增長趨勢。自2017年6月開始便持續(xù)走高,而2018年的增長幅度尤為明顯,平均每日合約創(chuàng)建數(shù)約為320個,尤其在今年三月份,一度達到峰值,一天內(nèi)創(chuàng)建的合約數(shù)量更是超過600個。隨著區(qū)塊鏈熱度的進一步加強,越來越多的區(qū)塊鏈項目也在萌芽,相信2018年新合約的數(shù)量還會逐步上升。
SECBIT實驗室通過查詢公開數(shù)據(jù)表明,大量已經(jīng)部署的智能合約或多或少都存在著一定的安全風(fēng)險和漏洞,BEC事件也只是冰山一角。那么就ERC20合約而言,除了整數(shù)溢出漏洞以外,還有可能面臨哪些風(fēng)險呢?
可重入
若一個程序或子程序可以「在任意時刻被中斷然后操作系統(tǒng)調(diào)度執(zhí)行另外一段代碼,這段代碼又調(diào)用了該子程序不會出錯」,則稱其為可重入(reentrant或re-entrancy)的。
在智能合約代碼中,黑客可以利用fallback函數(shù),來遞歸調(diào)用包含 call.value() 的函數(shù),從合約中重復(fù)提取以太幣。通常造成該類事件的原因是余額校驗不到位或余額更新不及時。
當(dāng)ERC20代幣合約中涉及以太幣的轉(zhuǎn)出,應(yīng)當(dāng)尤為注意。以一個 withdraw 函數(shù)為例,這也是ERC20合約中經(jīng)常出現(xiàn)的函數(shù)。以下代碼中首先進行余額校驗,并向msg.sender地址轉(zhuǎn)出以太幣,再修改 balance 數(shù)組中余額值。當(dāng)函數(shù)執(zhí)行到 msg.sender.call.value(amount)() 時,黑客就可以通過msg.sender的 fallback函數(shù)來重復(fù)調(diào)用 withdraw,進而重復(fù)執(zhí)行 msg.sender.call.value(_amount)(),直到gas全部消耗完畢或者合約中的以太余額全部被取完。于是就給黑客實現(xiàn) reentrancy 攻擊創(chuàng)造了條件。
function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { if(!msg.sender.call.value(_amount)()) { throw; } balances[msg.sender] -= _amount; }}
因此,SECBIT實驗室的工程師強烈建議智能合約開發(fā)者在轉(zhuǎn)賬之前做好余額校驗工作,并且將余額計算放在轉(zhuǎn)賬之前處理。
function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { balances[msg.sender] -= _amount; if(!msg.sender.call.value(_amount)()) { throw; } }}
另外,值得重視的是,除了 call.value() 以外,任何直接或者間接調(diào)用call方法的步驟,都有可能引起回調(diào),從而引發(fā)重入的安全事件。
轉(zhuǎn)賬方式風(fēng)險
當(dāng)然,杜絕上述問題的一個更好的方式就是不使用 call.value() 進行轉(zhuǎn)賬。發(fā)起以太幣轉(zhuǎn)賬的方式有三種transfer() ,send() 和 call.value()。我們對這三種方式進行比較。
如上圖所示,其中最安全的方式當(dāng)屬 transfer(),一旦轉(zhuǎn)賬失敗,transfer() 會拋出異常直接觸發(fā) revert() 事件,而另外兩者不會,需要開發(fā)人員手動處理返回值。send() 與 transfer() 唯一的區(qū)別也在于返回值,通常我們可以認(rèn)為addr.transfer(v) 就相當(dāng)于require(addr.send(v))。
而call.value() 與另外兩者一個最明顯的區(qū)別在于gas的限制上面,call.value()允許消耗掉所有的gas。但另外兩種方式由于gas消耗限制到2300,不足以完成遞歸調(diào)用,這也是能夠避免 reentrancy 攻擊的原因,上述 withdraw 的代碼可以按照以下的寫法來實現(xiàn)。
function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { balances[msg.sender] -= _amount; msg.sender.transfer(_amount); }}
所以,SECBIT實驗室的工程師建議開發(fā)人員,在ERC20合約開發(fā)過程中,
如果遇到以太幣轉(zhuǎn)出的情況,如非必要,盡量選擇使用transfer()函數(shù)來完成。
避免混淆?tx.origin?和?msg.sender
solidity提供了兩種標(biāo)準(zhǔn)的方式來獲取合約調(diào)用方的地址,tx.origin 和 msg.sender,但是這兩者的含義是不同的,其中 msg.sender 是指直接調(diào)用當(dāng)前合約的調(diào)用方地址,tx.origin 是指發(fā)起本次調(diào)用的起始調(diào)用方地址。
比如合約(或外賬戶地址)A去調(diào)用合約B,合約B調(diào)用合約C。此時在合約C中讀取到的 msg.sender 即為合約B的地址,tx.origin 即為A的地址。
若從一個地址直接對合約發(fā)起調(diào)用,那么 msg.sender 和 tx.origin 是一樣的,否則這兩個地址就不一樣。由于合約無法決定外部調(diào)用的關(guān)系,開發(fā)人員又往往容易混淆兩者的含義,進而留下隱患。
以兩段真實的ERC20合約為例,第一段將 tx.origin 地址設(shè)為合約的owner地址,第二段將 msg.sender 設(shè)為合約的owner地址。在智能合約開發(fā)過程中,一定先搞明白代碼的實現(xiàn)意圖,再選擇使用 tx.origin 還是 msg.sender,使用 tx.origin 的時候要尤為慎重。
function Ownable() public { owner = tx.origin;}modifier onlyOwner() { require(msg.sender == owner); _;}
function Ownable() { owner = msg.sender;}modifier onlyOwner() { require(msg.sender == owner); _;}
依賴時間戳或者塊高度
在 solidity 中,允許獲取當(dāng)前時間戳(或者說交易所在區(qū)塊的區(qū)塊高度)。但是,這并不是安全的。一方面,時間戳是打包交易時候由礦工設(shè)置的,存在一定的人為操作因素在里面,礦工完全可以對時間戳做輕微的改動;另一方面,我們不能完全排除以太坊未來會在出塊時間上做出調(diào)整的可能性,因此通過塊高度來預(yù)估時間是存在一定隱患的。
uint public startTime = 1507032000; uint public endTime = 1517659200; function purchase() whenNotPaused payable { require(!crowdsaleFinished); require(now >= startTime && now < endTime); ...}
上述例子中,通過塊高度來限制ERC20代幣購買的時間段,假如在合約發(fā)布后,購買結(jié)束前的時間段內(nèi),以太坊平臺的出塊時間做出了調(diào)整,那么購買時效也會發(fā)生變動。
另外,千萬不要使用這兩個值來產(chǎn)生隨機數(shù)。因為在合約外部一定范圍內(nèi)(即同一個區(qū)塊內(nèi))是可以獲取到時間戳和塊高度的,所以對于同一個區(qū)塊中的合約來說,這兩個值就變得不隨機了,這便給黑客留下了可乘之機。
整數(shù)相除
在solidity中,整數(shù)相除遵循向下取整的原則。因此,當(dāng)遇到不能整除的情況時,一定要謹(jǐn)慎處理。
function div(uint256 a, uint256 b) internal constant returns (uint256) { uint256 c = a / b; return c;}
如上述代碼所示,計算結(jié)果的關(guān)系是`a == b * c + a % b`,而并非`a == b*c`。在ERC20合約開發(fā)過程中,經(jīng)常會遇到使用除法的場景,要當(dāng)心除法取整的問題,避免引起數(shù)據(jù)前后不一致的麻煩。
關(guān)鍵字過時
隨著solidity不斷的升級更新,老版本上的一些用法也逐漸被標(biāo)記為過時然后廢棄掉。因此在合約開發(fā)和升級過程中,一定要當(dāng)心過時的用法,避免造成不必要的損失。下表展示了部分過時用法和其替代用法可供參考。
智能合約掌握著巨額的經(jīng)濟價值,其影響力之大,波及范圍之廣可見一斑。哪怕一個很不起眼的小問題,都有可能造成不可挽回的經(jīng)濟損失,這對大多數(shù)的項目來說無疑是滅頂之災(zāi)。因此,智能合約的開發(fā)一定要慎之又慎,不要忽略任何細(xì)枝末節(jié)。
另外,強烈建議開發(fā)團隊在合約發(fā)布之前,尋求專業(yè)的智能合約審計團隊,對合約代碼進行安全審計,杜絕隱患。值得一提的是,作為專業(yè)的智能合約安全審計團隊,SECBIT實驗室著力打造高效可靠的安全審計服務(wù),并且利用靜態(tài)分析工具,實現(xiàn)了對數(shù)十個風(fēng)險點的自動檢測。
以上分析及數(shù)據(jù)均由SECBIT實驗室提供。合作交流請聯(lián)系 info@secbit.io。
SECBIT實驗室由一群熱愛區(qū)塊鏈技術(shù)的極客組建,成員遍布在全球多個國家,專業(yè)領(lǐng)域涉及區(qū)塊鏈底層架構(gòu)、智能合約語言、形式化驗證、密碼學(xué)與安全協(xié)議、編譯與分析技術(shù)、博弈論與加密經(jīng)濟學(xué)等諸多學(xué)科。SECBIT實驗室目前聚焦智能合約的安全問題,助力區(qū)塊鏈項目團隊提高智能合約的可靠性與安全性,開展智能合約安全框架的理論技術(shù)研發(fā)。SECBIT實驗室致力于參與共建共識、可信、有序的區(qū)塊鏈經(jīng)濟體。