Solidity 使用狀態(tài)恢復(fù)異常來處理錯(cuò)誤。這種異常將撤消對(duì)當(dāng)前調(diào)用(及其所有子調(diào)用)中的狀態(tài)所做的所有更改蜜葱,并且還向調(diào)用者標(biāo)記錯(cuò)誤钠右。
如果異常在子調(diào)用發(fā)生邪意,那么異常會(huì)自動(dòng) 冒泡
到頂層(異常會(huì)重新拋出)综芥。除非它們?cè)?try/catch
語句中被捕獲丽蝎。 但是如果是在 send
和底層函數(shù)(low-level functions
)如:call
, delegatecall
和 staticcall
的調(diào)用里發(fā)生異常時(shí), 他們會(huì)返回 false
(第一個(gè)返回值) 而不是 冒泡異常
膀藐。
注意:根據(jù) EVM 的設(shè)計(jì)屠阻,如果被調(diào)用的地址不存在,底層函數(shù)
call
,delegatecall
和staticcall
也或第一個(gè)返回值同樣是true
额各。 如果需要国觉,請(qǐng)?jiān)谡{(diào)用之前檢查賬號(hào)的存在性。
外部調(diào)用的異诚豪玻可以被 try/catch
捕獲蛉加。
異常包含的錯(cuò)誤數(shù)據(jù)蚜枢,以錯(cuò)誤實(shí)例
的形式傳遞回調(diào)用者缸逃。內(nèi)置的 Error(string)
和 Panic(uint256)
被特殊函數(shù)使用针饥,如下所述。Error
用于“常規(guī)”錯(cuò)誤條件需频,而 Panic
用于不應(yīng)該出現(xiàn)在無錯(cuò)誤代碼中的錯(cuò)誤丁眼。
用 assert
檢查異常(Panic
) 和 require
檢查錯(cuò)誤(Error
)
函數(shù)assert
和 require
可用于檢查條件并在條件不滿足時(shí)拋出異常。
該assert
函數(shù)創(chuàng)建一個(gè)類型的錯(cuò)誤Panic(uint256)
昭殉。在某些情況下苞七,編譯器會(huì)創(chuàng)建相同的錯(cuò)誤,如下所示挪丢。
assert
函數(shù)只能用于測(cè)試內(nèi)部錯(cuò)誤蹂风,檢查不變量。
正常的函數(shù)代碼永遠(yuǎn)不會(huì)產(chǎn)生 Panic
, 甚至是基于一個(gè)無效的外部輸入時(shí)乾蓬。
如果發(fā)生了惠啄,那就說明出現(xiàn)了一個(gè)需要你修復(fù)的 bug
。如果使用得當(dāng)任内,語言分析工具可以識(shí)別出那些會(huì)導(dǎo)致 Panic
的 assert
條件和函數(shù)調(diào)用撵渡。
下列情況將會(huì)產(chǎn)生一個(gè)Panic異常: 提供的錯(cuò)誤碼編號(hào),用來指示Panic的類型死嗦。
- 0x01: 如果你調(diào)用
assert
的參數(shù)(表達(dá)式)結(jié)果為false
趋距。 - 0x11: 在
unchecked { … }
外,如果算術(shù)運(yùn)算結(jié)果向上或向下溢出越除。 - 0x12; 如果你用零當(dāng)除數(shù)做除法或模運(yùn)算(例如 或 )节腐。
5 / 0
23 % 0
- 0x21: 如果你將一個(gè)太大的數(shù)或負(fù)數(shù)值轉(zhuǎn)換為一個(gè)枚舉類型。
- 0x22: 如果你訪問一個(gè)沒有正確編碼的存儲(chǔ)
byte
數(shù)組. - 0x31: 如果在空數(shù)組上
.pop()
摘盆。 - 0x32: 如果你訪問
bytesN
數(shù)組(或切片)的索引太大或?yàn)樨?fù)數(shù)翼雀。
(例如:x[i]
而 或 ).i >= x.length
i < 0
- 0x41: 如果你分配了內(nèi)存過多或創(chuàng)建了的數(shù)組太大。
- 0x51: 如果調(diào)用內(nèi)部函數(shù)類型的零初始化變量骡澈。
require
函數(shù)要么創(chuàng)建一個(gè)Error(string)
類型的錯(cuò)誤锅纺,要么創(chuàng)建創(chuàng)建一個(gè)沒有任何數(shù)據(jù)的錯(cuò)誤,并且require
函數(shù)應(yīng)該用于確認(rèn)條件有效性肋殴,例如輸入變量囤锉,或合約狀態(tài)變量是否滿足條件,或驗(yàn)證外部合約調(diào)用返回的值护锤。
下列情況將會(huì)產(chǎn)生一個(gè) Error(string)
(或沒有數(shù)據(jù))的錯(cuò)誤:
- 如果你調(diào)用
require
的參數(shù)(表達(dá)式)最終結(jié)果為false
官地。 - 如果你使用
revert()
或revert("description")
- 如果你在不包含代碼的合約上執(zhí)行外部函數(shù)調(diào)用。
- 如果你通過合約接收以太幣烙懦,而又沒有
payable
修飾符的公有函數(shù)(包括構(gòu)造函數(shù)和 fallback 函數(shù))驱入。 - 如果你的合約通過公有 getter 函數(shù)接收 Ether 。
對(duì)于以下情況,來自外部調(diào)用(如果提供)的錯(cuò)誤數(shù)據(jù)將被轉(zhuǎn)發(fā)亏较。這意味著它可以引起 Error
或 Panic
(或任何其他給出的):
- 如果
.transfer()
失敗莺褒。 - 如果你通過消息調(diào)用調(diào)用某個(gè)函數(shù),但該函數(shù)沒有正確結(jié)束(例如, 它耗盡了 gas雪情,沒有匹配函數(shù)遵岩,或者本身拋出一個(gè)異常),不包括使用底層操作
call
巡通,send
尘执,delegatecall
,callcode
或staticcall
的函數(shù)調(diào)用宴凉。底層操作不會(huì)拋出異常誊锭,而通過返回false
來指示失敗。 - 如果你使用
new
關(guān)鍵字創(chuàng)建合約弥锄,但合約創(chuàng)建沒有正確完成
丧靡。
如果您不向
require
提供字符串參數(shù),它將返回空錯(cuò)誤數(shù)據(jù)叉讥,甚至不包括錯(cuò)誤選擇器窘行。
可以給 require
提供一個(gè)消息字符串,而 assert
不行图仓。
在下例中罐盔,你可以看到如何輕松使用require
檢查輸入條件以及如何使用 assert
檢查內(nèi)部錯(cuò)誤。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract Sharer {
function sendHalf(address payable addr) public payable returns (uint balance) {
require(msg.value % 2 == 0, "Even value required.");
uint balanceBeforeTransfer = address(this).balance;
addr.transfer(msg.value / 2);
// 因?yàn)檗D(zhuǎn)賬失敗時(shí)拋出一個(gè)異常救崔,并且不能回調(diào)到這里惶看,所以應(yīng)該沒有辦法讓我們?nèi)匀挥幸话氲腻X。
assert(address(this).balance == balanceBeforeTransfer - msg.value / 2);
return address(this).balance;
}
}
在內(nèi)部六孵, Solidity 對(duì)異常執(zhí)行回退操作(指令 0xfd
)纬黎,從而讓 EVM 回退對(duì)狀態(tài)所做的所有更改〗僦希回退的原因是不能繼續(xù)安全地執(zhí)行本今,因?yàn)闆]有實(shí)現(xiàn)預(yù)期的效果。 我們想要保持交易的原子性主巍,最安全的動(dòng)作是回退所有的更改冠息,并讓整個(gè)交易(或至少調(diào)用)沒有任何新影響。
在這兩種情況下孕索,調(diào)用者都可以使用 try/catch
來應(yīng)對(duì)此類失敗逛艰,但是調(diào)用者中的更改將始終被還原。
請(qǐng)注意: 在0.8.0 之前搞旭,
Panic
異常使用invalid
指令散怖,其會(huì)消耗了所有可用的gas
菇绵。 使用require
的異常,在Metropolis
版本之前會(huì)消耗所有的gas
镇眷。
revert語句/函數(shù)
- 可以使用
revert語句
和revert函數(shù)
來觸發(fā)直接還原咬最。 -
revert語句
接受一個(gè)自定義錯(cuò)誤
作為不帶括號(hào)的直接參數(shù):revert CustomError(arg1, arg2);
- 出于向后兼容的原因,還有
revert()函數(shù)
偏灿,它使用圓括號(hào)并接受一個(gè)字符串:revert();
revert(“description”);
- 錯(cuò)誤數(shù)據(jù)將被傳遞回調(diào)用者丹诀,并可以在那里捕獲。使用
revert()
導(dǎo)致不帶任何錯(cuò)誤數(shù)據(jù)的恢復(fù)翁垂,而revert("description")
將創(chuàng)建一個(gè)Error(string)
錯(cuò)誤。 - 使用
自定義錯(cuò)誤實(shí)例
通常比使用字符串描述
便宜得多硝桩,因?yàn)槟憧梢允褂缅e(cuò)誤名稱來描述它沿猜,該名稱僅用4個(gè)字節(jié)編碼。更長的描述可以通過NatSpec
提供碗脊,而不會(huì)產(chǎn)生任何費(fèi)用啼肩。
下邊的例子展示了錯(cuò)誤字符串如何使用revert
(等價(jià)于require
) :
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract VendingMachine {
address owner;
error Unauthorized();
function buy(uint amount) public payable {
if (amount > msg.value / 2 ether)
revert("Not enough Ether provided.");
// 另一種方法
require(
amount <= msg.value / 2 ether,
"Not enough Ether provided."
);
// 執(zhí)行購買操作...
}
function withdraw() public {
if (msg.sender != owner)
revert Unauthorized();
payable(msg.sender).transfer(address(this).balance);
}
}
如果直接提供錯(cuò)誤原因字符串,則這兩個(gè)語法是等效的衙伶,根據(jù)開發(fā)人員的偏好選擇祈坠。
注意:這個(gè)
require
函數(shù)的求值方式與任何其他函數(shù)一樣。這意味著在執(zhí)行函數(shù)本身之前會(huì)計(jì)算所有參數(shù)矢劲。特別是赦拘,在require(condition, f())
該函數(shù)f
將被執(zhí)行,即使在condition
是真的芬沉。
這里提供的字符串將經(jīng)過 ABI 編碼
如果它調(diào)用 Error(string)
函數(shù)躺同。 在上邊的例子里,revert("Not enough Ether provided.");
會(huì)產(chǎn)生如下的十六進(jìn)制錯(cuò)誤返回值:
0x08c379a0 // Error(string) 的函數(shù)選擇器
0x0000000000000000000000000000000000000000000000000000000000000020 // 數(shù)據(jù)的偏移量(32)
0x000000000000000000000000000000000000000000000000000000000000001a // 字符串長度(26)
0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // 字符串?dāng)?shù)據(jù)("Not enough Ether provided." 的 ASCII 編碼丸逸,26字節(jié))
調(diào)用者可以使用try
/catch
檢索所提供的消息蹋艺,如下所示。
revert()
之前有一個(gè)同樣用法的throw
關(guān)鍵字黄刚,它在v0.4.13
版本棄用捎谨,在v0.5.0
移除。
try/catch
外部調(diào)用的失敗憔维,可以通過 try/catch
語句來捕獲涛救,如下:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.1;
interface DataFeed { function getData(address token) external returns (uint value); }
contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// 如果錯(cuò)誤超過 10 次,永久關(guān)閉這個(gè)機(jī)制
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
//如果在getData內(nèi)部調(diào)用 revert埋同,并提供了一個(gè)原因字符串州叠,則執(zhí)行此操作。
errorCount++;
return (0, false);
} catch Panic(uint /*errorCode*/) {
// 這個(gè)是在Panic情況下執(zhí)行,例如一個(gè)嚴(yán)重的錯(cuò)誤凶赁,除以0或溢出咧栗。
//錯(cuò)誤代碼可以用來確定錯(cuò)誤的類型逆甜。
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// 這是在使用revert()時(shí)執(zhí)行的
errorCount++;
return (0, false);
}
}
}
try
關(guān)鍵字后面必須跟一個(gè)表示外部函數(shù)調(diào)用,或合約創(chuàng)建的表達(dá)式(new ContractName())
。
表達(dá)式內(nèi)部的錯(cuò)誤不會(huì)被捕獲(例如致板,如果它是一個(gè)包含內(nèi)部函數(shù)調(diào)用的復(fù)雜表達(dá)式)交煞,只會(huì)在外部調(diào)用本身內(nèi)部發(fā)生還原。
這個(gè) returns
后面的部分(可選)聲明與外部調(diào)用返回的類型匹配的返回變量斟或。
在沒有錯(cuò)誤的情況下素征,這些變量被賦值,并在第一個(gè)成功塊內(nèi)繼續(xù)執(zhí)行合約萝挤。如果到達(dá)成功塊的末尾御毅,則在 catch
塊之后繼續(xù)執(zhí)行。
Solidity支持不同類型的 catch
塊怜珍,具體取決于錯(cuò)誤類型:
-
catch Error(string memory reason){…}
:如果錯(cuò)誤是由revert("reasonString")
或require(false端蛆, "reasonString")
(或引起此類異常的內(nèi)部錯(cuò)誤)引起的,則執(zhí)行該catch
子句酥泛。 -
catch Panic(uint errorCode){…}
:如果錯(cuò)誤是由Panic
引起的今豆,即錯(cuò)誤的assert
、除0柔袁、無效的數(shù)組訪問呆躲、算術(shù)溢出等,則將運(yùn)行該catch
子句捶索。 -
catch (bytes memory lowLevelData){…}
:如果錯(cuò)誤簽名與任何其他子句不匹配插掂,如果在解碼錯(cuò)誤消息時(shí)出現(xiàn)錯(cuò)誤,或者異常中沒有提供錯(cuò)誤數(shù)據(jù)情组,則執(zhí)行該子句燥筷。在這種情況下,聲明的變量提供了對(duì)底層錯(cuò)誤數(shù)據(jù)的訪問院崇。 -
catch { ... }
:如果你對(duì)錯(cuò)誤數(shù)據(jù)不感興趣肆氓,你可以使用catch{…}
(即使是唯一的catch
子句)而不是前面的子句。
計(jì)劃在未來支持其他類型的錯(cuò)誤數(shù)據(jù)底瓣。字符串Error
和Panic
當(dāng)前按原樣解析谢揪,不作為標(biāo)識(shí)符處理。
為了捕獲所有的錯(cuò)誤情況捐凭,你至少需要有catch{…}
或子句catch (bytes memory lowLevelData){…}
拨扶。
returns
和catch
子句中聲明的變量僅在后面的塊中的作用域中。
注意:如果在對(duì)
try/catch
語句中的返回?cái)?shù)據(jù)進(jìn)行解碼期間發(fā)生錯(cuò)誤茁肠,則會(huì)d導(dǎo)致當(dāng)前執(zhí)行的合約出現(xiàn)異常患民,因此,catch
子句中不會(huì)捕獲該異常垦梆。如果在catch Error(string memory reason)
的解碼過程中出現(xiàn)錯(cuò)誤匹颤,并且存在底層catch
子句仅孩,則會(huì)在那里捕獲該錯(cuò)誤。
注意:如果執(zhí)行到達(dá)
catch
代碼塊印蓖,則外部調(diào)用的狀態(tài)更改效果已恢復(fù)辽慕。如果執(zhí)行達(dá)到成功塊,效果不會(huì)恢復(fù)赦肃。如果效果已恢復(fù)溅蛉,則執(zhí)行要么在catch
塊中繼續(xù),要么try/catch
語句本身的執(zhí)行恢復(fù)(例如他宛,由于如上所述的解碼失敗船侧,或者由于沒有提供底層catch
子句)。
注意:失敗調(diào)用的原因可能是多方面的堕汞。不要假設(shè)錯(cuò)誤消息直接來自被調(diào)用的合約:錯(cuò)誤可能發(fā)生在調(diào)用鏈的更深處勺爱,而被調(diào)用的合約只是轉(zhuǎn)發(fā)了它。這可能是由于
gas
不足的情況讯检,而不是故意的錯(cuò)誤情況:調(diào)用者始終在調(diào)用中保留至少 1/64 的gas
,因此即使被調(diào)用的合約耗盡gas
卫旱,調(diào)用者還剩一些gas
人灼。