前言
我想錯誤處理這個詞骑丸,對于有過編程經(jīng)驗的人來說都不陌生舌仍,它是指程序運行過程中發(fā)生錯誤(Error)或者異常(Exception)的處理方式。在類似Java這樣的語言中通危,我們是通過try...catch...捕捉異常來處理錯誤的铸豁,然而Solidity處理錯誤和我們常見的語言不一樣,下面我們就一起來了解一下在Solidity中的錯誤處理是怎么樣的菊碟。
Solidity是通過回退狀態(tài)的方式节芥,發(fā)生異常時會撤消當(dāng)前調(diào)用(及其所有子調(diào)用)所改變的狀態(tài),同時給調(diào)用者返回一個錯誤標(biāo)識逆害。
我們可以把區(qū)塊鏈理解為是全球共享的分布式事務(wù)性數(shù)據(jù)庫头镊。全球共享意味著參與這個網(wǎng)絡(luò)的每一個人都可以讀寫其中的記錄。如果想修改這個數(shù)據(jù)庫中的內(nèi)容魄幕,就必須創(chuàng)建一個事務(wù)相艇,事務(wù)意味著要做的修改(假如我們想同時修改兩個值)只能被完全的應(yīng)用或者一點都沒有進行。Solidity錯誤處理就是要保證每次調(diào)用都是事務(wù)性的纯陨。
錯誤處理
Solidity提供了兩個函數(shù)assert和require坛芽,用于條件檢查,如果條件不滿足則拋出異常翼抠。assert函數(shù)只能用于檢查內(nèi)部錯誤和不變量咙轩,require函數(shù)用于檢查輸入變量或合同狀態(tài)變量是否滿足條件以及驗證調(diào)用外部合約返回值。如果使用assert合理的話阴颖,有一個Solidity分析工具就可以幫我們分析出智能合約中的錯誤活喊,幫助我們發(fā)現(xiàn)合約中有邏輯錯誤的bug。
正確運行的代碼應(yīng)該永遠不會達到失敗的assert狀態(tài)膘盖,如果發(fā)生了這種情況胧弛,你應(yīng)該修復(fù)你合約中的bug。
還有2種方式可以觸發(fā)異常侠畔,revert函數(shù)可以用來標(biāo)識一個錯誤结缚,并且回退當(dāng)前調(diào)用。將來可能會在revert中包含有關(guān)錯誤的詳細(xì)信息软棺,throw關(guān)鍵字可以用作revert()的替代红竭。
注意:
從0.4.13版本,throw關(guān)鍵字已被棄用喘落,將來會被淘汰茵宪。
當(dāng)子調(diào)用中發(fā)生異常時,異常會自動向上“冒泡”瘦棋。 不過也有一些例外:send稀火,以及底層的函數(shù)調(diào)用call, delegatecall,callcode赌朋,當(dāng)發(fā)生異常時凰狞,這些函數(shù)返回false。
警告:
作為EVM虛擬機設(shè)計的一部分沛慢,在一個不存在的賬戶(地址)上調(diào)用底層的函數(shù)call赡若,delegatecall,callcode 也會返回成功团甲,所以我們在進行調(diào)用時逾冬,應(yīng)該總是優(yōu)先檢查地址是否存在。
注意:捕捉異常是不可能的躺苦,因為沒有try...catch...
在下面的例子中身腻,你可以了解到如何使用require來輕松檢查輸入條件,以及assert用于內(nèi)部錯誤檢查:
pragma solidity ^0.4.0;
contract Sharer {
function sendHalf(address addr) public payable returns (uint balance) {
require(msg.value % 2 == 0); // 僅允許偶數(shù)
uint balanceBeforeTransfer = this.balance;
addr.transfer(msg.value / 2);
// 如果失敗圾另,會拋出異常霸株,下面的代碼就不執(zhí)行
assert(this.balance == balanceBeforeTransfer - msg.value / 2);
return this.balance;
}
}
在Remix中去執(zhí)行上面的這段代碼:
測試1:附加1wei (奇數(shù))去調(diào)用sendHalf函數(shù),這時會發(fā)生異常集乔,如下圖:
運行測試2:附加2wei 去調(diào)用sendHalf函數(shù)去件,運行正常。
assert類型異常
在以下情景中會產(chǎn)生assert類型的異常:
- 如果訪問數(shù)組時發(fā)生了越界或者數(shù)組下標(biāo)為負(fù)數(shù)(如i >= x.length 或i < 0時訪問x[i])
- 如果序號越界扰路,或負(fù)的序號值時訪問一個定長的bytesN尤溜。
- 被除數(shù)為0(如5/0 或 23 % 0)。
- 在移位運算中汗唱,對一個二進制移動一個負(fù)的值(如:5<<i; i為-1時)宫莱。
- 整數(shù)進行可以顯式轉(zhuǎn)換為枚舉時,如果將過大值哩罪,負(fù)值轉(zhuǎn)為枚舉類型則拋出異常
- 如果調(diào)用未初始化的內(nèi)部函數(shù)類型的變量授霸。
- 如果調(diào)用assert巡验,它的參數(shù)的計算結(jié)果為false
require類型異常
在以下場景中會產(chǎn)生require類型的異常:
1、調(diào)用throw
2碘耳、調(diào)用require显设,其參數(shù)的運算結(jié)果為false
3、如果你通過消息調(diào)用一個函數(shù)辛辨,但在調(diào)用的過程中捕捂,并沒有正確結(jié)束(gas不足,沒有匹配到對應(yīng)的函數(shù)斗搞,或被調(diào)用的函數(shù)出現(xiàn)異常)指攒。底層調(diào)用如call,send,delegatecall或callcode除外,這幾個底層調(diào)用函數(shù)不會拋出異常僻焚,但它們會通過返回false來表示失敗允悦。
4、如果在使用new關(guān)鍵字創(chuàng)建一個新合約時溅呢,出現(xiàn)第3條的原因沒有正常完成澡屡。
5、如果調(diào)用外部函數(shù)調(diào)用時咐旧,被調(diào)用的對象不包含代碼驶鹉。
6、如果你的合約是通過沒有payable修飾符的public的函數(shù)來接收Ether(以太幣)時(包括構(gòu)造函數(shù)铣墨,和回退函數(shù))室埋。
7、如果合約通過一個public的getter函數(shù)(public getter funciton)接收以太幣伊约。
8姚淆、如果.transfer()執(zhí)行失敗
- 當(dāng)發(fā)生require類型的異常時,Solidity會執(zhí)行一個回退操作(指令0xfd)屡律。
- 當(dāng)發(fā)生assert類型的異常時腌逢,Solidity會執(zhí)行一個無效操作(指令0xfe)。
在上述的兩種情況下超埋,EVM都會撤回所有的狀態(tài)改變搏讶。是因為期望的結(jié)果沒有發(fā)生,就沒法繼續(xù)安全執(zhí)行霍殴。必須保證交易的原子性(也就是一致性媒惕,要么全部執(zhí)行,要么一點改變都沒有来庭,不能只改變一部分)妒蔚,所以最安全的做法就是撤銷所有操作,讓整個交易沒有任何影響。
注意assert類型的異常會消耗掉用的所有的gas, 而require不會消耗任何gas肴盏,從Metropolis版本( 即目前主網(wǎng)所在的版本)起科盛。