Solidity錯(cuò)誤處理及異常:Assert, Require, Revert和Exceptions

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, delegatecallstaticcall 的調(diào)用里發(fā)生異常時(shí), 他們會(huì)返回 false (第一個(gè)返回值) 而不是 冒泡異常膀藐。

注意:根據(jù) EVM 的設(shè)計(jì)屠阻,如果被調(diào)用的地址不存在,底層函數(shù) call, delegatecallstaticcall 也或第一個(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ù)assertrequire 可用于檢查條件并在條件不滿足時(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)致 Panicassert 條件和函數(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ò)誤:

  1. 如果你調(diào)用 require 的參數(shù)(表達(dá)式)最終結(jié)果為 false 官地。
  2. 如果你使用 revert()revert("description")
  3. 如果你在不包含代碼的合約上執(zhí)行外部函數(shù)調(diào)用。
  4. 如果你通過合約接收以太幣烙懦,而又沒有 payable 修飾符的公有函數(shù)(包括構(gòu)造函數(shù)和 fallback 函數(shù))驱入。
  5. 如果你的合約通過公有 getter 函數(shù)接收 Ether 。

對(duì)于以下情況,來自外部調(diào)用(如果提供)的錯(cuò)誤數(shù)據(jù)將被轉(zhuǎn)發(fā)亏较。這意味著它可以引起 ErrorPanic (或任何其他給出的):

  1. 如果 .transfer() 失敗莺褒。
  2. 如果你通過消息調(diào)用調(diào)用某個(gè)函數(shù),但該函數(shù)沒有正確結(jié)束(例如, 它耗盡了 gas雪情,沒有匹配函數(shù)遵岩,或者本身拋出一個(gè)異常),不包括使用底層操作 call 巡通, send 尘执, delegatecallcallcodestaticcall 的函數(shù)調(diào)用宴凉。底層操作不會(huì)拋出異常誊锭,而通過返回 false 來指示失敗。
  3. 如果你使用 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ù)底瓣。字符串ErrorPanic當(dāng)前按原樣解析谢揪,不作為標(biāo)識(shí)符處理。

為了捕獲所有的錯(cuò)誤情況捐凭,你至少需要有catch{…}或子句catch (bytes memory lowLevelData){…}拨扶。

returnscatch子句中聲明的變量僅在后面的塊中的作用域中。

注意:如果在對(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人灼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市顾翼,隨后出現(xiàn)的幾起案子投放,更是在濱河造成了極大的恐慌,老刑警劉巖适贸,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灸芳,死亡現(xiàn)場離奇詭異,居然都是意外死亡拜姿,警方通過查閱死者的電腦和手機(jī)烙样,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蕊肥,“玉大人谒获,你說我怎么就攤上這事”谌矗” “怎么了批狱?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長展东。 經(jīng)常有香客問我赔硫,道長,這世上最難降的妖魔是什么盐肃? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任爪膊,我火速辦了婚禮权悟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘惊完。我一直安慰自己僵芹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布小槐。 她就那樣靜靜地躺著拇派,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凿跳。 梳的紋絲不亂的頭發(fā)上件豌,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音控嗜,去河邊找鬼茧彤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛疆栏,可吹牛的內(nèi)容都是我干的曾掂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼壁顶,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼珠洗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起若专,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤许蓖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后调衰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體膊爪,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年嚎莉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了米酬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡萝喘,死狀恐怖淮逻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情阁簸,我是刑警寧澤爬早,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站启妹,受9級(jí)特大地震影響筛严,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜饶米,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一桨啃、第九天 我趴在偏房一處隱蔽的房頂上張望车胡。 院中可真熱鬧,春花似錦照瘾、人聲如沸匈棘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽主卫。三九已至,卻和暖如春鹃愤,著一層夾襖步出監(jiān)牢的瞬間簇搅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來泰國打工软吐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瘩将,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓凹耙,卻偏偏與公主長得像姿现,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肖抱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容