見出し画像

【ガス代を最大96%OFFにする】NFT独自コントラクトを軽量化する方法

独自コントラクトでNFTをミントしたいけれどガス代の高さを理由に断念する方も多いと思います。2021年12月現在(ハードフォーク前)、EthereumでNFT(ERC-721)発行用の独自コントラクトをデプロイすると20万円以上のガス代が発生します。

この記事ではガス代をできるだけ安く抑えた独自スマートコントラクトを作成する方法を説明します。
具体的には、
・OpenZeppelinの不必要なプリセットを継承しない
・ERC-1167(Minimal Proxy Contract) の活用
を実践していきます。

独自コントラクトとは何か、環境構築、ミントしたNFTをOpenSeaで表示する方法など、基本的な内容は以下記事をご参照ください。

独自コントラクトのガス代

2021年12月現在、Ethereumの独自コントラクトをデプロイした際のガス代は20万円程度が相場となっています。
Rarible ではプラットフォーム上で独自コントラクトを生成することができますが、作成する際に求められるガス代は以下のとおりです。

スクリーンショット 2021-12-17 3.50.43


ERC721PresetMinterPauserAutoId を継承した場合

実際にデプロイしてガス代を計測していきます。尚、筆者は富豪ではないのでテストネットであるGoerliを使います。

以前の記事でも使用しましたが、OpenZeppelin の ERC721PresetMinterPauserAutoId.sol というプリセットのコントラクトを継承すると非常に簡単にNFTコントラクトを作成できます。

pragma solidity ^0.8.4;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol";

contract NFT is ERC721PresetMinterPauserAutoId {
    constructor()
        ERC721PresetMinterPauserAutoId("{{コントラクト名}}", "{{シンボル名}}", "{{BASE URI}}")
    {}
}

これをデプロイすると、Goerliで 0.0111289ETH かかりました。

ERC721PresetMinterPauserAutoId.sol は以下の機能等が実装されています。
・burn(トークンを破壊する)
・トークンの転送を停止させる
・トークンIDとURIの自動生成
・ロールに応じた各種関数のロック

ようするに、機能盛々なプリセットなわけです。

ERC721 を継承した場合

次に基本的な機能のみを備えた ERC721.sol を継承してみます。
なお、ERC721.sol にはトークンIDとURLの自動生成機能がないため、いくつかの実装をします。
・Counters.sol と Ownable.sol をインポートする
・constructor で baseURI を設定する
・mint 関数を定義し、tokenID を自動採番でミントする

pragma solidity ^0.8.4;

import "hardhat/console.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFT is ERC721, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    string private baseTokenURI;

    constructor()
        ERC721("{{コントラクト名}}", "{{シンボル名}}")
    {
        baseTokenURI = "{{BASE URI}}";
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return baseTokenURI;
    }

    function mint(address to_) external onlyOwner() {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(to_, newItemId);
    }
}

これをデプロイすると、Goerliで 0.00640156ETH かかりました。
プリセットを使用した場合に比べて 54% 安くなりました。

ERC-1167(Minimal Proxy Contract) を使用した場合

ERC-1167(Minimal Proxy Contract)は、既存のコントラクトの複製を安価に行うための規格です。
様々なNFTを作成するにあたり、すべて同じコントラクトに詰め込むのはブランディング的によろしくないケースや、既存のコントラクトとほぼ同じ機能を備えながら一部だけ上書きしたいケースもあるでしょう。基本機能を備えたコントラクトを最初にデプロイしておき、それを Minimal Proxy Contract で利用することで、コントラクトを非常に軽量化することができます。

なお Minimal Proxy Contract には、
・constructorを使用できない
・function外で変数に値をセットできない
という制約があるため、先程のERC721.solを継承したコントラクトを変更していきます。

pragma solidity ^0.8.4;

import "hardhat/console.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract ProxyERC721 is Initializable, ERC721Upgradeable, OwnableUpgradeable{
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    string private baseTokenURI;

    function initialize(address _owner, string calldata _name, string calldata _symbol, string calldata _baseuri) public initializer {
        __Ownable_init_unchained();
        transferOwnership(_owner);
        __ERC721_init_unchained(_name, _symbol);
        baseTokenURI = _baseuri;
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return baseTokenURI;
    }

    function mint(address to_) external onlyOwner() {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(to_, newItemId);
    }
}

インポートするコントラクトをERC721Upgradeableに変更し、constructor() の代わりに initialize を定義します。

次に、サンプル を元にコントラクトをクローンするためのコントラクトを作ります。

pragma solidity ^0.8.4;

contract MinimalProxy {
   function createClone(address target) internal returns (address result) {
       bytes20 targetBytes = bytes20(target);

       assembly {
           let clone := mload(0x40)
           mstore(
               clone,
               0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000
           )
           mstore(add(clone, 0x14), targetBytes)
           mstore(
               add(clone, 0x28),
               0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000
           )
           result := create(0, clone, 0x37)
       }
   }
}

続いて、コントラクトを生成するためのFactoryコントラクトを作ります。

pragma solidity ^0.8.4;

import "./ProxyERC721.sol";
import "./MinimalProxy.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract ProxyERC721Factory is Ownable, MinimalProxy {
    address[] public tokens;

    function createThing(string calldata _name, string calldata _symbol, string calldata _baseuri) public onlyOwner {
        address proxyerc721 = {{ProxyERC721のコントラクトアドレス}};
        address clone = createClone(proxyerc721);
        ProxyERC721(clone).initialize(msg.sender, _name, _symbol, _baseuri);
        tokens.push(clone);
    }

    function tokenOf(uint256 tokenId) public view returns (address) {
        return tokens[tokenId];
    }
}

Factoryコントラクトを使って、新たなERC-721コントラクトを生成します。

const Web3 = require("web3");

const CONTRACT_ADDRESS = "{{ProxyERC721Factoryのコントラクトアドレス}}";
const PUBLIC_KEY = "{{アカウントのアドレス(公開鍵) }}";
const PRIVATE_KEY = "{{アカウントの秘密鍵}}";
const PROVIDER_URL = "https://eth-goerli.alchemyapi.io/v2/{{API KEY}}";
async function createNewContract() {
 const web3 = new Web3(PROVIDER_URL);
 const contract = require("../artifacts/contracts/ProxyERC721Factory.sol/ProxyERC721Factory.json");
 const nftContract = new web3.eth.Contract(contract.abi, CONTRACT_ADDRESS);
 const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest");
 const tx = {
   from: PUBLIC_KEY,
   to: CONTRACT_ADDRESS,
   nonce: nonce,
   gas: 500000,
   data: nftContract.methods.createThing("{{コントラクト名}}", "{{シンボル名}}", "{{Base URI}}").encodeABI(),
 };
 const signPromise = web3.eth.accounts.signTransaction(tx, PRIVATE_KEY);
 signPromise
   .then((signedTx) => {
     const tx = signedTx.rawTransaction;
     if (tx !== undefined) {
       web3.eth.sendSignedTransaction(tx, function (err, hash) {
         if (!err) {
           console.log("The hash of your transaction is: ", hash);
         } else {
           console.log(
             "Something went wrong when submitting your transaction:",
             err
           );
         }
       });
     }
   })
   .catch((err) => {
     console.log("Promise failed:", err);
   });
}

createNewContract();

これをデプロイすると、Goerliで 0.000410245498ETH かかりました。

ERC721.sol を継承した場合に比べて 94% 安く、最初のプリセットを使用した場合に比べて 96% 安くなりました。

まとめ

プリセットを使用せず、継承するコントラクトを最小限に押さえることで 54% のガス代節約、さらに Minimal Proxy Contract を活用することで最大96% のガス代を節約することができました。
Minimal Proxy Contract を使用する場合は、最初にベースとなるコントラクトをデプロイする必要があるため、コントラクトを1つだけ作るケースでは威力を発揮しませんが、複数コントラクトを生成したい場合は圧倒的な節約になります。

身近な例では、Chocofactory という非常に安価に独自コントラクトを作成できるサービスでも使われています。

コントラクトを生成できるサービスを作る場合や、個人で複数のブランドのNFTを発行する際に活用していこうと思います。


不明な点、ご感想などあればコメントよろしくお願いします!

筆者自身まだNFTに触れたばかりなため、誤った表現等あればご指摘いただけると幸いです。特に NFT や ブロックチェーン関連のエンジニアの方は是非交流しましょう!

この記事が気に入ったらサポートをしてみませんか?