レンタル機能の必要性

文化財の管理において、「貸出」は日常的に発生する重要な業務です。博物館間での作品の貸借、特別展への出品、調査のための一時的な移管など、文化財は所有機関以外の場所で利用されることが頻繁にあります。

NFTを使ったデジタル文化財管理システムでは、この「貸出」をどのように表現するかが課題となります。本章では、最も単純なアプローチとして「所有権の転送」によるレンタルの実装を試みますが、この方法には重大な問題があることを確認します。

:::message alert 本章の実装は問題を含んでおり、実用には適しません。次章でERC-4907を使った適切な実装に置き換えます。あえて問題のあるアプローチを先に示すことで、ERC-4907の必要性を理解していただくことを目的としています。 :::

所有権転送によるレンタルの実装

最初のアプローチでは、ERC721の transferFrom を使って所有権を一時的に移転し、レンタル期間終了後に返却(再転送)するという方法を考えました。

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

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

contract CulturalHeritageRentalV1 is ERC721, ERC721URIStorage, Ownable {
    struct RentalInfo {
        address originalOwner;  // 元の所有者
        address borrower;       // 借用者
        uint256 startTime;      // 貸出開始時刻
        uint256 endTime;        // 貸出終了時刻
        bool isActive;          // 貸出中フラグ
    }

    mapping(uint256 => RentalInfo) public rentals;

    event RentalStarted(
        uint256 indexed tokenId,
        address indexed originalOwner,
        address indexed borrower,
        uint256 startTime,
        uint256 endTime
    );

    event RentalEnded(
        uint256 indexed tokenId,
        address indexed returnedTo
    );

    constructor()
        ERC721("CulturalHeritageRentalV1", "CHR1")
        Ownable(msg.sender)
    {}

    /// @notice NFTを貸し出す(所有権を転送)
    function startRental(
        uint256 tokenId,
        address borrower,
        uint256 duration
    ) external {
        require(ownerOf(tokenId) == msg.sender, "Not the owner");
        require(!rentals[tokenId].isActive, "Already rented");

        // 元の所有者を記録
        rentals[tokenId] = RentalInfo({
            originalOwner: msg.sender,
            borrower: borrower,
            startTime: block.timestamp,
            endTime: block.timestamp + duration,
            isActive: true
        });

        // 所有権を借用者に転送
        _transfer(msg.sender, borrower, tokenId);

        emit RentalStarted(
            tokenId,
            msg.sender,
            borrower,
            block.timestamp,
            block.timestamp + duration
        );
    }

    /// @notice NFTを返却する(所有権を元に戻す)
    function endRental(uint256 tokenId) external {
        RentalInfo memory rental = rentals[tokenId];
        require(rental.isActive, "Not currently rented");
        require(
            msg.sender == rental.borrower ||
            msg.sender == rental.originalOwner,
            "Not authorized"
        );

        // 所有権を元の所有者に戻す
        _transfer(rental.borrower, rental.originalOwner, tokenId);

        rentals[tokenId].isActive = false;

        emit RentalEnded(tokenId, rental.originalOwner);
    }

    // 以下、必須オーバーライド
    function tokenURI(uint256 tokenId)
        public view override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public view override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

フロントエンドからの呼び出し

import { ethers } from "ethers";

// レンタルの開始
async function startRental(
  tokenId: number,
  borrowerAddress: string,
  durationDays: number
) {
  const durationSeconds = durationDays * 24 * 60 * 60;
  const tx = await contract.startRental(
    tokenId,
    borrowerAddress,
    durationSeconds
  );
  const receipt = await tx.wait();
  console.log("レンタル開始:", receipt.hash);
}

// レンタルの終了(返却)
async function endRental(tokenId: number) {
  const tx = await contract.endRental(tokenId);
  const receipt = await tx.wait();
  console.log("返却完了:", receipt.hash);
}

// 使用例: 30日間のレンタル
await startRental(
  0,
  "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
  30
);

発見された問題点

このアプローチをテストしていく中で、以下の重大な問題が明らかになりました。

問題1: 借用者がNFTを第三者に転送できてしまう

所有権が借用者に移っているため、借用者は transferFromapprove を使って自由にNFTを第三者に転送できてしまいます。

// 借用者が勝手に第三者に転送できてしまう
await contract.connect(borrower).transferFrom(
  borrower.address,
  thirdParty.address,
  tokenId
);
// 元の所有者はNFTを取り戻せなくなる!

問題2: 返却の保証がない

endRental 関数では _transfer を使って強制的に返却しようとしていますが、借用者がすでにNFTを第三者に転送していた場合、この処理は失敗します。

describe("問題の検証", function () {
  it("借用者が第三者に転送するとレンタル解除できない", async function () {
    const [owner, borrower, thirdParty] = await ethers.getSigners();

    // NFTをミントしてレンタル開始
    await contract.mint(owner.address, "ipfs://test");
    await contract.startRental(0, borrower.address, 86400);

    // 借用者がNFTを第三者に転送
    await contract.connect(borrower)
      .transferFrom(borrower.address, thirdParty.address, 0);

    // 返却しようとするとエラー
    await expect(
      contract.endRental(0)
    ).to.be.reverted;
    // borrowerはもうNFTを持っていないため転送できない
  });
});

問題3: レンタル期間の強制力がない

レンタル期間が終了しても、借用者が自発的に返却しない限り、NFTは借用者のもとに残ります。スマートコントラクトには、期限切れ時に自動的に所有権を戻すメカニズムがありません(Ethereumには自動実行の仕組みがないため)。

問題4: マーケットプレイスでの売却リスク

所有権が移転しているため、借用者はOpenSeaなどのマーケットプレイスでNFTを出品・売却できてしまいます。これは文化財の管理として致命的な問題です。

問題の根本原因

これらの問題の根本原因は、ERC721の所有権(ownership)を「利用権」の代わりに使おうとしている点にあります。ERC721の ownerOf は、トークンの完全な所有権を表しており、「一時的な利用権」を表現する仕組みを持っていません。

文化財の貸出では、以下の区別が必要です。

概念必要な権利ERC721での対応
所有権恒久的な所有ownerOf
利用権一時的なアクセス対応なし

この「所有権」と「利用権」の分離こそが、次章で導入するERC-4907規格の核心的な機能です。

次章への展望

所有権転送によるレンタル実装の問題点を確認しました。次章では、ERC-4907(Rental NFT)規格を使用して、所有者(owner)と利用者(user)を明確に分離した適切なレンタル機能を実装します。ERC-4907では、利用権に有効期限を設定でき、期限切れ後は自動的に利用権が消滅するため、上記の問題をすべて解決できます。

関連記事