🛢

NFTチェーン、EthereumかPolygonどっちにする? 真面目にガス手数料を見積った話

2022/09/06に公開

はじめに

先日のプレスリリース "国内初!千葉工業大学で学修歴証明書をNFTで発行" で発表させていただいたPRJのスマートコントラクト周辺を担当した @koshilife です。

https://prtimes.jp/main/html/rd/p/000000017.000037448.html

このシステムでは、Ethereum と Polygon の2つのチェーンを利用しています。
Ethereum は デジタルな資格情報のアンカリング先として利用し、Polygon はNFTとして利用しています。

開発当初、NFT側のチェーンもデファクトスタンダードである Ethereum で統一できないか検討していました。その際にガス手数料の見積りを行ったりガス手数料削減のために試したことなどを本記事でご紹介できればと思います。

EIP-1559 トランザクションのガス手数料について

ガス代の見積りにあたり、現在EthereumとPolygonで採用されているトランザクションのガス手数料を決めるメカニズムとなる EIP-1559 の理解に務めました。以下のGMOさん, Polygon Japanさん, Alchemyさんの記事がとても参考になりました。

https://recruit.gmo.jp/engineer/jisedai/blog/eip-1559-research/

https://note.com/0xpolygon_japan/n/n40f36f2c4268

https://docs.alchemy.com/docs/how-to-send-transactions-with-eip-1559#sending-transactions-with-eip-1559-a-hrefhow-sending-transactions-work-with-eip-1559-idhow-sending-transactions-work-with-eip-1559a

物凄く簡単にサマると、以下のようなイメージです。

式1)
Total cost = gas x (baseFeePerGas + maxPriorityFeePerGas)
式2)
maxFeePerGas = baseFeePerGas + maxPriorityFeePerGas
  • gas: チェーン上の取引を検証するために必要な計算資源の単位
  • baseFeePerGas: 基本ガス単価。前ブロックに占めるガス量に応じて決まる。
  • maxPriorityFeePerGas: 最大優先ガス単価 (マイナーへのチップ) 高い価格ほどブロックに書き込まれやすくなる。
  • maxFeePerGas: 許容できるガス単価の最大手数料。(式2の関係) baseFeePerGas が高額になるケース(前ブロックまでの状態が満杯近いブロックがかなり連続する等)を踏んで意図せずガス手数料が高額になるケースを避けるために、このパラメーターを使うことで回避できます。詳細

ガス単価を見積る

チェーンの利用状況によってブロックに書き込まれやすい最適なガス単価は変動します。
Webページでの確認方法とAPIでの確認方法をご紹介します。

Gas Tracker (Etherscan/Polygonscan)

Etherscan/Polygonscanが提供するGasTrackerページで現在のガス単価の状況が把握できます。

Etherscan Polygonscan

また、chart画面 Etherscan, Polygonscanから過去のガス単価の推移を確認することができ、CSVデータでのダウンロードもできるので、現在のガス単価が高いのか安いのかの水準を把握したり月間平均ガス単価など参考になるデータを得られます。本番実行前にこのGas Trackerページを確認するようにして極端にガス単価が高い状態での実行(予期しない高額出費)を避けられるよう工夫しました。

API: Etherscan Gas Oracle / Polygon Gas Station

トランザクション送信時にブロック化されやすい最適な maxPriorityFeePerGasmaxFeePerGas を指定値の
参考情報としてに各チェーン毎に利用したAPIをご紹介します。

  • Etherscan Gas Oracle
    • Ethereum のガス単価の参考値を知る上でこのAPIを使っています。 maxPriorityFeePerGas にレスポンスの result.ProposeGasPricemaxFeerPerGasFastGasPrice の値を利用しました。
    • sample response:
{
   "status":"1",
   "message":"OK",
   "result":{
      "LastBlock":"13053741",
      "SafeGasPrice":"20",
      "ProposeGasPrice":"22",
      "FastGasPrice":"24",
      "suggestBaseFee":"19.230609716",
      "gasUsedRatio":"0.370119078777807,0.8954731,0.550911766666667,0.212457033333333,0.552463633333333"
   }
}
  • Polygon Gas Station
    • Polygon のガス単価の参考値を知る上でこの公式APIを使っています。 maxPriorityFeePerGas にレスポンスの fast.maxPriorityFeemaxFeerPerGasfast.maxFee の値を利用しました。
    • sample response:
{
  "safeLow": {
    "maxPriorityFee":30.7611840636,
    "maxFee":30.7611840796
    },
  "standard": {
    "maxPriorityFee":32.146027800733336,
    "maxFee":32.14602781673334
    },
  "fast": {
    "maxPriorityFee":33.284344224133335,
    "maxFee":33.284344240133336
    },
  "estimatedBaseFee":1.6e-8,
  "blockTime":6,
  "blockNumber":24962816
}

コントラクトは ERC721 ではなく ERC721A を採用

1つのトランザクションで複数のNFTを一括でmintする際のガス使用量を効率化したAzukiが開発した ERC721A を採用しました。
(利用バージョンは 4.2.0)

ERC721A 公式
https://www.azuki.com/erc721a

ERC721Aの紹介の他、ERC721Enumerable VS ERC721A のガス使用量比較のテストコードも紹介されており、こちらの記事も参考になりました。
https://alchemy.com/blog/erc721-vs-erc721a-batch-minting-nfts

ERC721Aの特徴やアルゴリズムを把握するのに助けになりました。
https://mirror.xyz/rmanzoku.eth/vw5Qg6caMYIw5HljtVwAfDU5kXJmZ76ZosO4cfiyMHE

実際のコントラクトコード

mint処理の抜粋ですが、このようなコードを書きました。
全コードはこちら

    function mintAndTransfer(
        string memory _credentialId,   // 証明書ID (同一アドレスで同一証明書を複数所有を不可にする仕様の実現のために利用)
        string memory _description,    // OpenSeaメタデータ(description)に設定する値
        address[] memory _toAddresses, // 各トークンの送信先アドレス
        string[] memory _imageURIs,    // 各トークンの画像URLリスト OpenSeaメタデータ(image)に設定する値
        string[] memory _externalURIs  // 各トークンの外部URLリスト OpenSeaメタデータ(external_url)に設定する値
    ) public onlyOwner {

        // (省略: 引数のバリデーション処理)

        // put the next token ID down in the variable before the bulk mint
        uint256 startTokenId = _nextTokenId();
        _safeMint(owner(), requestNum);

        // do bulk transfer to each specified address only for the minted tokens
        uint256 tokenId = startTokenId;
        for (uint i = 0; i < requestNum; i++) {
            // update the credential ID mapping
            _ownedCredential[tokenId] = _credentialId;
            // transfer to the specified address
            safeTransferFrom(owner(), _toAddresses[i], tokenId);
            // update the token URI
            _setTokenURI(
                tokenId,
                generateTokenURI(_description, _imageURIs[i], _externalURIs[i])
            );
            tokenId += 1;
        }
    }

今回のNFTは、学習歴証明書の発行主である千葉工業大学様から各受講者に発行したという以下のようなログをOpenSea上で表示する振る舞いにしたいという要件がありました。

そのため、まずはオーナーのアドレスでmint、その後、各受講者のアドレスにtransferする内容を実装をしています。
また、OpenSeaのメタデータは各トークン用の値をBase64エンコードしたものをtokenURIメソッドで返却する ERC721URIStorage の内容を参考に実装しています。

ERC721A のメソッド _safeMint(address to, uint256 quantity) は指定した単一アドレスに対して指定数のNFTをmintする処理のため、
オーナーのアドレス owner() を指定して一括mintした後に、forブロック内の処理で各トークン毎にtransferとtokenURIの設定を行う形で実装しています。

テストコードで本番相当量のデータでガス総量を見積る

Ethereum か Polygon のどちらにするか決めるにあたり、それぞれのガス手数料を見積ることに。
前述のガス手数料を求める数式

Total cost = gas x (baseFeePerGas + maxPriorityFeePerGas)

の右辺の (baseFeePerGas + maxPriorityFeePerGas) は 前述のGasTrackerの過去データから、ある程度の幅で推測可能なので
残りは gas に対応する必要なガス量を見積ればガス手数料を見積ることができます。

事前に本番作業で発行予定のトークン数は400程度とわかったので、400のNFTをmintするのに必要なガス総量を、
hardhat のローカルテストネット環境 Hardhat network でガス総量を見積るテストコードを書きました。
また、この検証において最適な一括mintのバッチサイズ(1トランザクションあたりに発行するNFTの数)も決めたかったので、数値を散らして実験しました。

テストコードはこちら
以下のような感じで実行。

# ローカル環境でテストネットを起動
$ npx hardhat node

# テスト実行
$ npx hardhat test --network localhost

ガス総量の見積り結果

バッチサイズ 1 5 10 20 30
トランザクション数 400 80 40 20 14
ガス総量(Mega) 372.6M 358.2M 356.4M 360.1M 361.2M
1トランザクションあたりの平均ガス使用量(Mega) 0.93M 4.48M 8.91M 18.00M 25.80M
Ethereum ガス手数料見積り ※1 $16,716.39 $16,074.86 $15,991.74 $16,156.14 $16,208.90
Polygon ガス手数料見積り ※2 $49.32 $47.43 $47.19 $47.67 $47.83

※1: Ethereumの7月平均ガス単価=26.82768302(Gwei), 8/1時点の価格 1ETH=$1,672.41 を元に算出。
※2: Polygonの7月平均ガス単価=144.2445773(Gwei), 8/1時点の価格 1MATIC=$0.9178 を元に算出。
※3: 追加実験 OpenSeaメタデータ description="a" の1文字にした場合のバッチサイズ別のガス総量:

バッチサイズ 1 5 10 20 30
ガス総量(Mega) 149.9M 135.5M 133.6M 132.7M 132.5M

わかったこと

  • バッチサイズについて: 増やすとガス使用量は減るが、バッチサイズ5以上は微減。
    • バッチサイズを増やすとガス量は減る傾向ではあるが、5より増やした時は微減する程度でさほど変わらない。
    • 今回の要件OpenSeaのメタデータのdescriptionサイズが400文字弱≒571バイトと大きかったため、追加実験※3のdescriptionを1文字にした場合の実験も行った。その場合においてもバッチサイズを増やした時のガス量変化は5以上に増やしても微減程度の傾向が見られた。

結論

  • Ethereumのコストは許容できないと判断し、Polygonを採用することに決定。
  • バッチサイズは5に決定。
    • 1トランザクションあたりのガスリミットがチェーン毎に異なることが実験の過程でわかりました。Ethereum=30M, Polygon=10M
    • このガスリミットの上限ギリギリのバッチサイズにするほど、ガス節約のメリットが薄いので、5に決めました。

本番作業でかかったガス手数料の紹介

PolygonでNFTを発行することになり、本番作業を行った記録をご紹介します。

  • NFT発行件数: 357件
  • トランザクション数: 74件
  • mint/transferの所要時間: 3分30秒前後
  • コントラクトアドレス:
  • 合計トランザクションコスト: 16.52104 MATIC (≒ $15.36)
  • 合計ガス: 372,591,293 (≒372M)、平均ガス単価 44.34 Gwei
    • 見積りとの差異:見積り時点でガス総量 358.2M (+4%の誤差)、ガス単価はかなり安い時間帯で実行できたのはラッキーでした。

まとめ

  • 国内初!千葉工業大学で学修歴証明書をNFTで発行 PRJの裏側で行っていたNFTのチェーンをEthereum か Polygon にするのか判断をする過程に必要なガス手数料の見積り方法についてご紹介しました。
  • 結論としては、Polygonのコストメリットが際立つ形になりPolygonを採用しました。
  • ローカルの環境でも十分な精度でガス手数料の見積りができることが本記事で示せたのではないかと思っております。

さいごに

PitPaでは web3やメディア事業に興味のあるエンジニアの方とお話できればと考えてます。まだ発表できていないプロジェクトもあるので、ぜひお気軽に代表の石部にご連絡いただけたら幸いです。

▼石部のTwitterアカウントはこちらです。DMでお気軽にご連絡ください!
https://twitter.com/isbtty7

▼【ポッドキャスト公開中!〜PitPaの創業から現在まで〜】
代表の石部より、PitPa創業の話から現在取り組んでいるポッドキャスト事業・Web3事業の現在とこれからについてトークしました。ぜひこちらもご拝聴いただけますと幸いです。
https://open.spotify.com/show/2slv3BafA5c32038qFGYis

PitPa Tech Blog

Discussion