[ERC4799] NFTの所有権を別のNFTで管理する仕組みを理解しよう！

# はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、NFTの所有権を別のNFTで管理することができる仕組みを提案している**ERC4799**についてまとめていきます！

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

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

他にも様々なEIPについてまとめています。

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

# 概要

**ERC4799**が提案しているインターフェースは、NFTがスマートコントラクトによってエスクロー（保管）されている状態でも、別のアドレスに所有権を指定できる仕組みを提案しています。
このインターフェースを活用することで、NFT同士の所有権関係を有向非巡回グラフ（Directed Acyclic Graph: DAG）として構築することが可能になります。
結果として、元のNFTの真正性を保持したまま、ラップ、貸し出し、担保利用、分割所有などの追加機能を既存のNFTに対して拡張できるようになります。

:::note info
### 有向非巡回グラフとは？

有向非巡回グラフ（Directed Acyclic Graph、略してDAG：ダグ）とは、「**向きのある線（矢印）で結ばれた点の集まり**」であり、ぐるぐると一周して元に戻る道（＝巡回・ループ）が存在しないという性質を持った構造です。

#### 具体例：作業の順序

例えば、あるプロジェクトで以下のような手順が必要だとします。

- A：資料を作成する  
- B：資料を確認する（Aの後）  
- C：確認後に印刷する（Bの後）

この関係を図にすると以下のようになります。

```
A → B → C
```

このように「**Aが終わってからB**」、「**Bが終わってからC**」といった一方向の流れでつながっており、逆戻りするようなループがありません。
これがDAGです。

#### DAGの特徴

- **有向**
    - 矢印には方向があり、「**どちらからどちらへ**」が明確です。
- **非巡回**
    - 矢印をたどっても、同じ点に戻ってくることはありません。
    - ぐるぐる回ることがないという意味です。
- 階層構造が自然と生まれます。
    - 上位から下位へ流れる構造で、依存関係を明確に表現できます。
:::

# 動機

多くのNFTは保有者に対して何らかのユーティリティ（実用性）を提供することを目的としています。
例えば、アパートの入居権、イベントチケットへのアクセス、トークンのエアドロップなどが該当します。
しかし現在のNFTでは、唯一検証可能なウォレットアドレスは「所有者」のみであるため、こうしたユーティリティの配布先はNFTの表面上の所有者に限定されてしまいます。
その結果、複雑な所有権の合意（例えば、貸し出しや担保など）を行いたい場合、それらのロジックをNFTコントラクト自体に最初から組み込む必要がありました。
これでは、すでに発行されたNFTに対して新しい機能を追加する手段が限られてしまいます。

**ERC4799**では、すでにミントされたNFTに対して、新たなコントラクトを通じて柔軟な所有権構造を後付けで定義できるようにすることです。
これにより、信頼性のある既存のNFTと、その真正性を保ったまま連携できる新しいユースケースが実現可能になります。

従来は、NFTに機能を追加するためには「ラップ」する方法しかなく、この場合NFT自体は別のコントラクトに預けられ、所有者として認識されなくなっていました。
その結果、ユーティリティの恩恵を受けることが難しくなっていました。
**ERC4799**を用いれば、NFTがラップされていても、外部アプリケーションが標準化された方法でラップ元の「**真の所有者**」を把握できるようになるため、元NFTの価値と機能をそのまま保持しながら、新しい機能を追加することが可能になります。

# 仕様

## インターフェース

```js:IERC4799NFT.sol
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";

interface IERC4799NFT is IERC165 {
    /// @dev This emits when ownership of any NFT changes by any mechanism.
    ///  This event emits when NFTs are created (`from` == 0) and destroyed
    ///  (`to` == 0). Exception: during contract creation, any number of NFTs
    ///  may be created and assigned without emitting Transfer. At the time of
    ///  any transfer, the approved address for that NFT (if any) is reset to none.
    event Transfer(
        address indexed from,
        address indexed to,
        uint256 indexed tokenId
    );

    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them throw
    /// @param tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    function ownerOf(uint256 tokenId) external view returns (address);
}
```

```js:IERC4799.sol
/// @title ERC-4799 Non-Fungible Token Ownership Designation Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-4799
/// Note: the ERC-165 identifier for this interface is [TODO].

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "./IERC4799NFT.sol";

interface IERC4799 is IERC165 {
    /// @dev Emitted when a source token designates its ownership to the owner of the target token
    event OwnershipDesignation(
        IERC4799NFT indexed sourceContract,
        uint256 sourceTokenId,
        IERC4799NFT indexed targetContract,
        uint256 targetTokenId
    );

    /// @notice Find the designated NFT
    /// @param sourceContract The contract address of the source NFT
    /// @param sourceTokenId The tokenId of the source NFT
    /// @return (targetContract, targetTokenId) contract address and tokenId of the parent NFT
    function designatedTokenOf(IERC4799NFT sourceContract, uint256 sourceTokenId)
        external
        view
        returns (IERC4799NFT, uint256);
}
```

### `Transfer`

```js
event Transfer(
    address indexed from,
    address indexed to,
    uint256 indexed tokenId
);
```

NFTの所有権が変更されたときに発行されるイベント。

NFTのミント（`from`がゼロアドレス）やバーン（`to`がゼロアドレス）もこのイベントで通知されます。
`Transfer`が発行された時、承認済みアドレスはリセットされます。

**パラメータ**

- `from`
	- 送付元アドレス。
- `to`
	- 送付先アドレス。
- `tokenId`
	- 対象となるNFTの`tokenId`。

### `OwnershipDesignation`

```js
event OwnershipDesignation(
    IERC4799NFT indexed sourceContract,
    uint256 sourceTokenId,
    IERC4799NFT indexed targetContract,
    uint256 targetTokenId
);
```

あるNFTが別のNFTに所有権を委任した時に発行されるイベント。

`sourceContract`に属するNFTが、`targetContract`に属する別のNFTを所有者として指定したことを示します。
これにより、所有権の委任関係（リンク）が構築されます。

**パラメータ**

- `sourceContract`
	- 委任元NFTのコントラクトアドレス。
- `sourceTokenId`
	- 委任元NFTのトークンID。
- `targetContract`
	- 委任先NFTのコントラクトアドレス。
- `targetTokenId`
	- 委任先NFTのトークンID。

### `ownerOf`

```js
function ownerOf(uint256 tokenId) external view returns (address);
```

NFTの所有者を取得する関数。

指定された`tokenId`に紐づくNFTの現在の所有者アドレスを返します。
ゼロアドレスに割り当てられている場合はエラーになります。

**引数**

- `tokenId`
	- 所有者を知りたいNFTの識別子。

**戻り値**

- `address`
	- NFTの所有者アドレス。

### `designatedTokenOf`

```js
function designatedTokenOf(IERC4799NFT sourceContract, uint256 sourceTokenId)
    external
    view
    returns (IERC4799NFT, uint256);
```

NFTが所有権を委任している先のNFTを取得する関数。

指定されたNFT（`sourceContract`と`sourceTokenId`で定義）が所有権を委任しているNFTを返します。
この戻り値が新たなNFTの所有者と見なされます。

**引数**

- `sourceContract`
	- 所有権を委任する元のNFTコントラクトアドレス。
- `sourceTokenId`
	- 所有権を委任する元のNFTのトークンID。

**戻り値**

- `IERC4799NFT`
	- 委任先のNFTコントラクトアドレス。
- `uint256`
	- 委任先のNFTのトークンID。

## その他

- クライアントは、**ERC4799**コントラクト自体の所有権を信用するのではなく、そのコントラクトが指定する先のNFTの所有者を信頼する必要があります（`ownerOf(target)`で確認）。
- したがって、NFTにユーティリティを提供するアプリケーションは、ERC4799コントラクトではなく最終的な指定先NFTの所有者アドレスに対してユーティリティを提供する必要があります。

このように、**ERC4799**は「**NFTの所有者を別のNFTに委任する**」ことを標準化し、従来のラップ方式の限界を克服しつつ既存のNFTの価値や真正性を損なわずに複雑なユースケースを構築可能にします。

# 補足

**ERC4799**は将来の互換性を最大化するために、まずNFTの本質的な機能だけを抽出した共通インターフェース `IERC4799NFT` を定義しています。
これは、すでに多くのNFTコントラクト（特に**ERC721**に準拠したもの）が暗黙的に実装しているインターフェースであり、`ownerOf` 関数によって「**NFTの識別子と所有者アドレスとの関係**」を表します。

この設計により、**ERC4799**は既存のNFT資産の価値や信頼性を損なうことなく、拡張的な機能（例えば、所有権の委任）を導入できるようになります。

提案の中核は `IERC4799` インターフェースであり、これがNFTの所有権委任を扱う「**Ownership Designation Contract（ODC）**」の標準です。
ODCは `designatedTokenOf` 関数を通じて「**あるNFTは別のNFTを所有者とみなす**」と表現します。
ただし、この指定が有効（＝信頼できる）であるのは、ODC自体が元のNFTの正式な所有者である場合に限られます。
この仕組みにより、「**すべてのNFTはちょうど1つの指定所有者しか持たない**」という不変条件が保たれます。

# 互換性

`IERC4799NFT` は、**ERC721**と完全に互換性があります。

# 参考実装

```js:ERC721Composable.sol
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

import "./IERC4799.sol";
import "./IERC4799NFT.sol";
import "./ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";

contract ERC721Composable is IERC4799, IERC721Receiver {
    mapping(IERC4799NFT => mapping(uint256 => IERC4799NFT)) private _targetContracts;
    mapping(IERC4799NFT => mapping(uint256 => uint256)) private _targetTokenIds;

    function designatedTokenOf(IERC4799NFT sourceContract, uint256 sourceTokenId)
        external
        view
        override
        returns (IERC4799NFT, uint256)
    {
        return (
            IERC4799NFT(_targetContracts[sourceContract][sourceTokenId]),
            _targetTokenIds[sourceContract][sourceTokenId]
        );
    }

    function designateToken(
        IERC4799NFT sourceContract,
        uint256 sourceTokenId,
        IERC4799NFT targetContract,
        uint256 targetTokenId
    ) external {
        require(
            ERC721(address(sourceContract)).ownerOf(sourceTokenId) == msg.sender ||
            ERC721(address(sourceContract)).getApproved(sourceTokenId) == msg.sender, 
            "ERC721Composable: Only owner or approved address can set a designate ownership");
        _targetContracts[sourceContract][sourceTokenId] = targetContract;
        _targetTokenIds[sourceContract][sourceTokenId] = targetTokenId;
        emit OwnershipDesignation(
            sourceContract, 
            sourceTokenId,  
            targetContract,
            targetTokenId
        );
    }

    function onERC721Received(
        address,
        address from,
        uint256 sourceTokenId,
        bytes calldata
    ) external override returns (bytes4) {
        ERC721(msg.sender).approve(from, sourceTokenId);
        return IERC721Receiver.onERC721Received.selector;
    }

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

```js:DesignatedOwner.sol
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

import "./IERC4799.sol";
import "./IERC4799NFT.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";

contract DesignatedOwner {
    function designatedOwnerOf(
        IERC4799NFT tokenContract,
        uint256 tokenId,
        uint256 maxDepth
    ) public view returns (address owner) {
        owner = tokenContract.ownerOf(tokenId);
        if (ERC165Checker.supportsInterface(owner, type(IERC4799).interfaceId)) {
            require(maxDepth > 0, "designatedOwnerOf: depth limit exceeded");
            (tokenContract, tokenId) = IERC4799(owner).designatedTokenOf(
                tokenContract,
                tokenId
            );
            return designatedOwnerOf(tokenContract, tokenId, maxDepth - 1);
        }
    }
}
```

# セキュリティ

**ERC4799**における主なセキュリティリスクは、「**所有権の指定が過剰に長く、またはループするような構造になってしまうこと**」にあります。
悪意のあるユーザーが、あるNFTから別のNFTへと所有権を次々に委任し続け、非常に長いチェーンを構築する可能性があります。
さらに悪質なケースでは、同じNFT間で循環するような構造（サイクル）を意図的に作ることも考えられます。

このような状況では、所有者を辿ろうとするアプリケーションが「**どこまでが本当の所有者か？**」を調べる処理の中で、無限ループに陥ったりガスを使い果たしたりして最終的に処理不能に陥るリスクがあります。

この問題に対処するため、クライアント側（アプリケーション）は`maxDepth`のような制限を設けるべきです。
つまり、所有権の指定関係を追跡する時には、あらかじめ「**最大で何段階まで辿るか**」を決めておき、それ以上の深さになった場合は処理を中断するように設計する必要があります。

この制限により、極端なケースでもアプリケーションがクラッシュせず、セキュリティ上の脆弱性を回避することが可能になります。

# 引用

David Buckman (@davidbuckman), Isaac Buckman (@isaacbuckman), "ERC-4799: Non-Fungible Token Ownership Designation Standard [DRAFT]," Ethereum Improvement Proposals, no. 4799, February 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4799.

# 最後に

今回は「NFTの所有権を別のNFTで管理することができる仕組みを提案している**ERC4799**」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

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