# [ERC7510] 派生NFTの関係性を階層的に管理する仕組みを理解しよう！

> ERC721を拡張したERC7510では、異なるコントラクト間に存在するNFT同士の階層関係を管理する仕組みを提供しています。

- URL: https://cardene.dev/articles/800f042129b63977e8f3
- 公開日: 2025-12-04
- 更新日: 2025-12-04
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

今回は、複数のNFTコレクションにまたがる階層関係を保存・参照できる仕組みを提案している**ERC7510**についてまとめていきます！

:::message
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-7510

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

**ERC721**を拡張した**ERC7510**では、異なるコントラクト間に存在するNFT同士の階層関係を管理する仕組みを提供しています。
ここでいう階層関係とは、あるNFTが別のNFTを「**親**」として持つ構造のことで、例えば派生作品として作られたNFTが、その元となったNFTとのつながりを明確に示せるようにするための仕組みです。

:::message
**ERC721**については以下の記事を参考にしてください。
:::

[\[ERC721\] ERC721を1からわかりやすく解説](/articles/0570a8d05bc7c9b9593f)

**ERC7510**は、この関係を取得するためのインターフェースを提供し、特定のNFTがどのNFTを親として持つのか、また指定した2つのNFTの間に親子関係が存在するかどうかを問い合わせることができます。

この仕組みを導入することで、異なるNFTコレクション間でも体系的なつながりを持たせることができ、NFT同士の関係性を透明かつ追跡可能な形で扱えるようになります。

## 動機

NFTには、元となる作品をもとに新しい派生作品を作るケースが増えています。
例えば、2DアートのNFTから3Dモデルを制作し、それを新しいNFTとして発行するケースがあります。
このとき、新しく作られたNFTと元のNFTの関係をきちんと記録したいというニーズがあります。

また、1つのNFTが複数の親NFTから派生することもあります。
映画作品を例にすると、複数のNFTキャラクターが登場する映像作品NFTを制作する場合、それぞれのキャラクターNFTが「**親**」となり、動画NFTが「**子**」となる関係になります。

既存の**ERC6150**にはNFT間に階層構造を持たせる仕組みがありますが、これは同じコントラクト内のNFT同士にしか適用できません。実際のユースケースでは、新しく派生作品のNFTコレクションを別コントラクトとして作成するケースが多く、既存仕様では対応が難しい状況があります。

:::message
**ERC6150**については以下の記事を参考にしてください。
:::

[\[ERC6150\] NFTにLinuxのファイルシステムのような階層構造を持たせる仕組みを理解しよう！](/articles/38c75ae824373684aecd)

さらに、知的財産のライセンス活用などでは複数の親NFTを持つケースが一般化していますが、**ERC6150**では1つのNFTに複数の親を設定する機能がありません。
この点を解決するためにも、異なるコントラクト間で、かつ複数の親子関係を扱える拡張仕様が求められています。

## 仕様

```js:IERC7510.sol
/// @notice The struct used to reference a token in an NFT contract
struct Token {
    address collection;
    uint256 id;
}

interface IERC7510 {

    /// @notice Emitted when the parent tokens for an NFT is updated
    event UpdateParentTokens(uint256 indexed tokenId);

    /// @notice Get the parent tokens of an NFT
    /// @param tokenId The NFT to get the parent tokens for
    /// @return An array of parent tokens for this NFT
    function parentTokensOf(uint256 tokenId) external view returns (Token[] memory);

    /// @notice Check if another token is a parent of an NFT
    /// @param tokenId The NFT to check its parent for
    /// @param otherToken Another token to check as a parent or not
    /// @return Whether `otherToken` is a parent of `tokenId`
    function isParentToken(uint256 tokenId, Token memory otherToken) external view returns (bool);

    /// @notice Set the parent tokens for an NFT
    /// @param tokenId The NFT to set the parent tokens for
    /// @param parentTokens The parent tokens to set
    function setParentTokens(uint256 tokenId, Token[] memory parentTokens) external;

}
```

#### イベント

##### `UpdateParentTokens`

```javascript
event UpdateParentTokens(uint256 indexed tokenId);
```

NFTの親トークン情報が更新された時に発行されるイベント。
指定したNFTに紐づく親トークンの一覧が変更された時に発行されるイベントです。
外部のアプリケーションやインデックスサービスが、このイベントを受け取ることで、NFTがどの親から派生しているかを最新状態に同期できます。
`tokenId` がインデックス付きで設計されているため、特定のNFTに関連する更新のみを効率的に検出できます。

**パラメータ**

* `tokenId`
  * 親トークン情報が更新されたNFTのID。

#### 構造体

##### `Token`

```javascript
struct Token {
    address collection;
    uint256 id;
}
```

NFTを指し示すための情報をまとめた構造体。
親子関係を管理する際に、別のコントラクトに存在するNFTも参照できるよう、コントラクトアドレスとトークンIDをセットで扱える構造体です。
単一の識別子では表現できない「**どのコントラクトのどのNFTか**」を正確に示すために、この形式が採用されています。
階層構造を扱う際に、異なるNFTコレクション同士の関係を表現できることが特徴です。

**パラメータ**

* `collection`
  * NFTが所属するコントラクトのアドレス。
* `id`
  * コントラクト内でのトークンID。

#### 関数

##### `parentTokensOf`

```javascript
function parentTokensOf(uint256 tokenId) external view returns (Token[] memory);
```

指定したNFTが持つ親トークン一覧を取得する関数。
NFTに設定されている親トークンをすべて取得します。
この関数を使うことで、あるNFTが「**どのNFTから派生しているか**」を把握できます。
返り値は配列であるため、複数の親を持つケースにも対応しています。
派生元の関係をアプリケーション側で表示したり、ライセンス管理やIP活用の文脈で利用できます。

**引数**

* `tokenId`
  * 親トークンを取得したいNFTのID。

**戻り値**

* `Token[]`
  * 指定したNFTの親トークン情報をまとめた配列。

##### `isParentToken`

```javascript
function isParentToken(uint256 tokenId, Token memory otherToken) external view returns (bool);
```

指定したNFTに対して、別のトークンが親であるかどうかを判定する関数。
NFTに設定されている親トークンリストの中に、与えられたトークンが含まれているかをチェックします。
個別の関係性を確認したいときに使われます。
NFT同士の派生関係を検証したり、特定の親子関係を基準にアクションを制御するようなユースケースで役立ちます。

**引数**

* `tokenId`
  * 確認したいNFTのID。
* `otherToken`
  * 親かどうかを確かめたいトークンを表す `Token` 構造体。

**戻り値**

* `bool`
  * `otherToken` が `tokenId` の親であれば `true`、そうでなければ `false`。

##### `setParentTokens`

```javascript
function setParentTokens(uint256 tokenId, Token[] memory parentTokens) external;
```

NFTに紐づく親トークンの一覧を設定する関数。
管理者やコントラクトの権限者が、NFTの親トークン情報を更新するための関数です。
NFTの派生元が変わったり、親となるNFTを追加または削除したい場合にこの関数を利用します。
更新が成功すると `UpdateParentTokens` イベントが発行され、外部にも変更が伝わります。
複数親に対応するため、引数は配列になっています。

**引数**

* `tokenId`
  * 親トークンを設定したいNFTのID。
* `parentTokens`
  * NFTの親として登録する `Token` 構造体の配列。

## 補足

#### ERC6150との主な違い

**ERC7510**は**ERC6150**と似た目的を持っていますが、設計上の重要な違いが2つあります。

1つ目は異なるコントラクトに存在するNFTを親として参照できることです。
**ERC6150**は同じコントラクト内で階層構造を扱う仕組みでしたが、現実のNFT運用では、派生作品が別コントラクトで発行されるケースが多くあります。
例えば、元のアートNFTと、その3DモデルNFTが別コレクションとして存在する場合です。
**ERC7510**は、こうしたケースにも対応できるようになっています。

2つ目は複数の親NFTを設定できることです。
IP（知的財産）の活用では、1つの作品が複数の元作品から派生することがよくあります。
例えば、複数キャラクターNFTが登場する映像作品NFTなどです。
**ERC6150**は1つのNFTにつき親は1つしか持てませんでしたが、**ERC7510**では複数設定できるようになっています。

ただし、違いが大きくなりすぎないよう、命名ルールなどはなるべく**ERC6150**と揃えてあり、全体の使い勝手が統一されています。

#### 子トークンを記録しない理由

**ERC7510**が記録するのは「**親**」だけであり、「**子**」を記録する仕組みはインターフェースに含まれていません。
これは設計上の意図によるもので、子を扱わない理由は以下のように整理できます。

まず、元となるNFT（親）は先に発行され、派生NFT（子）は後から発行されます。
そのため、新しく派生NFTを作るときには、そのNFTがどの親から派生しているかが事前にわかっています。
結果として、派生NFT側で親を記録することは問題なくできます。

一方で、元となるNFTには、発行した時点では自分にどんな派生NFT（子）が生まれるかを知る方法がありません。
もし子を記録しようとすると、派生NFTの発行時に「**元のNFTに子を追加する**」処理を行う必要があります。
しかし、派生NFTと元NFTが別コントラクトだった場合、書き込み権限が異なるため、1つのトランザクションで同時に処理することができません。
さらに、権限の問題から元NFTのコントラクトへ直接書き込むことが許可されないケースもあります。

つまり、技術的にも運用的にも「**子を登録する**」ことがうまく成立しない状況があるため、**ERC7510**は親のみを記録するシンプルな形を採用しています。

この結果、派生NFTの発行時に必要な情報だけを確実に扱うことができ、コントラクトをまたいだ実装も容易になります。

## 参考実装

```js:ERC7510.sol
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

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

import "./IERC7510.sol";

contract ERC7510 is ERC721, IERC7510 {

    mapping(uint256 => Token[]) private _parentTokens;
    mapping(uint256 => mapping(address => mapping(uint256 => bool))) private _isParentToken;

    constructor(
        string memory name, string memory symbol
    ) ERC721(name, symbol) {}

    function supportsInterface(
        bytes4 interfaceId
    ) public view virtual override returns (bool) {
        return interfaceId == type(IERC7510).interfaceId || super.supportsInterface(interfaceId);
    }

    function parentTokensOf(
        uint256 tokenId
    ) public view virtual override returns (Token[] memory) {
        require(_exists(tokenId), "ERC7510: query for nonexistent token");
        return _parentTokens[tokenId];
    }

    function isParentToken(
        uint256 tokenId, Token memory otherToken
    ) public view virtual override returns (bool) {
        require(_exists(tokenId), "ERC7510: query for nonexistent token");
        return _isParentToken[tokenId][otherToken.collection][otherToken.id];
    }

    function setParentTokens(
        uint256 tokenId, Token[] memory parentTokens
    ) public virtual override {
        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC7510: caller is not owner or approved");
        _clear(tokenId);
        for (uint256 i = 0; i < parentTokens.length; i++) {
            _parentTokens[tokenId].push(parentTokens[i]);
            _isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id] = true;
        }
        emit UpdateParentTokens(tokenId);
    }

    function _burn(
        uint256 tokenId
    ) internal virtual override {
        super._burn(tokenId);
        _clear(tokenId);
    }

    function _clear(
        uint256 tokenId
    ) private {
        Token[] storage parentTokens = _parentTokens[tokenId];
        for (uint256 i = 0; i < parentTokens.length; i++) {
            delete _isParentToken[tokenId][parentTokens[i].collection][parentTokens[i].id];
        }
        delete _parentTokens[tokenId];
    }

}
```

## セキュリティ

#### 親トークンが無効になる可能性

**ERC7510**では、NFTに紐づく親トークンの情報を保持しますが、この親トークンの参照が後から無効になるケースがあります。
無効になる理由としては2つあります。

1つ目は、親トークンとして指定されているNFTが後から `burn` される可能性があることです。
NFTをBurnすると、そのトークンIDは存在しなくなります。
そのため、派生元として記録されている親トークンがすでに存在しない状態になることがあります。

2つ目は、`setParentTokens` を実装するコントラクトが、親トークンの正当性チェックを行わない可能性があることです。
この関数は標準インターフェースとして定義されていますが、実際にどのようなチェックを行うかは実装コントラクト次第です。
開発者が「**本当に存在するNFTか**」、「**適切なコントラクトか**」といった検証を省略すれば、存在しないNFTを親として登録できてしまいます。

これらの理由により、記録されている親トークンの情報が、必ずしも正しいとは限りません。

#### アプリケーション側で必要な確認

**ERC7510**を利用するアプリケーションは、`parentTokensOf` で取得した親トークン情報をそのまま信用してはいけません。
参照されている親トークンが本当に存在するかどうかを、アプリケーション側で確認する必要があります。

たとえば、`ownerOf` を呼び出して所有者が取得できるか、あるいは該当のトークンIDが存在しない場合の挙動をチェックするなど、コントラクトが提供する一般的なNFT検証の手法を利用できます。

この処理を怠ると、アプリケーション側で「**存在しないNFTに基づいた派生関係**」を表示してしまい、誤ったデータを扱う恐れがあります。
そのため、**ERC7510**を利用するアプリケーションでは、親トークンの正当性検証をセキュリティ対策として必ず行うことが推奨されます。

## 引用

Ming Jiang (@minkyn), Zheng Han (@hanbsd), Fan Yang (@fayang), "ERC-7510: Cross-Contract Hierarchical NFT [DRAFT]," Ethereum Improvement Proposals, no. 7510, August 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7510.

## 最後に

今回は「複数のNFTコレクションにまたがる階層関係を保存・参照できる仕組みを提案している**ERC7510**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

他の媒体でも情報発信しているのでぜひ他も見ていってください！

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
