レンタル機能の必要性
文化財の管理において、「貸出」は日常的に発生する重要な業務です。博物館間での作品の貸借、特別展への出品、調査のための一時的な移管など、文化財は所有機関以外の場所で利用されることが頻繁にあります。
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を第三者に転送できてしまう
所有権が借用者に移っているため、借用者は transferFrom や approve を使って自由に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では、利用権に有効期限を設定でき、期限切れ後は自動的に利用権が消滅するため、上記の問題をすべて解決できます。