🔒

【Solidity】譲渡や二次流通ができないNFTの作り方

2022/02/01に公開
4

前置き

この記事は特定のNFTや仮想通貨の購買を促進する記事ではありません。

概要

  • 譲渡(transfer)や二次流通(Opensea等)ができないNFTを作る
  • 譲渡制限、二次流通制限、転送制限と呼ばれているもの
  • NFTなのに譲渡や二次流通を制限したら意味ないじゃん・・・と思うかもしれませんが、一旦そこは置いといてください
  • 最近ではPOAP のような出席証明NFTも出てきていて、こちらは転送できないことに価値が生まれてるっぽい。

実装

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract SushiNeko is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenCounter;

    constructor() ERC721("SushiNeko", "NEKO") {}

    function mint() public {
        _tokenCounter.increment();

        uint256 newItemId = _tokenCounter.current();
        _mint(msg.sender, newItemId);
    }

    // ここを追加するだけ
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal pure override {
        // mintは許可(そのまま処理を通す)
	// transferは禁止(処理を中断させる)
        require(from == address(0));
    }
}

_beforeTokenTransfer?

なぜ _beforeTokenTransfer を実装すればよいだけなのか?

前提として openzeppelinのERC721を継承している必要があります。

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L280-L292

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L304-L318

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L331-L351

↑この3つのコードを見てみるとわかりますが、openzeppelinのERC721コントラクトの場合、どれもメソッド呼び出しの最初のほうで _beforeTokenTransfer が呼ばれていることがわかります。

    function _mint(address to, uint256 tokenId) internal virtual {
        require(to != address(0), "ERC721: mint to the zero address");
        require(!_exists(tokenId), "ERC721: token already minted");
        
	// ↓ここ↓
        _beforeTokenTransfer(address(0), to, tokenId);

        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);

        _afterTokenTransfer(address(0), to, tokenId);
    }

そもそも _beforeTokenTransfer のコメントを見るとわかりますが

@dev Hook that is called before any token transfer. This includes minting and burning.
トークン転送の前に呼び出されるフック。これには、mintとburningが含まれます。

NFTの mint, burn, transfer を実行する前に フックとして _beforeTokenTransfer メソッドを呼び出しています。

であれば、 _beforeTokenTransfer をoverrideし、mint時以外は失敗するようなコードを追加すればOKです。

// mintは許可(そのまま処理を通す)
// transferは禁止(処理を中断させる)
require(from == address(0));

本当にOpenSeaで取引できないのか?

先ほどのコントラクトをデプロイし、NFTをmintしてみました。
これをOpenSeaで売りに出してみます。

別ウォレットに切り替えて、購入ボタンを押してみます

Oops, the Ethereum network rejected this transaction :( The OpenSea devs have been alerted, but this problem is typically due an item being locked or untransferrable. The exact error was "execution reverted..
おっと、イーサリアムネットワークはこのトランザクションを拒否しました:( OpenSea開発者に警告がありましたが、この問題は通常、アイテムがロックされているか転送できないことが原因です。正確なエラーは「実行が元に戻されました。」でした。

エラーが出て購入ができないことがわかりました

応用

これを応用したものに Pausable と呼ばれるものがあります

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/Pausable.sol

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC721, Pausable, Ownable {
    constructor() ERC721("MyToken", "MTK") {}

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

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

    function _beforeTokenTransfer(address from, address to, uint256 tokenId)
        internal
        whenNotPaused
        override
    {
        super._beforeTokenTransfer(from, to, tokenId);
    }
}

コントラクトに何らかの脆弱性が見つかった場合や、連携するdAppsをメンテナンスモードにしている最中に(ゲームとか)二次流通を止めたい場合なんかに使います。

他にも、エウレカセブンのNFTは専用マケプレでしか流通できないようにするロジックを定義しているようですが、これも少し応用すればできそうです。(代理コントラクト経由でしか操作を受け付けない等)

https://twitter.com/DropMiin/status/1488320018693574656

まとめ

  • _beforeTokenTransfer メソッドをいじることでNFTの譲渡・二次流通制限をかけられる
    • 前提としてopenzeppelinのERC721の継承が必要
  • 応用にPausableがある
  • 譲渡制限されてること自体に価値が出てきてる・・・?価値というか意味?

EIP-1238

現時点2022/02/13ではまだ固まっていないが、ERC721を独自拡張するのではなく、譲渡不可なNFTの規格を定義しようというプロポーザルもあるようなので要チェックです

https://github.com/ethereum/EIPs/issues/1238

etc

Solidityについてワイワイ学ぶコミュニティ「solidity-jp」を作りました!
いまから学んでみたい/学習中だけどの日本語の情報が少ない/古くて時間がかかっているという方、一緒に学びましょう〜!!

https://solidity-jp.dev/

また、TwitterにてSolidityに関する技術情報を発信しています。良ければフォローお願いします!

https://twitter.com/k0uhashi

Discussion

島田紳助島田紳助

初めまして。良質な記事をありがとうございます!
こちらを参考に譲渡に制限をかけたNFTを発行しようと考えています。
internal pure override
の部分で
TypeError: Function has override specified but does not override anything.
というエラーが起きます。
overrideするものがないという意味合いだと思いますがどのようにすれば解決されるでしょうか?
ご回答よろしくお願い致します。

Ryo TakahashiRyo Takahashi

ERC721の継承を忘れている・・・とかは流石にないですかね??

contract MyToken is ERC721, Pausable, Ownable {

ここの部分です!

島田紳助島田紳助

継承はされていると思うのですが...初学者に近いので不安です。
一度全て見ていただけますでしょうか?

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@nomiclabs/hardhat-etherscan";

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

    constructor() ERC721("FarmersinWallet" , FIW){}

    function mintNFT(address recipient, string memory tokenURI)
        public onlyOwner
        returns (uint256)
    {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }

    function _beforereTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal pure override {
        require(from == address(0));
    }
}

先ほど申し上げていたoverrideのエラーは無くなったのですが、コンパイルしようとすると

Error HH411: The library @nomiclabs/hardhat-ethersca, imported from contracts/FarmersinWallet.sol, is not installed. Try installing it using npm.

と出てきます。
@nomiclabs/hardhat-etherscanは既にインストールしていて、nを取ってインストールしようとしても普通にエラーが起きます。
お時間頂戴して申し訳ありませんが解決策はございますでしょうか?

🥑🥑

とても参考になりました。感謝です。