# [ERC8217] AIエージェントを外部トークンの制御者へ結び付ける仕組みを理解しよう！

> ERC8004では、AIエージェントの識別情報を管理するレジストリが、各エージェントをNFTとして登録します。

- URL: https://cardene.dev/articles/e5892053dfc3cd83307c
- 公開日: 2026-09-06
- 更新日: 2026-09-06
- トピック: Blockchain
- タグ: ethereum, erc, ai, ブロックチェーン

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください！

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://twitter.com/cardene777

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

ERC8217は、ERC8004で登録したAIエージェントと、既存のNFTやトークンを結び付ける提案です。
外部トークンの所有者が変わると、結び付いたエージェントの制御者も同じ移転に追従できるようにします。

## 概要

#### ERC8004とは

ERC8004では、AIエージェントの識別情報を管理するレジストリが、各エージェントをNFTとして登録します。
このレジストリを `Identity Registry` と呼び、NFTのトークンIDをエージェントの `agentId` として使います。

登録したエージェントの詳しい情報は、`agentURI` が指す登録ファイルから確認できます。
一方、レジストリは追加情報をオンチェーンの `metadata` にキーと値の組み合わせで記録できます。
ERC8217が利用するのは、この `agentId` と `metadata` です。

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

[\[ERC8004\] トラストレスに動くエージェントの仕組みを理解しよう！](/articles/4958873135ba9892c298)

#### ERC8217が追加する仕組み

既存のプロジェクトNFTやキャラクターNFTは、ERC8004のエージェントとは別のトークンコントラクトで管理されています。
ERC8217は、`agentId` と外部トークンの組み合わせを、チェーンごとに1つ用意する専用コントラクトへ記録します。
規格ではこのコントラクトを `binding contract` と呼びます。
以下、バインディングコントラクトと表記します。

例えば、`agentId = 17` のエージェントを、あるERC721コントラクトの `tokenId = 42` に結び付ける場面を考えます。
エージェント側の `metadata` には、バインディングコントラクトの20バイトのアドレスだけを書き込みます。
そしてクライアントは、そのコントラクトの `bindingOf(17)` を呼び出し、`standard = ERC721`、トークンコントラクトのアドレス、`tokenId = 42` を読み出します。

![ERC8004のagentIdからバインディングコントラクトを経由して外部トークンを確認する全体像](/images/qiita-4172568a-6c1c-4faa-b520-97515d661b13.webp)

外部トークンがAliceからBobへ送付されても、`agentId = 17` が `tokenId = 42` を指す関係は変わりません。
その代わり、バインディングコントラクトが採用する制御条件に従い、現在の所有者であるBobをエージェントの制御者として確認できます。
ただし、所有権や残高をどのように認可へ使うかはERC8217の対象外であり、バインディングコントラクトが定めます。

## 動機

ERC8004の登録自体もNFTです。
既存のプロジェクトNFTを持つ人にエージェントを制御させる場合、どの登録NFTをどのプロジェクトNFTへ結び付けたかを記録する必要があります。
その記録方法がプロジェクトごとに異なると、ウォレット、マーケットプレイス、インデクサーは、プロジェクトごとのデータ形式と確認方法を個別に組み込むことになります。

ERC8217がない場合、あるプロジェクトでは `metadata` へトークンコントラクトを直接書き、別のプロジェクトでは独自の管理コントラクトを置くかもしれません。
すると、クライアントは `metadata` を読んだだけでは、外部トークンによる制御が使われているのか、どのコントラクトへ問い合わせるのかを判断できません。
そこでERC8217は、`agent-binding` という共通の項目名と、20バイトのアドレス形式を固定します。

![プロジェクトごとに異なる確認方法とERC8217で共通化した確認手順の比較](/images/qiita-7af16580-7c92-48c1-aa9f-24c3284c32e4.webp)

共通化するのは、バインディングコントラクトを見つけ、`bindingOf` から `standard`、`tokenContract`、`tokenId` を読むところまでです。
認可の条件まで1つに固定すると、ERC721の所有権と、ERC1155やERC6909の残高を同じ方法で判定できません。
そのため、発見方法はERC8217が固定し、現在の制御者を決める処理はバインディングコントラクトへ残します。

## 仕様

ERC8217の仕様は、登録時と検証時で役割が分かれます。
エージェントを登録するレジストリやアダプターは、`agent-binding` へ正規のバインディングコントラクトを書き込みます。
バインディングコントラクトは初回登録時に `Binding` を保存し、その後は `standard`、`tokenContract`、`tokenId` を変更しません。
クライアントは `bindingOf(agentId)` から、この3つの値を読み出します。

### metadataとBinding

`agent-binding` は、ERC8004の `metadata` で使う予約項目です。
値には、チェーンごとに1つだけ使う正規のバインディングコントラクトのEVMアドレスを、ちょうど20バイトで書き込みます。

```js
abi.encodePacked(bindingContract)
```

`bindingContract` はEVMアドレスなので、余分なデータや32バイトへの埋め合わせを加えません。
例えば、アドレスが以下であれば、`metadata` の値も同じ20バイトです。

```text
0x9c4e8f2a1b7d6e3c0a5f8d2b9e1c4a7f3d6e8b0c
```

この20バイトから、トークン規格、トークンコントラクト、トークンIDを復元するわけではありません。
クライアントは20バイトを呼び出し先のアドレスとして解釈し、そのコントラクトの `bindingOf(agentId)` から3つの値を取得します。

![metadataには20バイトの参照先だけを保存しBindingから外部トークン情報を取得する関係](/images/qiita-0193fc8f-03d4-49fa-83be-5cfd89148d3e.webp)

`metadata` は問い合わせ先を示し、`Binding` は `standard`、`tokenContract`、`tokenId` を示します。
クライアントは、20バイトのアドレスと外部トークンの情報を取り違えずに読み出せます。

### `IERCAgentBindings`

バインディングコントラクトは、少なくとも以下のインターフェースを公開します。
登録用関数、管理機能、URI更新、ウォレットとの結び付け、アップグレード用の管理機能を追加しても構いませんが、それらはERC8217の検証対象に含まれません。

```js
pragma solidity ^0.8.24;

interface IERCAgentBindings {
    enum TokenStandard {
        ERC721,
        ERC1155,
        ERC6909
    }

    struct Binding {
        TokenStandard standard;
        address tokenContract;
        uint256 tokenId;
    }

    event AgentBound(
        uint256 indexed agentId,
        TokenStandard indexed standard,
        address indexed tokenContract,
        uint256 tokenId,
        address registeredBy
    );

    function bindingOf(uint256 agentId) external view returns (Binding memory);
}
```

このインターフェースは、`Binding` を読む `bindingOf` と、初回登録を追跡する `AgentBound` イベントだけを検証用に定めています。
つまり、クライアントは実装固有の登録方法を知らなくても、登録後の結果を同じ呼び出しで確認できます。

#### `TokenStandard`

```js
enum TokenStandard {
    ERC721,
    ERC1155,
    ERC6909
}
```

`TokenStandard` は、結び付け先のトークン規格を区別するEnum型です。
`Binding.standard` の値を見れば、`tokenContract` と `tokenId` をどのトークン規格として解釈するかが分かります。

| 値 | 対象 |
| --- | --- |
| `ERC721` | 1つのトークンIDに1つの所有者を記録するNFT |
| `ERC1155` | 同じコントラクトで複数種類のトークンと残高を管理するトークン |
| `ERC6909` | 1つのコントラクトで複数のトークンIDと残高を管理する軽量なトークン |

Enum型の各値はトークンの種類を特定するだけで、誰を制御者とみなすかまでは決めません。
例えばERC1155では、1単位以上の保有を条件にするのか、指定数量以上を求めるのかをバインディングコントラクトが決めます。

#### `Binding`

```js
struct Binding {
    TokenStandard standard;
    address tokenContract;
    uint256 tokenId;
}
```

`Binding` は、1つの `agentId` へ結び付けた外部トークンを表す構造体です。
バインディングコントラクトが初回登録時に保存し、クライアントが `bindingOf(agentId)` から読み出します。

- **フィールド**

| フィールド | 詳細 |
| --- | --- |
| `standard` | `tokenContract` と `tokenId` を解釈するトークン規格です。 |
| `tokenContract` | 外部トークンを管理するコントラクトのアドレスです。 |
| `tokenId` | 外部トークンコントラクトの中で、結び付け対象を特定するIDです。 |

例えば `standard = ERC721`、`tokenContract = 0x111...1111`、`tokenId = 42` なら、`agentId` はそのERC721の42番へ結び付いています。
ERC8217に準拠するバインディングコントラクトは、この3つの値を初回登録後に変更しません。

#### `AgentBound`

```js
event AgentBound(
    uint256 indexed agentId,
    TokenStandard indexed standard,
    address indexed tokenContract,
    uint256 tokenId,
    address registeredBy
);
```

`AgentBound` イベントは、ある `agentId` の `Binding` を初めて保存した時に発行されます。
インデクサーは、インデックス付きの `agentId`、`standard`、`tokenContract` で対象を絞り込み、どの外部トークンが登録されたかを追跡できます。

- **フィールド**

| フィールド | 詳細 |
| --- | --- |
| `agentId` | 結び付けるERC8004のエージェントIDです。イベント検索に使えるようインデックスが付きます。 |
| `standard` | 外部トークンの規格です。イベント検索に使えるようインデックスが付きます。 |
| `tokenContract` | 外部トークンコントラクトのアドレスです。イベント検索に使えるようインデックスが付きます。 |
| `tokenId` | 外部トークンコントラクト内のトークンIDです。 |
| `registeredBy` | `Binding` の登録処理を呼び出したアドレスです。 |

イベントログからは、誰がどの `agentId` をどのトークンへ結び付けたかを追えます。
ただし、クライアントはイベントログではなく、`bindingOf(agentId)` が返す `Binding` を使って検証します。
イベントを取りこぼしても、この関数を呼び出せば現在保存されている `Binding` を確認できます。

#### `bindingOf`

```js
function bindingOf(uint256 agentId) external view returns (Binding memory);
```

`bindingOf` は、指定したエージェントに保存された `Binding` を返す参照関数です。
クライアントは `metadata` から見つけたバインディングコントラクトに対して呼び出します。

1. **`agentId` の `Binding` を読み出す**

    バインディングコントラクトは、引数の `agentId` に結び付いた `Binding` をコントラクトストレージから読み出す。

2. **`Binding` を返す**

    `standard`、`tokenContract`、`tokenId` を含む構造体を呼び出し元へ返す。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `agentId` | 結び付けた外部トークンを確認したいERC8004のエージェントIDです。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `Binding memory` | トークン規格、トークンコントラクト、トークンIDをまとめた `Binding` です。 |

- **実行条件**

- 呼び出し元の権限条件は定められていません。
- `metadata` から取得した正規のバインディングコントラクトへ呼び出します。
- 未登録の `agentId` に対する返り値やrevert条件は、このインターフェースでは定められていません。

最後の条件は、バインディングコントラクトごとに異なります。
クライアントは呼び出しの成功だけでなく、返された `Binding` をバインディングコントラクトの仕様に従って検証します。

### 登録時に守る7つの条件

`agentId` と外部トークンをバインディングコントラクトへ初めて登録する時は、以下の7つの条件を満たす必要があります。
登録後は、`agent-binding` のアドレスと、`Binding` に保存した `standard`、`tokenContract`、`tokenId` を変更できません。

- **予約項目**

    `agent-binding` を予約し、信頼できない呼び出し元が任意の値で上書きできないようにします。

- **20バイトの値**

    `metadata` には正規のバインディングコントラクトのアドレスを、ちょうど20バイトで書き込みます。

- **呼び出し先との一致**

    `metadata` のアドレスは、その `agentId` に対して `bindingOf` を提供するコントラクトと一致させます。

- **チェーンごとの正規コントラクト**

    同じチェーンでは、正規のバインディングコントラクトを1つだけ使います。

- **初回イベント**

    ある `agentId` の `Binding` を初めて保存する時に `AgentBound` イベントを発行します。

- **登録したアドレスを変更しない**

    `agent-binding` へ書き込んだバインディングコントラクトのアドレスを変更しません。

- **`Binding` の3つの値を変更しない**

    `bindingOf(agentId)` が返す `standard`、`tokenContract`、`tokenId` を変更しません。

`agent-binding` のアドレスと `Binding` の3つの値が変わらないため、クライアントやインデクサーは1度検証した `Binding` をキャッシュできます。
ただし、外部トークンの所有者や残高は変化するので、現在の制御者を判断する時は外部トークン側の値を改めて確認します。

### 検証手順

クライアントは、`metadata` とバインディングコントラクトを分けて検証します。
どこか1段階でも失敗した場合、クライアントはエージェントと外部トークンの結び付きを確認できないものとして処理を止めます。

![ERC8004のmetadata読取りからbindingOfの返り値を確認するまでの検証手順](/images/qiita-363d500d-eb57-480a-8976-091505c0204b.webp)

1. **`agent-binding` を読み出す**

    クライアントは、ERC8004のレジストリから対象 `agentId` の `agent-binding` を取得する。

2. **20バイトのアドレスとして解釈する**

    値の長さが20バイトであることを確認し、バインディングコントラクトのEVMアドレスへ変換する。
    項目がない場合や長さが異なる場合は、未検証として処理を止める。

3. **`bindingOf(agentId)` を呼び出す**

    取得したアドレスへ `agentId` を渡し、`Binding` を読み出す。
    呼び出しが失敗する場合は、未検証として処理を止める。

4. **制御条件を確認する**

    返された `standard`、`tokenContract`、`tokenId` を使い、バインディングコントラクトが定める認可処理へ進む。
    例えばERC721の所有権を使うバインディングコントラクトなら、外部トークンコントラクトの `ownerOf(tokenId)` から現在の所有者を確認する。

4番目は、`Binding` の検証後に実際の制御者を求めるための処理です。
ERC8217が固定するのは3番目までであり、所有権や残高から制御者を決める規則はバインディングコントラクトに残ります。

## 補足

ERC8217は、`metadata` を小さくするためだけにバインディングコントラクトを置くわけではありません。
外部トークンの識別と、そのトークンを認可へ使う規則を分けることで、クライアントが確認すべきコントラクトを明示します。

![metadataから発見できる情報とバインディングコントラクトを信頼する範囲](/images/qiita-8bfb4972-96f0-45a7-b40c-92a9bee5c20c.webp)

#### バインディングコントラクトを保存する理由

トークンコントラクトとトークンIDだけでは、誰がエージェントを制御できるかを決められません。
例えば同じERC1155トークンでも、1単位以上を持つアドレスを制御者とするコントラクトと、10単位以上を持つアドレスだけを制御者とするコントラクトでは、認可結果が異なります。

そこで `metadata` には、認可処理を実装したバインディングコントラクトのアドレスを保存します。
クライアントは `bindingOf` の返り値に加え、そのコントラクトが所有権や残高をどう判定するかを確認できます。
同じトークンを参照していても、認可規則が異なるコントラクトを別々に評価できます。

#### metadataへ複製しない情報

トークン規格、トークンコントラクト、トークンIDは、バインディングコントラクトの `Binding` へ保存します。
`metadata` には、そのコントラクトの20バイトのアドレスだけを書き込みます。

もし両方へ3項目を保存し、片方だけを更新すると、`metadata` と `bindingOf` の返り値が食い違います。
ERC8217では、トークン規格、トークンコントラクト、トークンIDを `bindingOf` の返り値だけから読み出します。
さらに `Binding` は変更できないため、1度確認した3つの値を後から別のトークンへ差し替えられません。

## 互換性

ERC8217は、既存の登録レジストリや外部トークンの動作を変更しません。
追加するのは、バインディングコントラクトのアドレスを保存する `agent-binding` と、`Binding` を返す `IERCAgentBindings` です。

#### ERC8004との関係

ERC8004のレジストリは、エージェントごとに `agentId` と `metadata` を管理します。
ERC8217に対応するレジストリやアダプターは、既存の `metadata` に `agent-binding` を書き込むため、新しいエージェント識別子を作りません。

一方、既存のERC8004レジストリやアダプターは、`agent-binding` に対応しなくても構いません。
`agent-binding` に対応するレジストリやアダプターは、20バイトの共通形式を読み、同じ手順でバインディングコントラクトを見つけられます。

#### 外部トークンとの関係

ERC721は、トークンIDごとに1つの所有者を記録するNFTの規格です。
バインディングコントラクトは、その所有者をエージェントの制御者として使えます。

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

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

ERC1155は、1つのコントラクトで複数種類のトークンと保有数量を管理します。
そのため、バインディングコントラクトは、指定したトークンIDの残高を制御条件へ使えます。

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

[\[ERC1155\] ERC1155を1からわかりやすく解説](/articles/c3ef92722b057161bebd)

ERC6909も、1つのコントラクトで複数のトークンIDと残高を管理します。
ただし、何単位を持てば制御できるか、複数の保有者が同時に条件を満たした時にどうするかは、ERC8217ではなくバインディングコントラクトが決めます。

つまり、ERC8217は3つのトークン規格を結び付け先として識別しますが、各規格の所有権、残高、送付の動作は変更しません。
外部トークンの所有者や残高が変わると、バインディングコントラクトの認可結果だけが変わり、`Binding` 自体は同じ値を返し続けます。

## テスト

テストでは、`metadata` の20バイトと、`bindingOf` が返す3項目を分けて確認します。
`agent-binding` だけからトークン情報を読み取る処理は、ERC8217の仕様と一致しません。

#### 20バイトのmetadata

バインディングコントラクトを以下のアドレスとします。

```text
bindingContract = 0x9c4e8f2a1b7d6e3c0a5f8d2b9e1c4a7f3d6e8b0c
```

`agent-binding` へ保存する値は、接頭辞の `0x` を除いて40桁の16進数、つまり20バイトです。

```text
0x9c4e8f2a1b7d6e3c0a5f8d2b9e1c4a7f3d6e8b0c
```

このテストでは、保存値がアドレスと完全に一致することを確認します。
`TokenStandard`、外部トークンコントラクト、トークンIDを連結した値は不合格です。
また、20バイトより短い値や、32バイトへ埋め合わせた値も不合格として扱います。

#### bindingOfの返り値

同じバインディングコントラクトに対して、例えば `bindingOf(17)` を呼び出します。
ERC721の42番へ結び付けた場合の返り値は、以下の内容です。

```js
Binding({
    standard: TokenStandard.ERC721,
    tokenContract: 0x1111111111111111111111111111111111111111,
    tokenId: 42
})
```

このテストでは、`standard`、`tokenContract`、`tokenId` が初回登録時の値と一致することを確認します。
さらに、登録後に更新処理を呼び出しても、`bindingOf(17)` の返り値と `agent-binding` のアドレスが変わらないことを確認します。

なお、提案のテスト例にはERC1155の `tokenId = 5` や、ERC721の `tokenId = 0x1234` も示されています。
これらの値も `Binding` から返され、`metadata` には入りません。

## セキュリティ

`agent-binding` の20バイトを正しく読み取れても、現在の制御者まで確定したことにはなりません。
クライアントは参照先のコントラクトと、外部トークン側の所有権または残高を分けて確認します。

#### metadataだけでは足りない理由

`metadata` が示すのは、`bindingOf(agentId)` を呼ぶコントラクトのアドレスだけです。
外部トークンは `bindingOf` の返り値から確認し、制御条件はバインディングコントラクトが所有権や残高を調べる処理から確認します。

例えば `Binding` がERC721の42番を返したとしても、現在の所有者アドレスは `Binding` に含まれません。
所有権を制御条件にするバインディングコントラクトでは、外部トークンコントラクトの `ownerOf(42)` を呼び出して初めて、現在の制御者を判断できます。
そのため、`metadata` のデコードだけで署名や資産操作を許可してはいけません。

#### 正規コントラクトとアップグレードのリスク

チェーンごとに正規のバインディングコントラクトを1つにすると、クライアントとインデクサーは評価・監視する対象を1つへ絞れます。
しかし、そのコントラクトが誤った `Binding` を返せば、そこへ登録されたすべての `agentId` を正しく検証できません。

クライアントは、バインディングコントラクトのコード、監査結果、運用実績、アップグレードできる構成かどうかを確認します。
コードをアップグレードできるバインディングコントラクトでも、変更後に `agent-binding` のアドレスと登録済みの `Binding` を同じ値に保つ必要があります。
管理権限を使って登録済みの `standard`、`tokenContract`、`tokenId` を変更できるコントラクトは、ERC8217の条件を満たしません。

また、同じチェーンに複数のバインディングコントラクトを置くと、クライアントは各コントラクトを個別に評価し、インデクサーは各 `AgentBound` イベントを監視することになります。
ERC8217は、信頼と監視の基準が分散しないよう、チェーンごとに1つの正規コントラクトを使います。

#### 形式の境界

`agent-binding` は20バイトのEVMアドレスだけを表します。
チェーンIDを含まないため、クライアントは、どのチェーンのERC8004レジストリから `metadata` を読んだかという呼び出し元の情報と組み合わせて解釈します。

また、この形式では非EVMチェーンのアドレスを表現できません。
非EVMのトークンへ結び付ける形式や、異なるチェーンをまたぐ結び付けはERC8217の対象外です。

エージェントを登録するレジストリやアダプターは、`agent-binding` を予約項目として保護し、信頼できない呼び出し元による上書きを拒否します。
クライアント側では、値が20バイトでも正規のバインディングコントラクトとは限らないため、そのチェーンで採用されたコントラクトかを確認してから `bindingOf` を信頼します。

ERC8217の公式仕様は以下です。

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

## 最後に

ERC8217は、ERC8004の `metadata` へバインディングコントラクトの20バイトのアドレスを記録し、`bindingOf(agentId)` からトークン規格、トークンコントラクト、トークンIDを読み出す仕組みです。
`Binding` を変更できない値として残す一方で、現在の制御者は外部トークンの所有者や残高から判断します。
これにより、登録情報を書き換えずに、外部トークンの送付へエージェントの制御を追従させられます。

他でも色々記事を書いているのでぜひよろしければ読んでいってください！

https://amzn.asia/d/gxvJ0Pw

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://twitter.com/cardene777

https://cardene.substack.com/
