# [ERC4987] トークンをコントラクトに預けた時にアドレスと残高を紐付ける仕組みを理解しよう！

> ERC4987は、コントラクトが保有するトークン（ERC20、ERC721、ERC1155）に関する所有者情報および残高情報をインターフェースで外部に公開する提案をしています。

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

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、**ERC20**、**ERC721**、**ERC1155**をコントラクトに預けている時、預け元のアドレスが保有していたトークン残高を取得する仕組みを提案している**ERC4987**についてまとめていきます！

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

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

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

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

## 概要

**ERC4987**は、コントラクトが保有するトークン（**ERC20**、**ERC721**、**ERC1155**）に関する所有者情報および残高情報をインターフェースで外部に公開する提案をしています。

通常、トークンの保有者はEOAやコントラクトになりますが、**ERC4987**では「実際の保有者（コントラクト）」と「機能的な所有者（実際に操作・権利を持つユーザー）」という2つに分離して扱います。

**ERC4987**で提案されているインターフェースは、オンチェーン／オフチェーン問わず、さまざまなアプリケーションやサービスが「**機能的な所有者が誰であるか**」や「**トークンの残高がいくらか**」を確認できるようにするためのものです。

## 動機

近年、DeFiやNFTなど異なる分野が急速に融合し、コントラクトを介してトークンを保有・運用するユースケースが一般的になってきました。
これに伴い、以下のような問題が発生しています。

- ステーキングや貸出プールなどの仕組みでは、ユーザーは一度トークンをコントラクトに預ける必要があります。
- その結果、ウォレットで保有していないと見なされ、ガバナンス投票権やメンバーシップ判定などの対象から外れることがあります。

この問題を解決するために、**ERC4987**では「**保有しているのはコントラクトだが、実際の所有者は誰か**」を示すことができる仕組みを提供します。

これにより、ユーザーはトークンをステーキングや貸出に使いながらも、他のシステムでそのトークンの存在を証明して機能を活用できるようになります。
結果として、異なるプロトコル間でのコンポーザビリティが大幅に向上し、ユーザー体験と開発者の自由度が高まります。

### アプリケーション例

**ERC4987**の情報を利用することで、以下のようなシステムがトークンの機能的な所有者や残高を正しく判断できるようになります。

- ガバナンスシステム（投票権の付与など）
- ブロックチェーンゲーム（キャラクター所持判定など）
- PFP認証（プロフィール画像連携など）
- アートギャラリーやショーケース（作品の展示者の確認）
- トークンに基づいた会員制プログラム（保有者限定機能など）

## 仕様

### 共通要件

- 各インターフェースはそれぞれ `IERC165` を実装する必要があります。
- 以下のインターフェースIDを `supportsInterface` で `true` と返す必要があります。

| トークン種別 | インターフェース | Interface ID |
|--------------|------------------|--------------|
| ERC20        | IERC20Holder     | `0x74c89d54` |
| ERC721       | IERC721Holder    | `0x16b900ff` |
| ERC1155      | IERC1155Holder   | `0xced24c37` |

### IERC20Holder

```js:IERC20Holder.sol
/**
 * @notice the ERC20 holder standard provides a common interface to query
 * token balance information
 */
interface IERC20Holder is IERC165 {
  /**
   * @notice emitted when the token is transferred to the contract
   * @param owner functional token owner
   * @param tokenAddress held token address
   * @param tokenAmount held token amount
   */
  event Hold(
    address indexed owner,
    address indexed tokenAddress,
    uint256 tokenAmount
  );

  /**
   * @notice emitted when the token is released back to the user
   * @param owner functional token owner
   * @param tokenAddress held token address
   * @param tokenAmount held token amount
   */
  event Release(
    address indexed owner,
    address indexed tokenAddress,
    uint256 tokenAmount
  );

  /**
   * @notice get the held balance of the token owner
   * @dev should throw for invalid queries and return zero for no balance
   * @param tokenAddress held token address
   * @param owner functional token owner
   * @return held token balance
   */
  function heldBalanceOf(address tokenAddress, address owner)
    external
    view
    returns (uint256);
}
```

#### イベント

##### `Hold`  
  
トークンがコントラクトに送られた時に発行されるイベント。

- `owner`
    - 所有者のアドレス（ユーザー）。
- `tokenAddress`
    - 保有されている**ERC20**トークンのアドレス。
- `tokenAmount`
    - 保有されたトークン量。

##### `Release`  

トークンがコントラクトからユーザーに戻された時に発行されるイベント。

- `owner`
    - 所有者のアドレス（ユーザー）。
- `tokenAddress`
    - 保有されているERC20トークンのアドレス。
- `tokenAmount`
    - 保有されたトークン量。

#### 関数

##### `heldBalanceOf`  

指定されたユーザーがコントラクト内で保有しているERC20トークンの量を返す関数。

- トークンのコントラクトアドレスと機能的所有者を引数に指定します。
- 所有していない場合は `0` を返します。
- 存在しないトークンなどを指定した場合はエラーを返します。

### IERC721Holder

```js:IERC721Holder.sol
/**
 * @notice the ERC721 holder standard provides a common interface to query
 * token ownership and balance information
 */
interface IERC721Holder is IERC165 {
  /**
   * @notice emitted when the token is transferred to the contract
   * @param owner functional token owner
   * @param tokenAddress held token address
   * @param tokenId held token ID
   */
  event Hold(
    address indexed owner,
    address indexed tokenAddress,
    uint256 indexed tokenId
  );

  /**
   * @notice emitted when the token is released back to the user
   * @param owner functional token owner
   * @param tokenAddress held token address
   * @param tokenId held token ID
   */
  event Release(
    address indexed owner,
    address indexed tokenAddress,
    uint256 indexed tokenId
  );

  /**
   * @notice get the functional owner of a held token
   * @dev should throw for invalid queries and return zero for a token ID that is not held
   * @param tokenAddress held token address
   * @param tokenId held token ID
   * @return functional token owner
   */
  function heldOwnerOf(address tokenAddress, uint256 tokenId)
    external
    view
    returns (address);

  /**
   * @notice get the held balance of the token owner
   * @dev should throw for invalid queries and return zero for no balance
   * @param tokenAddress held token address
   * @param owner functional token owner
   * @return held token balance
   */
  function heldBalanceOf(address tokenAddress, address owner)
    external
    view
    returns (uint256);
}
```

#### イベント

##### `Hold`  

**ERC721**のNFTがコントラクトに移動した時に発行されるイベント。

- `tokenId`
    - 保有されたNFTのID。

##### `Release`  

NFTが元のユーザーに戻された時に発行されるイベント。

#### 関数

##### `heldOwnerOf`  

指定されたNFTの「機能的な所有者（ユーザー）」のアドレスを返す関数。  
トークンが保持されていない場合は`0x0`を返します。

##### `heldBalanceOf`  

指定されたユーザーが保有しているNFTの数を返す関数。  
戻り値は `owner` がそのトークンコントラクトに対して保有しているNFTの総数です。

### IERC1155Holder

```js:IERC1155Holder.sol
/**
 * @notice the ERC1155 holder standard provides a common interface to query
 * token balance information
 */
interface IERC1155Holder is IERC165 {
  /**
   * @notice emitted when the token is transferred to the contract
   * @param owner functional token owner
   * @param tokenAddress held token address
   * @param tokenId held token ID
   * @param tokenAmount held token amount
   */
  event Hold(
    address indexed owner,
    address indexed tokenAddress,
    uint256 indexed tokenId,
    uint256 tokenAmount
  );

  /**
   * @notice emitted when the token is released back to the user
   * @param owner functional token owner
   * @param tokenAddress held token address
   * @param tokenId held token ID
   * @param tokenAmount held token amount
   */
  event Release(
    address indexed owner,
    address indexed tokenAddress,
    uint256 indexed tokenId,
    uint256 tokenAmount
  );

  /**
   * @notice get the held balance of the token owner
   * @dev should throw for invalid queries and return zero for no balance
   * @param tokenAddress held token address
   * @param owner functional token owner
   * @param tokenId held token ID
   * @return held token balance
   */
  function heldBalanceOf(
    address tokenAddress,
    address owner,
    uint256 tokenId
  ) external view returns (uint256);
}
```

#### イベント

##### `Hold`  

**ERC1155**トークンがコントラクトに送られた時に発行されるイベント。
- `tokenId`
    - トークンのID。
- `tokenAmount`
    - 保有された数量。

##### `Release`  

トークンがユーザーに戻された時に発行されるイベント。

#### 関数

##### `heldBalanceOf`  

指定されたユーザーが、特定の**ERC1155**トークンのトークンIDの保有数を返す関数。

## 補足

### トークンアドレスを引数に含める理由
`heldBalanceOf`などの関数に`tokenAddress`を引数として含めるのは、1つのコントラクトが複数の異なるトークンを扱う場合を想定しているためです。

- 単一トークン専用コントラクト
    - 引数を無視するか、固定のアドレスを想定すればよい。
- 複数トークン対応コントラクト
    - 引数を使ってトークンごとに分けて管理可能。

この設計により、より汎用性の高いインターフェースとなっています。

### トークン種別ごとにインターフェースを分ける理由

**ERC20** / **ERC721** / **ERC1155** ではトークンの管理方法や特性が大きく異なるため、それぞれに対して独立したインターフェースが用意されています。

- **ERC20**
    - 単純な残高管理。
- **ERC721**
    - 個別の`tokenId`ごとの所有管理。
- **ERC1155**
    - トークンIDと数量の組み合わせで管理。

そのため、ひとつのコントラクトが複数のトークン規格に対応する場合は、それぞれのインターフェースを必要に応じて実装すればよいという設計です。

## 互換性

**ERC4987**は全ての既存の**ERC20**、**ERC721**、**ERC1155**トークンと完全に互換性があります。

既存のトークンを保有しているコントラクトでは、インターフェースの実装が必要です。

**ERC4987**に準拠したDapps（ガバナンス、ゲーム、認証など）は、`heldBalanceOf` や `heldOwnerOf` といった関数を参照するように設計を更新する必要があります。

## セキュリティ

### 信頼できないコントラクトからの情報に注意

任意のコントラクトがこのインターフェースを実装できるため、悪意のあるコントラクトが虚偽の所有情報を返す可能性があります。
例えば以下のような悪用が考えられます。

- ガバナンスシステムで実際には保有していないトークンを保有しているように装うことで投票権を不正取得する。
- ゲームで特定のアイテムやキャラクターを保有していると偽って優遇される。
- NFTによる会員制サービスで偽の保有者がアクセス権を得る。

**ERC4987**が提案しているインターフェースを参照するだけで意思決定を行うようなアプリケーションは、実装しているコントラクトの信頼性を十分に検証する必要があります。

### 実際のトークン保有状態との整合性を確認する

情報を使用する時は、以下のような整合性チェックを組み込むことが推奨されます。

- 該当するトークンコントラクト（**ERC20**、**ERC721**、**ERC1155**）に対して、ホルダーコントラクトが実際にトークンを保有しているかを確認する。
- `heldBalanceOf` の値と、トークンコントラクトの `balanceOf` の結果が矛盾していないかを確認する。
- **ERC721** であれば `ownerOf` の戻り値がホルダーコントラクトになっていることを確認する。

## 引用

Devin Conley (@devinaconley), "ERC-4987: Held token interface [DRAFT]," Ethereum Improvement Proposals, no. 4987, September 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4987.

## 最後に

今回は「**ERC20**、**ERC721**、**ERC1155**をコントラクトに預けている時、預け元のアドレスが保有していたトークン残高を取得する仕組みを提案している**ERC4987**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下の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
