
本文翻譯自:https://github.com/ConsenSys/smart-contract-best-practices。
為了使語句表達(dá)更加貼切,個(gè)別地方未按照原文逐字逐句翻譯,如有出入請以原文為準(zhǔn)。
[圖片上傳失敗...(image-c5d959-1543546424209)]
主要章節(jié)如下:
這篇文檔旨在為Solidity開發(fā)人員提供一些智能合約的安全準(zhǔn)則(security baseline)。當(dāng)然也包括智能合約的安全開發(fā)理念、bug賞金計(jì)劃指南、文檔例程以及工具。
我們邀請社區(qū)對該文檔提出修改或增補(bǔ)建議,歡迎各種合并請求(Pull Request)。若有相關(guān)的文章或者博客的發(fā)表,也清將其加入到參考文獻(xiàn)中,具體詳情請參見我們的社區(qū)貢獻(xiàn)指南。
更多期待內(nèi)容
我們歡迎并期待社區(qū)開發(fā)者貢獻(xiàn)以下幾個(gè)方面的內(nèi)容:
- Solidity代碼測試(包括代碼結(jié)構(gòu),程序框架 以及 常見軟件工程測試)
- 智能合約開發(fā)經(jīng)驗(yàn)總結(jié),以及更廣泛的基于區(qū)塊鏈的開發(fā)技巧分享
基本理念
<a name="general-philosophy"></a>
以太坊和其他復(fù)雜的區(qū)塊鏈項(xiàng)目都處于早期階段并且有很強(qiáng)的實(shí)驗(yàn)性質(zhì)。因此,隨著新的bug和安全漏洞被發(fā)現(xiàn),新的功能不斷被開發(fā)出來,其面臨的安全威脅也是不斷變化的。這篇文章對于開發(fā)人員編寫安全的智能合約來說只是個(gè)開始。
開發(fā)智能合約需要一個(gè)全新的工程思維,它不同于我們以往項(xiàng)目的開發(fā)。因?yàn)樗稿e(cuò)的代價(jià)是巨大的,并且很難像傳統(tǒng)軟件那樣輕易的打上補(bǔ)丁。就像直接給硬件編程或金融服務(wù)類軟件開發(fā),相比于web開發(fā)和移動(dòng)開發(fā)都有更大的挑戰(zhàn)。因此,僅僅防范已知的漏洞是不夠的,你還需要學(xué)習(xí)新的開發(fā)理念:
-
對可能的錯(cuò)誤有所準(zhǔn)備。任何有意義的智能合約或多或少都存在錯(cuò)誤。因此你的代碼必須能夠正確的處理出現(xiàn)的bug和漏洞。始終保證以下規(guī)則:
- 當(dāng)智能合約出現(xiàn)錯(cuò)誤時(shí),停止合約,(“斷路開關(guān)”)
- 管理賬戶的資金風(fēng)險(xiǎn)(限制(轉(zhuǎn)賬)速率、最大(轉(zhuǎn)賬)額度)
- 有效的途徑來進(jìn)行bug修復(fù)和功能提升
-
謹(jǐn)慎發(fā)布智能合約。 盡量在正式發(fā)布智能合約之前發(fā)現(xiàn)并修復(fù)可能的bug。
- 對智能合約進(jìn)行徹底的測試,并在任何新的攻擊手法被發(fā)現(xiàn)后及時(shí)的測試(包括已經(jīng)發(fā)布的合約)
- 從alpha版本在測試網(wǎng)(testnet)上發(fā)布開始便提供bug賞金計(jì)劃
- 階段性發(fā)布,每個(gè)階段都提供足夠的測試
-
保持智能合約的簡潔。復(fù)雜會(huì)增加出錯(cuò)的風(fēng)險(xiǎn)。
- 確保智能合約邏輯簡潔
- 確保合約和函數(shù)模塊化
- 使用已經(jīng)被廣泛使用的合約或工具(比如,不要自己寫一個(gè)隨機(jī)數(shù)生成器)
- 條件允許的話,清晰明了比性能更重要
- 只在你系統(tǒng)的去中心化部分使用區(qū)塊鏈
-
保持更新。通過下一章節(jié)所列出的資源來確保獲取到最新的安全進(jìn)展。
- 在任何新的漏洞被發(fā)現(xiàn)時(shí)檢查你的智能合約
- 盡可能快的將使用到的庫或者工具更新到最新
- 使用最新的安全技術(shù)
-
清楚區(qū)塊鏈的特性。盡管你先前所擁有的編程經(jīng)驗(yàn)同樣適用于以太坊開發(fā),但這里仍然有些陷阱你需要留意:
- 特別小心針對外部合約的調(diào)用,因?yàn)槟憧赡軋?zhí)行的是一段惡意代碼然后更改控制流程
- 清楚你的public function是公開的,意味著可以被惡意調(diào)用。(在以太坊上)你的private data也是對他人可見的
- 清楚gas的花費(fèi)和區(qū)塊的gas limit
基本權(quán)衡:簡單性與復(fù)雜性
<a name="fundamental-tradeoffs"></a>
在評估一個(gè)智能合約的架構(gòu)和安全性時(shí)有很多需要權(quán)衡的地方。對任何智能合約的建議是在各個(gè)權(quán)衡點(diǎn)中找到一個(gè)平衡點(diǎn)。
從傳統(tǒng)軟件工程的角度出發(fā):一個(gè)理想的智能合約首先需要模塊化,能夠重用代碼而不是重復(fù)編寫,并且支持組件升級。從智能合約安全架構(gòu)的角度出發(fā)同樣如此,模塊化和重用被嚴(yán)格審查檢驗(yàn)過的合約是最佳策略,特別是在復(fù)雜智能合約系統(tǒng)里。
然而,這里有幾個(gè)重要的例外,它們從合約安全和傳統(tǒng)軟件工程兩個(gè)角度考慮,所得到的重要性排序可能不同。當(dāng)中每一條,都需要針對智能合約系統(tǒng)的特點(diǎn)找到最優(yōu)的組合方式來達(dá)到平衡。
- 固化 vs 可升級
- 龐大 vs 模塊化
- 重復(fù) vs 可重用
固化 vs 可升級
在很多文檔或者開發(fā)指南中,包括該指南,都會(huì)強(qiáng)調(diào)延展性比如:可終止,可升級或可更改的特性,不過對于智能合約來說,延展性和安全之間是個(gè)基本權(quán)衡。
延展性會(huì)增加程序復(fù)雜性和潛在的攻擊面。對于那些只在特定的時(shí)間段內(nèi)提供有限的功能的智能合約,簡單性比復(fù)雜性顯得更加高效,比如無管治功能,有限短期內(nèi)使用的代幣發(fā)行的智能合約系統(tǒng)(governance-fee,finite-time-frame token-sale contracts)。
龐大 vs 模塊化
一個(gè)龐大的獨(dú)立的智能合約把所有的變量和模塊都放到一個(gè)合約中。盡管只有少數(shù)幾個(gè)大家熟知的智能合約系統(tǒng)真的做到了大體量,但在將數(shù)據(jù)和流程都放到一個(gè)合約中還是享有部分優(yōu)點(diǎn)--比如,提高代碼審核(code review)效率。
和在這里討論的其他權(quán)衡點(diǎn)一樣,傳統(tǒng)軟件開發(fā)策略和從合約安全角度出發(fā)考慮,兩者不同主要在對于簡單、短生命周期的智能合約;對于更復(fù)雜、長生命周期的智能合約,兩者策略理念基本相同。
重復(fù) vs 可重用
從軟件工程角度看,智能合約系統(tǒng)希望在合理的情況下最大程度地實(shí)現(xiàn)重用。 在Solidity中重用合約代碼有很多方法。 使用你擁有的以前部署的經(jīng)過驗(yàn)證的智能合約是實(shí)現(xiàn)代碼重用的最安全的方式。
在以前所擁有已部署智能合約不可重用時(shí)重復(fù)還是很需要的。 現(xiàn)在Live Libs 和Zeppelin Solidity 正尋求提供安全的智能合約組件使其能夠被重用而不需要每次都重新編寫。任何合約安全性分析都必須標(biāo)明重用代碼,特別是以前沒有建立與目標(biāo)智能合同系統(tǒng)中處于風(fēng)險(xiǎn)中的資金相稱的信任級別的代碼。
安全通知
以下這些地方通常會(huì)通報(bào)在Ethereum或Solidity中新發(fā)現(xiàn)的漏洞。安全通告的官方來源是Ethereum Blog,但是一般漏洞都會(huì)在其他地方先被披露和討論。
-
Ethereum Blog: The official Ethereum blog
- Ethereum Blog - Security only: 所有相關(guān)博客都帶有Security標(biāo)簽
- Ethereum Gitter 聊天室
- Network Stats
強(qiáng)烈建議你經(jīng)常瀏覽這些網(wǎng)站,尤其是他們提到的可能會(huì)影響你的智能合約的漏洞。
另外, 這里列出了以太坊參與安全模塊相關(guān)的核心開發(fā)成員, 瀏覽 bibliography 獲取更多信息。
- Vitalik Buterin: Twitter, Github, Reddit, Ethereum Blog
- Dr. Christian Reitwiessner: Twitter, Github, Ethereum Blog
- Dr. Gavin Wood: Twitter, Blog, Github
- Vlad Zamfir: Twitter, Github, Ethereum Blog
除了關(guān)注核心開發(fā)成員,參與到各個(gè)區(qū)塊鏈安全社區(qū)也很重要,因?yàn)榘踩┒吹呐痘蜓芯繉⑼ㄟ^各方進(jìn)行。
<a name="solidity-tips"></a>
關(guān)于使用Solidity開發(fā)的智能合約安全建議
外部調(diào)用
盡量避免外部調(diào)用
<a name="avoid-external-calls"></a>
調(diào)用不受信任的外部合約可能會(huì)引發(fā)一系列意外的風(fēng)險(xiǎn)和錯(cuò)誤。外部調(diào)用可能在其合約和它所依賴的其他合約內(nèi)執(zhí)行惡意代碼。因此,每一個(gè)外部調(diào)用都會(huì)有潛在的安全威脅,盡可能的從你的智能合約內(nèi)移除外部調(diào)用。當(dāng)無法完全去除外部調(diào)用時(shí),可以使用這一章節(jié)其他部分提供的建議來盡量減少風(fēng)險(xiǎn)。
<a name="send-vs-call-value"></a>
仔細(xì)權(quán)衡“send()”、“transfer()”、以及“call.value()”
當(dāng)轉(zhuǎn)賬Ether時(shí),需要仔細(xì)權(quán)衡“someAddress.send()”、“someAddress.transfer()”、和“someAddress.call.value()()”之間的差別。
-
x.transfer(y)和if (!x.send(y)) throw;是等價(jià)的。send是transfer的底層實(shí)現(xiàn),建議盡可能直接使用transfer。 -
someAddress.send()和someAddress.transfer()能保證可重入 安全 。
盡管這些外部智能合約的函數(shù)可以被觸發(fā)執(zhí)行,但補(bǔ)貼給外部智能合約的2,300 gas,意味著僅僅只夠記錄一個(gè)event到日志中。 -
someAddress.call.value()()將會(huì)發(fā)送指定數(shù)量的Ether并且觸發(fā)對應(yīng)代碼的執(zhí)行。被調(diào)用的外部智能合約代碼將享有所有剩余的gas,通過這種方式轉(zhuǎn)賬是很容易有可重入漏洞的,非常 不安全。
使用send() 或transfer() 可以通過制定gas值來預(yù)防可重入, 但是這樣做可能會(huì)導(dǎo)致在和合約調(diào)用fallback函數(shù)時(shí)出現(xiàn)問題,由于gas可能不足,而合約的fallback函數(shù)執(zhí)行至少需要2,300 gas消耗。
一種被稱為push 和pull的 機(jī)制試圖來平衡兩者, 在 push 部分使用send() 或transfer(),在pull 部分使用call.value()()。(*譯者注:在需要對外未知地址轉(zhuǎn)賬Ether時(shí)使用send() 或transfer(),已知明確內(nèi)部無惡意代碼的地址轉(zhuǎn)賬Ether使用call.value()())
需要注意的是使用send() 或transfer() 進(jìn)行轉(zhuǎn)賬并不能保證該智能合約本身重入安全,它僅僅只保證了這次轉(zhuǎn)賬操作時(shí)重入安全的。
<a name="handle-external-errors"></a>
處理外部調(diào)用錯(cuò)誤
Solidity提供了一系列在raw address上執(zhí)行操作的底層方法,比如: address.call(),address.callcode(), address.delegatecall()和address.send。這些底層方法不會(huì)拋出異常(throw),只是會(huì)在遇到錯(cuò)誤時(shí)返回false。另一方面, contract calls (比如,ExternalContract.doSomething()))會(huì)自動(dòng)傳遞異常,(比如,doSomething()拋出異常,那么ExternalContract.doSomething() 同樣會(huì)進(jìn)行throw) )。
如果你選擇使用底層方法,一定要檢查返回值來對可能的錯(cuò)誤進(jìn)行處理。
// 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);
<a name="expect-control-flow-loss"></a>
不要假設(shè)你知道外部調(diào)用的控制流程
無論是使用raw calls 或是contract calls,如果這個(gè)ExternalContract是不受信任的都應(yīng)該假設(shè)存在惡意代碼。即使ExternalContract不包含惡意代碼,但它所調(diào)用的其他合約代碼可能會(huì)包含惡意代碼。一個(gè)具體的危險(xiǎn)例子便是惡意代碼可能會(huì)劫持控制流程導(dǎo)致競態(tài)。(瀏覽Race Conditions獲取更多關(guān)于這個(gè)問題的討論)
<a name="favor-pull-over-push-payments"></a>
對于外部合約優(yōu)先使用pull 而不是push
外部調(diào)用可能會(huì)有意或無意的失敗。為了最小化這些外部調(diào)用失敗帶來的損失,通常好的做法是將外部調(diào)用函數(shù)與其余代碼隔離,最終是由收款發(fā)起方負(fù)責(zé)發(fā)起調(diào)用該函數(shù)。這種做法對付款操作尤為重要,比如讓用戶自己撤回資產(chǎn)而不是直接發(fā)送給他們。(譯者注:事先設(shè)置需要付給某一方的資產(chǎn)的值,表明接收方可以從當(dāng)前賬戶撤回資金的額度,然后由接收方調(diào)用當(dāng)前合約提現(xiàn)函數(shù)完成轉(zhuǎn)賬)。(這種方法同時(shí)也避免了造成 gas limit相關(guān)問題。)
// bad
contract auction {
address highestBidder;
uint highestBid;
function bid() payable {
if (msg.value < highestBid) throw;
if (highestBidder != 0) {
if (!highestBidder.send(highestBid)) { // if this call consistently fails, no one else can bid
throw;
}
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
// good
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() payable external {
if (msg.value < highestBid) throw;
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;
if (!msg.sender.send(refund)) {
refunds[msg.sender] = refund; // reverting state because send failed
}
}
}
<a name="mark-untrusted-contracts"></a>
標(biāo)記不受信任的合約
當(dāng)你自己的函數(shù)調(diào)用外部合約時(shí),你的變量、方法、合約接口命名應(yīng)該表明和他們可能是不安全的。
// bad
Bank.withdraw(100); // Unclear whether trusted or untrusted
function makeWithdrawal(uint amount) { // Isn't clear that this function is potentially unsafe
Bank.withdraw(amount);
}
// good
UntrustedBank.withdraw(100); // untrusted external call
TrustedBank.withdraw(100); // external but trusted bank contract maintained by XYZ Corp
function makeUntrustedWithdrawal(uint amount) {
UntrustedBank.withdraw(amount);
}
使用assert()強(qiáng)制不變性
當(dāng)斷言條件不滿足時(shí)將觸發(fā)斷言保護(hù) -- 比如不變的屬性發(fā)生了變化。舉個(gè)例子,代幣在以太坊上的發(fā)行比例,在代幣的發(fā)行合約里可以通過這種方式得到解決。斷言保護(hù)經(jīng)常需要和其他技術(shù)組合使用,比如當(dāng)斷言被觸發(fā)時(shí)先掛起合約然后升級。(否則將一直觸發(fā)斷言,你將陷入僵局)
例如:
contract Token {
mapping(address => uint) public balanceOf;
uint public totalSupply;
function deposit() public payable {
balanceOf[msg.sender] += msg.value;
totalSupply += msg.value;
assert(this.balance >= totalSupply);
}
}
注意斷言保護(hù) 不是 嚴(yán)格意義的余額檢測, 因?yàn)橹悄芎霞s可以不通過deposit() 函數(shù)被 強(qiáng)制發(fā)送Ether!
正確使用assert()和require()
在Solidity 0.4.10 中assert()和require()被加入。require(condition)被用來驗(yàn)證用戶的輸入,如果條件不滿足便會(huì)拋出異常,應(yīng)當(dāng)使用它驗(yàn)證所有用戶的輸入。 assert(condition) 在條件不滿足也會(huì)拋出異常,但是最好只用于固定變量:內(nèi)部錯(cuò)誤或你的智能合約陷入無效的狀態(tài)。遵循這些范例,使用分析工具來驗(yàn)證永遠(yuǎn)不會(huì)執(zhí)行這些無效操作碼:意味著代碼中不存在任何不變量,并且代碼已經(jīng)正式驗(yàn)證。
<a name="beware-rounding-with-integer-division"></a>
小心整數(shù)除法的四舍五入
所有整數(shù)除數(shù)都會(huì)四舍五入到最接近的整數(shù)。 如果您需要更高精度,請考慮使用乘數(shù),或存儲分子和分母。
(將來Solidity會(huì)有一個(gè)fixed-point類型來讓這一切變得容易。)
// bad
uint x = 5 / 2; // Result is 2, all integer divison rounds DOWN to the nearest integer
// good
uint multiplier = 10;
uint x = (5 * multiplier) / 2;
uint numerator = 5;
uint denominator = 2;
<a name="ether-forcibly-sent"></a>
記住Ether可以被強(qiáng)制發(fā)送到賬戶
謹(jǐn)慎編寫用來檢查賬戶余額的不變量。
攻擊者可以強(qiáng)制發(fā)送wei到任何賬戶,而且這是不能被阻止的(即使讓fallback函數(shù)throw也不行)
攻擊者可以僅僅使用1 wei來創(chuàng)建一個(gè)合約,然后調(diào)用selfdestruct(victimAddress)。在victimAddress中沒有代碼被執(zhí)行,所以這是不能被阻止的。
不要假設(shè)合約創(chuàng)建時(shí)余額為零
攻擊者可以在合約創(chuàng)建之前向合約的地址發(fā)送wei。合約不能假設(shè)它的初始狀態(tài)包含的余額為零。瀏覽issue 61 獲取更多信息。
記住鏈上的數(shù)據(jù)是公開的
許多應(yīng)用需要提交的數(shù)據(jù)是私有的,直到某個(gè)時(shí)間點(diǎn)才能工作。游戲(比如,鏈上游戲rock-paper-scissors(石頭剪刀布))和拍賣機(jī)(比如,sealed-bid second-price auctions)是兩個(gè)典型的例子。如果你的應(yīng)用存在隱私保護(hù)問題,一定要避免過早發(fā)布用戶信息。
例如:
- 在游戲石頭剪刀布中,需要參與游戲的雙方提交他們“行動(dòng)計(jì)劃”的hash值,然后需要雙方隨后提交他們的行動(dòng)計(jì)劃;如果雙方的“行動(dòng)計(jì)劃”和先前提交的hash值對不上則拋出異常。
- 在拍賣中,要求玩家在初始階段提交其所出價(jià)格的hash值(以及超過其出價(jià)的保證金),然后在第二階段提交他們所出價(jià)格的資金。
- 當(dāng)開發(fā)一個(gè)依賴隨機(jī)數(shù)生成器的應(yīng)用時(shí),正確的順序應(yīng)當(dāng)是(1)玩家提交行動(dòng)計(jì)劃,(2)生成隨機(jī)數(shù),(3)玩家支付。產(chǎn)生隨機(jī)數(shù)是一個(gè)值得研究的領(lǐng)域;當(dāng)前最優(yōu)的解決方案包括比特幣區(qū)塊頭(通過http://btcrelay.org驗(yàn)證),hash-commit-reveal方案(比如,一方產(chǎn)生number后,將其散列值提交作為對這個(gè)number的“提交”,然后在隨后再暴露這個(gè)number本身)和 RANDAO。
- 如果你正在實(shí)現(xiàn)頻繁的批量拍賣,那么hash-commit機(jī)制也是個(gè)不錯(cuò)的選擇。
權(quán)衡Abstract合約和Interfaces
Interfaces和Abstract合約都是用來使智能合約能更好的被定制和重用。Interfaces是在Solidity 0.4.11中被引入的,和Abstract合約很像但是不能定義方法只能申明。Interfaces存在一些限制比如不能夠訪問storage或者從其他Interfaces那繼承,通常這些使Abstract合約更實(shí)用。盡管如此,Interfaces在實(shí)現(xiàn)智能合約之前的設(shè)計(jì)智能合約階段仍然有很大用處。另外,需要注意的是如果一個(gè)智能合約從另一個(gè)Abstract合約繼承而來那么它必須實(shí)現(xiàn)所有Abstract合約內(nèi)的申明并未實(shí)現(xiàn)的函數(shù),否則它也會(huì)成為一個(gè)Abstract合約。
在雙方或多方參與的智能合約中,參與者可能會(huì)“脫機(jī)離線”后不再返回
不要讓退款和索賠流程依賴于參與方執(zhí)行的某個(gè)特定動(dòng)作而沒有其他途徑來獲取資金。比如,在石頭剪刀布游戲中,一個(gè)常見的錯(cuò)誤是在兩個(gè)玩家提交他們的行動(dòng)計(jì)劃之前不要付錢。然而一個(gè)惡意玩家可以通過一直不提交它的行動(dòng)計(jì)劃來使對方蒙受損失 -- 事實(shí)上,如果玩家看到其他玩家泄露的行動(dòng)計(jì)劃然后決定他是否會(huì)損失(譯者注:發(fā)現(xiàn)自己輸了),那么他完全有理由不再提交他自己的行動(dòng)計(jì)劃。這些問題也同樣會(huì)出現(xiàn)在通道結(jié)算。當(dāng)這些情形出現(xiàn)導(dǎo)致問題后:(1)提供一種規(guī)避非參與者和參與者的方式,可能通過設(shè)置時(shí)間限制,和(2)考慮為參與者提供額外的經(jīng)濟(jì)激勵(lì),以便在他們應(yīng)該這樣做的所有情況下仍然提交信息。
<a name="keep-fallback-functions-simple"></a>
使Fallback函數(shù)盡量簡單
Fallback函數(shù)在合約執(zhí)行消息發(fā)送沒有攜帶參數(shù)(或當(dāng)沒有匹配的函數(shù)可供調(diào)用)時(shí)將會(huì)被調(diào)用,而且當(dāng)調(diào)用 .send() or .transfer()時(shí),只會(huì)有2,300 gas 用于失敗后fallback函數(shù)的執(zhí)行(譯者注:合約收到Ether也會(huì)觸發(fā)fallback函數(shù)執(zhí)行)。如果你希望能夠監(jiān)聽.send()或.transfer()接收到Ether,則可以在fallback函數(shù)中使用event(譯者注:讓客戶端監(jiān)聽相應(yīng)事件做相應(yīng)處理)。謹(jǐn)慎編寫fallback函數(shù)以免gas不夠用。
// bad
function() payable { balances[msg.sender] += msg.value; }
// good
function deposit() payable external { balances[msg.sender] += msg.value; }
function() payable { LogDepositReceived(msg.sender); }
<a name="mark-visibility"></a>
明確標(biāo)明函數(shù)和狀態(tài)變量的可見性
明確標(biāo)明函數(shù)和狀態(tài)變量的可見性。函數(shù)可以聲明為 external,public, internal 或 private。 分清楚它們之間的差異, 例如external 可能已夠用而不是使用 public。對于狀態(tài)變量,external是不可能的。明確標(biāo)注可見性將使得更容易避免關(guān)于誰可以調(diào)用該函數(shù)或訪問變量的錯(cuò)誤假設(shè)。
// bad
uint x; // the default is private for state variables, but it should be made explicit
function buy() { // the default is public
// public code
}
// good
uint private y;
function buy() external {
// only callable externally
}
function utility() public {
// callable externally, as well as internally: changing this code requires thinking about both cases.
}
function internalAction() internal {
// internal code
}
<a name="lock-pragmas"></a>
將程序鎖定到特定的編譯器版本
智能合約應(yīng)該應(yīng)該使用和它們測試時(shí)使用最多的編譯器相同的版本來部署。鎖定編譯器版本有助于確保合約不會(huì)被用于最新的可能還有bug未被發(fā)現(xiàn)的編譯器去部署。智能合約也可能會(huì)由他人部署,而pragma標(biāo)明了合約作者希望使用哪個(gè)版本的編譯器來部署合約。
// bad
pragma solidity ^0.4.4;
// good
pragma solidity 0.4.4;
<a name="beware-division-by-zero"></a>
(譯者注:這當(dāng)然也會(huì)付出兼容性的代價(jià))
小心分母為零 (Solidity < 0.4)
早于0.4版本, 當(dāng)一個(gè)數(shù)嘗試除以零時(shí),Solidity 返回zero 并沒有 throw 一個(gè)異常。確保你使用的Solidity版本至少為 0.4。
<a name="differentiate-functions-events"></a>
區(qū)分函數(shù)和事件
為了防止函數(shù)和事件(Event)產(chǎn)生混淆,命名一個(gè)事件使用大寫并加入前綴(我們建議LOG)。對于函數(shù), 始終以小寫字母開頭,構(gòu)造函數(shù)除外。
// bad
event Transfer() {}
function transfer() {}
// good
event LogTransfer() {}
function transfer() external {}
<a name="prefer-newer-constructs"></a>
使用Solidity更新的構(gòu)造器
更合適的構(gòu)造器/別名,如selfdestruct(舊版本為'suicide)和keccak256(舊版本為sha3)。 像require(msg.sender.send(1 ether))``的模式也可以簡化為使用transfer(),如msg.sender.transfer(1 ether)。
<a name="known-attacks"></a>
已知的攻擊
<a name="race-conditions"></a>
競態(tài)<a href='#footnote-race-condition-terminology'>*</a>
調(diào)用外部契約的主要危險(xiǎn)之一是它們可以接管控制流,并對調(diào)用函數(shù)意料之外的數(shù)據(jù)進(jìn)行更改。 這類bug有多種形式,導(dǎo)致DAO崩潰的兩個(gè)主要錯(cuò)誤都是這種錯(cuò)誤。
<a name="reentrancy"></a>
重入
這個(gè)版本的bug被注意到是其可以在第一次調(diào)用這個(gè)函數(shù)完成之前被多次重復(fù)調(diào)用。對這個(gè)函數(shù)不斷的調(diào)用可能會(huì)造成極大的破壞。
// INSECURE
mapping (address => uint) private userBalances;
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // At this point, the caller's code is executed, and can call withdrawBalance again
userBalances[msg.sender] = 0;
}
(譯者注:使用msg.sender.call.value()())傳遞給fallback函數(shù)可用的氣是當(dāng)前剩余的所有氣,在這里,假如從你賬戶執(zhí)行提現(xiàn)操作的惡意合約的fallback函數(shù)內(nèi)遞歸調(diào)用你的withdrawBalance()便可以從你的賬戶轉(zhuǎn)走更多的幣。)
可以看到當(dāng)調(diào)msg.sender.call.value()()時(shí),并沒有將userBalances[msg.sender] 清零,于是在這之前可以成功遞歸調(diào)用很多次withdrawBalance()函數(shù)。 一個(gè)非常相像的bug便是出現(xiàn)在針對 DAO 的攻擊。
在給出來的例子中,最好的方法是 使用 send() 而不是call.value()()。這將避免多余的代碼被執(zhí)行。
然而,如果你沒法完全移除外部調(diào)用,另一個(gè)簡單的方法來阻止這個(gè)攻擊是確保你在完成你所有內(nèi)部工作之前不要進(jìn)行外部調(diào)用:
mapping (address => uint) private userBalances;
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
userBalances[msg.sender] = 0;
if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // The user's balance is already 0, so future invocations won't withdraw anything
}
注意如果你有另一個(gè)函數(shù)也調(diào)用了 withdrawBalance(), 那么這里潛在的存在上面的攻擊,所以你必須認(rèn)識到任何調(diào)用了不受信任的合約代碼的合約也是不受信任的。繼續(xù)瀏覽下面的相關(guān)潛在威脅解決辦法的討論。
跨函數(shù)競態(tài)
攻擊者也可以使用兩個(gè)共享狀態(tài)變量的不同的函數(shù)來進(jìn)行類似攻擊。
// INSECURE
mapping (address => uint) private userBalances;
function transfer(address to, uint amount) {
if (userBalances[msg.sender] >= amount) {
userBalances[to] += amount;
userBalances[msg.sender] -= amount;
}
}
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // At this point, the caller's code is executed, and can call transfer()
userBalances[msg.sender] = 0;
}
著這個(gè)例子中,攻擊者在他們外部調(diào)用withdrawBalance函數(shù)時(shí)調(diào)用transfer(),如果這個(gè)時(shí)候withdrawBalance還沒有執(zhí)行到userBalances[msg.sender] = 0;這里,那么他們的余額就沒有被清零,那么他們就能夠調(diào)用transfer()轉(zhuǎn)走代幣盡管他們其實(shí)已經(jīng)收到了代幣。這個(gè)弱點(diǎn)也可以被用到對DAO的攻擊。
同樣的解決辦法也會(huì)管用,在執(zhí)行轉(zhuǎn)賬操作之前先清零。也要注意在這個(gè)例子中所有函數(shù)都是在同一個(gè)合約內(nèi)。然而,如果這些合約共享了狀態(tài),同樣的bug也可以發(fā)生在跨合約調(diào)用中。
競態(tài)解決辦法中的陷阱
由于競態(tài)既可以發(fā)生在跨函數(shù)調(diào)用,也可以發(fā)生在跨合約調(diào)用,任何只是避免重入的解決辦法都是不夠的。
作為替代,我們建議首先應(yīng)該完成所有內(nèi)部的工作然后再執(zhí)行外部調(diào)用。這個(gè)規(guī)則可以避免競態(tài)發(fā)生。然而,你不僅應(yīng)該避免過早調(diào)用外部函數(shù)而且應(yīng)該避免調(diào)用那些也調(diào)用了外部函數(shù)的外部函數(shù)。例如,下面的這段代碼是不安全的:
// INSECURE
mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;
function withdraw(address recipient) public {
uint amountToWithdraw = userBalances[recipient];
rewardsForA[recipient] = 0;
if (!(recipient.call.value(amountToWithdraw)())) { throw; }
}
function getFirstWithdrawalBonus(address recipient) public {
if (claimedBonus[recipient]) { throw; } // Each recipient should only be able to claim the bonus once
rewardsForA[recipient] += 100;
withdraw(recipient); // At this point, the caller will be able to execute getFirstWithdrawalBonus again.
claimedBonus[recipient] = true;
}
盡管getFirstWithdrawalBonus() 沒有直接調(diào)用外部合約,但是它調(diào)用的withdraw() 卻會(huì)導(dǎo)致競態(tài)的產(chǎn)生。在這里你不應(yīng)該認(rèn)為withdraw()是受信任的。
mapping (address => uint) private userBalances;
mapping (address => bool) private claimedBonus;
mapping (address => uint) private rewardsForA;
function untrustedWithdraw(address recipient) public {
uint amountToWithdraw = userBalances[recipient];
rewardsForA[recipient] = 0;
if (!(recipient.call.value(amountToWithdraw)())) { throw; }
}
function untrustedGetFirstWithdrawalBonus(address recipient) public {
if (claimedBonus[recipient]) { throw; } // Each recipient should only be able to claim the bonus once
claimedBonus[recipient] = true;
rewardsForA[recipient] += 100;
untrustedWithdraw(recipient); // claimedBonus has been set to true, so reentry is impossible
}
除了修復(fù)bug讓重入不可能成功,不受信任的函數(shù)也已經(jīng)被標(biāo)記出來 。同樣的情景: untrustedGetFirstWithdrawalBonus() 調(diào)用untrustedWithdraw(), 而后者調(diào)用了外部合約,因此在這里untrustedGetFirstWithdrawalBonus() 是不安全的。
另一個(gè)經(jīng)常被提及的解決辦法是(譯者注:像傳統(tǒng)多線程編程中一樣)使用mutex。它會(huì)"lock" 當(dāng)前狀態(tài),只有鎖的當(dāng)前擁有者能夠更改當(dāng)前狀態(tài)。一個(gè)簡單的例子如下:
// Note: This is a rudimentary example, and mutexes are particularly useful where there is substantial logic and/or shared state
mapping (address => uint) private balances;
bool private lockBalances;
function deposit() payable public returns (bool) {
if (!lockBalances) {
lockBalances = true;
balances[msg.sender] += msg.value;
lockBalances = false;
return true;
}
throw;
}
function withdraw(uint amount) payable public returns (bool) {
if (!lockBalances && amount > 0 && balances[msg.sender] >= amount) {
lockBalances = true;
if (msg.sender.call(amount)()) { // Normally insecure, but the mutex saves it
balances[msg.sender] -= amount;
}
lockBalances = false;
return true;
}
throw;
}
如果用戶試圖在第一次調(diào)用結(jié)束前第二次調(diào)用 withdraw(),將會(huì)被鎖住。 這看上去很有效果,但當(dāng)你使用多個(gè)合約互相交互時(shí)問題變得嚴(yán)峻了。 下面是一段不安全的代碼:
// INSECURE
contract StateHolder {
uint private n;
address private lockHolder;
function getLock() {
if (lockHolder != 0) { throw; }
lockHolder = msg.sender;
}
function releaseLock() {
lockHolder = 0;
}
function set(uint newState) {
if (msg.sender != lockHolder) { throw; }
n = newState;
}
}
攻擊者可以只調(diào)用getLock(),然后就不再調(diào)用 releaseLock()。如果他們真這樣做,那么這個(gè)合約將會(huì)被永久鎖住,任何接下來的操作都不會(huì)發(fā)生了。如果你使用mutexs來避免競態(tài),那么一定要確保沒有地方能夠打斷鎖的進(jìn)程或絕不釋放鎖。(這里還有一個(gè)潛在的威脅,比如死鎖和活鎖。在你決定使用鎖之前最好大量閱讀相關(guān)文獻(xiàn)(譯者注:這是真的,傳統(tǒng)的在多線程環(huán)境下對鎖的使用一直是個(gè)容易犯錯(cuò)的地方))
<a name="footnote-race-condition-terminology"></a>
<div style='font-size: 80%; display: inline;'>* 有些人可能會(huì)發(fā)反對使用該術(shù)語 <i>競態(tài)</i>,因?yàn)橐蕴徊]有真正意思上實(shí)現(xiàn)并行執(zhí)行。然而在邏輯上依然存在對資源的競爭,同樣的陷阱和潛在的解決方案。 </div>
<a name="transaction-ordering-dependence"></a>
交易順序依賴(TOD) / 前面的先運(yùn)行
以上是涉及攻擊者在單個(gè)交易內(nèi)執(zhí)行惡意代碼產(chǎn)生競態(tài)的示例。接下來演示在區(qū)塊鏈本身運(yùn)作原理導(dǎo)致的競態(tài):(同一個(gè)block內(nèi)的)交易順序很容易受到操縱。
由于交易在短暫的時(shí)間內(nèi)會(huì)先存放到mempool中,所以在礦工將其打包進(jìn)block之前,是可以知道會(huì)發(fā)生什么動(dòng)作的。這對于一個(gè)去中心化的市場來說是麻煩的,因?yàn)榭梢圆榭吹酱鷰诺慕灰仔畔?,并且可以在它被打包進(jìn)block之前改變交易順序。避免這一點(diǎn)很困難,因?yàn)樗鼩w結(jié)為具體的合同本身。例如,在市場上,最好實(shí)施批量拍賣(這也可以防止高頻交易問題)。 另一種使用預(yù)提交方案的方法(“我稍后會(huì)提供詳細(xì)信息”)。
<a name="timestamp-dependence"></a>
時(shí)間戳依賴
請注意,塊的時(shí)間戳可以由礦工操縱,并且應(yīng)考慮時(shí)間戳的所有直接和間接使用。 區(qū)塊數(shù)量和平均出塊時(shí)間可用于估計(jì)時(shí)間,但這不是區(qū)塊時(shí)間在未來可能改變(例如Casper期望的更改)的證明。
uint someVariable = now + 1;
if (now % 2 == 0) { // the now can be manipulated by the miner
}
if ((someVariable - 100) % 2 == 0) { // someVariable can be manipulated by the miner
}
<a name="integer-overflow-and-underflow"></a>
整數(shù)上溢和下溢
這里大概有 20關(guān)于上溢和下溢的例子。
考慮如下這個(gè)簡單的轉(zhuǎn)賬操作:
mapping (address => uint256) public balanceOf;
// INSECURE
function transfer(address _to, uint256 _value) {
/* Check if sender has balance */
if (balanceOf[msg.sender] < _value)
throw;
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
// SECURE
function transfer(address _to, uint256 _value) {
/* Check if sender has balance and for overflows */
if (balanceOf[msg.sender] < _value || balanceOf[_to] + _value < balanceOf[_to])
throw;
/* Add and subtract new balances */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
如果余額到達(dá)uint的最大值(2^256),便又會(huì)變?yōu)?。應(yīng)當(dāng)檢查這里。溢出是否與之相關(guān)取決于具體的實(shí)施方式。想想uint值是否有機(jī)會(huì)變得這么大或和誰會(huì)改變它的值。如果任何用戶都有權(quán)利更改uint的值,那么它將更容易受到攻擊。如果只有管理員能夠改變它的值,那么它可能是安全的,因?yàn)闆]有別的辦法可以跨越這個(gè)限制。
對于下溢同樣的道理。如果一個(gè)uint別改變后小于0,那么將會(huì)導(dǎo)致它下溢并且被設(shè)置成為最大值(2^256)。
對于較小數(shù)字的類型比如uint8、uint16、uint24等也要小心:他們更加容易達(dá)到最大值。
<a name="dos-with-unexpected-throw"></a>
通過(Unexpected) Throw發(fā)動(dòng)DoS
考慮如下簡單的智能合約:
// INSECURE
contract Auction {
address currentLeader;
uint highestBid;
function bid() payable {
if (msg.value <= highestBid) { throw; }
if (!currentLeader.send(highestBid)) { throw; } // Refund the old leader, and throw if it fails
currentLeader = msg.sender;
highestBid = msg.value;
}
}
當(dāng)有更高競價(jià)時(shí),它將試圖退款給曾經(jīng)最高競價(jià)人,如果退款失敗則會(huì)拋出異常。這意味著,惡意投標(biāo)人可以成為當(dāng)前最高競價(jià)人,同時(shí)確保對其地址的任何退款始終失敗。這樣就可以阻止任何人調(diào)用“bid()”函數(shù),使自己永遠(yuǎn)保持領(lǐng)先。建議向之前所說的那樣建立基于pull的支付系統(tǒng) 。
另一個(gè)例子是合約可能通過數(shù)組迭代來向用戶支付(例如,眾籌合約中的支持者)時(shí)。 通常要確保每次付款都成功。 如果沒有,應(yīng)該拋出異常。 問題是,如果其中一個(gè)支付失敗,您將恢復(fù)整個(gè)支付系統(tǒng),這意味著該循環(huán)將永遠(yuǎn)不會(huì)完成。 因?yàn)橐粋€(gè)地址沒有轉(zhuǎn)賬成功導(dǎo)致其他人都沒得到報(bào)酬。
address[] private refundAddresses;
mapping (address => uint) public refunds;
// bad
function refundAll() public {
for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
if(refundAddresses[x].send(refunds[refundAddresses[x]])) {
throw; // doubly bad, now a single failure on send will hold up all funds
}
}
}
再一次強(qiáng)調(diào),同樣的解決辦法: 優(yōu)先使用pull 而不是push支付系統(tǒng)。
<a name="dos-with-block-gas-limit"></a>
通過區(qū)塊Gas Limit發(fā)動(dòng)DoS
在先前的例子中你可能已經(jīng)注意到另一個(gè)問題:一次性向所有人轉(zhuǎn)賬,很可能會(huì)導(dǎo)致達(dá)到以太坊區(qū)塊gas limit的上限。以太坊規(guī)定了每一個(gè)區(qū)塊所能花費(fèi)的gas limit,如果超過你的交易便會(huì)失敗。
即使沒有故意的攻擊,這也可能導(dǎo)致問題。然而,最為糟糕的是如果gas的花費(fèi)被攻擊者操控。在先前的例子中,如果攻擊者增加一部分收款名單,并設(shè)置每一個(gè)收款地址都接收少量的退款。這樣一來,更多的gas將會(huì)被花費(fèi)從而導(dǎo)致達(dá)到區(qū)塊gas limit的上限,整個(gè)轉(zhuǎn)賬的操作也會(huì)以失敗告終。
又一次證明了 優(yōu)先使用pull 而不是push支付系統(tǒng)。
如果你實(shí)在必須通過遍歷一個(gè)變長數(shù)組來進(jìn)行轉(zhuǎn)賬,最好估計(jì)完成它們大概需要多少個(gè)區(qū)塊以及多少筆交易。然后你還必須能夠追蹤得到當(dāng)前進(jìn)行到哪以便當(dāng)操作失敗時(shí)從那里開始恢復(fù),舉個(gè)例子:
struct Payee {
address addr;
uint256 value;
}
Payee payees[];
uint256 nextPayeeIndex;
function payOut() {
uint256 i = nextPayeeIndex;
while (i < payees.length && msg.gas > 200000) {
payees[i].addr.send(payees[i].value);
i++;
}
nextPayeeIndex = i;
}
如上所示,你必須確保在下一次執(zhí)行payOut()之前另一些正在執(zhí)行的交易不會(huì)發(fā)生任何錯(cuò)誤。如果必須,請使用上面這種方式來處理。
<a name="call-depth-attack"></a>
Call Depth攻擊
由于EIP 150 進(jìn)行的硬分叉,Call Depth攻擊已經(jīng)無法實(shí)施<a >*</a> (由于以太坊限制了Call Depth最大為1024,確保了在達(dá)到最大深度之前gas都能被正確使用)
<a name="eng-techniques"></a>
軟件工程開發(fā)技巧
正如我們先前在基本理念 章節(jié)所討論的那樣,避免自己遭受已知的攻擊是不夠的。由于在鏈上遭受攻擊損失是巨大的,因此你還必須改變你編寫軟件的方式來抵御各種攻擊。
我們倡導(dǎo)“時(shí)刻準(zhǔn)備失敗",提前知道你的代碼是否安全是不可能的。然而,我們可以允許合約以可預(yù)知的方式失敗,然后最小化失敗帶來的損失。本章將帶你了解如何為可預(yù)知的失敗做準(zhǔn)備。
注意:當(dāng)你向你的系統(tǒng)添加新的組件時(shí)總是伴隨著風(fēng)險(xiǎn)的。一個(gè)不良設(shè)計(jì)本身會(huì)成為漏洞-一些精心設(shè)計(jì)的組件在交互過程中同樣會(huì)出現(xiàn)漏洞。仔細(xì)考慮你在合約里使用的每一項(xiàng)技術(shù),以及如何將它們整合共同創(chuàng)建一個(gè)穩(wěn)定可靠的系統(tǒng)。
升級有問題的合約
如果代碼中發(fā)現(xiàn)了錯(cuò)誤或者需要對某些部分做改進(jìn)都需要更改代碼。在以太坊上發(fā)現(xiàn)一個(gè)錯(cuò)誤卻沒有辦法處理他們是太多意義的。
關(guān)于如何在以太坊上設(shè)計(jì)一個(gè)合約升級系統(tǒng)是一個(gè)正處于積極研究的領(lǐng)域,在這篇文章當(dāng)中我們沒法覆蓋所有復(fù)雜的領(lǐng)域。然而,這里有兩個(gè)通用的基本方法。最簡單的是專門設(shè)計(jì)一個(gè)注冊合約,在注冊合約中保存最新版合約的地址。對于合約使用者來說更能實(shí)現(xiàn)無縫銜接的方法是設(shè)計(jì)一個(gè)合約,使用它轉(zhuǎn)發(fā)調(diào)用請求和數(shù)據(jù)到最新版的合約。
無論采用何種技術(shù),組件之間都要進(jìn)行模塊化和良好的分離,由此代碼的更改才不會(huì)破壞原有的功能,造成孤兒數(shù)據(jù),或者帶來巨大的成本。 尤其是將復(fù)雜的邏輯與數(shù)據(jù)存儲分開,這樣你在使用更改后的功能時(shí)不必重新創(chuàng)建所有數(shù)據(jù)。
當(dāng)需要多方參與決定升級代碼的方式也是至關(guān)重要的。根據(jù)你的合約,升級代碼可能會(huì)需要通過單個(gè)或多個(gè)受信任方參與投票決定。如果這個(gè)過程會(huì)持續(xù)很長時(shí)間,你就必須要考慮是否要換成一種更加高效的方式以防止遭受到攻擊,例如緊急停止或斷路器。
Example 1:使用注冊合約存儲合約的最新版本
在這個(gè)例子中,調(diào)用沒有被轉(zhuǎn)發(fā),因此用戶必須每次在交互之前都先獲取最新的合約地址。
contract SomeRegister {
address backendContract;
address[] previousBackends;
address owner;
function SomeRegister() {
owner = msg.sender;
}
modifier onlyOwner() {
if (msg.sender != owner) {
throw;
}
_;
}
function changeBackend(address newBackend) public
onlyOwner()
returns (bool)
{
if(newBackend != backendContract) {
previousBackends.push(backendContract);
backendContract = newBackend;
return true;
}
return false;
}
}
這種方法有兩個(gè)主要的缺點(diǎn):
1、用戶必須始終查找當(dāng)前合約地址,否則任何未執(zhí)行此操作的人都可能會(huì)使用舊版本的合約
2、在你替換了合約后你需要仔細(xì)考慮如何處理原合約中的數(shù)據(jù)
另外一種方法是設(shè)計(jì)一個(gè)用來轉(zhuǎn)發(fā)調(diào)用請求和數(shù)據(jù)到最新版的合約:
例2: 使用DELEGATECALL 轉(zhuǎn)發(fā)數(shù)據(jù)和調(diào)用
contract Relay {
address public currentVersion;
address public owner;
modifier onlyOwner() {
if (msg.sender != owner) {
throw;
}
_;
}
function Relay(address initAddr) {
currentVersion = initAddr;
owner = msg.sender; // this owner may be another contract with multisig, not a single contract owner
}
function changeContract(address newVersion) public
onlyOwner()
{
currentVersion = newVersion;
}
function() {
if(!currentVersion.delegatecall(msg.data)) throw;
}
}
這種方法避免了先前的問題,但也有自己的問題。它使得你必須在合約里小心的存儲數(shù)據(jù)。如果新的合約和先前的合約有不同的存儲層,你的數(shù)據(jù)可能會(huì)被破壞。另外,這個(gè)例子中的模式?jīng)]法從函數(shù)里返回值,只負(fù)責(zé)轉(zhuǎn)發(fā)它們,由此限制了它的適用性。(這里有一個(gè)更復(fù)雜的實(shí)現(xiàn) 想通過內(nèi)聯(lián)匯編和返回大小的注冊表來解決這個(gè)問題)
無論你的方法如何,重要的是要有一些方法來升級你的合約,否則當(dāng)被發(fā)現(xiàn)不可避免的錯(cuò)誤時(shí)合約將沒法使用。
斷路器(暫停合約功能)
由于斷路器在滿足一定條件時(shí)將會(huì)停止執(zhí)行,如果發(fā)現(xiàn)錯(cuò)誤時(shí)可以使用斷路器。例如,如果發(fā)現(xiàn)錯(cuò)誤,大多數(shù)操作可能會(huì)在合約中被掛起,這是唯一的操作就是撤銷。你可以授權(quán)給任何你受信任的一方,提供給他們觸發(fā)斷路器的能力,或者設(shè)計(jì)一個(gè)在滿足某些條件時(shí)自動(dòng)觸發(fā)某個(gè)斷路器的程序規(guī)則。
例如:
bool private stopped = false;
address private owner;
modifier isAdmin() {
if(msg.sender != owner) {
throw;
}
_;
}
function toggleContractActive() isAdmin public
{
// You can add an additional modifier that restricts stopping a contract to be based on another action, such as a vote of users
stopped = !stopped;
}
modifier stopInEmergency { if (!stopped) _; }
modifier onlyInEmergency { if (stopped) _; }
function deposit() stopInEmergency public
{
// some code
}
function withdraw() onlyInEmergency public
{
// some code
}
速度碰撞(延遲合約動(dòng)作)
速度碰撞使動(dòng)作變慢,所以如果發(fā)生了惡意操作便有時(shí)間恢復(fù)。例如,The DAO 從發(fā)起分割DAO請求到真正執(zhí)行動(dòng)作需要27天。這樣保證了資金在此期間被鎖定在合約里,增加了系統(tǒng)的可恢復(fù)性。在DAO攻擊事件中,雖然在速度碰撞給定的時(shí)間段內(nèi)沒有有效的措施可以采取,但結(jié)合我們其他的技術(shù),它們是非常有效的。
例如:
struct RequestedWithdrawal {
uint amount;
uint time;
}
mapping (address => uint) private balances;
mapping (address => RequestedWithdrawal) private requestedWithdrawals;
uint constant withdrawalWaitPeriod = 28 days; // 4 weeks
function requestWithdrawal() public {
if (balances[msg.sender] > 0) {
uint amountToWithdraw = balances[msg.sender];
balances[msg.sender] = 0; // for simplicity, we withdraw everything;
// presumably, the deposit function prevents new deposits when withdrawals are in progress
requestedWithdrawals[msg.sender] = RequestedWithdrawal({
amount: amountToWithdraw,
time: now
});
}
}
function withdraw() public {
if(requestedWithdrawals[msg.sender].amount > 0 && now > requestedWithdrawals[msg.sender].time + withdrawalWaitPeriod) {
uint amountToWithdraw = requestedWithdrawals[msg.sender].amount;
requestedWithdrawals[msg.sender].amount = 0;
if(!msg.sender.send(amountToWithdraw)) {
throw;
}
}
}
速率限制
速率限制暫?;蛐枰鷾?zhǔn)進(jìn)行實(shí)質(zhì)性更改。 例如,只允許存款人在一段時(shí)間內(nèi)提取總存款的一定數(shù)量或百分比(例如,1天內(nèi)最多100個(gè)ether) - 該時(shí)間段內(nèi)的額外提款可能會(huì)失敗或需要某種特別批準(zhǔn)。 或者將速率限制做在合約級別,合約期限內(nèi)只能發(fā)出發(fā)送一定數(shù)量的代幣。
<a name="contract-rollout"></a>
合約發(fā)布
在將大量資金放入合約之前,合約應(yīng)當(dāng)進(jìn)行大量的長時(shí)間的測試。
至少應(yīng)該:
- 擁有100%測試覆蓋率的完整測試套件(或接近它)
- 在自己的testnet上部署
- 在公共測試網(wǎng)上部署大量測試和錯(cuò)誤獎(jiǎng)勵(lì)
- 徹底的測試應(yīng)該允許各種玩家與合約進(jìn)行大規(guī)?;?dòng)
- 在主網(wǎng)上部署beta版以限制風(fēng)險(xiǎn)總額
自動(dòng)棄用
在合約測試期間,你可以在一段時(shí)間后強(qiáng)制執(zhí)行自動(dòng)棄用以阻止任何操作繼續(xù)進(jìn)行。例如,alpha版本的合約工作幾周,然后自動(dòng)關(guān)閉所有除最終退出操作的操作。
modifier isActive() {
if (block.number > SOME_BLOCK_NUMBER) {
throw;
}
_;
}
function deposit() public
isActive() {
// some code
}
function withdraw() public {
// some code
}
限制每個(gè)用戶/合約的Ether數(shù)量
在早期階段,你可以限制任何用戶(或整個(gè)合約)的Ether數(shù)量 - 以降低風(fēng)險(xiǎn)。
<a name="bounties"> </a>
Bug賞金計(jì)劃
運(yùn)行賞金計(jì)劃的一些提示:
- 決定賞金以哪一種代幣分配(BTC和/或ETH)
- 決定賞金獎(jiǎng)勵(lì)的預(yù)算總額
- 從預(yù)算來看,確定三級獎(jiǎng)勵(lì):
- 你愿意發(fā)放的最小獎(jiǎng)勵(lì)
- 通??砂l(fā)放的最高獎(jiǎng)勵(lì)
- 設(shè)置額外的限額以避免非常嚴(yán)重的漏洞被發(fā)現(xiàn)
- 確定賞金發(fā)放給誰(3是一個(gè)典型)
- 核心開發(fā)人員應(yīng)該是賞金評委之一
- 當(dāng)收到錯(cuò)誤報(bào)告時(shí),核心開發(fā)人員應(yīng)該評估bug的嚴(yán)重性
- 在這個(gè)階段的工作應(yīng)該在私有倉庫進(jìn)行,并且在Github上的issue板塊提出問題
- 如果這個(gè)bug需要被修復(fù),開發(fā)人員應(yīng)該在私有倉庫編寫測試用例來復(fù)現(xiàn)這個(gè)bug
- 開發(fā)人員需要修復(fù)bug并編寫額外測試代碼進(jìn)行測試確保所有測試都通過
- 展示賞金獵人的修復(fù);并將修復(fù)合并回公共倉庫也是一種方式
- 確定賞金獵人是否有任何關(guān)于修復(fù)的其他反饋
- 賞金評委根據(jù)bug的可能性和影響來確定獎(jiǎng)勵(lì)的大小
- 在整個(gè)過程中保持賞金獵人參與討論,并確保賞金發(fā)放不會(huì)延遲
有關(guān)三級獎(jiǎng)勵(lì)的例子,參見 Ethereum's Bounty Program:
獎(jiǎng)勵(lì)的價(jià)值將根據(jù)影響的嚴(yán)重程度而變化。 獎(jiǎng)勵(lì)輕微的“無害”錯(cuò)誤從0.05 BTC開始。 主要錯(cuò)誤,例如導(dǎo)致協(xié)商一致的問題,將獲得最多5個(gè)BTC的獎(jiǎng)勵(lì)。 在非常嚴(yán)重的漏洞的情況下,更高的獎(jiǎng)勵(lì)是可能的(高達(dá)25 BTC)。
安全相關(guān)的文件和程序
當(dāng)發(fā)布涉及大量資金或重要任務(wù)的合約時(shí),必須包含適當(dāng)?shù)奈臋n。有關(guān)安全性的文檔包括:
規(guī)范和發(fā)布計(jì)劃
- 規(guī)格說明文檔,圖表,狀態(tài)機(jī),模型和其他文檔,幫助審核人員和社區(qū)了解系統(tǒng)打算做什么。
- 許多bug從規(guī)格中就能找到,而且它們的修復(fù)成本最低。
- 發(fā)布計(jì)劃所涉及到的參考這里列出的詳細(xì)信息和完成日期。
狀態(tài)
- 當(dāng)前代碼被部署到哪里
- 編譯器版本,使用的標(biāo)志以及用于驗(yàn)證部署的字節(jié)碼的步驟與源代碼匹配
- 將用于不同階段的編譯器版本和標(biāo)志
- 部署代碼的當(dāng)前狀態(tài)(包括未決問題,性能統(tǒng)計(jì)信息等)
已知問題
- 合約的主要風(fēng)險(xiǎn) (例如, 你可能會(huì)丟掉所有的錢,黑客可能會(huì)通過投票支持某些結(jié)果)
- 所有已知的錯(cuò)誤/限制
- 潛在的攻擊和解決辦法
- 潛在的利益沖突(例如,籌集的Ether將納入自己的腰包,像Slock.it與DAO一樣)
歷史記錄
- 測試(包括使用統(tǒng)計(jì),發(fā)現(xiàn)的錯(cuò)誤,測試時(shí)間)
- 已審核代碼的人員(及其關(guān)鍵反饋)
程序
- 發(fā)現(xiàn)錯(cuò)誤的行動(dòng)計(jì)劃(例如緊急情況選項(xiàng),公眾通知程序等)
- 如果出現(xiàn)問題,就可以降級程序(例如,資金擁有者在被攻擊之前的剩余資金占現(xiàn)在剩余資金的比例)
- 負(fù)責(zé)任的披露政策(例如,在哪里報(bào)告發(fā)現(xiàn)的bug,任何bug賞金計(jì)劃的規(guī)則)
- 在失敗的情況下的追索權(quán)(例如,保險(xiǎn),罰款基金,無追索權(quán))
聯(lián)系信息
- 發(fā)現(xiàn)問題后和誰聯(lián)系
- 程序員姓名和/或其他重要參與方的名稱
- 可以詢問問題的論壇/聊天室
安全工具
- Oyente - 根據(jù)這篇文章分析Ethereum代碼以找到常見的漏洞。
- solidity-coverage - Solidity代碼覆蓋率測試
- Solgraph - 生成一個(gè)DOT圖,顯示了Solidity合約的功能控制流程,并highlight了潛在的安全漏洞。
Linters
Linters通過約束代碼風(fēng)格和排版來提高代碼質(zhì)量,使代碼更容易閱讀和查看。
- Solium - 另一種Solidity linting。
- Solint - 幫助你實(shí)施代碼一致性約定來避免你合約中的錯(cuò)誤的Solidity linting
- Solcheck - 用JS寫的Solidity linter,(實(shí)現(xiàn)上)深受eslint的影響。
將來的改進(jìn)
- 編輯器安全警告:編輯器將很快能夠?qū)崿F(xiàn)醒常見的安全錯(cuò)誤,而不僅僅是編譯錯(cuò)誤。 Solidity瀏覽器即將推出這些功能。
- 新的能夠被編譯成EVM字節(jié)碼的函數(shù)式編程語言: 像Solidity這種函數(shù)式編程語言相比面向過程編程語言能夠保證功能的不變性和編譯時(shí)間檢查。通過確定性行為來減少出現(xiàn)錯(cuò)誤的風(fēng)險(xiǎn)。(更多相關(guān)信息請參閱 這里, Curry-Howard 一致性和線性邏輯)
<a name="bibliography"></a>
智能合約安全參考書目
很多包含代碼,示例和見解的文檔已經(jīng)由社區(qū)編寫完成。這里是其中的一些,你可以隨意添加更多新的內(nèi)容。
來自以太坊核心開發(fā)人員
- How to Write Safe Smart Contracts (Christian Reitwiessner)
- Smart Contract Security (Christian Reitwiessner)
- Thinking about Smart Contract Security (Vitalik Buterin)
- Solidity
- Solidity Security Considerations
來自社區(qū)
- http://forum.ethereum.org/discussion/1317/reentrant-contracts
- http://hackingdistributed.com/2016/06/16/scanning-live-ethereum-contracts-for-bugs/
- http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/
- http://hackingdistributed.com/2016/06/22/smart-contract-escape-hatches/
- http://martin.swende.se/blog/Devcon1-and-contract-security.html
- http://publications.lib.chalmers.se/records/fulltext/234939/234939.pdf
- http://vessenes.com/deconstructing-thedao-attack-a-brief-code-tour
- http://vessenes.com/ethereum-griefing-wallets-send-w-throw-considered-harmful
- http://vessenes.com/more-ethereum-attacks-race-to-empty-is-the-real-deal
- https://blog.blockstack.org/simple-contracts-are-better-contracts-what-we-can-learn-from-the-dao-6293214bad3a
- https://blog.slock.it/deja-vu-dao-smart-contracts-audit-results-d26bc088e32e
- https://blog.vdice.io/wp-content/uploads/2016/11/vsliceaudit_v1.3.pdf
- https://eprint.iacr.org/2016/1007.pdf
- https://github.com/Bunjin/Rouleth/blob/master/Security.md
- https://github.com/LeastAuthority/ethereum-analyses
- https://medium.com/@ConsenSys/assert-guards-towards-automated-code-bounties-safe-smart-contract-coding-on-ethereum-8e74364b795c
- https://medium.com/@coriacetic/in-bits-we-trust-4e464b418f0b
- https://medium.com/@hrishiolickel/why-smart-contracts-fail-undiscovered-bugs-and-what-we-can-do-about-them-119aa2843007
- https://medium.com/@peterborah/we-need-fault-tolerant-smart-contracts-ec1b56596dbc
- https://medium.com/zeppelin-blog/zeppelin-framework-proposal-and-development-roadmap-fdfa9a3a32ab
- https://pdaian.com/blog/chasing-the-dao-attackers-wake
- http://www.comp.nus.edu.sg/~loiluu/papers/oyente.pdf
Reviewers
The following people have reviewed this document (date and commit they reviewed in parentheses):
Bill Gleim (07/29/2016 3495fb5)
Bill Gleim (03/15/2017 0244f4e)
License
Licensed under Apache 2.0
Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
版權(quán)所有,轉(zhuǎn)載請注明出處。
鏈萌區(qū)塊鏈致力于打造領(lǐng)先的商業(yè)區(qū)塊鏈業(yè)務(wù)支撐系統(tǒng)。鏈萌區(qū)塊鏈將業(yè)務(wù)邏輯模塊化,比如提供數(shù)字資產(chǎn)融通、供應(yīng)鏈溯源等業(yè)務(wù)場景的合約模板和操作接口。用戶可以控制臺在線快速搭建并部署自己的區(qū)塊鏈業(yè)務(wù)網(wǎng)絡(luò)。
訪問https://nilmo.org了解更多詳情