【Solidity】requireとrevert
前置き
以前、Foundryでrevertを期待する試験の方法を調べているときに、以下のツイートを見つけました
基本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を採用しているとのことだったので見てみます
確かにERC721A内では全てrevertで実装され、requireは使用されていませんでした。上で使われているMintZeroQuantity()
はエラー定義で、IERC721A.sol内にあります。
エラーを全部定義するのはめんどくさいかもですが、よく考えると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なのかもしれない……
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;
Discussion