錯(cuò)誤處理:斷言、要求、還原和異常

2022-05-12 17:37 更新

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ò)誤。

恐慌通過(guò)assert和錯(cuò)誤通過(guò)require

便利功能assertrequire可用于檢查條件并在不滿足條件時(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ò)誤代碼指示了恐慌的類型。

  1. 0x00:用于通用編譯器插入的恐慌。

  2. 0x01:如果您assert使用評(píng)估為假的參數(shù)調(diào)用。

  3. 0x11:如果算術(shù)運(yùn)算導(dǎo)致塊外下溢或溢出。unchecked { ... }

  4. 0x12; 如果您將或除以零(例如或)。5 / 023 % 0

  5. 0x21:如果將一個(gè)太大或負(fù)數(shù)的值轉(zhuǎn)換為枚舉類型。

  6. 0x22:如果訪問(wèn)的存儲(chǔ)字節(jié)數(shù)組編碼不正確。

  7. 0x31:如果你調(diào)用.pop()一個(gè)空數(shù)組。

  8. 0x32:如果您在越界或負(fù)索引處訪問(wèn)數(shù)組bytesN或數(shù)組切片(即x[i]where或)。i >= x.lengthi < 0

  9. 0x41:如果分配的內(nèi)存過(guò)多或創(chuàng)建的數(shù)組太大。

  10. 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ù)的異常):

  1. 調(diào)用require(x)wherex計(jì)算結(jié)果為false。

  2. 如果您使用revert()revert("description")。

  3. 如果您針對(duì)不包含代碼的合約執(zhí)行外部函數(shù)調(diào)用。

  4. payable如果你的合約通過(guò)沒(méi)有修飾符的公共函數(shù)(包括構(gòu)造函數(shù)和回退函數(shù))接收以太 幣。

  5. 如果你的合約通過(guò)公共 getter 函數(shù)接收以太幣。

對(duì)于以下情況,將轉(zhuǎn)發(fā)來(lái)自外部調(diào)用(如果提供)的錯(cuò)誤數(shù)據(jù)。這意味著它可能會(huì)導(dǎo)致錯(cuò)誤恐慌(或給出的任何其他內(nèi)容):

  1. 如果一個(gè).transfer()失敗。

  2. 如果您通過(guò)消息調(diào)用調(diào)用一個(gè)函數(shù),但它沒(méi)有正確完成(即,它耗盡了氣體,沒(méi)有匹配的函數(shù),或者本身拋出異常),除非使用低級(jí)操作 callsenddelegatecall,callcodestaticcall 。低級(jí)操作從不拋出異常,而是通過(guò)返回來(lái)指示失敗false。

  3. 如果您使用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)用者中的更改將始終被還原。

筆記

用于使用invalidSolidity 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, ...);revertrequire

筆記

require函數(shù)與任何其他函數(shù)一樣被評(píng)估。這意味著在執(zhí)行函數(shù)本身之前評(píng)估所有參數(shù)。特別是在函數(shù)被執(zhí)行時(shí)即使 為真。require(condition, f())fcondition

提供的字符串是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()returnscatch

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ù)。字符串ErrorPanic當(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)用者還剩一些氣。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)