# [ERC8183] 依頼の報酬をエスクローで支払う仕組み

> 依頼者がデータ分析を依頼し、結果を確認してから報酬を支払う場面を考えます。提出後に支払うまたは返金するを決める役割が判定主体です。

- URL: https://cardene.dev/articles/44e076586c6faeabfb3b
- 公開日: 2026-09-23
- 更新日: 2026-09-23
- トピック: Blockchain
- タグ: ethereum, smartcontract, 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

AIエージェントや外部サービスへ依頼し、成果物を確認してから報酬を支払う仕組みを説明します。
ERC8183は、依頼者が予算を預け、実行者が成果物を提出し、評価者の結果に応じて支払う規格です。

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

## 概要

#### 依頼を受けて成果物を返す場面

依頼者がデータ分析を依頼し、結果を確認してから報酬を支払う場面を考えます。
提出後に**支払う**または**返金する**を決める役割が判定主体です。ERC8183では、依頼者を`client`、成果物を提出する実行者を`provider`、この判定主体を`evaluator`として記録します。`evaluator`は人のアドレスに限らず、判定ロジックを実装したコントラクトも指定できます。

![ERC8183の依頼、提出、判定、資金結果](/images/qiita-5328633a-3606-479b-b962-97d6c1a61cb6.webp)

clientは予算を預け、providerは成果物を参照する値を提出します。
evaluatorが`complete`を呼べばproviderへ支払い、`reject`を呼べばclientへ返金します。期限切れの返金は`evaluator`の判定ではなく、`claimRefund`が実行する回復経路です。
この三役を1件のジョブに保存するため、誰が依頼し、誰が提出し、誰が判定したかを同じ識別子で追跡できます。
ERC8183のコアは、成果物の内容を評価することではなく、三役の権限と判定後の資金移動を状態遷移へ結び付けることです。

#### 予算を判定まで預ける

ERC20は、コントラクトが預かり、条件を満たした相手へ送れるトークンです。
ERC8183は、1件のジョブの予算を判定までコントラクトに置きます。
clientが提出後に資金を回収できないため、providerが提出した後の支払い条件を保てます。
支払いに使う資産はERC20です。
fundの前にclientが承認を設定し、コントラクトが`transferFrom`で予算を受け取ります。

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

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

#### 判定結果で資金を分ける

完了時の送金先はproviderです。
却下時と期限切れ時の返金先はclientです。
状態、呼び出し元、ERC20の送付先を同じ`jobId`で追跡できます。
ERC8183は、ジョブごとに決めた予算をいったんコントラクトへ移し、判定結果に応じてproviderまたはclientへ送ります。

## 動機

#### clientだけに資金回収を任せる失敗

clientだけが提出後の資金回収を実行できると、providerは作業を終えても報酬を受け取れるか分かりません。
反対に、providerが提出しただけで支払いが確定すると、clientは不完全な成果物でも返金できません。
そこで提出後の完了と却下をevaluatorに限定します。

#### providerの提出だけで支払いを確定する失敗

小さな依頼ではclient自身をevaluatorにできます。第三者の検証が必要な依頼では、評価ロジックを持つコントラクトをevaluatorに指定できます。つまり、`evaluator`は**ERC8183が成果物の中身を自動採点する機能**ではなく、判定結果をオンチェーンへ確定する権限です。evaluatorの判断が正しいか、紛争をどう仲裁するか、成果物の中身が本物かはERC8183の範囲外です。

#### 判定が止まった時の返金を残す

評価が止まったまま期限を過ぎると、clientは`claimRefund`を呼んで預けた予算を回収できます。
期限後の返金はhookを呼ばないため、外部拡張が停止しても回収経路を残せます。
期限切れの`claimRefund`は、evaluatorの判定を代替する評価機能ではありません。
判定が実行されない場合に、預託資金をclientへ戻す回復経路です。

## 仕様

### ジョブの進み方

ジョブは`Open`、`Funded`、`Submitted`、終端状態のいずれかです。
終端状態は`Completed`、`Rejected`、`Expired`であり、そこから戻りません。
`Open`は依頼を作ったが、まだ予算を預けていない段階です。
この段階ではclientがproviderや予算を変更でき、要件が合わなければ送金なしで`Rejected`にできます。
`Funded`はERC20がコントラクトへ移った段階です。
ここからclientが一方的に資金を引き戻せないため、providerは提出作業へ進めます。
`Submitted`はproviderが成果物の参照値を出し、evaluatorの判定を待つ段階です。
`Completed`ではproviderへ支払い、`Rejected`ではclientへ返金し、`Expired`では期限後の返金を記録します。

![ERC8183の状態遷移](/images/qiita-4dc8c26c-68b5-468f-8e61-c889ebb0dbd5.webp)

| 遷移 | 呼び出し条件 | 資金の結果 |
| --- | --- | --- |
| `Open → Funded` | clientが予算を預ける。 | ERC20をエスクローへ移す。 |
| `Open → Rejected` | clientが却下する。 | 預託前なので送金しない。 |
| `Funded → Submitted` | providerが成果物参照を提出する。 | 資金を保持する。 |
| `Funded → Rejected` | evaluatorが却下する。 | clientへ返金する。 |
| `Funded → Expired` | 期限後に`claimRefund`を呼ぶ。 | clientへ返金する。 |
| `Submitted → Completed` | evaluatorが完了を選ぶ。 | providerへ支払う。 |
| `Submitted → Rejected` | evaluatorが却下する。 | clientへ返金する。 |
| `Submitted → Expired` | 期限後に`claimRefund`を呼ぶ。 | clientへ返金する。 |

期限後に`Funded`または`Submitted`から返金できることは必須です。
`claimRefund`を誰でも呼べる実装が推奨されますが、clientだけに制限する実装も許可されます。
したがって、評価者がオフラインになっても資金が永遠にロックされる設計にはしません。
一方、期限が到来する前にclientが返金できるわけではなく、`complete`や`reject`の認可も状態ごとに固定されます。

### 3者が担当する操作

clientはジョブを作り、予算を設定してERC20を預けます。
providerは成果物を提出し、完了時の支払いを受けます。
evaluatorは`Submitted`後の完了または却下を決めます。
providerを作成時に未設定へできるため、clientは資金を預ける前に`setProvider`を呼びます。
clientとproviderは`setBudget`で合意する金額を保存しますが、実際のトークン移動はclientが`fund`を呼んだ時だけ起きます。
providerは`complete`や`reject`を呼べず、evaluatorは成果物を提出できません。
この認可を`msg.sender`とJobに保存した3つのアドレスで照合することで、操作の担当を状態遷移へ結び付けます。

### `Job`

```js
struct Job {
    uint256 id;
    address client;
    address provider;
    address evaluator;
    string description;
    uint256 budget;
    uint256 expiredAt;
    JobStatus status;
    address hook;
}
```

`Job`は、1件の依頼の当事者、説明、予算、期限、状態、拡張先を保存する構造体です。

- **フィールド**

| フィールド | 詳細 |
| --- | --- |
| `id` | ジョブを識別する連番です。 |
| `client` | 依頼を作り、予算を預け、返金を受け取るアドレスです。 |
| `provider` | 成果物を提出し、完了時に支払いを受けるアドレスです。 |
| `evaluator` | 完了または却下を決めるアドレスです。 |
| `description` | 依頼内容を表す文字列です。 |
| `budget` | ERC20で預ける金額です。 |
| `expiredAt` | 期限を表すタイムスタンプです。 |
| `status` | 現在の状態です。 |
| `hook` | 前後処理を受け取る任意のコントラクトです。 |

### ジョブを作成して予算を預ける

#### `createJob`

```js
function createJob(address provider, address evaluator, uint256 expiredAt, string calldata description, address hook) external returns (uint256);
```

clientが`Open`状態のジョブを作成し、`jobId`を返します。

1. **入力を確認する**
   evaluator、期限、hookを確認します。
2. **ジョブを保存する**
   当事者、説明、期限を保存します。
3. **結果を返す**
   `JobCreated`を発行して`jobId`を返します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `provider` | 成果物を提出するアドレスです。ゼロアドレスなら後で設定します。 |
| `evaluator` | 完了または却下を決めるアドレスです。 |
| `expiredAt` | 返金可能になる期限です。 |
| `description` | 依頼内容です。 |
| `hook` | 前後処理を受け取る任意コントラクトです。 |

- **戻り値**

| 戻り値 | 詳細 |
| --- | --- |
| `jobId` | 作成したジョブの識別子です。 |

**実行条件**

| 項目 | 条件 |
| --- | --- |
| 判定を行うアドレス | 完了・却下を確定する主体を空欄にしません。<br>判定者がいないJobは支払い結果を確定できないためです。 |
| 設定できる期限 | `expiredAt`は実装が定める最小期限より後にします。<br>短すぎる期限で、providerが作業する前に返金可能になることを防ぎます。 |

#### `setProvider`

```js
function setProvider(uint256 jobId, address provider, bytes calldata optParams) external;
```

clientが未設定のproviderを登録します。

1. **状態を確認する**
   `Open`で呼び出し元がclientか確認します。
2. **providerを保存する**
   providerアドレスをジョブへ保存します。
3. **変更を通知する**
   `ProviderSet`を発行します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `jobId` | 対象のジョブです。 |
| `provider` | 成果物を提出するアドレスです。 |
| `optParams` | hookへ渡す追加データです。 |

**実行条件**

| 項目 | 条件 |
| --- | --- |
| ジョブの段階 | ジョブは`Open`状態です。<br>作成後で、まだ予算を預ける前の段階だけ変更できます。 |
| 操作するアドレス | `client`です。<br>依頼条件と役割を決めたclientだけが提出担当を登録します。 |
| providerの設定 | まだ設定されていません。<br>既にproviderが登録されたJobを上書きして、提出先を途中で変えないためです。 |

#### `setBudget`

```js
function setBudget(uint256 jobId, uint256 amount, bytes calldata optParams) external;
```

clientまたはproviderが`Open`のジョブへ予算を設定します。

1. **権限を確認する**
   呼び出し元がclientまたはproviderか確認します。
2. **予算を保存する**
   amountをジョブへ保存します。
3. **変更を通知する**
   `BudgetSet`を発行します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `jobId` | 対象のジョブです。 |
| `amount` | 合意する予算です。 |
| `optParams` | hookへ渡す追加データです。 |

**実行条件**

| 項目 | 条件 |
| --- | --- |
| ジョブの段階 | ジョブは`Open`状態です。<br>予算を預けた後は金額を変更できないようにします。 |
| 操作するアドレス | `client`または`provider`です。<br>両者が合意した金額を保存し、どちらか一方だけの変更に限定しません。 |

#### `fund`

```js
function fund(uint256 jobId, uint256 expectedBudget, bytes calldata optParams) external;
```

clientが保存済みの予算と`expectedBudget`を確認してERC20を預け、`Funded`へ進めます。

1. **予算を照合する**
   保存値と`expectedBudget`を比較します。
2. **ERC20を預ける**
   clientからコントラクトへ送付します。
3. **状態を更新する**
   `Funded`へ移して`JobFunded`を発行します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `jobId` | 対象のジョブです。 |
| `expectedBudget` | clientが確認した予算です。 |
| `optParams` | hookへ渡す追加データです。 |

**実行条件**

| 項目 | 条件 |
| --- | --- |
| 操作するアドレス | `client`です。<br>予算を実際に送付する資金の所有者だけがfundを開始します。 |
| providerの設定 | providerが設定されています。<br>送金後に提出先が未確定になるJobを作らないためです。 |
| 保存済み予算との照合 | 保存済み予算と`expectedBudget`が一致します。<br>clientが確認した金額とコントラクトの記録を照合し、古い金額で送金する事故を防ぎます。 |
| トークンの支払い準備 | ERC20の承認額と残高が足ります。<br>不足している場合は送金と`Funded`への状態変更を成立させません。 |

### 成果物を提出して判定する

#### `submit`

```js
function submit(uint256 jobId, bytes32 deliverable, bytes calldata optParams) external;
```

providerが成果物を参照する`deliverable`を提出し、`Submitted`へ進めます。

1. **提出者を確認する**
   呼び出し元がproviderか確認します。
2. **参照値を記録する**
   deliverableをイベントへ記録します。
3. **状態を更新する**
   `Submitted`へ移して`JobSubmitted`を発行します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `jobId` | 対象のジョブです。 |
| `deliverable` | CIDや証拠ハッシュなど、成果物を参照する32バイトの値です。 |
| `optParams` | hookへ渡す追加データです。 |

**実行条件**

| 項目 | 条件 |
| --- | --- |
| 操作するアドレス | `provider`です。<br>登録された提出担当以外が成果物を差し替えないためです。 |
| ジョブの段階 | ジョブは`Funded`状態です。<br>予算が預けられているが、まだ成果物が提出されていない段階です。 |

#### `complete`

```js
function complete(uint256 jobId, bytes32 reason, bytes calldata optParams) external;
```

evaluatorが`Submitted`のジョブを完了にし、エスクローからproviderへ支払います。

1. **評価者を確認する**
   呼び出し元がevaluatorか確認します。
2. **支払額を計算する**
   手数料を控除する実装では残額を計算します。
3. **支払う**
   `Completed`へ移してproviderへ送金します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `jobId` | 対象のジョブです。 |
| `reason` | 評価理由や証拠を参照する32バイトの値です。 |
| `optParams` | hookへ渡す追加データです。 |

**実行条件**

| 項目 | 条件 |
| --- | --- |
| 操作するアドレス | `evaluator`です。<br>提出者自身が完了を確定して支払いを得る経路を分けます。 |
| ジョブの段階 | ジョブは`Submitted`状態です。<br>成果物参照が記録された後でなければ、支払いを確定できません。 |

#### `reject`

```js
function reject(uint256 jobId, bytes32 reason, bytes calldata optParams) external;
```

ジョブを却下し、予算が預けられていればclientへ返金します。

1. **呼び出し元を確認する**
   状態に応じてclientまたはevaluatorか確認します。
2. **状態を更新する**
   `Rejected`へ移します。
3. **返金を記録する**
   預託済みならclientへ返金してイベントを発行します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `jobId` | 対象のジョブです。 |
| `reason` | 却下理由や証拠を参照する32バイトの値です。 |
| `optParams` | hookへ渡す追加データです。 |

**実行条件**

| ジョブの段階 | 操作できるアドレス |
| --- | --- |
| `Open` | `client`だけです。<br>預金前のJobを依頼者が取り消せる状態です。 |
| `Funded`または`Submitted` | `evaluator`だけです。<br>預金後の返金・却下を依頼者が一方的に決めないためです。 |

### 期限切れの返金

#### `claimRefund`

```js
function claimRefund(uint256 jobId) external;
```

期限を過ぎた`Funded`または`Submitted`のジョブを`Expired`へ進め、clientへ返金します。

1. **期限を確認する**
   現在時刻が`expiredAt`以後か確認します。
2. **状態を更新する**
   `Expired`へ移します。
3. **返金する**
   clientへERC20を返して`Refunded`を発行します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `jobId` | 返金対象のジョブです。 |

**実行条件**

| 項目 | 条件 |
| --- | --- |
| ジョブの段階 | ジョブは`Funded`または`Submitted`状態です。<br>預金後に判定待ちで止まったJobだけを対象にします。 |
| 返金できる時刻 | 現在時刻が`expiredAt`以後です。<br>期限前のclientによる資金回収を防ぎます。 |
| 外部hookの扱い | 呼び出しません。<br>hookや外部サービスが停止していても、clientが期限後に資金を回収できるようにします。 |

### `IACPHook`

```js
interface IACPHook {
    function beforeAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
    function afterAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
}
```

`IACPHook`は、コア関数の前後へ検証や通知を追加するinterfaceです。

#### `beforeAction`

```js
function beforeAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
```

状態更新やERC20送付の前に呼び出します。
失敗すると、本処理を止めます。

1. **対象を確認する**
   jobIdとselectorを確認します。
2. **条件を検証する**
   dataを読み取って追加条件を確認します。
3. **処理へ戻る**
   成功ならコア処理へ戻ります。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `jobId` | 対象ジョブの識別子です。 |
| `selector` | 呼び出し対象関数を識別する4バイトです。 |
| `data` | 対象関数の引数を表すデータです。 |

**実行条件**

| 項目 | 条件 |
| --- | --- |
| 呼び出すコントラクト | コアコントラクトだけが呼び出します。<br>任意の外部アドレスが直接callbackを実行して、Jobの前提を偽装する経路を分けます。 |
| hookの判定 | hookが追加条件を満たさない場合はrevertします。<br>コア処理の状態更新や送金も同じトランザクションで取り消されます。 |

#### `afterAction`

```js
function afterAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
```

状態更新と送金の後に呼び出します。
失敗すると、コア処理を含むトランザクション全体を戻します。

1. **対象を確認する**
   jobIdとselectorを確認します。
2. **結果を通知する**
   dataを読み取り、外部レジストリなどへ通知します。
3. **処理を確定する**
   成功ならコア処理と通知を確定します。

- **引数**

| 引数 | 詳細 |
| --- | --- |
| `jobId` | 対象ジョブの識別子です。 |
| `selector` | 呼び出し対象関数を識別する4バイトです。 |
| `data` | 対象関数の引数を表すデータです。 |

**実行条件**

| 項目 | 条件 |
| --- | --- |
| 呼び出すコントラクト | コアコントラクトだけが呼び出します。<br>状態更新と送金が終わった後の通知を、対象Jobに結び付けます。 |
| hookが失敗した場合 | 失敗すると、コア処理を含むトランザクション全体をrevertします。<br>通知だけ失敗して支払いだけ残る状態を防ぎます。 |

![hookの対象関数と返金経路の境界](/images/qiita-e3d5bb41-c319-40c9-8b1b-284df434d8a0.webp)

`claimRefund`はhookの対象外です。

### 判定結果をイベントで追跡する

イベントは`jobId`を検索キーにして、状態変化と資金移動を追跡します。

![イベントで追えるジョブと資金の変化](/images/qiita-ff697df4-5a36-493d-8937-dee1c8175c1d.webp)

#### `JobCreated`

```js
event JobCreated(uint256 indexed jobId, address indexed client, address indexed provider, address evaluator, uint256 expiredAt, address hook);
```

ジョブ作成時に当事者、期限、hookを記録します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | Job storageと後続イベントを結び付ける識別子です。 |
| `client` | 依頼と預託を開始した主体です。 |
| `provider` | 作成時点の提出担当です。ゼロアドレスなら後で設定します。 |
| `evaluator` | 完了または却下を決める主体です。 |
| `expiredAt` | 期限後返金の判定に使う時刻です。 |
| `hook` | 前後処理を受ける拡張先です。 |

#### `ProviderSet`

```js
event ProviderSet(uint256 indexed jobId, address indexed provider);
```

未設定だったproviderを登録した時に発行します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | providerを設定したJobです。 |
| `provider` | clientが登録した提出担当です。 |

#### `BudgetSet`

```js
event BudgetSet(uint256 indexed jobId, uint256 amount);
```

ジョブの予算を保存した時に発行します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | 予算が変更されたJobです。 |
| `amount` | `fund`前に合意した予算です。 |

#### `JobFunded`

```js
event JobFunded(uint256 indexed jobId, address indexed client, uint256 amount);
```

ERC20がエスクローへ移り、`Funded`になった時に発行します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | 預託されたJobです。 |
| `client` | ERC20を預けた主体です。 |
| `amount` | エスクローへ移った予算です。 |

#### `JobSubmitted`

```js
event JobSubmitted(uint256 indexed jobId, address indexed provider, bytes32 deliverable);
```

providerが成果物参照を提出した時に発行します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | 提出対象のJobです。 |
| `provider` | 提出した主体です。 |
| `deliverable` | 成果物を参照するCIDやハッシュです。 |

#### `JobCompleted`

```js
event JobCompleted(uint256 indexed jobId, address indexed evaluator, bytes32 reason);
```

evaluatorがジョブを完了にした時に発行します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | 完了したJobです。 |
| `evaluator` | 判定した主体です。 |
| `reason` | 判定理由や証拠を参照する値です。 |

#### `JobRejected`

```js
event JobRejected(uint256 indexed jobId, address indexed rejector, bytes32 reason);
```

clientまたはevaluatorがジョブを却下した時に発行します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | 却下されたJobです。 |
| `rejector` | Openで却下したclient、またはFunded/Submittedで却下したevaluatorです。 |
| `reason` | 却下理由や証拠を参照する値です。 |

#### `JobExpired`

```js
event JobExpired(uint256 indexed jobId);
```

期限後の`claimRefund`で`Expired`になった時に発行します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | 期限切れになったJobです。 |

#### `PaymentReleased`

```js
event PaymentReleased(uint256 indexed jobId, address indexed provider, uint256 amount);
```

完了したジョブのエスクローからproviderへ送った金額を記録します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | 支払い元のJobです。 |
| `provider` | 純額を受け取った主体です。 |
| `amount` | fee控除後にproviderへ送った金額です。 |

#### `Refunded`

```js
event Refunded(uint256 indexed jobId, address indexed client, uint256 amount);
```

却下または期限切れでclientへ返金した金額を記録します。

| フィールド | 読み取れる内容 |
| --- | --- |
| `jobId` | 返金元のJobです。 |
| `client` | 返金を受け取った主体です。 |
| `amount` | clientへ戻した金額です。 |

## 補足

#### 成果物と評価理由を参照値で残す

`deliverable`と`reason`は成果物や評価理由そのものではありません。
オフチェーンのファイル、CID、証拠ハッシュを参照する32バイトの値です。
元データの正しさを検証する処理は、別の評価・検証層が担当します。

#### 評価と支払いを別の層へ分ける

ERC8004はagentの識別情報と評判を扱います。
ERC8183は依頼の予算、提出、支払い、返金を扱います。
ERC8004はagentのアイデンティティと評判を扱います。
ERC8183は支払いとエスクローを担当します。

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

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

## 互換性

#### ERC20トークンと接続する

`fund`ではclientの残高からコントラクトへ`transferFrom`し、完了時はprovider、却下または期限切れ時はclientへ`transfer`します。
採用するERC20については、clientの残高とallowance、コントラクトの受取額、終端状態後の送付先を確認します。

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

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

#### 代送信を組み込む

ERC2771のtrusted forwarderが署名済み要求を送信します。
ACP側は元の署名者を `_msgSender()` で判定します。

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

[\[ERC2771\] ガス代を他のアドレスに負担させるメタトランザクションの仕組みを理解しよう！](/articles/e84f53471b7a79240f33)

#### 署名承認を組み込む

ERC2612を組み合わせると署名でトークン承認を作れます。
`permit` と `fund` を1つの送信へまとめられます。

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

[\[ERC2612\] ERC20のapproveとtransferFromを1つのトランザクションで実行できる仕組みを理解しよう！](/articles/6809173b2ac428b1ccd0)

#### 規格外の拡張を切り分ける

per-job token、platform fee、evaluator fee、hook whitelist、meta-transactionは実装が選ぶ拡張です。
ERC8183のJob状態、client/provider/evaluatorの権限、完了時の支払い、却下・期限切れ時の返金と混同しません。

## 参考実装

#### `IACPHook.sol`を読む

参照実装の`IACPHook.sol`は、`IERC165`を継承して2つのcallbackを公開します。

```js
interface IACPHook is IERC165 {
    function beforeAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
    function afterAction(uint256 jobId, bytes4 selector, bytes calldata data) external;
}
```

`beforeAction`はコア処理の前、`afterAction`は状態更新と送金の後に呼ばれます。
hookがrevertすると対象操作全体が戻るため、hookはallowlistや入力検証を追加できますが、コアのエスクローを直接変更する前提ではありません。
`claimRefund`をhook対象外にして、hookが停止しても期限後の返金を残します。

#### `AgenticCommerce.sol`を読む

`AgenticCommerce.sol`はupgradeableな中核コントラクトです。
`initialize`で支払いトークンとtreasuryを保存し、管理ロールを付与してゼロアドレスhookを許可します。

```js
function initialize(address paymentToken_, address treasury_) public initializer;
```

`createJob`は期限、evaluator、hook whitelist、ERC165を確認して`jobs[jobId]`へ保存します。
`setBudget`は予算を保存し、`fund`は`SafeERC20.safeTransferFrom`でclientからコントラクトへ資金を移して`Funded`にします。
`complete`はfeeを計算してtreasury、evaluator、providerへ順に送り、`reject`は預託済みならclientへ返金します。
送金を行う関数は`nonReentrant`で保護されます。

#### 参照実装固有の制約

| コード要素 | 実装で行うこと | 規格との関係 |
| --- | --- | --- |
| `AccessControlUpgradeable` | `ADMIN_ROLE`でfee、treasury、hook whitelistを更新する。 | 参照実装固有です。 |
| `UUPSUpgradeable` | `DEFAULT_ADMIN_ROLE`だけが実装を更新する。 | 参照実装固有です。 |
| `ReentrancyGuardTransient` | token送金を含む操作の再入を防ぐ。 | 実装上の保護です。 |
| `SafeERC20` | ERC20の送付失敗を検出する。 | 実装上の保護です。 |
| `ERC165Checker` | hookが`IACPHook`を実装するか確認する。 | 参照実装固有です。 |
| `platformFeeBP` / `evaluatorFeeBP` | `complete`時だけ手数料を控除する。 | feeは任意拡張です。 |

`ExpiryTooShort`、`ProviderNotSet`、`FeesTooHigh`、`HookNotWhitelisted`などのcustom errorも、参照実装が追加した失敗条件です。

#### 規格必須要件との差分

規格の必須要件は、Jobの状態、三役の認可、ERC20エスクロー、完了時のprovider支払い、却下・期限切れ時のclient返金です。
一方、参照実装の`createJob`は5分以上の期限、hook whitelist、`ERC165Checker`を要求し、`complete`はplatform feeとevaluator feeを計算します。
upgradeability、role管理、fee、hook whitelistをERC8183本体の全実装に必須な条件と誤認しないようにします。

## セキュリティ

#### evaluatorを信頼する範囲

`Submitted`後の完了と却下はevaluatorが決めます。
ERC8183はevaluatorの正しさや紛争仲裁を提供しないため、高額なジョブでは評判、stake、外部証明を別に設計します。
つまり、evaluatorが悪意を持つか誤判定すると、providerへの支払いまたはclientへの返金がオンチェーンで確定し、ERC8183の操作だけでは覆せない、という信頼境界です。`deliverable`と`reason`は参照値なので、参照先の元データが存在することや、その判定が妥当であることもコントラクト単独では検証しません。採用側はevaluatorを誰が選び、stakeを没収するか、異議申立てをどの層で受け付けるかを別に決めます。

![ERC8183の役割と信頼境界](/images/qiita-eadcde63-3373-4efb-8459-9707404614b4.webp)

#### hookで操作が止まる条件

hookが失敗すると、対象の操作は状態更新や送金を含めて戻ります。
hookが外部registryや別のtokenを呼ぶ場合、その外部呼出しの失敗もrevert原因になります。
`beforeAction`は状態更新前、`afterAction`は状態更新後に呼ばれます。どちらかが`revert`すると、対象トランザクション全体がロールバックされるため、Jobの状態だけ進んで送金だけ失敗する形にはなりません。一方で、hookの外部依存が停止した時は通常の操作も止まります。hookの権限、外部registryの可用性、ガス上限、失敗時の再試行方針を確認し、期限到来後はhookを通らない`claimRefund`へ切り替えられることを回復条件として検証します。

#### ERC20送金を再入から守る

`fund`、`complete`、`reject`、`claimRefund`はERC20送金を行います。
参照実装は`ReentrancyGuardTransient`と`SafeERC20`を組み合わせ、状態更新と送金失敗を同じトランザクションで扱います。
再入防止だけでは、tokenの戻り値異常や送金先ごとの計算ミスを防げません。たとえば`complete`でtreasury、evaluator、providerへ分配する順序や、fee控除後のprovider純額を誤ると、二重支払いまたは残高不足になります。採用時は`nonReentrant`の適用範囲、状態をCompleted/Rejected/Expiredへ進める順序、`SafeERC20`が扱う失敗、非標準ERC20の戻り値、token自体のupgrade権限を確認します。tokenが悪意ある実装なら、コントラクト側の保護だけで完全には隔離できません。

#### 期限返金を回復経路として残す

`claimRefund`は`Funded`または`Submitted`かつ期限到来でだけ実行でき、hookを呼ばずにclientへ全額返金します。
そのため、hookが停止しても資金回収を妨げません。
確認すべき条件は、`expiredAt`を過ぎていること、状態が`Funded`または`Submitted`であること、返金先がJobのclientであること、期限前に同じ経路を呼べないことです。期限後はhook非依存で全額を戻せる一方、期限設定が短すぎればproviderの提出時間を奪い、長すぎればclientの資金が拘束されます。紛争仲裁、off-chain成果物の真偽、期限設定の妥当性は別の運用・評価層で決めます。

## 最後に

ERC8183は、依頼者が予算を預け、実行者が成果物を提出し、評価者が支払いまたは返金を決めるジョブを標準化します。
採用時は、evaluatorの信頼性、期限切れの返金、hookの停止、参考実装との差分を確認します。

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

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/
