本文翻譯自:https://github.com/ConsenSys/smart-contract-best-practices。
為了使語句表達(dá)更加貼切碳蛋,個別地方未按照原文逐字逐句翻譯,如有出入請以原文為準(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)以下幾個方面的內(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ā)人員編寫安全的智能合約來說只是個開始猫十。
開發(fā)智能合約需要一個全新的工程思維,它不同于我們以往項(xiàng)目的開發(fā)呆盖。因?yàn)樗稿e的代價是巨大的拖云,并且很難像傳統(tǒng)軟件那樣輕易的打上補(bǔ)丁。就像直接給硬件編程或金融服務(wù)類軟件開發(fā)应又,相比于web開發(fā)和移動開發(fā)都有更大的挑戰(zhàn)求冷。因此,僅僅防范已知的漏洞是不夠的蛉顽,你還需要學(xué)習(xí)新的開發(fā)理念:
-
對可能的錯誤有所準(zhǔn)備。任何有意義的智能合約或多或少都存在錯誤汇荐。因此你的代碼必須能夠正確的處理出現(xiàn)的bug和漏洞。始終保證以下規(guī)則:
- 當(dāng)智能合約出現(xiàn)錯誤時盆繁,停止合約掀淘,(“斷路開關(guān)”)
- 管理賬戶的資金風(fēng)險(限制(轉(zhuǎn)賬)速率、最大(轉(zhuǎn)賬)額度)
- 有效的途徑來進(jìn)行bug修復(fù)和功能提升
-
謹(jǐn)慎發(fā)布智能合約油昂。 盡量在正式發(fā)布智能合約之前發(fā)現(xiàn)并修復(fù)可能的bug革娄。
- 對智能合約進(jìn)行徹底的測試,并在任何新的攻擊手法被發(fā)現(xiàn)后及時的測試(包括已經(jīng)發(fā)布的合約)
- 從alpha版本在測試網(wǎng)(testnet)上發(fā)布開始便提供bug賞金計(jì)劃
- 階段性發(fā)布冕碟,每個階段都提供足夠的測試
-
保持智能合約的簡潔拦惋。復(fù)雜會增加出錯的風(fēng)險。
- 確保智能合約邏輯簡潔
- 確保合約和函數(shù)模塊化
- 使用已經(jīng)被廣泛使用的合約或工具(比如鸣哀,不要自己寫一個隨機(jī)數(shù)生成器)
- 條件允許的話架忌,清晰明了比性能更重要
- 只在你系統(tǒng)的去中心化部分使用區(qū)塊鏈
-
保持更新。通過下一章節(jié)所列出的資源來確保獲取到最新的安全進(jìn)展我衬。
- 在任何新的漏洞被發(fā)現(xiàn)時檢查你的智能合約
- 盡可能快的將使用到的庫或者工具更新到最新
- 使用最新的安全技術(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òu)和安全性時有很多需要權(quán)衡的地方破加。對任何智能合約的建議是在各個權(quán)衡點(diǎn)中找到一個平衡點(diǎn)俱恶。
從傳統(tǒng)軟件工程的角度出發(fā):一個理想的智能合約首先需要模塊化,能夠重用代碼而不是重復(fù)編寫范舀,并且支持組件升級合是。從智能合約安全架構(gòu)的角度出發(fā)同樣如此,模塊化和重用被嚴(yán)格審查檢驗(yàn)過的合約是最佳策略锭环,特別是在復(fù)雜智能合約系統(tǒng)里聪全。
然而,這里有幾個重要的例外辅辩,它們從合約安全和傳統(tǒng)軟件工程兩個角度考慮难礼,所得到的重要性排序可能不同。當(dāng)中每一條玫锋,都需要針對智能合約系統(tǒng)的特點(diǎn)找到最優(yōu)的組合方式來達(dá)到平衡蛾茉。
- 固化 vs 可升級
- 龐大 vs 模塊化
- 重復(fù) vs 可重用
固化 vs 可升級
在很多文檔或者開發(fā)指南中,包括該指南撩鹿,都會強(qiáng)調(diào)延展性比如:可終止谦炬,可升級或可更改的特性,不過對于智能合約來說节沦,延展性和安全之間是個基本權(quán)衡吧寺。
延展性會增加程序復(fù)雜性和潛在的攻擊面窜管。對于那些只在特定的時間段內(nèi)提供有限的功能的智能合約,簡單性比復(fù)雜性顯得更加高效稚机,比如無管治功能,有限短期內(nèi)使用的代幣發(fā)行的智能合約系統(tǒng)(governance-fee,finite-time-frame token-sale contracts)获搏。
龐大 vs 模塊化
一個龐大的獨(dú)立的智能合約把所有的變量和模塊都放到一個合約中赖条。盡管只有少數(shù)幾個大家熟知的智能合約系統(tǒng)真的做到了大體量,但在將數(shù)據(jù)和流程都放到一個合約中還是享有部分優(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)代碼重用的最安全的方式幽勒。
在以前所擁有已部署智能合約不可重用時重復(fù)還是很需要的嗜侮。 現(xiàn)在Live Libs 和Zeppelin Solidity 正尋求提供安全的智能合約組件使其能夠被重用而不需要每次都重新編寫。任何合約安全性分析都必須標(biāo)明重用代碼啥容,特別是以前沒有建立與目標(biāo)智能合同系統(tǒng)中處于風(fēng)險中的資金相稱的信任級別的代碼锈颗。
安全通知
以下這些地方通常會通報(bào)在Ethereum或Solidity中新發(fā)現(xiàn)的漏洞。安全通告的官方來源是Ethereum Blog咪惠,但是一般漏洞都會在其他地方先被披露和討論击吱。
-
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)站,尤其是他們提到的可能會影響你的智能合約的漏洞硝逢。
另外, 這里列出了以太坊參與安全模塊相關(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ā)成員,參與到各個區(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)用不受信任的外部合約可能會引發(fā)一系列意外的風(fēng)險和錯誤。外部調(diào)用可能在其合約和它所依賴的其他合約內(nèi)執(zhí)行惡意代碼徽缚。因此憨奸,每一個外部調(diào)用都會有潛在的安全威脅,盡可能的從你的智能合約內(nèi)移除外部調(diào)用凿试。當(dāng)無法完全去除外部調(diào)用時排宰,可以使用這一章節(jié)其他部分提供的建議來盡量減少風(fēng)險似芝。
<a name="send-vs-call-value"></a>
仔細(xì)權(quán)衡“send()”、“transfer()”板甘、以及“call.value()”
當(dāng)轉(zhuǎn)賬Ether時党瓮,需要仔細(xì)權(quán)衡“someAddress.send()”、“someAddress.transfer()”盐类、和“someAddress.call.value()()”之間的差別寞奸。
-
x.transfer(y)
和if (!x.send(y)) throw;
是等價的。send是transfer的底層實(shí)現(xiàn)在跳,建議盡可能直接使用transfer枪萄。 -
someAddress.send()
和someAddress.transfer()
能保證可重入 安全 。
盡管這些外部智能合約的函數(shù)可以被觸發(fā)執(zhí)行猫妙,但補(bǔ)貼給外部智能合約的2,300 gas瓷翻,意味著僅僅只夠記錄一個event到日志中。 -
someAddress.call.value()()
將會發(fā)送指定數(shù)量的Ether并且觸發(fā)對應(yīng)代碼的執(zhí)行割坠。被調(diào)用的外部智能合約代碼將享有所有剩余的gas齐帚,通過這種方式轉(zhuǎn)賬是很容易有可重入漏洞的,非常 不安全韭脊。
使用send()
或transfer()
可以通過制定gas值來預(yù)防可重入童谒, 但是這樣做可能會導(dǎo)致在和合約調(diào)用fallback函數(shù)時出現(xiàn)問題,由于gas可能不足沪羔,而合約的fallback函數(shù)執(zhí)行至少需要2,300 gas消耗饥伊。
一種被稱為push 和pull的 機(jī)制試圖來平衡兩者, 在 push 部分使用send()
或transfer()
蔫饰,在pull 部分使用call.value()()
琅豆。(*譯者注:在需要對外未知地址轉(zhuǎn)賬Ether時使用send()
或transfer()
,已知明確內(nèi)部無惡意代碼的地址轉(zhuǎn)賬Ether使用call.value()()
)
需要注意的是使用send()
或transfer()
進(jìn)行轉(zhuǎn)賬并不能保證該智能合約本身重入安全篓吁,它僅僅只保證了這次轉(zhuǎn)賬操作時重入安全的茫因。
<a name="handle-external-errors"></a>
處理外部調(diào)用錯誤
Solidity提供了一系列在raw address上執(zhí)行操作的底層方法,比如: address.call()
杖剪,address.callcode()
冻押, address.delegatecall()
和address.send
。這些底層方法不會拋出異常(throw)盛嘿,只是會在遇到錯誤時返回false洛巢。另一方面, contract calls (比如次兆,ExternalContract.doSomething()
))會自動傳遞異常稿茉,(比如,doSomething()
拋出異常,那么ExternalContract.doSomething()
同樣會進(jìn)行throw
) )漓库。
如果你選擇使用底層方法恃慧,一定要檢查返回值來對可能的錯誤進(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渺蒿,如果這個ExternalContract
是不受信任的都應(yīng)該假設(shè)存在惡意代碼痢士。即使ExternalContract
不包含惡意代碼,但它所調(diào)用的其他合約代碼可能會包含惡意代碼蘸嘶。一個具體的危險例子便是惡意代碼可能會劫持控制流程導(dǎo)致競態(tài)良瞧。(瀏覽Race Conditions獲取更多關(guān)于這個問題的討論)
<a name="favor-pull-over-push-payments"></a>
對于外部合約優(yōu)先使用pull 而不是push
外部調(diào)用可能會有意或無意的失敗。為了最小化這些外部調(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)賬)。(這種方法同時也避免了造成 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)用外部合約時为肮,你的變量、方法肤京、合約接口命名應(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)斷言條件不滿足時將觸發(fā)斷言保護(hù) -- 比如不變的屬性發(fā)生了變化。舉個例子忘分,代幣在以太坊上的發(fā)行比例棋枕,在代幣的發(fā)行合約里可以通過這種方式得到解決。斷言保護(hù)經(jīng)常需要和其他技術(shù)組合使用妒峦,比如當(dāng)斷言被觸發(fā)時先掛起合約然后升級重斑。(否則將一直觸發(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)證用戶的輸入笛丙,如果條件不滿足便會拋出異常漾脂,應(yīng)當(dāng)使用它驗(yàn)證所有用戶的輸入。 assert(condition)
在條件不滿足也會拋出異常若债,但是最好只用于固定變量:內(nèi)部錯誤或你的智能合約陷入無效的狀態(tài)符相。遵循這些范例,使用分析工具來驗(yàn)證永遠(yuǎn)不會執(zhí)行這些無效操作碼:意味著代碼中不存在任何不變量,并且代碼已經(jīng)正式驗(yàn)證啊终。
<a name="beware-rounding-with-integer-division"></a>
小心整數(shù)除法的四舍五入
所有整數(shù)除數(shù)都會四舍五入到最接近的整數(shù)镜豹。 如果您需要更高精度,請考慮使用乘數(shù)蓝牲,或存儲分子和分母趟脂。
(將來Solidity會有一個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)建一個合約,然后調(diào)用selfdestruct(victimAddress)
佛玄。在victimAddress
中沒有代碼被執(zhí)行硼一,所以這是不能被阻止的。
不要假設(shè)合約創(chuàng)建時余額為零
攻擊者可以在合約創(chuàng)建之前向合約的地址發(fā)送wei梦抢。合約不能假設(shè)它的初始狀態(tài)包含的余額為零般贼。瀏覽issue 61 獲取更多信息。
記住鏈上的數(shù)據(jù)是公開的
許多應(yīng)用需要提交的數(shù)據(jù)是私有的奥吩,直到某個時間點(diǎn)才能工作哼蛆。游戲(比如,鏈上游戲rock-paper-scissors(石頭剪刀布))和拍賣機(jī)(比如霞赫,sealed-bid second-price auctions)是兩個典型的例子腮介。如果你的應(yīng)用存在隱私保護(hù)問題,一定要避免過早發(fā)布用戶信息端衰。
例如:
- 在游戲石頭剪刀布中叠洗,需要參與游戲的雙方提交他們“行動計(jì)劃”的hash值,然后需要雙方隨后提交他們的行動計(jì)劃靴迫;如果雙方的“行動計(jì)劃”和先前提交的hash值對不上則拋出異常惕味。
- 在拍賣中,要求玩家在初始階段提交其所出價格的hash值(以及超過其出價的保證金)玉锌,然后在第二階段提交他們所出價格的資金名挥。
- 當(dāng)開發(fā)一個依賴隨機(jī)數(shù)生成器的應(yīng)用時,正確的順序應(yīng)當(dāng)是(1)玩家提交行動計(jì)劃主守,(2)生成隨機(jī)數(shù)禀倔,(3)玩家支付。產(chǎn)生隨機(jī)數(shù)是一個值得研究的領(lǐng)域参淫;當(dāng)前最優(yōu)的解決方案包括比特幣區(qū)塊頭(通過http://btcrelay.org驗(yàn)證)救湖,hash-commit-reveal方案(比如,一方產(chǎn)生number后涎才,將其散列值提交作為對這個number的“提交”鞋既,然后在隨后再暴露這個number本身)和 RANDAO力九。
- 如果你正在實(shí)現(xiàn)頻繁的批量拍賣,那么hash-commit機(jī)制也是個不錯的選擇邑闺。
權(quán)衡Abstract合約和Interfaces
Interfaces和Abstract合約都是用來使智能合約能更好的被定制和重用跌前。Interfaces是在Solidity 0.4.11中被引入的,和Abstract合約很像但是不能定義方法只能申明陡舅。Interfaces存在一些限制比如不能夠訪問storage或者從其他Interfaces那繼承抵乓,通常這些使Abstract合約更實(shí)用。盡管如此靶衍,Interfaces在實(shí)現(xiàn)智能合約之前的設(shè)計(jì)智能合約階段仍然有很大用處肩钠。另外挑随,需要注意的是如果一個智能合約從另一個Abstract合約繼承而來那么它必須實(shí)現(xiàn)所有Abstract合約內(nèi)的申明并未實(shí)現(xiàn)的函數(shù),否則它也會成為一個Abstract合約吁朦。
在雙方或多方參與的智能合約中箫柳,參與者可能會“脫機(jī)離線”后不再返回
不要讓退款和索賠流程依賴于參與方執(zhí)行的某個特定動作而沒有其他途徑來獲取資金涛目。比如质涛,在石頭剪刀布游戲中阐斜,一個常見的錯誤是在兩個玩家提交他們的行動計(jì)劃之前不要付錢。然而一個惡意玩家可以通過一直不提交它的行動計(jì)劃來使對方蒙受損失 -- 事實(shí)上煤杀,如果玩家看到其他玩家泄露的行動計(jì)劃然后決定他是否會損失(譯者注:發(fā)現(xiàn)自己輸了),那么他完全有理由不再提交他自己的行動計(jì)劃沪哺。這些問題也同樣會出現(xiàn)在通道結(jié)算沈自。當(dāng)這些情形出現(xiàn)導(dǎo)致問題后:(1)提供一種規(guī)避非參與者和參與者的方式,可能通過設(shè)置時間限制辜妓,和(2)考慮為參與者提供額外的經(jīng)濟(jì)激勵枯途,以便在他們應(yīng)該這樣做的所有情況下仍然提交信息。
<a name="keep-fallback-functions-simple"></a>
使Fallback函數(shù)盡量簡單
Fallback函數(shù)在合約執(zhí)行消息發(fā)送沒有攜帶參數(shù)(或當(dāng)沒有匹配的函數(shù)可供調(diào)用)時將會被調(diào)用籍滴,而且當(dāng)調(diào)用 .send()
or .transfer()
時酪夷,只會有2,300 gas 用于失敗后fallback函數(shù)的執(zhí)行(譯者注:合約收到Ether也會觸發(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ù)或訪問變量的錯誤假設(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)該使用和它們測試時使用最多的編譯器相同的版本來部署。鎖定編譯器版本有助于確保合約不會被用于最新的可能還有bug未被發(fā)現(xiàn)的編譯器去部署嘲叔。智能合約也可能會由他人部署亡呵,而pragma標(biāo)明了合約作者希望使用哪個版本的編譯器來部署合約。
// bad
pragma solidity ^0.4.4;
// good
pragma solidity 0.4.4;
<a name="beware-division-by-zero"></a>
(譯者注:這當(dāng)然也會付出兼容性的代價)
小心分母為零 (Solidity < 0.4)
早于0.4版本, 當(dāng)一個數(shù)嘗試除以零時借跪,Solidity 返回zero 并沒有 throw
一個異常政己。確保你使用的Solidity版本至少為 0.4。
<a name="differentiate-functions-events"></a>
區(qū)分函數(shù)和事件
為了防止函數(shù)和事件(Event)產(chǎn)生混淆掏愁,命名一個事件使用大寫并加入前綴(我們建議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)用外部契約的主要危險之一是它們可以接管控制流萝衩,并對調(diào)用函數(shù)意料之外的數(shù)據(jù)進(jìn)行更改回挽。 這類bug有多種形式,導(dǎo)致DAO崩潰的兩個主要錯誤都是這種錯誤猩谊。
<a name="reentrancy"></a>
重入
這個版本的bug被注意到是其可以在第一次調(diào)用這個函數(shù)完成之前被多次重復(fù)調(diào)用千劈。對這個函數(shù)不斷的調(diào)用可能會造成極大的破壞。
// 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()()時暗甥,并沒有將userBalances[msg.sender] 清零喜滨,于是在這之前可以成功遞歸調(diào)用很多次withdrawBalance()函數(shù)。 一個非常相像的bug便是出現(xiàn)在針對 DAO 的攻擊撤防。
在給出來的例子中虽风,最好的方法是 使用 send()
而不是call.value()()
。這將避免多余的代碼被執(zhí)行即碗。
然而焰情,如果你沒法完全移除外部調(diào)用,另一個簡單的方法來阻止這個攻擊是確保你在完成你所有內(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
}
注意如果你有另一個函數(shù)也調(diào)用了 withdrawBalance()
剥懒, 那么這里潛在的存在上面的攻擊内舟,所以你必須認(rèn)識到任何調(diào)用了不受信任的合約代碼的合約也是不受信任的。繼續(xù)瀏覽下面的相關(guān)潛在威脅解決辦法的討論初橘。
跨函數(shù)競態(tài)
攻擊者也可以使用兩個共享狀態(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;
}
著這個例子中充岛,攻擊者在他們外部調(diào)用withdrawBalance
函數(shù)時調(diào)用transfer()
,如果這個時候withdrawBalance
還沒有執(zhí)行到userBalances[msg.sender] = 0;
這里耕蝉,那么他們的余額就沒有被清零崔梗,那么他們就能夠調(diào)用transfer()
轉(zhuǎn)走代幣盡管他們其實(shí)已經(jīng)收到了代幣。這個弱點(diǎn)也可以被用到對DAO的攻擊垒在。
同樣的解決辦法也會管用蒜魄,在執(zhí)行轉(zhuǎn)賬操作之前先清零。也要注意在這個例子中所有函數(shù)都是在同一個合約內(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)用。這個規(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()
卻會導(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()
是不安全的。
另一個經(jīng)常被提及的解決辦法是(譯者注:像傳統(tǒng)多線程編程中一樣)使用mutex媒吗。它會"lock" 當(dāng)前狀態(tài)仑氛,只有鎖的當(dāng)前擁有者能夠更改當(dāng)前狀態(tài)。一個簡單的例子如下:
// 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()
闸英,將會被鎖住锯岖。 這看上去很有效果,但當(dāng)你使用多個合約互相交互時問題變得嚴(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()
。如果他們真這樣做辙喂,那么這個合約將會被永久鎖住捶牢,任何接下來的操作都不會發(fā)生了鸠珠。如果你使用mutexs來避免競態(tài),那么一定要確保沒有地方能夠打斷鎖的進(jìn)程或絕不釋放鎖秋麸。(這里還有一個潛在的威脅渐排,比如死鎖和活鎖。在你決定使用鎖之前最好大量閱讀相關(guān)文獻(xiàn)(譯者注:這是真的灸蟆,傳統(tǒng)的在多線程環(huán)境下對鎖的使用一直是個容易犯錯的地方))
<a name="footnote-race-condition-terminology"></a>
<div style='font-size: 80%; display: inline;'>* 有些人可能會發(fā)反對使用該術(shù)語 <i>競態(tài)</i>驯耻,因?yàn)橐蕴徊]有真正意思上實(shí)現(xiàn)并行執(zhí)行。然而在邏輯上依然存在對資源的競爭炒考,同樣的陷阱和潛在的解決方案可缚。 </div>
<a name="transaction-ordering-dependence"></a>
交易順序依賴(TOD) / 前面的先運(yùn)行
以上是涉及攻擊者在單個交易內(nèi)執(zhí)行惡意代碼產(chǎn)生競態(tài)的示例。接下來演示在區(qū)塊鏈本身運(yùn)作原理導(dǎo)致的競態(tài):(同一個block內(nèi)的)交易順序很容易受到操縱票腰。
由于交易在短暫的時間內(nèi)會先存放到mempool中城看,所以在礦工將其打包進(jìn)block之前,是可以知道會發(fā)生什么動作的杏慰。這對于一個去中心化的市場來說是麻煩的测柠,因?yàn)榭梢圆榭吹酱鷰诺慕灰仔畔ⅲ⑶铱梢栽谒淮虬M(jìn)block之前改變交易順序缘滥。避免這一點(diǎn)很困難轰胁,因?yàn)樗鼩w結(jié)為具體的合同本身。例如朝扼,在市場上赃阀,最好實(shí)施批量拍賣(這也可以防止高頻交易問題)。 另一種使用預(yù)提交方案的方法(“我稍后會提供詳細(xì)信息”)擎颖。
<a name="timestamp-dependence"></a>
時間戳依賴
請注意榛斯,塊的時間戳可以由礦工操縱,并且應(yīng)考慮時間戳的所有直接和間接使用搂捧。 區(qū)塊數(shù)量和平均出塊時間可用于估計(jì)時間驮俗,但這不是區(qū)塊時間在未來可能改變(例如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)于上溢和下溢的例子允跑。
考慮如下這個簡單的轉(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)王凑,便又會變?yōu)?。應(yīng)當(dāng)檢查這里聋丝。溢出是否與之相關(guān)取決于具體的實(shí)施方式索烹。想想uint值是否有機(jī)會變得這么大或和誰會改變它的值。如果任何用戶都有權(quán)利更改uint的值弱睦,那么它將更容易受到攻擊百姓。如果只有管理員能夠改變它的值,那么它可能是安全的况木,因?yàn)闆]有別的辦法可以跨越這個限制瓣戚。
對于下溢同樣的道理端圈。如果一個uint別改變后小于0,那么將會導(dǎo)致它下溢并且被設(shè)置成為最大值(2^256)子库。
對于較小數(shù)字的類型比如uint8舱权、uint16、uint24等也要小心:他們更加容易達(dá)到最大值仑嗅。
<a name="dos-with-unexpected-throw"></a>
通過(Unexpected) Throw發(fā)動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)有更高競價時宴倍,它將試圖退款給曾經(jīng)最高競價人,如果退款失敗則會拋出異常仓技。這意味著鸵贬,惡意投標(biāo)人可以成為當(dāng)前最高競價人,同時確保對其地址的任何退款始終失敗脖捻。這樣就可以阻止任何人調(diào)用“bid()”函數(shù)阔逼,使自己永遠(yuǎn)保持領(lǐng)先。建議向之前所說的那樣建立基于pull的支付系統(tǒng) 地沮。
另一個例子是合約可能通過數(shù)組迭代來向用戶支付(例如嗜浮,眾籌合約中的支持者)時。 通常要確保每次付款都成功摩疑。 如果沒有危融,應(yīng)該拋出異常。 問題是雷袋,如果其中一個支付失敗吉殃,您將恢復(fù)整個支付系統(tǒng),這意味著該循環(huán)將永遠(yuǎn)不會完成楷怒。 因?yàn)橐粋€地址沒有轉(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ā)動DoS
在先前的例子中你可能已經(jīng)注意到另一個問題:一次性向所有人轉(zhuǎn)賬迫卢,很可能會導(dǎo)致達(dá)到以太坊區(qū)塊gas limit的上限。以太坊規(guī)定了每一個區(qū)塊所能花費(fèi)的gas limit冶共,如果超過你的交易便會失敗。
即使沒有故意的攻擊每界,這也可能導(dǎo)致問題捅僵。然而,最為糟糕的是如果gas的花費(fèi)被攻擊者操控眨层。在先前的例子中庙楚,如果攻擊者增加一部分收款名單,并設(shè)置每一個收款地址都接收少量的退款趴樱。這樣一來馒闷,更多的gas將會被花費(fèi)從而導(dǎo)致達(dá)到區(qū)塊gas limit的上限酪捡,整個轉(zhuǎn)賬的操作也會以失敗告終。
又一次證明了 優(yōu)先使用pull 而不是push支付系統(tǒng)纳账。
如果你實(shí)在必須通過遍歷一個變長數(shù)組來進(jìn)行轉(zhuǎn)賬逛薇,最好估計(jì)完成它們大概需要多少個區(qū)塊以及多少筆交易。然后你還必須能夠追蹤得到當(dāng)前進(jìn)行到哪以便當(dāng)操作失敗時從那里開始恢復(fù)疏虫,舉個例子:
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í)行的交易不會發(fā)生任何錯誤。如果必須卧秘,請使用上面這種方式來處理呢袱。
<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)“時刻準(zhǔn)備失敗",提前知道你的代碼是否安全是不可能的恋昼。然而看靠,我們可以允許合約以可預(yù)知的方式失敗,然后最小化失敗帶來的損失液肌。本章將帶你了解如何為可預(yù)知的失敗做準(zhǔn)備挟炬。
注意:當(dāng)你向你的系統(tǒng)添加新的組件時總是伴隨著風(fēng)險的。一個不良設(shè)計(jì)本身會成為漏洞-一些精心設(shè)計(jì)的組件在交互過程中同樣會出現(xiàn)漏洞嗦哆。仔細(xì)考慮你在合約里使用的每一項(xiàng)技術(shù)谤祖,以及如何將它們整合共同創(chuàng)建一個穩(wěn)定可靠的系統(tǒng)。
升級有問題的合約
如果代碼中發(fā)現(xiàn)了錯誤或者需要對某些部分做改進(jìn)都需要更改代碼老速。在以太坊上發(fā)現(xiàn)一個錯誤卻沒有辦法處理他們是太多意義的粥喜。
關(guān)于如何在以太坊上設(shè)計(jì)一個合約升級系統(tǒng)是一個正處于積極研究的領(lǐng)域,在這篇文章當(dāng)中我們沒法覆蓋所有復(fù)雜的領(lǐng)域橘券。然而额湘,這里有兩個通用的基本方法。最簡單的是專門設(shè)計(jì)一個注冊合約旁舰,在注冊合約中保存最新版合約的地址锋华。對于合約使用者來說更能實(shí)現(xiàn)無縫銜接的方法是設(shè)計(jì)一個合約,使用它轉(zhuǎn)發(fā)調(diào)用請求和數(shù)據(jù)到最新版的合約箭窜。
無論采用何種技術(shù)毯焕,組件之間都要進(jìn)行模塊化和良好的分離,由此代碼的更改才不會破壞原有的功能磺樱,造成孤兒數(shù)據(jù)纳猫,或者帶來巨大的成本婆咸。 尤其是將復(fù)雜的邏輯與數(shù)據(jù)存儲分開,這樣你在使用更改后的功能時不必重新創(chuàng)建所有數(shù)據(jù)。
當(dāng)需要多方參與決定升級代碼的方式也是至關(guān)重要的。根據(jù)你的合約难菌,升級代碼可能會需要通過單個或多個受信任方參與投票決定。如果這個過程會持續(xù)很長時間乖仇,你就必須要考慮是否要換成一種更加高效的方式以防止遭受到攻擊,例如緊急停止或斷路器询兴。
Example 1:使用注冊合約存儲合約的最新版本
在這個例子中乃沙,調(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;
}
}
這種方法有兩個主要的缺點(diǎn):
1警儒、用戶必須始終查找當(dāng)前合約地址,否則任何未執(zhí)行此操作的人都可能會使用舊版本的合約
2眶根、在你替換了合約后你需要仔細(xì)考慮如何處理原合約中的數(shù)據(jù)
另外一種方法是設(shè)計(jì)一個用來轉(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ù)可能會被破壞。另外族扰,這個例子中的模式?jīng)]法從函數(shù)里返回值厌丑,只負(fù)責(zé)轉(zhuǎn)發(fā)它們,由此限制了它的適用性渔呵。(這里有一個更復(fù)雜的實(shí)現(xiàn) 想通過內(nèi)聯(lián)匯編和返回大小的注冊表來解決這個問題)
無論你的方法如何怒竿,重要的是要有一些方法來升級你的合約,否則當(dāng)被發(fā)現(xiàn)不可避免的錯誤時合約將沒法使用扩氢。
斷路器(暫停合約功能)
由于斷路器在滿足一定條件時將會停止執(zhí)行耕驰,如果發(fā)現(xiàn)錯誤時可以使用斷路器。例如录豺,如果發(fā)現(xiàn)錯誤朦肘,大多數(shù)操作可能會在合約中被掛起,這是唯一的操作就是撤銷双饥。你可以授權(quán)給任何你受信任的一方媒抠,提供給他們觸發(fā)斷路器的能力,或者設(shè)計(jì)一個在滿足某些條件時自動觸發(fā)某個斷路器的程序規(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
}
速度碰撞(延遲合約動作)
速度碰撞使動作變慢,所以如果發(fā)生了惡意操作便有時間恢復(fù)夫嗓。例如迟螺,The DAO 從發(fā)起分割DAO請求到真正執(zhí)行動作需要27天冲秽。這樣保證了資金在此期間被鎖定在合約里,增加了系統(tǒng)的可恢復(fù)性矩父。在DAO攻擊事件中锉桑,雖然在速度碰撞給定的時間段內(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ì)性更改球订。 例如后裸,只允許存款人在一段時間內(nèi)提取總存款的一定數(shù)量或百分比(例如,1天內(nèi)最多100個ether) - 該時間段內(nèi)的額外提款可能會失敗或需要某種特別批準(zhǔn)冒滩。 或者將速率限制做在合約級別微驶,合約期限內(nèi)只能發(fā)出發(fā)送一定數(shù)量的代幣。
<a name="contract-rollout"></a>
合約發(fā)布
在將大量資金放入合約之前开睡,合約應(yīng)當(dāng)進(jìn)行大量的長時間的測試因苹。
至少應(yīng)該:
- 擁有100%測試覆蓋率的完整測試套件(或接近它)
- 在自己的testnet上部署
- 在公共測試網(wǎng)上部署大量測試和錯誤獎勵
- 徹底的測試應(yīng)該允許各種玩家與合約進(jìn)行大規(guī)模互動
- 在主網(wǎng)上部署beta版以限制風(fēng)險總額
自動棄用
在合約測試期間篇恒,你可以在一段時間后強(qiáng)制執(zhí)行自動棄用以阻止任何操作繼續(xù)進(jìn)行扶檐。例如,alpha版本的合約工作幾周胁艰,然后自動關(guān)閉所有除最終退出操作的操作款筑。
modifier isActive() {
if (block.number > SOME_BLOCK_NUMBER) {
throw;
}
_;
}
function deposit() public
isActive() {
// some code
}
function withdraw() public {
// some code
}
限制每個用戶/合約的Ether數(shù)量
在早期階段,你可以限制任何用戶(或整個合約)的Ether數(shù)量 - 以降低風(fēng)險蝗茁。
<a name="bounties"> </a>
Bug賞金計(jì)劃
運(yùn)行賞金計(jì)劃的一些提示:
- 決定賞金以哪一種代幣分配(BTC和/或ETH)
- 決定賞金獎勵的預(yù)算總額
- 從預(yù)算來看醋虏,確定三級獎勵:
- 你愿意發(fā)放的最小獎勵
- 通常可發(fā)放的最高獎勵
- 設(shè)置額外的限額以避免非常嚴(yán)重的漏洞被發(fā)現(xiàn)
- 確定賞金發(fā)放給誰(3是一個典型)
- 核心開發(fā)人員應(yīng)該是賞金評委之一
- 當(dāng)收到錯誤報(bào)告時哮翘,核心開發(fā)人員應(yīng)該評估bug的嚴(yán)重性
- 在這個階段的工作應(yīng)該在私有倉庫進(jìn)行颈嚼,并且在Github上的issue板塊提出問題
- 如果這個bug需要被修復(fù),開發(fā)人員應(yīng)該在私有倉庫編寫測試用例來復(fù)現(xiàn)這個bug
- 開發(fā)人員需要修復(fù)bug并編寫額外測試代碼進(jìn)行測試確保所有測試都通過
- 展示賞金獵人的修復(fù)饭寺;并將修復(fù)合并回公共倉庫也是一種方式
- 確定賞金獵人是否有任何關(guān)于修復(fù)的其他反饋
- 賞金評委根據(jù)bug的可能性和影響來確定獎勵的大小
- 在整個過程中保持賞金獵人參與討論阻课,并確保賞金發(fā)放不會延遲
有關(guān)三級獎勵的例子,參見 Ethereum's Bounty Program:
獎勵的價值將根據(jù)影響的嚴(yán)重程度而變化艰匙。 獎勵輕微的“無害”錯誤從0.05 BTC開始限煞。 主要錯誤,例如導(dǎo)致協(xié)商一致的問題员凝,將獲得最多5個BTC的獎勵署驻。 在非常嚴(yán)重的漏洞的情況下,更高的獎勵是可能的(高達(dá)25 BTC)。
安全相關(guān)的文件和程序
當(dāng)發(fā)布涉及大量資金或重要任務(wù)的合約時旺上,必須包含適當(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)險 (例如卒煞, 你可能會丟掉所有的錢痪宰,黑客可能會通過投票支持某些結(jié)果)
- 所有已知的錯誤/限制
- 潛在的攻擊和解決辦法
- 潛在的利益沖突(例如,籌集的Ether將納入自己的腰包畔裕,像Slock.it與DAO一樣)
歷史記錄
- 測試(包括使用統(tǒng)計(jì)衣撬,發(fā)現(xiàn)的錯誤,測試時間)
- 已審核代碼的人員(及其關(guān)鍵反饋)
程序
- 發(fā)現(xiàn)錯誤的行動計(jì)劃(例如緊急情況選項(xiàng)扮饶,公眾通知程序等)
- 如果出現(xiàn)問題具练,就可以降級程序(例如,資金擁有者在被攻擊之前的剩余資金占現(xiàn)在剩余資金的比例)
- 負(fù)責(zé)任的披露政策(例如甜无,在哪里報(bào)告發(fā)現(xiàn)的bug扛点,任何bug賞金計(jì)劃的規(guī)則)
- 在失敗的情況下的追索權(quán)(例如,保險岂丘,罰款基金陵究,無追索權(quán))
聯(lián)系信息
- 發(fā)現(xiàn)問題后和誰聯(lián)系
- 程序員姓名和/或其他重要參與方的名稱
- 可以詢問問題的論壇/聊天室
安全工具
- Oyente - 根據(jù)這篇文章分析Ethereum代碼以找到常見的漏洞。
- solidity-coverage - Solidity代碼覆蓋率測試
- Solgraph - 生成一個DOT圖奥帘,顯示了Solidity合約的功能控制流程铜邮,并highlight了潛在的安全漏洞。
Linters
Linters通過約束代碼風(fēng)格和排版來提高代碼質(zhì)量寨蹋,使代碼更容易閱讀和查看松蒜。
- Solium - 另一種Solidity linting。
- Solint - 幫助你實(shí)施代碼一致性約定來避免你合約中的錯誤的Solidity linting
- Solcheck - 用JS寫的Solidity linter已旧,(實(shí)現(xiàn)上)深受eslint的影響秸苗。
將來的改進(jìn)
- 編輯器安全警告:編輯器將很快能夠?qū)崿F(xiàn)醒常見的安全錯誤,而不僅僅是編譯錯誤运褪。 Solidity瀏覽器即將推出這些功能惊楼。
- 新的能夠被編譯成EVM字節(jié)碼的函數(shù)式編程語言: 像Solidity這種函數(shù)式編程語言相比面向過程編程語言能夠保證功能的不變性和編譯時間檢查玖瘸。通過確定性行為來減少出現(xiàn)錯誤的風(fēng)險。(更多相關(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了解更多詳情