見出し画像

BDP GenXはERC721Psiを採用することにしました

BDP GenXのスマートコントラクトおよびMintサイト実装を担当しております、Gentaと申します。

これまで、NFTのジェネラティブPJとしては、
WATCHERASAGIChimerative monstersHAGと様々なプロジェクトへ参加させていただきました。
Chimerative monstersのみ、私がすべての開発を担当しており、それ以外のPJでのコントラクトの実装はsocoさんが担当しております。(私はあくまでもMintサイトや着せ替えサイトの実装担当)

そして今回、スーパー3Dクリエイターのkamikazeさんが手掛けるBDP GenXという3DジェネラティブのPJにて、基本的にすべての開発周りを担当させていただいております。

---- ✁ ----

前置きはこのあたりにしておきまして、本題に。(あまりERC721Psiの深い技術面に触れているわけではありませんが、もし誤りや懸念事項等がありましたら、Twitter(@genta_ikikata) までご教示いただけますと幸いです🙇)

BDP GenXはERC721Psiを採用しました

通常ジェネラティブPJの規格として採用されるのはOpenZeppelinのERC721やERC721Enumerableや、みんな大好きAZUKIチーム製のERC721Aを利用して実装されているものをよく目にするかと思います。

かくいう私が携わってきたPJも基本的にすべてERC721Aを利用させていただいてきました。

しかし、今回私がBDP GenXにて採用した規格はERC721Psiという比較的若い規格です。

ERC721Aを採用しなかったワケ

ERC721Aには、複数枚まとめてMintするガス代を安く抑えられるメリットがあります。
1回のトランザクションで大量にNFTをMintしたいジェネラティブPJではとても重宝されています。

しかし、ERC721Aには「Transferコストが高くなるという」デメリットがあり、よく懸念材料として挙がっています。

大抵の場合、ジェネラティブPJはMintが進まなければ二次市場でのフロアも上がりにくく、盛り上がりを生むことが難しくなってしまうので、最初のMintを最も大切にするかと思います。(二次以降をおざなりにするかと言えば、そんなことは無いかと思いますが)

それ故に、ERC721Aの採用はとても合理的ですし、素晴らしい規格だなと思っております。

ではなぜ、今回は採用しなかったのか?

理由はBDP GenXが提供したい「Reveal」体験にあります。

BDP GenXはホルダーさまが所有作品を任意のタイミングで個別にRevealできる仕掛けを取り入れています。

Reveal前でも、サイト上で事前にReveal後の画像が何になるのかを確認できるので、リビール前の画像(動画)の状態を大切に持って「未開封状態」を楽しむも良し。リビールさせてドールをPFPなどにするも良し……
といういわゆる
「フィギュアのブリスターパック」のような開封体験を提供したいと考えております。(私1人の考えでなく、チーム全体としての考えです!)

ERC721Aは所有の確認にガスが大きく発生する

この、「個別Reveal」を実装する際に、ERC721Aではtoken ID後半番号でとてもガス代がかかることに気が付きました。

細かく原因を探っていくと、requireで記述していた「ownerOf」の箇所でガスが膨らんでいる様子……。

ERC721Aの所有確認は、token IDを頭(0番)から、該当のtoken IDまで総なめする方法での実装になっているため、番号が後ろになればなるほど膨らんでいきます。

    function ownerOf(uint256 tokenId) public view virtual override returns (address) {
        return address(uint160(_packedOwnershipOf(tokenId)));
    }

    function _packedOwnershipOf(uint256 tokenId) private view returns (uint256) {
        uint256 curr = tokenId;

        unchecked {
            if (_startTokenId() <= curr)
                if (curr < _currentIndex) {
                    uint256 packed = _packedOwnerships[curr];
                    // If not burned.
                    if (packed & _BITMASK_BURNED == 0) {
                        // Invariant:
                        // There will always be an initialized ownership slot
                        // (i.e. `ownership.addr != address(0) && ownership.burned == false`)
                        // before an unintialized ownership slot
                        // (i.e. `ownership.addr == address(0) && ownership.burned == false`)
                        // Hence, `curr` will not underflow.
                        //
                        // We can directly compare the packed value.
                        // If the address is zero, packed will be zero.
                        while (packed == 0) {
                            packed = _packedOwnerships[--curr];
                        }
                        return packed;
                    }
                }
        }
        revert OwnerQueryForNonexistentToken();
    }

実際にガスコストを比較

EstimateGasを利用して、実際にガスコストを比較してみました。

ownerOf

// ERC721A OwnerOf
// ownerOfGas: 0 BigNumber { value: "26,423" }
// ownerOfGas: 999 BigNumber { value: "2,264,207" }

// ERC721 OwnerOf
// ownerOfGas: 0 BigNumber { value: "24,219" }
// ownerOfGas: 999 BigNumber { value: "24,243" }

// ERC721Psi OwnerOf
// ownerOfGas: 0 BigNumber { value: "29,152" }
// ownerOfGas: 999 BigNumber { value: "36,048" }

ERC721Aはおったまげる程の跳ね上がり具合……
ERC721はmappingを参照するので大きな差はない感じですね。
ERC721Psiはたしかに後半番号になると高くはなりますが、ERC721Aに比べればかなり可愛い差です。

Mint関数

実際にPJ用に書いていたコードでの比較なので_mint()だけを叩くのとは条件が変わってしまいますが、今回は一番reqire記述の少ない、ownerMint関数を使い10枚Mintする…という内容で比較しました。

// ownerMint Gas: ERC721A
// ownerMintGas: 10 BigNumber { value: "113,772" }

// ownerMint Gas: ERC721
// ownerMintGas: 10 BigNumber { value: "1,053,117" }

// ownerMint Gas: ERC721Psi
// ownerMintGas: 10 BigNumber { value: "115,846" }

さすがはERC721A。一番安いですね。
しかし、ERC721PsiもERC721Aにインスパイアされて生まれた規格だけあって、とても良い勝負です。
ERC721は、中々痛い跳ね上がり具合ですね。

Reveal

mapping(uint256 => uint256) public revealed;

function reveal(uint256 _tokenId) public {
    address owner = ownerOf(_tokenId);
    require(owner == msg.sender, "only the owner can reveal");
    if (revealed[_tokenId] != 1) {
        revealed[_tokenId] = 1;
    }
}

revealはmappingに状態を格納しています。
通常であればboolで reveal済み: true / 未reveal: false とするのですが、
uint256で reveal済み: 1 / 未reveal: 0 としたほうがガスコストを抑えられるので、uint256のmappingになっています。

// ERC721A mappingをbool or uint256での検証

// bool
// 0 BigNumber { value: "48,766" }
// 999 BigNumber { value: "2,286,550" }

// uint256
// 0 BigNumber { value: "48,583" }
// 999 BigNumber { value: "2,286,367" }

今見ると僅かな節約ですが、当初はERC721Aのガスの跳ね上がりにビビって、小手先のガス節約に奔走していました。

では、各規格毎の比較です。(すべてuint256でのmappingで検証)

// ERC721A uint256
// 0 BigNumber { value: "48,583" }
// 999 BigNumber { value: "2,286,367" }

// ERC721
// 0 BigNumber { value: "46,357" }
// 999 BigNumber { value: "46,381" }

// ERC721Psi
// 0 BigNumber { value: "51,293" }
// 999 BigNumber { value: "58,189" }

token ID 0番の場合、ERC721AもERC721どちらも大きな差異はなく、むしろERC721Psiは少し高めです。

token ID 999番になると、ERC721Aは跳ね上がります。
一方でERC721とERC721Psiはtoken ID 0番と差は小さいです。(あくまで721Aと比べて…なので、721Psiもそれなりに膨らんではいます。OZ製ERC721の節約具合はさすがです)


それぞれのPJ主要機能を比較した結果、
Mintだけを重視するなら「ERC721A」
Mint以外の部分を重視するなら「OZ製ERC721」

どちらも跳ね上げずに、いいとこ取りをするなら「ERC721Psi」かな……
という結論にいたり採用させていただく事となりました。

ERC721Psiの素晴らしいガス削減の仕組みや、開発にいたった背景などは、以下のページにて掲載されておりますので、興味を持たれた方は是非ご覧いただけたらと思います!
開発された0xEstarriolに敬意と感謝を。


そして、BDP GenXはReveal前、後ともに素晴らしいクリエイティブに仕上がっております!

10/29(土) 20:00 JSTからプレセール開始
10/29(土) 21:00 JSTからはパブリックセール開始(前段階で完売の場合実施はなし)ですので、みなさま是非とも奮ってご参加いただけますと幸いです🙇

いいなと思ったら応援しよう!