🤖

【Solidity】requireとrevert

2022/10/15に公開

前置き

以前、Foundryでrevertを期待する試験の方法を調べているときに、以下のツイートを見つけました
https://twitter.com/yamapyblack/status/1553015626993909760
!?
基本requireを使うものだと思っていたので衝撃を受けました。
そしてリプ欄の情報によると、ガス代も安くなるらしい…これを自分で調べてみた備忘録になります。

ガス代比較

早速ガス代を比較していきます。
何の変哲もないNFTのmint処理において、requireとrevertを使った場合想定です。
確かにデプロイ時も関数実行時のガス代もrevertの方が安い…!

ソースコード

require使用

    function mint(address to, uint256 amount) external {
        require(minters[msg.sender], "Only minters can mint");
        _mint(to, amount);
    }

revert使用

    function mint(address to, uint256 amount) external {
        if (!minters[msg.sender]) revert();
        _mint(to, amount);
    }

ガス代

forge test --gas-reportを使用して計測します。

require使用

revert使用

実例:ERC721A

最初のツイートのリプ欄の情報によるとERC721Aはrevertを採用しているとのことだったので見てみます
https://github.com/chiru-labs/ERC721A/blob/main/contracts/ERC721A.sol#L735
確かにERC721A内では全てrevertで実装され、requireは使用されていませんでした。

上で使われているMintZeroQuantity()はエラー定義で、IERC721A.sol内にあります。
https://github.com/chiru-labs/ERC721A/blob/main/contracts/IERC721A.sol#L34
エラーを全部定義するのはめんどくさいかもですが、よく考えるとSolidity以外では普通にやってるな…と思いました。
また、ちゃんと定義しておくとテスト時に役立ちそうです。

Foundryでrevertを期待するテストの実施

Foundryでrevertを期待したい場合は、その処理の前にexpectRevert()を書く必要があります。
このexpectRevert()の引数に定義したエラーを渡してやれば、特定のエラーを期待する試験を実施できます。

    function testMintFromGuests(uint96 amount) public {
        vm.expectRevert(YourContract.CustomError.selector);
        tokenContract.mint(bob, amount);
    }

もちろんrequireを使用している場合でも、以下のようにrequire引数内の文字列を設定してあげることで、テストを書くことはできます。
これでも問題ないと言えば問題ないですが、上のrevertの方が綺麗な感じがします。(超主観)

    function testMintFromGuests(address addr) public {
        vm.expectRevert(bytes("Ownable: caller is not the owner"));
        tokenContract.mint(bob, amount);
    }

まとめ

まだあまり情報がなかったり検証が十分でなかったりしますが、revertを使用する意義はありそうだなと感じました。(特にFoundryを使用する場合)
しばらく使ってみて、何か困ったことがあったら追記できたらなと思います。

おまけ

KillerGFのコントラクト。
WLを持っていない場合に直コンしてmintNFTs()をコールすると、Nice try lolと返されるようになっている。
こういうユーモアのあるメッセージを見られるのはrequireなのかもしれない……
https://etherscan.io/address/0x6be69b2a9b153737887cfcdca7781ed1511c7e36#code#L1101

  function mintNFTs(address to, uint256[] memory tokenIds) public virtual {
    require(msg.sender == saleContract, "Nice try lol");
    uint256 length = tokenIds.length;
    for (uint256 i; i < length; ++i) {
      require(tokenIds[i] != 0 && tokenIds[i] <= MAX_SUPPLY, "ID > MAX_SUPPLY");
    }
    _mintNFTs(to, tokenIds);
  }

2022/10/17追記

Foundationのマーケットコントラクトでもエラー定義+revertの方式が使われてました。
クラス名_エラー内容で定義するの簡単だし良さそう。

error FoundationTreasuryNode_Address_Is_Not_A_Contract();
error FoundationTreasuryNode_Caller_Not_Admin();
error FoundationTreasuryNode_Caller_Not_Operator();

/**
 * @title A mixin that stores a reference to the Foundation treasury contract.
 * @notice The treasury collects fees and defines admin/operator roles.
 */
abstract contract FoundationTreasuryNode is Initializable {
  using AddressUpgradeable for address payable;

  /// @dev This value was replaced with an immutable version.
  address payable private __gap_was_treasury;

https://etherscan.io/address/0x9b5d1e314a8c8af17150fe4e327e8523ee15d25f#code#F5#L1

Discussion