智能合約審計是一個仔細(xì)調(diào)查一段代碼的過程匾竿,在這種情況下是一個solidity合約,用于在代碼部署之前發(fā)現(xiàn)錯誤狈孔、漏洞和風(fēng)險食店。
審計的結(jié)構(gòu):
- 免責(zé)申明:在此您可以說審計不是具有法律約束力的文件,并且不保證任何內(nèi)容颖对。這只是一個討論文件捻撑。
- 審計和優(yōu)秀功能概述:快速查看將要審計的智能合約以及找到良好實踐。
- 對合約的攻擊:在本節(jié)中缤底,您將討論對合約和結(jié)果所做的攻擊顾患。只是為了驗證它確實是安全的。
- 合約中發(fā)現(xiàn)的嚴(yán)重漏洞:可能嚴(yán)重?fù)p害合約完整性的關(guān)鍵問題个唧。一些允許攻擊者竊取ether的錯誤是一個關(guān)鍵問題江解。
- 合約中發(fā)現(xiàn)的中等漏洞:可能損害合約但存在某種限制的漏洞。就像一個允許人們修改隨機變量的bug徙歼。
- 發(fā)現(xiàn)低嚴(yán)重性漏洞:這些漏洞并未真正損害合約膘流,并且可能存在于合約的部署版本中。
- 逐行注釋:在本節(jié)中鲁沥,您將分析最重要的行呼股,您可以看到潛在的改進。
- 審計摘要:您對合約的意見和有關(guān)審計的最終結(jié)論画恰。
免責(zé)申明
審計不對代碼的實用性彭谁、代碼的安全性、業(yè)務(wù)模型的適用性允扇、業(yè)務(wù)模型的監(jiān)管制度缠局、或任何其他關(guān)于合約適用性或其無bug狀態(tài)的陳述作出陳述或保證。審核文檔僅供討論之用考润。
常見的攻擊類型以及注意事項
-
私人信息和隨機性:在智能合約使用的所有內(nèi)容都是公開可見的狭园,甚至是標(biāo)記的局部變量和狀態(tài)變量
private
。如果你不希望礦工能夠作弊糊治,在智能合約中使用隨機數(shù)是非常棘手的唱矛。 - Re-Entrancy(重入):為避免重新入侵,您可以使用Checks-Effects-Interactions模式
call.value()如果用戶balance在發(fā)送ether之前沒有更新發(fā)送方井辜,
則此攻擊包括遞歸調(diào)用ERC20 token中的方法以提取存儲在合約上的ether绎谦。
? if (msg.sender.call.value(shares[msg.sender])(""))
shares[msg.sender] = 0;
? if (msg.sender.send(shares[msg.sender]))
shares[msg.sender] = 0;
?? uint share = shares[msg.sender];
shares[msg.sender] = 0;
msg.sender.transfer(share);
- 變量過大和過低:當(dāng)超出類型變量uint256的限制2**256時發(fā)生溢出。會發(fā)生什么粥脚?是該值重置為零而不是遞增更多窃肠。建議使用像OpenZeppelin的SafeMath.sol這樣的庫。
import './SafeMath.sol';
contract Casino {
using SafeMath for uint256;
function example(uint256 _value) {
uint number = msg.value.add(_value);
}
}
- Replay(重放攻擊):它不再是問題刷允,因為Geth版本1.5.3和Parity 1.4.4都實現(xiàn)了Vitalik Buterin的攻擊保護EIP155
- Reordering attack(重組攻擊):這種攻擊在于冤留,礦工或其他方試圖通過將自己的信息插入列表或映射來與智能合約參與者“競賽”碧囊,以便攻擊者可以幸運地將他們自己的信息存儲在合約中。
- 短地址攻擊:此攻擊影響ERC20 token纤怒。購買令牌時呕臂,請務(wù)必檢查地址的長度。合約不易受此攻擊肪跋,因為它不是ERC20 token。參考:https://vessenes.com/the-erc20-short-address-attack-explained/
-
assert()
和require()
行為幾乎完全相同土砂,但assert()用于在進行更改后驗證合約狀態(tài)州既,而require()通常用于函數(shù)頂部以驗證函數(shù)的輸入。 - 調(diào)用堆棧深度:1024萝映。外部函數(shù)調(diào)用可能會隨時失敗吴叶,在這種情況下,Solidity會拋出異常序臂。惡意攻擊者可能會在與您的合約交互之前強制調(diào)用堆棧達到較高值蚌卤。請注意,如果調(diào)用堆棧耗盡奥秆,
.send()
則不會拋出異常逊彭,而是false
在這種情況下返回。低級別的功能.call()
构订,.callcode()
以及.delegatecall()
行為以同樣的方式侮叮。 - 切勿使用
tx.origin
進行授權(quán)。
安全建議
外部調(diào)用
盡量避免外部調(diào)用:調(diào)用不受信任的外部合約可能會引發(fā)一系列意外的風(fēng)險和錯誤悼瘾。外部調(diào)用可能在其合約和它所依賴的其他合約內(nèi)執(zhí)行惡意代碼囊榜。
-
仔細(xì)權(quán)衡
send()
、transfer()
亥宿、call.value()
:在需要對外未知地址轉(zhuǎn)賬Ether時使用send()
或transfer()
卸勺,已知明確內(nèi)部無惡意代碼的地址轉(zhuǎn)賬Ether使用call.value()
。-
x.transfer(y)
和if(!x.send(y)) throw;
是等價的烫扼。send是transfer的底層實現(xiàn)曙求,建議盡可能直接使用transfer。 -
someAddress.send()
和someAddress.transfer()
能保證可重入安全映企。盡管這些外部智能合約的函數(shù)可以被觸發(fā)執(zhí)行圆到,但補貼給外部智能合約的2,300 gas,意味著僅僅只夠記錄一個event到日志中卑吭。 -
someAddress.call.value()
將會發(fā)送指定數(shù)量的Ether并且觸發(fā)對應(yīng)代碼的執(zhí)行芽淡。被調(diào)用的外部智能合約代碼將享有所有剩余的gas,通過這種方式轉(zhuǎn)賬是很容易有可重入漏洞的豆赏,非常不安全挣菲。
-
-
處理外部調(diào)用錯誤:如果你選擇使用底層方法富稻,如
call()
、callcode()
白胀、delegatecall()
等椭赋,一定要檢查返回值來對可能的錯誤進行處理。// bad someAddress.send(55); someAddress.call.value(55)(); // this is doubly dangerous, as it will forward all remaining gas and doesn't check for result someAddress.call.value(100)(bytes4(sha3("deposit()"))); // if deposit throws an exception, the raw call() will only return false and transaction will NOT be reverted // good if(!someAddress.send(55)) { // Some failure code } ExternalContract(someAddress).deposit.value(100);
不要假設(shè)你知道外部調(diào)用的控制流程:無論是使用
raw calls
或是contract calls
或杠,如果這個ExternalContract
是不受信任的都應(yīng)該假設(shè)存在惡意代碼哪怔。即使ExternalContract
不包含惡意代碼,但它所調(diào)用的其他合約代碼可能會包含惡意代碼向抢。一個具體的危險例子便是惡意代碼可能會劫持控制流程導(dǎo)致競態(tài)认境。對于外部合約優(yōu)先使用pull 而不是push:外部調(diào)用可能會有意或無意的失敗。為了最小化這些外部調(diào)用失敗帶來的損失挟鸠,通常好的做法是將外部調(diào)用函數(shù)與其余代碼隔離叉信,最終是由收款發(fā)起方負(fù)責(zé)發(fā)起調(diào)用該函數(shù)。
標(biāo)記不受信任的合約:當(dāng)你自己的函數(shù)調(diào)用外部合約時艘希,你的變量硼身、方法、合約接口命名應(yīng)該表明和他們可能是不安全的覆享。
其他
- 使用assert()強制不變性:當(dāng)斷言條件不滿足時將觸發(fā)斷言保護 -- 比如不變的屬性發(fā)生了變化佳遂。
-
正確使用assert()和require():
require(condition)
被用來驗證用戶的輸入,如果條件不滿足便會拋出異常撒顿,應(yīng)當(dāng)使用它驗證所有用戶的輸入讶迁。assert(condition)
在條件不滿足也會拋出異常,但是最好只用于固定變量:內(nèi)部錯誤或你的智能合約陷入無效的狀態(tài)核蘸。 - 小心整數(shù)除法的四舍五入:所有整數(shù)除數(shù)都會四舍五入到最接近的整數(shù)巍糯。 如果您需要更高精度,請考慮使用乘數(shù)客扎,或存儲分子和分母祟峦。
- 記住Ether可以被強制發(fā)送到賬戶:謹(jǐn)慎編寫用來檢查賬戶余額的不變量。
- 不要假設(shè)合約創(chuàng)建時余額為零:攻擊者可以在合約創(chuàng)建之前向合約的地址發(fā)送wei徙鱼。合約不能假設(shè)它的初始狀態(tài)包含的余額為零宅楞。
- 記住鏈上的數(shù)據(jù)是公開的:許多應(yīng)用需要提交的數(shù)據(jù)是私有的,直到某個時間點才能工作袱吆。如果你的應(yīng)用存在隱私保護問題厌衙,一定要避免過早發(fā)布用戶信息。
- 權(quán)衡Abstract合約和Interfaces:Interfaces和Abstract合約都是用來使智能合約能更好的被定制和重用绞绒。Interfaces和Abstract合約很像但是不能定義方法只能申明婶希。Interfaces存在一些限制比如不能夠訪問storage或者從其他Interfaces那繼承,通常這些使Abstract合約更實用蓬衡。盡管如此喻杈,Interfaces在實現(xiàn)智能合約之前的設(shè)計智能合約階段仍然有很大用處彤枢。另外,需要注意的是如果一個智能合約從另一個Abstract合約繼承而來那么它必須實現(xiàn)所有Abstract合約內(nèi)的申明并未實現(xiàn)的函數(shù)筒饰,否則它也會成為一個Abstract合約缴啡。
- 在雙方或多方參與的智能合約中,參與者可能會“脫機離線”后不再返回:不要讓退款和索賠流程依賴于參與方執(zhí)行的某個特定動作而沒有其他途徑來獲取資金瓷们。
- 使Fallback函數(shù)盡量簡單:謹(jǐn)慎編寫fallback函數(shù)以免gas不夠用业栅。
-
明確標(biāo)明函數(shù)和狀態(tài)變量的可見性:明確標(biāo)明函數(shù)和狀態(tài)變量的可見性。函數(shù)可以聲明為
external
谬晕,public
碘裕,internal
或private
。 - 將程序鎖定到特定的編譯器版本:智能合約應(yīng)該應(yīng)該使用和它們測試時使用最多的編譯器相同的版本來部署固蚤。鎖定編譯器版本有助于確保合約不會被用于最新的可能還有bug未被發(fā)現(xiàn)的編譯器去部署。智能合約也可能會由他人部署歹茶,而pragma標(biāo)明了合約作者希望使用哪個版本的編譯器來部署合約夕玩。
- 小心分母為零 (Solidity < 0.4)
- 區(qū)分函數(shù)和事件:為了防止函數(shù)和事件(Event)產(chǎn)生混淆,命名一個事件使用大寫并加入前綴(我們建議LOG)惊豺。對于函數(shù)燎孟,始終以小寫字母開頭,構(gòu)造函數(shù)除外尸昧。
- 使用Solidity更新的構(gòu)造器
開發(fā)理念
-
對可能的錯誤有所準(zhǔn)備
- 有效的途徑來進行bug修復(fù)和功能升級
-
謹(jǐn)慎發(fā)布智能合約
- 階段性發(fā)布揩页,每個階段都提供足夠的測試
-
保持智能合約簡潔
- 確保智能合約邏輯簡潔
- 確保合約合函數(shù)模塊化
- 使用已經(jīng)被廣泛使用的合約或工具(比如,不要自己寫一個隨機生成器)
- 條件允許的話烹俗,清晰明了比性能更重要
- 只在你系統(tǒng)的去中心化部分使用區(qū)塊鏈
-
保持更新
- 在任何新的漏洞被發(fā)現(xiàn)時檢查你的智能合約
- 盡可能快的將使用到的庫或者工具更新到最新
- 使用最新的安全技術(shù)
-
清楚區(qū)塊鏈的特性
- 特別小心針對外部合約的調(diào)用爆侣,因為你可能執(zhí)行的是一段惡意代碼然后更改控制流程
- 清楚你的public function是公開的,意味著可以被惡意調(diào)用幢妄。(在以太坊上)你的private data也是對他人可見的
- 清楚gas的花費和區(qū)塊的gas limit
開發(fā)建議
- 嚴(yán)肅對待:如果編譯器警告你某事兔仰,應(yīng)該更好地改變它。
- 限制ether的數(shù)量:限制可以存儲在智能合約中的ether(或其他token)的數(shù)量蕉鸳。如果您的源代碼乎赴、編譯器或平臺有錯誤,這些資金可能會丟失潮尝。
- 保持簡潔而且模塊化
- 使用Checks-Effects-Interactions模式
- 包含故障安全模式:如果自檢失敗榕吼,合約會自動切換到某種“故障保護”模式,例如勉失,禁用大多數(shù)功能羹蚣,將控制交給固定和可信任的第三方,或者只是將合約轉(zhuǎn)換為簡單的“把我的錢還給我“合約乱凿。
- 規(guī)范驗證:證明源代碼符合某種正式規(guī)范度宦。