Solidity 使用狀態(tài)恢復(fù)異常來(lái)處理錯(cuò)誤。這樣的異常會(huì)撤消對(duì)當(dāng)前調(diào)用(及其所有子調(diào)用)中狀態(tài)所做的所有更改,并向調(diào)用者標(biāo)記錯(cuò)誤。
當(dāng)子調(diào)用中發(fā)生異常時(shí),它們會(huì)自動(dòng)“冒泡”(即異常被重新拋出),除非它們?cè)?code>try/catch語(yǔ)句中被捕獲。此規(guī)則的例外是send
低級(jí)函數(shù)call
,delegatecall
和 staticcall
: 它們false
在出現(xiàn)異常時(shí)作為第一個(gè)返回值返回,而不是“冒泡”。
警告
作為 EVM 設(shè)計(jì)的一部分,低級(jí)函數(shù)call
,如果調(diào)用的帳戶不存在delegatecall
, 則作為它們的第一個(gè)返回值返回staticcall
。true
如果需要,必須在致電之前檢查帳戶是否存在。
異??梢园藻e(cuò)誤實(shí)例的形式傳回調(diào)用者的錯(cuò)誤數(shù)據(jù)。內(nèi)置錯(cuò)誤Error(string)
并由Panic(uint256)
特殊功能使用,如下所述。Error
用于“常規(guī)”錯(cuò)誤條件,而Panic
用于不應(yīng)該出現(xiàn)在無(wú)錯(cuò)誤代碼中的錯(cuò)誤。
assert
和錯(cuò)誤通過(guò)require
便利功能assert
和require
可用于檢查條件并在不滿足條件時(shí)拋出異常。
該assert
函數(shù)創(chuàng)建一個(gè)類型的錯(cuò)誤Panic(uint256)
。在某些情況下,編譯器會(huì)創(chuàng)建相同的錯(cuò)誤,如下所示。
Assert 只能用于測(cè)試內(nèi)部錯(cuò)誤和檢查不變量。正常運(yùn)行的代碼不應(yīng)該造成恐慌,即使是在無(wú)效的外部輸入上也是如此。如果發(fā)生這種情況,那么您的合同中有一個(gè)錯(cuò)誤,您應(yīng)該修復(fù)它。語(yǔ)言分析工具可以評(píng)估你的合約以識(shí)別會(huì)導(dǎo)致恐慌的條件和函數(shù)調(diào)用。
在以下情況下會(huì)生成 Panic 異常。與錯(cuò)誤數(shù)據(jù)一起提供的錯(cuò)誤代碼指示了恐慌的類型。
0x00:用于通用編譯器插入的恐慌。
0x01:如果您assert
使用評(píng)估為假的參數(shù)調(diào)用。
0x11:如果算術(shù)運(yùn)算導(dǎo)致塊外下溢或溢出。unchecked { ... }
0x12; 如果您將或除以零(例如或)。5 / 0
23 % 0
0x21:如果將一個(gè)太大或負(fù)數(shù)的值轉(zhuǎn)換為枚舉類型。
0x22:如果訪問(wèn)的存儲(chǔ)字節(jié)數(shù)組編碼不正確。
0x31:如果你調(diào)用.pop()
一個(gè)空數(shù)組。
0x32:如果您在越界或負(fù)索引處訪問(wèn)數(shù)組bytesN
或數(shù)組切片(即x[i]
where或)。i >= x.length
i < 0
0x41:如果分配的內(nèi)存過(guò)多或創(chuàng)建的數(shù)組太大。
0x51:如果調(diào)用內(nèi)部函數(shù)類型的零初始化變量。
該require
函數(shù)要么創(chuàng)建一個(gè)沒(méi)有任何數(shù)據(jù)的錯(cuò)誤,要么創(chuàng)建一個(gè)類型為 的錯(cuò)誤Error(string)
。它應(yīng)該用于確保在執(zhí)行之前無(wú)法檢測(cè)到的有效條件。這包括對(duì)外部合約調(diào)用的輸入或返回值的條件。
筆記
目前無(wú)法將自定義錯(cuò)誤與require
. 請(qǐng)改用。if (!condition) revert CustomError();
Error(string)
編譯器在以下情況下會(huì)生成異常(或沒(méi)有數(shù)據(jù)的異常):
調(diào)用require(x)
wherex
計(jì)算結(jié)果為false
。
如果您使用revert()
或revert("description")
。
如果您針對(duì)不包含代碼的合約執(zhí)行外部函數(shù)調(diào)用。
payable
如果你的合約通過(guò)沒(méi)有修飾符的公共函數(shù)(包括構(gòu)造函數(shù)和回退函數(shù))接收以太 幣。
如果你的合約通過(guò)公共 getter 函數(shù)接收以太幣。
對(duì)于以下情況,將轉(zhuǎn)發(fā)來(lái)自外部調(diào)用(如果提供)的錯(cuò)誤數(shù)據(jù)。這意味著它可能會(huì)導(dǎo)致錯(cuò)誤或恐慌(或給出的任何其他內(nèi)容):
如果一個(gè).transfer()
失敗。
如果您通過(guò)消息調(diào)用調(diào)用一個(gè)函數(shù),但它沒(méi)有正確完成(即,它耗盡了氣體,沒(méi)有匹配的函數(shù),或者本身拋出異常),除非使用低級(jí)操作 call
, send
, delegatecall
,callcode
或staticcall
。低級(jí)操作從不拋出異常,而是通過(guò)返回來(lái)指示失敗false
。
如果您使用new
關(guān)鍵字創(chuàng)建合同,但合同創(chuàng)建未正確完成。
您可以選擇為 提供消息字符串require
,但不能為提供消息字符串assert
。
筆記
如果您不向 提供字符串參數(shù)require
,它將返回空錯(cuò)誤數(shù)據(jù),甚至不包括錯(cuò)誤選擇器。
以下示例顯示了如何使用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); // Since transfer throws an exception on failure and // cannot call back here, there should be no way for us to // still have half of the money. assert(address(this).balance == balanceBeforeTransfer - msg.value / 2); return address(this).balance; } }
在內(nèi)部,Solidity 執(zhí)行還原操作(指令 0xfd
)。這會(huì)導(dǎo)致 EVM 恢復(fù)對(duì)狀態(tài)所做的所有更改?;謴?fù)的原因是沒(méi)有安全的方法繼續(xù)執(zhí)行,因?yàn)闆](méi)有發(fā)生預(yù)期的效果。因?yàn)槲覀円3质聞?wù)的原子性,所以最安全的做法是還原所有更改并使整個(gè)事務(wù)(或至少調(diào)用)無(wú)效。
try
在這兩種情況下,調(diào)用者都可以使用/對(duì)此類失敗做出反應(yīng)catch
,但被調(diào)用者中的更改將始終被還原。
筆記
用于使用invalid
Solidity 0.8.0 之前的操作碼的緊急異常,消耗了調(diào)用可用的所有氣體。require
在 Metropolis 發(fā)布之前,用于消耗所有 gas 的異常。
revert
?可以使用revert
語(yǔ)句和revert
函數(shù)觸發(fā)直接還原。
該revert
語(yǔ)句將自定義錯(cuò)誤作為不帶括號(hào)的直接參數(shù):
恢復(fù)自定義錯(cuò)誤(arg1,arg2);
出于向后兼容的原因,還有一個(gè)revert()
函數(shù),它使用括號(hào)并接受一個(gè)字符串:
恢復(fù)(); 還原(“描述”);
錯(cuò)誤數(shù)據(jù)將被傳遞回調(diào)用者,并且可以在那里被捕獲。使用revert()
會(huì)導(dǎo)致沒(méi)有任何錯(cuò)誤數(shù)據(jù)的還原,而revert("description")
會(huì)產(chǎn)生Error(string)
錯(cuò)誤。
使用自定義錯(cuò)誤實(shí)例通常會(huì)比字符串描述便宜得多,因?yàn)槟梢允褂缅e(cuò)誤名稱來(lái)描述它,它僅編碼為四個(gè)字節(jié)。可以通過(guò) NatSpec 提供更長(zhǎng)的描述,這不會(huì)產(chǎn)生任何費(fèi)用。
以下示例顯示了如何將錯(cuò)誤字符串和自定義錯(cuò)誤實(shí)例與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."); // Alternative way to do it: require( amount <= msg.value / 2 ether, "Not enough Ether provided." ); // Perform the purchase. } function withdraw() public { if (msg.sender != owner) revert Unauthorized(); payable(msg.sender).transfer(address(this).balance); } }
這兩種方式and是等價(jià)的,只要參數(shù)and沒(méi)有副作用,例如,如果它們只是字符串。if (!condition) revert(...);
require(condition, ...);
revert
require
筆記
該require
函數(shù)與任何其他函數(shù)一樣被評(píng)估。這意味著在執(zhí)行函數(shù)本身之前評(píng)估所有參數(shù)。特別是在函數(shù)被執(zhí)行時(shí)即使 為真。require(condition, f())
f
condition
提供的字符串是abi 編碼的,就好像它是對(duì)函數(shù)的調(diào)用一樣Error(string)
。在上面的示例中,返回以下十六進(jìn)制作為錯(cuò)誤返回?cái)?shù)據(jù):revert("Not enough Ether provided.");
0x08c379a0 // Function selector for Error(string) 0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset 0x000000000000000000000000000000000000000000000000000000000000001a // String length 0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data
try
調(diào)用者可以使用/檢索提供的消息catch
,如下所示。
筆記
曾經(jīng)有一個(gè)關(guān)鍵字與0.4.13 版中已棄用并在 0.5.0 版中刪除的throw
語(yǔ)義相同。revert()
try
/catch
可以使用 try/catch 語(yǔ)句捕獲外部調(diào)用中的失敗,如下所示:
// 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) { // Permanently disable the mechanism if there are // more than 10 errors. require(errorCount < 10); try feed.getData(token) returns (uint v) { return (v, true); } catch Error(string memory /*reason*/) { // This is executed in case // revert was called inside getData // and a reason string was provided. errorCount++; return (0, false); } catch Panic(uint /*errorCode*/) { // This is executed in case of a panic, // i.e. a serious error like division by zero // or overflow. The error code can be used // to determine the kind of error. errorCount++; return (0, false); } catch (bytes memory /*lowLevelData*/) { // This is executed in case revert() was used. errorCount++; return (0, false); } } }
try
關(guān)鍵字后面必須跟一個(gè)表示外部函數(shù)調(diào)用或合約創(chuàng)建的表達(dá)式 ( ) 。表達(dá)式內(nèi)部的錯(cuò)誤不會(huì)被捕獲(例如,如果它是一個(gè)還涉及內(nèi)部函數(shù)調(diào)用的復(fù)雜表達(dá)式),只會(huì)在外部調(diào)用本身內(nèi)部發(fā)生還原。后面的部分(可選)聲明了與外部調(diào)用返回的類型匹配的返回變量。在沒(méi)有錯(cuò)誤的情況下,這些變量被分配并且合約的執(zhí)行在第一個(gè)成功塊內(nèi)繼續(xù)。如果到達(dá)成功塊的末尾,則在塊之后繼續(xù)執(zhí)行。new ContractName()
returns
catch
Solidity 根據(jù)錯(cuò)誤類型支持不同類型的 catch 塊:
catch Error(string memory reason) { ... }
:如果錯(cuò)誤是由revert("reasonString")
or (或?qū)е麓祟惍惓5膬?nèi)部錯(cuò)誤)引起的,則執(zhí)行此 catch 子句。require(false, "reasonString")
catch Panic(uint errorCode) { ... }
:如果錯(cuò)誤是由恐慌引起的,即失敗assert
、被零除、無(wú)效數(shù)組訪問(wèn)、算術(shù)溢出等,則將運(yùn)行此 catch 子句。
catch (bytes memory lowLevelData) { ... }
:如果錯(cuò)誤簽名與任何其他子句不匹配,如果在解碼錯(cuò)誤消息時(shí)出現(xiàn)錯(cuò)誤,或者如果沒(méi)有提供錯(cuò)誤數(shù)據(jù)和異常,則執(zhí)行此子句。在這種情況下,聲明的變量提供對(duì)低級(jí)錯(cuò)誤數(shù)據(jù)的訪問(wèn)。
catch { ... }
: 如果你對(duì)錯(cuò)誤數(shù)據(jù)不感興趣,你可以只使用 (甚至作為唯一的 catch 子句)而不是前面的子句。catch { ... }
計(jì)劃在未來(lái)支持其他類型的錯(cuò)誤數(shù)據(jù)。字符串Error
和Panic
當(dāng)前按原樣解析,不被視為標(biāo)識(shí)符。
為了捕獲所有錯(cuò)誤情況,您至少必須有子句 或子句。catch { ...}
catch (bytes memory lowLevelData) { ... }
returns
和子句中聲明的變量catch
僅在后面的塊中。
筆記
如果在 try/catch 語(yǔ)句中的返回?cái)?shù)據(jù)解碼過(guò)程中發(fā)生錯(cuò)誤,這會(huì)導(dǎo)致當(dāng)前執(zhí)行的合約出現(xiàn)異常,因此不會(huì)在 catch 子句中捕獲。如果在解碼過(guò)程中出現(xiàn)錯(cuò)誤 并且有一個(gè)低級(jí)的catch 子句,那么這個(gè)錯(cuò)誤就會(huì)被捕獲。catch Error(string memory reason)
筆記
如果執(zhí)行到達(dá)一個(gè)catch-block,則外部調(diào)用的狀態(tài)改變效果已經(jīng)恢復(fù)。如果執(zhí)行到達(dá)成功塊,則效果不會(huì)恢復(fù)。如果效果已恢復(fù),則在 catch 塊中繼續(xù)執(zhí)行或 try/catch 語(yǔ)句本身的執(zhí)行恢復(fù)(例如,由于上述解碼失敗或由于未提供低級(jí) catch 子句)。
筆記
呼叫失敗背后的原因可能是多方面的。不要假設(shè)錯(cuò)誤消息直接來(lái)自被調(diào)用的合約:錯(cuò)誤可能發(fā)生在調(diào)用鏈的更深處,而被調(diào)用的合約只是轉(zhuǎn)發(fā)了它。此外,這可能是由于氣體不足的情況,而不是故意的錯(cuò)誤情況:調(diào)用者始終在調(diào)用中保留至少 1/64 的氣體,因此即使被調(diào)用的合約耗盡氣體,調(diào)用者還剩一些氣。
更多建議: