NFTトークンのレンタル規格を実装してみた【前半】
NRIデジタルでは、新技術について調査・開発を行う社内活動を推進しています。その取り組みの一環として、新たなビジネス創出チャンスとして注目を集めているブロックチェーン・NFT技術についての調査を行いました。
本記事では、その活動の一部を共有していきます。今回は【前半】として、2022年6月に標準として承認されたばかりのNFTトークン規格「ERC-4907」の概要と、プロトタイプアプリの実装方法について紹介します。
ERC-4907の概要
NFTとは
NFTとは、ブロックチェーン上で発行および取引される「偽造不可な鑑定書・所有証明書付きのデジタルデータ」です。デジタルデータに代替不可能な唯一無二の価値を持たせることを可能にし、特にゲームやアートなどの分野で注目を集めている最新技術の1つです。(詳細は、NFTホワイトペーパー(自民党発表) NFTの動向整理(消費者庁)参照)
1つ1つのトークンを区別し替えがきかないものとして発行し扱うための規格「ERC-721」が採択され、この「ERC-721」規格を利用して発行されたトークンが「NFT」と呼ばれるようになりました。
NFTの規格「ERC-4907」とは
NFTは、主にERC-721という標準規格に基づいて作成されていますが、ERC-4907はこのERC-721の規格を拡張するための規格です。詳しい実装方法は後述しますが、ERC-721のNFTをレンタルしやすいように、所有者(Owner)が一定期間NFTをレンタルできる使用者(user)とその期間(expires)を設定し、期間が過ぎれば自動で使用者(user)の権利が失効するという仕組みを付与します。
従来の方法でNFTでもレンタルは可能でしたが、NFTを勝手に転売されたり、期限が過ぎても返還してもらえないリスクがあるため、リスク回避のために、担保を取ったり契約を締結する等の手続きが必要でした。一方で、この新規格を使うことで、持ち逃げや転売のリスクがなくなり、煩雑な手続きが不要になるというメリットがあります。
NFTの取引に使われるスマートコントラクトとは
スマートコントラクトとは、ブロックチェーンに保存された改ざん不可能なプログラムのことで、あらかじめ設定されたルールに従って決められた処理を実行します。
NFTは、スマートコントラクトで定められたルールに従って取引されることで、自動的にブロックチェーン上の所有者が書き換えられ、改ざん不可能な取引履歴を持つことが可能です。
プロトタイプアプリの実装(レンタルNFTの実装イメージ紹介)
実装方針
ERC-4907を用いてレンタルNFTのサービスを提供するには幾つかの方法が考えられますが、シンプルなものとして以下の2つのパターンがあります。
- スクラッチでERC-4907のコントラクトコードを実装
- 外部サービスでERC-4907のコントラクトコードを生成
1つ目のパターンの場合は、ERC-4907で定義されるインターフェースをSolidityコードで実装し、そのインターフェースを活用するコントラクトのコードを実装します。
2つ目のパターンの場合は、いわゆるローコードなどの外部サービスを用いてERC-4907のコードを半自動的に生成し、その外部サービスのライブラリを活用して、主にNFTサービスを提供するUI部分の実装を進めていく方針です。
それでは、ここから、1つ目のスクラッチでコントラクトコードを実装する流れをご紹介します。
スクラッチ実装
スクラッチ実装する場合は、IERC4907というインターフェースを定義して実装する必要があります。
interface IERC4907 { // Logged when the user of an NFT is changed or expires is changed /// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed /// The zero address for user indicates that there is no user address event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires); /// @notice set the user and expires of an NFT /// @dev The zero address indicates there is no user /// Throws if `tokenId` is not valid NFT /// @param user The new user of the NFT /// @param expires UNIX timestamp, The new user could use the NFT before expires function setUser(uint256 tokenId, address user, uint64 expires) external; /// @notice Get the user address of an NFT /// @dev The zero address indicates that there is no user or the user is expired /// @param tokenId The NFT to get the user address for /// @return The user address for this NFT function userOf(uint256 tokenId) external view returns(address); /// @notice Get the user expires of an NFT /// @dev The zero value indicates that there is no user /// @param tokenId The NFT to get the user expires for /// @return The user expires for this NFT function userExpires(uint256 tokenId) external view returns(uint256); }
IERC4907はNFT標準規格のERC-721を継承し、NFTレンタルを実現するインターフェースを提供します。
- UpdateUser:指定NFTをレンタルしているユーザーアドレスや有効期限が変わったときに呼ぶ
- setUser:指定NFTをレンタルする借主のユーザーアドレスをセットする
- userOf:指定NFTをレンタルしている借主のユーザーアドレスを返す
- userExpires:指定NFTのレンタルの有効期限を返す
もちろんこの4つのインターフェースだけでは、NFTレンタルサービスは提供できないため、継承元であるERC-721の規格で提供されるNFTの残高確認や転送などの機能も適宜実装していく必要があります。
NFTをレンタルした人をセットするsetUserの実装例を以下に示します。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "./IERC4907.sol"; contract ERC4907 is ERC721URIStorage, IERC4907 { struct UserInfo { address user; // ユーザーアドレス uint64 expires; // 有効期限日時 } mapping(uint256 => UserInfo) internal _users; constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {} function setUser(uint256 tokenId, address user, uint64 expires) public virtual override { // NFTのレンタルを承認できるのはNFTオーナーか許可された承認者のみ require( _isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved" ); UserInfo storage info = _users[tokenId]; info.user = user; info.expires = expires; emit UpdateUser(tokenId, user, expires); }
上述したIERC4907に加え、継承元のERC-721のI/FであるURIStorageをインポートすることで、NFTトークンへのURI設定などNFTが以前から提供する機能を実現できます。
開発を進めていくうえで必要となるのがデバッグですが、OpenZeppelinのTest Helperを用いてJavaScriptコードでテストコードの実装とデバッグが可能です。
require("@openzeppelin/test-helpers/configure")({ provider: web3.currentProvider, singletons: { abstraction: "truffle" }, }); const { constants, expectRevert, expectEvent } = require('@openzeppelin/test-helpers'); const RentablePets = artifacts.require("RentablePets"); contract("TestContract", function (accounts) { it("should return the correct UserInfo", async () => { const testContract = await TestContract.deployed(); const expirationDate = 1668390813; // 期限切れの日付をセット await testContract.mint(constants.TOKEN_URI); var expiredTxHash = await testContract.setUser(1, accounts[1], expirationDate) var expiredNFTUser = await testContract.userOf.call(1); var expiredNFTDate = await testContract.userExpires.call(1); assert.equal(expiredNFTUser, constants.WRONG_ADDRESS, "誤ったユーザーアドレス"); assert.equal(expiredNFTDate, expirationDate, "誤った有効期限"); expectEvent(expiredTxHash, "UpdateUser", { tokenId: "1", user: accounts[1], expires: expirationDate.toString()}); }); });
以上、スクラッチでのコントラクトコードの実装例を紹介しました。
Dapps開発を支えるサードパーティサービス群
上記のようなコードをベースに実際のサービスとして組み立てていく場合には、後述するAlchemyやTruffleなどを活用することでより便利に開発することが可能になります。
スマートコントラクトやNFTなどブロックチェーン技術を活用したサービスは、Decentralized Applications:通称Dappsと呼ばれ、一例として、NFTマーケットプレイスOpenseaや仮想通貨が稼げるゲームAxie Infinityなどがあります。
今回ERC-4907の実装プロトタイプを作成するにあたり、Dapps開発の方法や、フレームワーク・開発プラットフォームの調査を実施したため、その一部をご紹介します。
Dappsのアーキテクチャ
まず、Dappsのアーキテクチャは主に以下3つの要素から成り立っています。
- スマートコントラクトを実行するネットワークノード
- ネットワークにデプロイされたスマートコントラクト
- スマートコントラクトの実行をリクエストするフロントエンド