NFTジェネラティブコレクション「MetaKozo」に利用した技術
概要
先月末にMetaKozoという国産NFTコレクションがリリースされました。
この記事では備忘録等も兼ねてMetaKozoのローンチまでに利用した技術を紹介していきます。
NFT発行のコントラクト自体はチュートリアルでもあるような初歩的な部分ではありますが、今回利用したツールや処理内容、注意すべきポイントなどを記載しています。
MetaKozoリリース後まだ1ヶ月程度ですが、すでにMetaKozo Birthday、MetaKozo Friendsと続けて2コレクションをリリースしています。(MetaKozo以外はホルダー向けのフリーミント)
オフィシャルサイト
OpenSeaコレクション
※MetaKozoはエンジニアは1名で期間的には今見返したら初コミットは7/15でリリースが11/27、期間的には長いですが、工数的には1人月程度かなという体感。お作法覚えてたら単純なWebサイト作るのと同じぐらいの工数でできるとは思う。
Mintサイト
Frontend (Next.js x Vercel)
Next.js x TypeScript x Chakra UIで作ったものをVercel上にデプロイしています。
Vercelはもう定番になっていますが無料で使えるにもかかわらず機能が充実していて愛用しています。
簡易的ですが、Analyticsも埋め込むことが可能です。(Freeプランの場合は1サイトのみ可能)
最初に跳ねてるのがMetaKozoのセール、次に跳ねてるのがホルダー限定フリーミント、その次がホルダー限定フリーミント第2弾の事前ALチェックです。
アクセス元の国や端末など基本情報も見れます。
Backend (Next API)
特段バックエンドは立てておらずNext.jsに付属しているNext APIを利用しています。
バックエンドの処理内容は以下のみです。
- 対象WalletのAL数確認
- MerkleRootの算出
- MerkleProofの算出
他プロジェクトがどのようにMerkleRootを登録してるか知らないですが、MetaKozoではアドミン用ページにMetamask繋いでそこからMerkleRootを登録できるようにしています。
またAL保有有無の確認もBackendとContract上の2回チェックを行う仕様にしています。
ジェネラティブ
今回一番苦戦したと言っても過言でないのがジェネラティブです。。
生成処理
0から作るのはめんどくさすぎたのでベースとしては以下のコードを拡張する形で対応しています。
ただ、今回のBizの要望に不足していた部分もあるため以下のような改修を加えています。
- json形式でのmetadataの出力
- 特定の属性を保持している場合に特定の制御を加える
- 画像のアルファ属性の書き出し方法の変更
- 特定のTokenID以降の書き出し
検閲
予想外に苦戦したのがジェネラティブのチェック作業です。
プロジェクトのメンバーがデザイナー含めて今回が初のジェネラティブであったため、フォルダやファイルの命名規則、属性の書き出し有無等ルールが整っていない状態で書き出しを行ったためデータとしての欠損が多く発生しました。
そのため、一括で各属性をリネームしたり、リネーム用のスクリプトを書いたりジェネラティブのコードを書く以上に多くの時間を手動作業に費やしました。。
手動作業も入ったため、ジェネラティブが正しくできているかを確認する必要がありました。
そのため、チェック作業用の簡易的なサービスを作成し目視チェック等もしました。
また、今回は完全ランダムでの書き出しだったためジェネラティブのパーツの組み合わせがイマイチなものも多く(そこがジェネラティブの醍醐味でもありますが...)手動で好きな属性を選んで画像とメタデータを出力するサービスも即席で作成してコレクションを作成しています。
一部のMetaKozoはぽちぽちこのツールで作られています。
このツール自体はまだ活きているのでどこかで日の目を見ることもあるかもしれません。(多分ない)
ストレージ
メタデータはオフチェーンに配置しているため、ストレージサービスを利用しています。
ストレージは今回リリースまでに2回乗り換えてます。
IPFS (Pinata) > Cloud Storage > Arweave
最初は無難にIPFSで良いかーと思っていましたが、自分でサーバー立てて管理するのも面倒だし、Pinataに課金するとなると月$20とかでただのストレージとして考えると超絶高額....
なので検証の最初期はIPFSに載せましたが、割とすぐにCloud Storageへ移行。
そんな時にちょこちょこArweaveの話題を聞くようになったかつ思想やコスト的にもありだと思ったので本番のリリースのタイミングではArweaveに乗り換えました。
Arweaveへのアップロード周りのことを書いた記事
ArDriveを使ったのですが、地味に癖があるので今後はArDriveは使わないかなーという気持ち。
GUI上での操作が必要な場合はArDrive一択なのですが、そうでない場合は特に使う理由もなく。
ただ、ArDrive使っているせいかArDriveのトークンのエアドロがいつの間にかきていましたのは単純にラッキーでした(普通にアップロードで払ってる金額より多くもらえてた)
今Arweave使うならCLI操作に抵抗がない人であれば個人的にはBundlrがおすすめです。
コントラクト
コントラクトはERC721Aを継承して実装しています。
開発に利用したのはHardhatです。
オプション的にガス代の見積に利用可能なhardhat-gas-reporterやContractサイズの確認にhardhat-contract-sizerを利用しています。
最初はThirdWebを利用することを検討しましたが、当時はThirdWebにいくつか制約があったため自前でコントラクトを書くことにしました。(ThirdWebはGnosis Safeとも連携できたり、GUIでAL登録できたりセールフェーズ設定できたりするのに利用料はかからなかったりで要件合うなら使うべきサービス)
MetaKozoではコントラクトで特別なことは特にはしていませんが、特筆するなら以下の3点になるかと思います。
- OpenSea Operator Filterの実装
- MerkleTreeによるAL管理
- 事前AL確認と3段階の時限セール方式
OpenSea Operator Filterの実装
OpenSeaからOperator Filter実装しないと今後クリエイターフィー払いませんというアナウンスが11/10に発表され界隈がざわついたやつです。
その後二転三転して最終までは追いかけていませんが、当時は発表後に即検証を行い実装的に問題ないことを確認したため、本番のコントラクトにもOperator Filterは実装しています。
当時の検証周りの記事はこちら
MerkleTreeによるAL管理
最初期はmappingによる管理で考えていましたが、登録時のガス代がエグいことになるということで当時すでに王道的な手法になっていたMerkleTreeの存在を知ってそちらの実装へ切り替えました。
その時に参考にしたのはこの記事
MetaKozoでは1アドレスに対して複数のAL保持が可能であったため、MerkleTreeにはウォレットアドレスと共にAL数も組み込んだ形で実装を行いました。
function checkMint(uint8 _wlCount, bytes32[] calldata _merkleProof) public view callerIsUser returns (bool isOk) {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, _wlCount));
require(
MerkleProof.verify(_merkleProof, merkleRoot, leaf),
"Invalid Merkle Proof"
);
return true;
}
この実装は結構ハマってしまって実際に動作が正常に動くまでは地味に時間をかけてしまいました。問題のポイントとしてはSolidityとTypescriptの型の違いです。
型問題はぼんやり認識していましたが、TypescriptでMerkleProof作ってそれをSolidityでVerifyする際には注意が必要です。
事前AL確認と3段階の時限セール方式
事前AL確認はセール開始前にミントサイト上でAL保有数を確認できるというものです。
実際の確認してる画像は以下で、2件ALが登録されているのがわかります。
これは開発側の立場からするとミント当日にミントできないというトラブルを避けるのに加えて初のミントサイト実装のため、ミントとほぼ同等のロジックが多数のウォレットで通るかを確認するという意味合いの2つの目的がありました。(当時はアドレス小文字大文字問題もあって登録してるのに弾かれるという事例も聞いていたので)
また、ユーザー側からすると事前にサイトにWallet繋げてAL数確認できるという意味では安心感を与えることはできたかなと思います。またこのタイミングでALあるはずなのに無いという問い合わせを受け付けることもできたので当日に炎上するというのは限りなく少なくできたのかなと思います。
また、実際のセール時間をそれぞれずらしてALセールが2回とPublicセールが1回の3回のセールが計画されていました。
複数フェーズのセールはSolidity上でフェーズのフラグを管理してそれを切り替えることで実施するケースもありますが、MetaKozoではあらかじめセール開始時刻を登録しておくことでセール開始時刻を過ぎると自動的にセールが開始されるような実装を選択しました。
逆にこうしておかないと手動の切り替えが必要になるため、時間ちょうどでの切り替えや単純なヒューマンエラーが発生する可能性があります。(今回の時限式もblock.timestampを利用しているため、厳密には時間ちょうどにはならず数十秒タイミングがずれる可能性はあります。かつ強者の手にかかれば.....ですが、対応が難しいかつ貫通対策はしているのでそこは許容しました。)
require(block.timestamp >= saleStartTime1st,"AL 1st Sale has not started yet");
MetaKozoの貫通対策
貫通(WL or AL等でミント可能な人と数を制限しているにもかかわらず、脆弱性をついてミント可能数を超えてミントを行うこと)は流行りの言葉みたいになっていますが、事例としては結構聞く内容です。
MetaKozoの貫通対策はそこまで特殊なことはせずにオーソドックスなやり方で対応しています。
機械的に処理を上げるとこの辺りはチェックしています。
- コントラクトがPause状態でないか
- ミント数量が0より大きいか
- 1度のTXでミント可能な数量以下で指定されているか
- コレクション数の上限を超えてミントされないか
- 送られてきているEthとミント価格があっているか
- セール開始時刻を過ぎているか
- 数量がAL x 1ALでのミント可能数を超えていないか
- ミント数が対象ウォレットでミント可能な数を超えていないか
- MerkleProofが正しいか(ALに登録されているウォレットか)
MerkleProofの生成自体はサーバーで行いますが、Verifyはコントラクト上で行います。またアドレス毎にALを利用してミントした数もコントラクト上で保持しているため、基本的にはそれだけで貫通は防げます。(100%安全というわけではないと思いますが。)
それ以外にも重要な処理は基本的にはコントラクト側に寄せてサーバー上の処理はあくまでも補助的に利用する想定で実装をしています。
今朝チラッと見かけたこの貫通に関しても1と3に関してはコントラクトではなくクライアント側で制御をしている場合には発生しうる。2に関してはミント数を balanceOfでチェックしている場合に発生しうるがそのあたりはバリデーションをコントラクトに寄せて、ミント数をちゃんとmappingで保持しておけば基本防げる。
リリースを終えて
いろんなトラブルや煽り(エンジニアは経験者を入れないと失敗する説)を聞いたり見たりしていたので実際にセールを終えるまではドキドキでしたが、比較的トラブルもなくセールを終えたかなと思っています。(内部的にはジェネラティブ修正やウォレット収集botのバグ??でかなりセール前にバタつきましたが...)
NFTの発行自体はチュートリアル的に紹介されることも多いため、技術的に難しいものではないですがWeb3独自のお作法(ハマりポイント)があるのでそれさえ一度経験してしまえばあとは何とかなるかなーという所感です。
ただ、初見だと結構ミスるポイントもあるとは思うのでそういう時は周りにいる経験者に聞いたほうが良さそうです。(自分はひたすら調べたりサービス関連の質問は各Discordに入って質問投げてました)
※自分も相談乗れる範囲は乗るので気軽に@crypto_inuuu宛にDMとかください。
あとは技術的なところとは少し話がそれますが、Discord上でのNFT認証やAL用のウォレット収集などもある程度手法が確立してきていますが、それも基礎知識として必要になります。
今後は発行したNFTを活用したり、NFTの基盤的なところにもう少し力を入れていきたい所存。
Discussion