見出し画像

NFTのSCAM防止機能、ContractAllowListの導入手順

前回、下記の記事でContractAllowListの機能について紹介しました。次に、実際の導入手順を説明したいと思います。

導入する機能や方法を決める

「NFTを転送するコントラクトを制限する機能」と「NFTをロックする機能」の片方、もしくは両方を導入するかどうかを決めます。
「NFTをロックする機能」を導入する場合、ロックする仕組みは何にするのか、ロックはどの単位でのロックを有効にするのか、ロックと解除は誰が出来るのかを決めます。

今回の例では一通りの導入手順を説明するために、「NFTを転送するコントラクトを制限する機能」と「NFTをロックする機能」の両方を導入する想定で進めます。ロックできるのは、コレクション=コレクションオーナー、ウォレット=ウォレット、トークン=ホルダーウォレットとします。

NFTコントラクトに導入する

それでは実際にコードを触ります。今回は、NFTBoilをベースに話を進めます。元々の、はやっちさんのリポジトリは最新版だと私の環境でうまく動かないので、私がforkしたものをベースにします。ODENPETSやAstarPrinceはこれをベースにしています。

gitやnpmなどは導入されている前提で話を進めます。IDEはVisualStudioCodeを使用してます。必要に応じて導入してください。

NFTBoilの取得

GitHubからcloneします

git clone git@github.com:syunduel/NFTBoil.git

持ってこれました。

パッケージを取得する

ここからしばらくは、NFTBoilのREADMEのとおり実行していきます。
まずはNFTBoilディレクトリに移動し、npmコマンドで必要なパッケージを持ってきます

$ cd NFTBoil
$ npm i
npm WARN deprecated ganache-core@2.13.2: ganache-core is now ganache; visit https://trfl.io/g7 for details
npm WARN deprecated ganache-core@2.13.2: ganache-core is now ganache; visit https://trfl.io/g7 for details
〜中略〜
ngine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.

> nftboil@1.0.0 prepare
> husky install

husky - Git hooks installed

added 3946 packages, and audited 3953 packages in 31s

298 packages are looking for funding
  run `npm fund` for details

93 vulnerabilities (8 low, 16 moderate, 35 high, 34 critical)

To address issues that do not require attention, run:
  npm audit fix

To address all issues possible (including breaking changes), run:
  npm audit fix --force

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.

Warningが出つつも、取ってこれました。

env ファイルの設定

設定ファイルを作ります。とりあえずサンプルをコピーします。

$ cp ./contract/.env.example ./contract/.env

ACCOUNT_PRIVATE_KEYに値を設定します。設定するのは、デプロイする際のアカウントの秘密鍵です。
この.envファイルを他人に見せないように気をつけてください。このディレクトリの.envファイルはGitHubにアップされないように設定されていますが、別の名前でコピーしたりするとアップされてしまうので気をつけてください。

コントラクトのテスト

まずlocalのノードを起動します

$ npx hardhat typechain
Generating typings for: 26 artifacts in dir: typechain-types for target: ethers-v5
Successfully generated 82 typings!
Compiled 25 Solidity files successfully

$ npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0 : 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
〜中略〜

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

ターミナルをもう一つ起動してcontractディレクトリまで移動し、テストを実行します。全てパスするはずです。

$ cd Documents/repos/CALSample/NFTBoil/contract
$ npx hardhat test
No need to generate any newer typings.

  NFTBoilMerkle contract
    Basic checks
      ✓ check the owner
      ✓ check default is PreSale
      ✓ Confirm pre price
      ✓ Confirm public price (24054 gas)
    Public Minting checks
      ✓ PublicMint fail if presale is active (45966 gas)
      ✓ Non-owner cannot mint without enough balance
〜中略〜
···················|·····························|·············|·············|·············|··············|··············
|  NFTBoilMerkleA  ·  unpause                    ·          -  ·          -  ·      27753  ·           2  ·          -  │
···················|·····························|·············|·············|·············|··············|··············
|  Deployments                                   ·                                         ·  % of limit  ·             │
·················································|·············|·············|·············|··············|··············
|  NFTBoilMerkle                                 ·          -  ·          -  ·    2739631  ·      40.8 %  ·          -  │
·················································|·············|·············|·············|··············|··············
|  NFTBoilMerkleA                                ·          -  ·          -  ·    2748210  ·      40.9 %  ·          -  │
·------------------------------------------------|-------------|-------------|-------------|--------------|-------------·

  78 passing (21s)

CALパッケージのインストール

次はCALのREADMEに従ってCALをインストールします。
NFTBoilを使用している場合は、contractディレクトリで実行してください。

$ npm i contract-allow-list

added 1 package, and audited 3954 packages in 5s

298 packages are looking for funding
  run `npm fund` for details

1 high severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

CALはERC721Psiを使用しているため、ERC721Psiをインストールします。

$ npm i erc721psi

added 48 packages, and audited 4002 packages in 11s

321 packages are looking for funding
  run `npm fund` for details

1 high severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

自分のコントラクトの作成

contract/contracts/NFTBoilMerkleA.sol をコピーして、自分のコントラクトを作成します。

ファイル名とコントラクト名を自分のプロジェクトのNFTの名前に変更します。ここでは「MyAwesomeNFT」としました。サンプルっぽい良い名前です。

あわせて、envファイルの方も変えておきます。

次に、テストをコピーします。NFTBoilMerkleA.test.tsをコピーして、ファイル名を変更します。ファイルの中に書いてあるコントラクト名も、一括で置換します。

typechainがエラーになるので、typechainコマンドを再実行します

$ npx hardhat typechain
Generating typings for: 27 artifacts in dir: typechain-types for target: ethers-v5
Successfully generated 84 typings!
Compiled 26 Solidity files successfully

エラーが消えました

テストを再実施します

$ npx hardhat test
No need to generate any newer typings.

  NFTBoilMerkleA contract
    Basic checks
      ✓ check the owner
      ✓ check default is PreSale
      ✓ check default is Mintable
      ✓ check default is NOT publicSaleWithoutProof
      ✓ Confirm pre price
〜中略〜

  MyAwesomeNFT contract
    Basic checks
      ✓ check the owner
      ✓ check default is PreSale
      ✓ check default is Mintable
      ✓ check default is NOT publicSaleWithoutProof
〜中略〜
|  NFTBoilMerkleA  ·  unpause                    ·          -  ·          -  ·      27753  ·           2  ·          -  │
···················|·····························|·············|·············|·············|··············|··············
|  Deployments                                   ·                                         ·  % of limit  ·             │
·················································|·············|·············|·············|··············|··············
|  MyAwesomeNFT                                  ·          -  ·          -  ·    2748186  ·      40.9 %  ·          -  │
·················································|·············|·············|·············|··············|··············
|  NFTBoilMerkle                                 ·          -  ·          -  ·    2739631  ·      40.8 %  ·          -  │
·················································|·············|·············|·············|··············|··············
|  NFTBoilMerkleA                                ·          -  ·          -  ·    2748210  ·      40.9 %  ·          -  │
·------------------------------------------------|-------------|-------------|-------------|--------------|-------------·

  124 passing (30s)

先ほど作成したコントラクトのテストが、追加で実行されていると思います。これがCAL導入前のベースになります。

CALのコードの説明

CALは役割によってクラスが分けられています。それぞれの役割は下記のようになります。

  • contracts/ERC721AntiScam/

    • ERC721を継承したコントラクトで、CALの各機能を取り込んだコントラクトです。各NFTプロジェクトのコントラクトが継承するコントラクトになります。

  • contracts/core/

    • CALの本体が入っています。前回の機能説明の図の中の「CAL」です。CAL運営チームによって既にデプロイされています。

    • 各プロジェクトのエンジニアがこれを触る場面はありません。

  • contracts/governor/

    • CALのアドレスの追加/削除を、投票によって決定するためのコードです。現在まだ本番では利用していません。

  • contracts/mock/

    • CALを導入する際のサンプルコードが入っています。CALを導入する際には、ここのコードを参考にします。

  • contracts/proxy/

    • contracts/core/と同様、CALProxyの本体です。各プロジェクトのエンジニアがこれを触る場面はありません。

  • contracts/votes/

    • governorと同じく投票用…だと思います。現在まだ本番では利用していません。

さらに「contracts/ERC721AntiScam/」は、利用方法によってどれを継承するかが変わります。
文字数省略のために下記として説明します。
「NFTを転送するコントラクトを制限する機能」→「sAFA抑止機能」
「NFTをロックする機能」→「ロック機能」

  • 「sAFA抑止機能」と「ロック機能」の両方を使う場合で、ロックするのが運営が指定したウォレットの場合

    • contracts/ERC721AntiScam/extensions/ERC721AntiScamControl.sol

  • 「sAFA抑止機能」と「ロック機能」の両方を使う場合で、ロックするアドレスをコントラクトオーナーやトークンオーナーなどに独自に設定したい場合

    • contracts/ERC721AntiScam/ERC721AntiScam.sol

    • ※_setContractLock、_setWalletLock、_setTokenLockを呼び出す関数を独自に実装する

  • 「sAFA抑止機能」のみ使用する場合

    • contracts/ERC721AntiScam/restrictApprove/ERC721RestrictApprove.sol

  • 「ロック機能」のみ使用する場合(あまり無い気がしますが)

    • contracts/ERC721AntiScam/lockable/ERC721Lockable.sol

CAL関連のコードを追加

上記を参考に、自分のプロジェクトにあったコントラクトを継承し、必要な変数やメソッドを追加します。今回は「sAFA抑止機能」と「ロック機能」の両方を使う想定でした。ERC721AntiScamかERC721AntiScamControlのどちらかですが、今回はERC721AntiScamを採用して進めます。

「contracts/mock/TestNFTcollection.sol」を開きます

TestNFTcollectionをそのままベースにしても良いのですが、今回はMerkleTreeのアローリスト機能があるNFTBoilのコードにコピペで導入していきます。

下記をざざっと行います。最後にコードまるごと貼ってありますので、そちらもどうぞ。

  • 継承するERC721AntiScamとAccessControlをインポート

import "@openzeppelin/contracts/access/AccessControl.sol";
import "contract-allow-list/contracts/ERC721AntiScam/ERC721AntiScam.sol";
  • 継承するコントラクトをERC721AからERC721AntiScamに変更

  • AccessControlの継承を追加

contract MyAwesomeNFT is ERC721AntiScam, AccessControl, ERC2981, Pausable, CantBeEvil(LicenseVersion.CBE_NECR_HS)  {
  • ADMIN変数を追加

    bytes32 public ADMIN = "ADMIN";
  • ERC721Aを使用していた部分をERC721Psiに変更

    constructor(
        string memory _name,
        string memory _symbol
    ) ERC721Psi(_name, _symbol) {
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        return string(abi.encodePacked(ERC721Psi.tokenURI(tokenId), BASE_EXTENSION));
  • コンストラクターで、コントラクトの作成者を最初のADMINに追加

        _setupRole(ADMIN, msg.sender);
  • _startTokenIdの削除(ERC721Psiは_startTokenIdがオーバーライド出来ないようになっているため。NFTの実際のtoken_idを1から始めるには、1版最初にmintしたtokenをburnする必要があるようです。

    // function _startTokenId() internal view virtual override returns (uint256) {
    //     return 1;
    // }
  • ERC721Lockable、ERC721RestrictApprove、ERC721AntiScamをオーバーライドしている関数をコピペ

    /*///////////////////////////////////////////////////////////////
                        OVERRIDES ERC721Lockable
    //////////////////////////////////////////////////////////////*/
    function setTokenLock(uint256[] calldata tokenIds, LockStatus lockStatus)
        external
        override
〜中略〜

        return
            AccessControl.supportsInterface(interfaceId) ||
            ERC721AntiScam.supportsInterface(interfaceId);
    }

最後のsupportsInterfaceがメソッドが元から存在しているため、重複してしまいます。メソッドの中身をマージして、下記のようにします。

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721AntiScam, AccessControl, CantBeEvil, ERC2981)
        returns (bool)
    {
        return
            AccessControl.supportsInterface(interfaceId) ||
            ERC721AntiScam.supportsInterface(interfaceId) ||
            ERC2981.supportsInterface(interfaceId) ||
            CantBeEvil.supportsInterface(interfaceId);
    }
  • 「sAFA抑止機能」と「ロック機能」のオン/オフ


    function setEnableRestrict(bool value) external onlyOwner {
        enableRestrict = value;
    }

    function setEnableLock(bool value) external onlyOwner {
        enableLock = value;
    }


完成したコード

バグってても責任は持てませんので、よろしくおねがいします。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.16;

/// @title: MyAwesomeNFT
/// @author: Shunichiro
/// @dev: This contract using NFTBoil (https://github.com/syunduel/NFTBoil)


import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "erc721a/contracts/ERC721A.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "contract-allow-list/contracts/ERC721AntiScam/ERC721AntiScam.sol";

// This NFT License is a16z Can't be Evil Lisence
import {LicenseVersion, CantBeEvil} from "@a16z/contracts/licenses/CantBeEvil.sol";

contract MyAwesomeNFT is ERC721AntiScam, AccessControl, ERC2981, Pausable, CantBeEvil(LicenseVersion.CBE_NECR_HS)  {
    using Strings for uint256;

    bytes32 public ADMIN = "ADMIN";
    string private baseURI = "";

    uint256 public preCost = 0.001 ether;
    uint256 public publicCost = 0.001 ether;
    bool public presale = true;
    bool public mintable = false;
    bool public publicSaleWithoutProof = false;
    uint256 public maxPerWallet = 300;
    uint256 public publicMaxPerTx = 5;

    address public royaltyAddress;
    uint96 public royaltyFee = 500;

    uint256 constant public MAX_SUPPLY = 10000;
    string constant private BASE_EXTENSION = ".json";
    address constant private DEFAULT_ROYALITY_ADDRESS = 0xA9028b1EA3A8485130eB86Dc1F26654c823D9849;
    bytes32 public merkleRootPreMint;
    bytes32 public merkleRootPublicMint;
    mapping(address => uint256) private claimed;

    constructor(
        string memory _name,
        string memory _symbol
    ) ERC721Psi(_name, _symbol) {
        _setDefaultRoyalty(DEFAULT_ROYALITY_ADDRESS, royaltyFee);
        _setupRole(ADMIN, msg.sender);
    }

    modifier whenMintable() {
        require(mintable == true, "Mintable: paused");
        _;
    }

    /**
     * @dev The modifier allowing the function access only for real humans.
     */
    modifier callerIsUser() {
        require(tx.origin == msg.sender, "The caller is another contract");
        _;
    }

    // internal
    function _baseURI() internal view override returns (string memory) {
        return baseURI;
    }

    function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
        return string(abi.encodePacked(ERC721Psi.tokenURI(tokenId), BASE_EXTENSION));
    }

    /**
     * @notice Set the merkle root for the allow list mint
     */
    function setMerkleRootPreMint(bytes32 _merkleRoot) external onlyOwner {
        merkleRootPreMint = _merkleRoot;
    }

    /**
     * @notice Set the merkle root for the public mint
     */
    function setMerkleRootPublicMint(bytes32 _merkleRoot) external onlyOwner {
        merkleRootPublicMint = _merkleRoot;
    }

    function publicMint(uint256 _mintAmount, uint256 _publicMintMax, bytes32[] calldata _merkleProof) public
    payable
    whenNotPaused
    whenMintable
    callerIsUser
    {
        uint256 cost = publicCost * _mintAmount;
        mintCheck(_mintAmount, cost);
        require(!presale, "Presale is active.");
        bytes32 leaf = keccak256(abi.encodePacked(msg.sender, _publicMintMax));
        require(
            MerkleProof.verify(_merkleProof, merkleRootPublicMint, leaf),
            "Invalid Merkle Proof"
        );
        require(
            claimed[msg.sender] + _mintAmount <= _publicMintMax,
            "Already claimed max"
        );
        require(
            _mintAmount <= publicMaxPerTx,
            "Mint amount over"
        );

        _mint(msg.sender, _mintAmount);
        claimed[msg.sender] += _mintAmount;
    }

    function preMint(uint256 _mintAmount, uint256 _preMintMax, bytes32[] calldata _merkleProof)
        public
        payable
        whenMintable
        whenNotPaused
    {
        uint256 cost = preCost * _mintAmount;
        mintCheck(_mintAmount,  cost);
        require(presale, "Presale is not active.");
        bytes32 leaf = keccak256(abi.encodePacked(msg.sender, _preMintMax));
        require(
            MerkleProof.verify(_merkleProof, merkleRootPreMint, leaf),
            "Invalid Merkle Proof"
        );

        require(
            claimed[msg.sender] + _mintAmount <= _preMintMax,
            "Already claimed max"
        );

        _mint(msg.sender, _mintAmount);
        claimed[msg.sender] += _mintAmount;
    }

    function publicMintWithoutProof(uint256 _mintAmount) public
    payable
    whenNotPaused
    whenMintable
    callerIsUser
    {
        uint256 cost = publicCost * _mintAmount;
        mintCheck(_mintAmount, cost);
        require(!presale, "Presale is active.");
        require(publicSaleWithoutProof, "publicSaleWithoutProof is not open.");
        require(
            _mintAmount <= publicMaxPerTx,
            "Mint amount over"
        );
        require(
            claimed[msg.sender] + _mintAmount <= maxPerWallet,
            "Already claimed max"
        );

        _mint(msg.sender, _mintAmount);
        claimed[msg.sender] += _mintAmount;
    }


    function mintCheck(
        uint256 _mintAmount,
        uint256 cost
    ) private view {
        require(_mintAmount > 0, "Mint amount cannot be zero");
        require(
            totalSupply() + _mintAmount <= MAX_SUPPLY,
            "MAXSUPPLY over"
        );
        require(msg.value >= cost, "Not enough funds");
    }

    function ownerMint(address _address, uint256 count) public onlyOwner {
       _mint(_address, count);
    }

    function setPresale(bool _state) public onlyOwner {
        presale = _state;
    }

    function setPublicSaleWithoutProof(bool _state) public onlyOwner {
        publicSaleWithoutProof = _state;
    }

    function setPreCost(uint256 _preCost) public onlyOwner {
        preCost = _preCost;
    }

    function setPublicCost(uint256 _publicCost) public onlyOwner {
        publicCost = _publicCost;
    }

    function setMintable(bool _state) public onlyOwner {
        mintable = _state;
    }

    function setMaxPerWallet(uint256 _maxPerWallet) external onlyOwner {
        maxPerWallet = _maxPerWallet;
    }

    function setPublicMaxPerTx(uint256 _publicMaxPerTx) external onlyOwner {
        publicMaxPerTx = _publicMaxPerTx;
    }

    function getCurrentCost() public view returns (uint256) {
        if (presale) {
            return preCost;
        } else{
            return publicCost;
        }
    }

    function setBaseURI(string memory _newBaseURI) public onlyOwner {
        baseURI = _newBaseURI;
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function withdraw() external onlyOwner {
        Address.sendValue(payable(owner()), address(this).balance);
    }

    /**
     * @notice Change the royalty fee for the collection
     */
    function setRoyaltyFee(uint96 _feeNumerator) external onlyOwner {
        royaltyFee = _feeNumerator;
        _setDefaultRoyalty(royaltyAddress, royaltyFee);
    }

    /**
     * @notice Change the royalty address where royalty payouts are sent
     */
    function setRoyaltyAddress(address _royaltyAddress) external onlyOwner {
        royaltyAddress = _royaltyAddress;
        _setDefaultRoyalty(royaltyAddress, royaltyFee);
    }

    function setEnableRestrict(bool value) external onlyOwner {
        enableRestrict = value;
    }

    function setEnableLock(bool value) external onlyOwner {
        enableLock = value;
    }

    /*///////////////////////////////////////////////////////////////
                        OVERRIDES ERC721Lockable
    //////////////////////////////////////////////////////////////*/
    function setTokenLock(uint256[] calldata tokenIds, LockStatus lockStatus)
        external
        override
    {
        for (uint256 i = 0; i < tokenIds.length; i++) {
            require(msg.sender == ownerOf(tokenIds[i]), "not owner.");
        }
        _setTokenLock(tokenIds, lockStatus);
    }

    function setWalletLock(address to, LockStatus lockStatus)
        external
        override
    {
        require(to == msg.sender, "not yourself.");
        _setWalletLock(to, lockStatus);
    }

    function setContractLock(LockStatus lockStatus)
        external
        override
        onlyOwner
    {
        _setContractLock(lockStatus);
    }

    /*///////////////////////////////////////////////////////////////
                    OVERRIDES ERC721RestrictApprove
    //////////////////////////////////////////////////////////////*/
    function addLocalContractAllowList(address transferer)
        external
        override
        onlyRole(ADMIN)
    {
        _addLocalContractAllowList(transferer);
    }

    function removeLocalContractAllowList(address transferer)
        external
        override
        onlyRole(ADMIN)
    {
        _removeLocalContractAllowList(transferer);
    }

    function getLocalContractAllowList()
        external
        override
        view
        returns(address[] memory)
    {
        return _getLocalContractAllowList();
    }

    function setCALLevel(uint256 level) external override onlyRole(ADMIN) {
        CALLevel = level;
    }

    function setCAL(address calAddress) external onlyRole(ADMIN) {
        _setCAL(calAddress);
    }

    /*///////////////////////////////////////////////////////////////
                    OVERRIDES ERC721AntiScam
    //////////////////////////////////////////////////////////////*/
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721AntiScam, AccessControl, ERC2981, CantBeEvil)
        returns (bool)
    {
        return
            AccessControl.supportsInterface(interfaceId) ||
            ERC721AntiScam.supportsInterface(interfaceId) ||
            ERC2981.supportsInterface(interfaceId) ||
            CantBeEvil.supportsInterface(interfaceId);
    }
}

テストの実行

テストを実行します。token_idが1から始まるようになったため、テストが2つほど失敗しますが、それ以外は成功すると思います。

$ npx hardhat test
Generating typings for: 42 artifacts in dir: typechain-types for target: ethers-v5
Successfully generated 126 typings!
Compiled 41 Solidity files successfully

  NFTBoilMerkleA contract
    Basic checks
      ✓ check the owner
      ✓ check default is PreSale
      ✓ check default is Mintable

〜中略〜

···················|·····························|·············|·············|·············|··············|··············
|  Deployments                                   ·                                         ·  % of limit  ·             │
·················································|·············|·············|·············|··············|··············
|  MyAwesomeNFT                                  ·          -  ·          -  ·    4851403  ·      72.2 %  ·          -  │
·················································|·············|·············|·············|··············|··············
|  NFTBoilMerkle                                 ·          -  ·          -  ·    2739631  ·      40.8 %  ·          -  │
·················································|·············|·············|·············|··············|··············
|  NFTBoilMerkleA                                ·          -  ·          -  ·    2748210  ·      40.9 %  ·          -  │
·------------------------------------------------|-------------|-------------|-------------|--------------|-------------·

  122 passing (33s)
  2 failing

  1) MyAwesomeNFT contract
       Public Minting checks
         Owner and Bob mint:

      AssertionError: Expected "0" to be equal 1
      + expected - actual

       {
      -  "_hex": "0x01"
      +  "_hex": "0x00"
         "_isBigNumber": true
       }

      at assertArgsArraysEqual (/Users/shunichiro/Documents/repos/CALSample/NFTBoil/node_modules/@ethereum-waffle/chai/dist/cjs/matchers/emit.js:58:54)
      at tryAssertArgsArraysEqual (/Users/shunichiro/Documents/repos/CALSample/NFTBoil/node_modules/@ethereum-waffle/chai/dist/cjs/matchers/emit.js:65:20)
      at /Users/shunichiro/Documents/repos/CALSample/NFTBoil/node_modules/@ethereum-waffle/chai/dist/cjs/matchers/emit.js:77:13
      at runMicrotasks (<anonymous>)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async Context.<anonymous> (test/NFTBoilMerkleA.test.ts:168:7)

  2) MyAwesomeNFT contract
       Public Minting checks
         Bob mints 1 plus 4:

      AssertionError: Expected "0" to be equal 1
      + expected - actual

       {
      -  "_hex": "0x01"
      +  "_hex": "0x00"
         "_isBigNumber": true
       }

      at assertArgsArraysEqual (/Users/shunichiro/Documents/repos/CALSample/NFTBoil/node_modules/@ethereum-waffle/chai/dist/cjs/matchers/emit.js:58:54)
      at tryAssertArgsArraysEqual (/Users/shunichiro/Documents/repos/CALSample/NFTBoil/node_modules/@ethereum-waffle/chai/dist/cjs/matchers/emit.js:65:20)
      at /Users/shunichiro/Documents/repos/CALSample/NFTBoil/node_modules/@ethereum-waffle/chai/dist/cjs/matchers/emit.js:77:13
      at runMicrotasks (<anonymous>)
      at processTicksAndRejections (node:internal/process/task_queues:96:5)
      at async Context.<anonymous> (test/NFTBoilMerkleA.test.ts:250:7)

自分のNFTコントラクトをデプロイする

ここからは、Goerliテストネットで行います。

.envファイルの設定

Goerliテストネットにデプロイしてコードをverifyするために、 .envファイルに設定を追加します。
RPCはinfuraやalchemyで取得してください。ETH_APIはetherscanで取得してください。

GOERLI_RPC="https://goerli.infura.io/v3/xxxxxxxxxx"
ETH_API="xxxxxxxx"

デプロイ

hardhatコマンドでデプロイします

$ npx hardhat run scripts/deploy.ts --network goerli
No need to generate any newer typings.
Deploying ERC721 token...
Contract deployed to: 0x9224Cf3A746471D49b71292F124c839Bae52D2fa

デプロイ出来ました。今回の場合、作成されたコントラクトはこちらです。

https://goerli.etherscan.io/address/0x9224Cf3A746471D49b71292F124c839Bae52D2fa

コードのverify

同じくhardhatコマンドでverifyします

$ npx hardhat verify --network goerli 0x9224Cf3A746471D49b71292F124c839Bae52D2fa MyAwesomeNFT AWESOME
Nothing to compile
No need to generate any newer typings.
Successfully submitted source code for contract
contracts/MyAwesomeNFT.sol:MyAwesomeNFT at 0x9224Cf3A746471D49b71292F124c839Bae52D2fa
for verification on the block explorer. Waiting for verification result...

Successfully verified contract MyAwesomeNFT on Etherscan.
https://goerli.etherscan.io/address/0x9224Cf3A746471D49b71292F124c839Bae52D2fa#code

verify出来ました。

CALProxyのアドレスを設定する

CALのホワイトリストを使うためには、CALProxyのアドレスをセットする必要があります。

etherscanのwrite画面で「setCAL」を開き、CALProxyのアドレスをセットします。各ネットワークでのCALProxyとCALのアドレスは下記のとおりです。
※各NFTから設定するアドレスは、CALではなくCALProxyです。

Goerli

Ethereum Mainnet

Shibuya

Astar

ケースに応じて必要になること

「sAFA抑止機能」をオフにする

setEnableRestrictにfalseを設定するとオフになります。

「ロック機能」をオフにする

setEnableLockにfalseを設定するとオフになります。

独自の許可アドレスを設定する

独自に許可したいマーケットやツールがある場合、そのアドレスをセットします。

addLocalContractAllowListで追加してください。不要になった場合は、removeLocalContractAllowListで削除してください。

NFTコレクションをロックする

コレクション全体のロックを設定したい場合、setContractLockでロックしてください。設定する値は下記の通り。

  • ロックを外したい場合、1 = UnLock

  • ロックしたい場合、2 = Lock

ウォレット単位で設定する場合はsetWalletLock、トークン単位で設定する場合はsetTokenLockを使用してください。


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