SPECIALIST

多様な専門性を持つNRIデジタル社員のコラム、インタビューやインサイトをご紹介します。

BACK

NFTトークンのレンタル規格を実装してみた【後半】

NFTトークンのレンタル規格を実装してみた【前半】ではNFTの新規格であるERC-4907の概要や実装方法について紹介しました。【後半】では、実際のユースケースを想定した実装方法と外部サービスを活用した開発方法について紹介します。

ブロックチェーンゲームにおけるスカラーシップの実装

ERC-4907のユースケースとして一つ考えられるのは、ブロックチェーンゲームにおけるスカラーシップでの活用です。

スカラーシップとは

スカラーシップとは、NFTの保持者(マネージャー)が、スカラーと呼ばれる人にNFT、あるいはそのNFTが利用できるサブアカウントを貸与することで自身がゲームを行う代わりにスカラーにゲームをプレイしてもらい、報酬を受け取れる仕組みのことです。この時スカラーは報酬の一部を貰い受けます。

このスカラーシップを導入している代表的なゲームの一つがAxie Infinityです。Axie Infinityとはベトナムのスタートアップ企業SkyMavisによって開発されている世界で最も有名なブロックチェーンゲームの一つです。Axieと呼ばれるNFTのキャラクターを購入、育成し別のプレイヤーのAxieと対戦して勝利することで仮想通貨「AXS」や「SLP」を稼ぐことができます。ゲームをしながら仮想通貨を稼ぐことが可能な「Play2Earn」のゲームに該当し、ブロックチェーンゲームブームの火付け役にもなりました。

Axie InfinityのスカラーシップはRoanin Walletと呼ばれる独自のデジタルウォレットで利用することができます。Roanin Walletの中でサブアカウントと呼ばれるマネージャーのNFTにアクセス可能なアカウントを発行し、それを利用することでスカラーはマネージャーのNFTキャラを使って代わりにゲームをプレイすることができるようになります。

実装方法

このスカラーシップをERC-4907を応用して実現する方法について考えていきましょう。ゲームの内容としては、1対1の対戦形式で勝者には一定報酬をプロバイダー(スマートコントラクトの開発元)が支払う形式を想定しています。スカラーはこの報酬のうち一定の割合を受け取ることができるようにします。
ここで実際に作成するスマートコントラクトは下記です。

  • NFTの発行(Mint)機能
  • レンタルフィーの設定機能
  • スカラー設定機能
    • 報酬割合の設定もこの中で実施
  • ゲーム勝利時に報酬(今回はEthereum)を受け取る機能

ERC-4907そのものの実装方法については前項でも述べていますので、ここではERC-4907を拡張する形で実装しました。
ソースコードの全量は下記になります。

// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "./ERC4907.sol";

contract TestAxie is ERC4907, ERC721Enumerable, Ownable {
    // レンタル料とスカラーの報酬割合
    struct ScholarInfo {
        uint256 fee;
        uint256 ratio;
    }

    event UpdateScholar(uint256 fee, uint256 ratio);

    mapping(uint256 => ScholarInfo) internal _scholars;

    constructor(string memory name_, string memory symbol_)
        ERC4907(name_, symbol_)
    {}

    // mint時にレンタル料の初期値を設定する
    function mint() public {
        uint256 ts = totalSupply();
        _safeMint(msg.sender, ts);
        ScholarInfo storage scholarInfo = _scholars[ts];
        scholarInfo.fee = 0;
        emit UpdateScholar(0, 0);
    }

    // setUserはコントラクトのコントラクトプロバイダーのみ利用可能
    function setUser(
        uint256 tokenId,
        address user,
        uint64 expires
    ) public override onlyOwner {
        super.setUser(tokenId, user, expires);
    }

    // スカラーを設定。スカラー希望者からレンタルのオファーがあった場合はレンタル料をNFT保持者に支払う。
    function setScholar(
        uint256 tokenId,
        address scholar,
        uint64 expires,
        uint64 ratio
    ) public payable {
        // スカラー希望者かNFT保持者のみ実行可能
        require(msg.sender == scholar || msg.sender == ownerOf(tokenId));

        ScholarInfo storage scholarInfo = _scholars[tokenId];

        // スカラー希望者の場合はレンタルフィーを支払う。
        if (msg.sender == scholar) {
            require(scholarInfo.fee == msg.value);
            address payable manager = payable(ownerOf(tokenId));
            manager.transfer(msg.value);
        }

        // スカラー情報の設定
        UserInfo storage userInfo = _users[tokenId];
        userInfo.user = scholar;
        userInfo.expires = expires;
        emit UpdateUser(tokenId, scholar, expires);
        scholarInfo.ratio = ratio;
        emit UpdateScholar(scholarInfo.fee, ratio);
    }

    // レンタル料金の設定
    function setRentalFee(uint256 tokenId, uint256 fee) public {
        require(msg.sender == ownerOf(tokenId));
        ScholarInfo storage scholarInfo = _scholars[tokenId];
        scholarInfo.fee = fee;
        emit UpdateScholar(fee, scholarInfo.ratio);
    }

    /// @dev See {IERC165-supportsInterface}.
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override(ERC4907, ERC721Enumerable)
        returns (bool)
    {
        return
            interfaceId == type(IERC4907).interfaceId ||
            interfaceId == type(IERC721Enumerable).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    // レンタル期間中はNFTの移動を制限する
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId,
        uint256 batchSize
    ) internal override(ERC4907, ERC721Enumerable) {
        require(uint256(_users[tokenId].expires) < block.timestamp);
    }

    // ゲームに勝利した場合の報酬。スカラーが勝利した場合は報酬の一部をスカラーが入手できる。
    function win(uint256 tokenId) public payable onlyOwner {
        UserInfo storage userInfo = _users[tokenId];
        ScholarInfo storage scholarInfo = _scholars[tokenId];

        uint256 profits = msg.value;
        uint256 reward = 0;
        if (userInfo.user != address(0)) {
            reward = profits * scholarInfo.ratio / 100;
            payable(userInfo.user).transfer(reward);
        }
        profits = profits - reward;
        payable(ownerOf(tokenId)).transfer(profits);
    }
}

一つずつ解説していきます。
まず、構造体の中身について確認してみましょう。

// レンタル料とスカラーの報酬割合
struct ScholarInfo {
    uint256 fee;
    uint256 ratio;
}

importしているERC-4907.solの中でUserInfoがすでに定義されており、借主や貸出期間については用意されているので、ここではレンタル料金とスカラーが受け取れる報酬の割合を設定します。レンタル料金を入れている理由は、自身のスカラーがプレイによる報酬を獲得できなかった場合マネージャーには一切の利益がないので、レンタルそのものにも対価を求められるようにするためです。
続いて、NFTのmintについて見ていきます。

NFTの発行(Mint)機能

// mint時にレンタル料の初期値を設定する
function mint() public {
    uint256 ts = totalSupply();
    _safeMint(msg.sender, ts);
    ScholarInfo storage scholarInfo = _scholars[ts];
    scholarInfo.fee = 0;
    emit UpdateScholar(0, 0);
}

特に複雑な処理は入れていませんが、このタイミングでレンタル料金の初期値として0を設定するようにしています。レンタル料金を0以外に設定したい場合は後から下記の関数を実行します。

レンタルフィーの設定機能

// レンタル料金の設定
function setRentalFee(uint256 tokenId, uint256 fee) public {
    require(msg.sender == ownerOf(tokenId));
    ScholarInfo storage scholarInfo = _scholars[tokenId];
    scholarInfo.fee = fee;
    emit UpdateScholar(fee, scholarInfo.ratio);
}

ここからスカラーの設定方法について説明します。
スカラーは下記関数でセットしています。

スカラー設定機能

// スカラーを設定。スカラー希望者からレンタルのオファーがあった場合はレンタル料をNFT保持者に支払う。
function setScholar(
    uint256 tokenId,
    address scholar,
    uint64 expires,
    uint64 ratio
) public payable {
    // スカラー希望者かNFT保持者のみ実行可能
    require(msg.sender == scholar || msg.sender == ownerOf(tokenId));

    ScholarInfo storage scholarInfo = _scholars[tokenId];

    // スカラー希望者の場合はレンタルフィーを支払う。
    if (msg.sender == scholar) {
        require(scholarInfo.fee == msg.value);
        address payable manager = payable(ownerOf(tokenId));
        manager.transfer(msg.value);
    }

    // スカラー情報の設定
    UserInfo storage userInfo = _users[tokenId];
    userInfo.user = scholar;
    userInfo.expires = expires;
    emit UpdateUser(tokenId, scholar, expires);
    scholarInfo.ratio = ratio;
    emit UpdateScholar(scholarInfo.fee, ratio);
}

NFTの保持者であるマネージャーかスカラーを希望する人以外からは、コントラクトを実行できないようにしています。スカラー希望者がレンタルを申請する場合は、あらかじめ設定していたレンタル料を支払う必要があります。また、スカラーの報酬割合についてもここで設定することができます。

最後は報酬の受け渡しを行うプログラムになります。

ゲーム勝利時に報酬を受け取る機能

// ゲームに勝利した場合の報酬。スカラーが勝利した場合は報酬の一部をスカラーが入手できる。
function win(uint256 tokenId) public payable onlyOwner {
    UserInfo storage userInfo = _users[tokenId];
    ScholarInfo storage scholarInfo = _scholars[tokenId];

    uint256 profits = msg.value;
    uint256 reward = 0;
    if (userInfo.user != address(0)) {
        require(uint256(_users[tokenId].expires) < block.timestamp);
        reward = profits * scholarInfo.ratio / 100;
        payable(userInfo.user).transfer(reward);
    }
    profits = profits - reward;
    payable(ownerOf(tokenId)).transfer(profits);
}

1対1での対戦の後、勝者が確定した際実行されるコントラクトです。スカラーがプレイした場合は、報酬のうち一定の割合をスカラーに送金した後、NFTを保持しているマネージャーに残りの金額が送金されるようになっています。

レンタル期間中のNFT移動を制限する機能

また、スカラーシップをERC-4907で実現する上で一点気をつけなければならないポイントがあります。それはイーサリアムコミュニティにより承認され、Githubで公開されているERC-4907のプログラムはかなりNFT保持者に優位な作りになっているということです。NFT保持者はレンタルしている人がいる場合でも、自身の判断だけでそのレンタルをキャンセルしたり他の人に移し替えることが可能です。加えて、NFTを売買などで他のウォレットに送った際も、レンタルは勝手に取り消されてしまいます。スカラーシップは人によってはそれを仕事として請け負う場合もあるため、マネージャーの都合で一方的に利用できなくなるケースは防ぐ必要があります。そのため、サービス要件に応じてレンタルプログラムの実行に制限を加える実装等も検討すべきです。

今回のプログラムでは一例として、NFTの移動などのトランザクションが発生する前にレンタル期間を確認してトランザクションの実行可否を判断するようにしています。

// レンタル期間中はNFTの移動を制限する
function _beforeTokenTransfer(
    address from,
    address to,
    uint256 tokenId,
    uint256 batchSize
) internal override(ERC4907, ERC721Enumerable) {
    require(uint256(_users[tokenId].expires) < block.timestamp);
}

外部サービスを利用した実装

上述のスカラーシップ実装で示した通り、サービスに必要な独自機能は個別に実装していく必要がありますが、IERC4907で定義されるNFTレンタルの基本的なメソッドは機械的に実装していくことになります。
そこで、【前半】でご紹介した外部サービスの「Thirdweb」ではローコードのweb3開発フレームワークとして、少ない設定でコントラクトの自動生成を実現したり、NFTを取り扱う豊富なライブラリを提供しています。
今回はThirdwebを用いて、ERC-4907のコントラクトを生成してNFTのレンタルを実施するUIの実装までをご紹介します。

※内容は本ブログ投稿時点のThirdwebのサービス活用によるものです。今後のサービスのアップデート内容によっては、本手順とは異なっている場合があることをご了承ください。

Thirdwebサービス活用方法

まず、ThirdwebのERC-4907のページにアクセスします。このページよりERC-4907のコントラクトの初期設定を行い、コードを自動生成します。

Tech Blog komatsu NFT 02 01

Deploy Nowをクリックするとコントラクトのパラメータ設定が開きます。コントラクト名、シンボル、ロイヤリティの受取人アドレス、ロイヤリティのBPS(Basis Point: 利率のようなもの)を設定します。
各パラメータの詳細はThirdwebのDocumentation(ERC721Base|thirdweb developer portal )から確認できます。

Tech Blog komatsu NFT 02 02

Thirdwebのサービスを利用するにはNFTのウォレットを接続する必要があります。
実際のEthereumなどをウォレットに保持している方は、Ethereumなどを利用することもできますが、今回はテストのためテストネットであるGoeril( Goerli Testnet )を利用します。

Goerliとはイーサリアムのテストネットです。テストネットでは検証用のネットワークで、Faucetと呼ばれる特定のサイトから指定のGoerliETH(EthereumのGoerli版のようなもの)を一定量だけ無料でウォレットに送付することができます。検証用のため実際のNFTマーケットプレイスで販売されているNFTトークンなどと交換はできませんが、テストネット内で生成したNFTトークンと交換することは可能です。このGoerliETHを任意のウォレット(今回はMetamask)に入金しておきます。

ネットワークでGoerliを選択し、Connect WalletでMetamaskに接続してサービスをデプロイします。

Tech Blog komatsu NFT 02 03

デプロイが完了したら以下のような画面に遷移します。現時点では6つのタブが用意されています。

  • Explorer:デプロイされたコントラクトのメソッド一覧がリストアップされ、パラメータをセットしてメソッドを実行可能。
  • Events:サービスで実施したトランザクション履歴を表示。
  • NFTs:GUIでNFTをMintすることができ、MintしたNFTの一覧を表示。
  • Code:デプロイされたコントラクトのメソッド一覧を、コード上でどのように呼び出すかのサンプルコードをメソッドごとに表示。
  • Settings:メタデータやロイヤリティなどの設定。
  • Sources:自動生成されたコントラクトなどのソースコードを閲覧可能。

Tech Blog komatsu NFT 02 04

以下のようにNFTレンタルのERC-4907のコントラクトコードもパラメーターを設定するだけで自動で生成されます。

Tech Blog komatsu NFT 02 05

まずは、レンタル用のNFTを用意するために、NFTタブから任意の画像をアップロードしてMintします。
これによってThirdweb上で取り扱えるNFTが用意できました。

Tech Blog komatsu NFT 02 06

次にこれらNFTをレンタルするための画面を実装していきます。
ThirdwebはReactやTypeScriptでの実装ガイドが用意されています。今回はTypeScriptで実装をします。
thirdweb React SDK | thirdweb developer portal
thirdweb TypeScript SDK | thirdweb developer portal

まずはThirdwebの環境を作成します。
Visual Studio Codeなどで以下のコマンドを実行して、プロジェクト名、フレームワーク、実装言語を指定するとサンプルプロジェクトを生成します。

npx thirdweb create --app
  • What is your project named? … (任意のプロジェクト名)
  • What framework do you want to use? » Next.js
  • What language do you want to use? » TypeScript

 
次にThirdwebのSDKをインストールします。

npm install @thirdweb-dev/sdk ethers

ビルドを実施して、サンプルコードを起動します。

yarn install
yarn dev

ブラウザでhttp://localhost:3000にアクセスしてサンプルが起動していれば成功です。

Tech Blog komatsu NFT 02 07

NFTレンタルサービス提供画面の実装方法

続いてNFTのレンタルサービスを提供する画面を実装していきます。

まず、ウォレットの接続ウィジェットですが以下のコードのみで実装することができます。これにより自身のMetamaskなどに接続・切断、そして接続先のネットワークの切り替えができるウォレットのウィジェットを配置することができます。

import { ConnectWallet, useAddress } from "@thirdweb-dev/react";
export const YourApp = () => {
  const address = useAddress();
  return (
    <div>
      <ConnectWallet />
    </div>
  );
};

次にNFTの表示です。以下のコードによりMintしたNFTの一覧を表示することができます。

import { useContract, useNFTs, ThirdwebNftMedia } from "@thirdweb-dev/react";
export default function Home() {
  const { contract } = useContract("<CONTRACT_ADDRESS>");
  const { data: nfts, isLoading: isReadingNfts } = useNFTs(contract);
  return (
    <div>
      <h2>My NFTs</h2>
      {isReadingNfts ? (
        <p>Loading...</p>
      ) : (
        <div>
          {nfts.map((nft) => (
            <ThirdwebNftMedia
              key={nft.metadata.id}
              metadata={nft.metadata}
              height={200}
            />
          ))}
        </div>
      )}
    </div>
  );
}

続いて、NFTをレンタルするコードです。

ERC-4907で定義されるsetUserメソッドを利用することで、NFTトークンを指定のアドレスに対して有効期限までレンタルさせることができます。

import { useContract, useContractWrite, Web3Button } from "@thirdweb-dev/react";
import { useState } from "react";
const Home: NextPage = () => {
  const { mutateAsync: setUser, isLoading: isLoadingWrite } = useContractWrite(contract, "setUser")
  return (
    <div className="App">
      Rental Address
      <input
        value={rentalAddress}
        onChange={(event) => setAddrText(event.target.value)}
      />
      Token ID
      <input
        value={tokenIdText}
        onChange={(event) => setTokenIdText(event.target.value)}
      />
      Expired Time
      <input
        value={expires}
        onChange={(event) => setText(event.target.value)}
      />
    </div>
 
    <div>
      {/* ... Existing Display Logic here ... */}
      <Web3Button
        contractAddress={contractAddress}
        action={(contract) =>
          setUser([tokenIdText, rentalAddress, expires])
        }
      >
        SetUser
      </Web3Button>
    </div>
  );
};

これらのコードを組み合わせて実装したサンプルサービスが以下です。

Tech Blog komatsu NFT 02 08

Rental Addressに借主のアドレス、Token IDにレンタル対象のNFTのトークンID、Expired Timeに有効期限を入力します。その状態でSetUserをコールすると指定のNFTを有効期限までレンタルします。レンタル前は借主なしとしてRental User Addressに0x000…がセットされています。

例のようにNFTのToken IDに0を指定してレンタルすると、左のNFT画像の借主(Rental User Address)に指定したAddressがセットされます。有効期限になると再び借主なしとしてAddressが0x000…に戻ります。

Tech Blog komatsu NFT 02 09

以上がNFTレンタルの2つの実装方法と、プロトタイプの実装サンプルの説明です。

終わりに

ERC-4907が実現するレンタル機能はNFTの利用シーンを大幅に広げるポテンシャルを持っています。さらに、記事内で紹介しているようにそれを簡単に実装できるサービスもどんどん登場しています。今後NFT市場の拡大が期待されていますが、本記事もその一助になれますと幸いです。

 
参考文献