Skip to content

Commit 4aa2228

Browse files
Voyce-Studioclaude
andcommitted
eips: finalize native-ETH A2A escrow as Standards-Track ERC (closes #50)
Bring eips/draft-native-eth-a2a-escrow.md to editor-ready ERC quality and add the two submission-support docs. Draft: - ERC-165 interface id VERIFIED = 0x5c3738e9. Reproduced the six selectors from keccak256 of the canonical signatures and showed the running XOR in the spec; matches the pinned value. cast-sig recipe included. - New "ERC-165 interface identifier" subsection: states explicitly that selectors depend only on argument types (return shape, payable/view, address-vs-address-payable are irrelevant) — this is why the reference contract's bool/struct return forms keep the same id. - New "Errors" subsection in §Specification: normative revert table per function + RECOMMENDED custom-error set. Closes the missing-errors gap. - Reference Implementation section corrected to the real repo layout (test/AgentEscrow.t.sol + contracts/test/AgentEscrowOracle.t.sol + contracts/mocks/MockOracleAggregator.sol), describes the contract as a superset of IAgentEscrow, and flags two honest conformance gaps: (1) reference contract does not yet expose ERC-165 supportsInterface, (2) reference enum has extra members. Neither affects the interface id. - Test Cases table replaced with the actual Foundry test names (the prior table listed tests that do not exist, incl. a fictional supportsInterface test). - Frontmatter: eip/discussions-to placeholders annotated with an editor note explaining they are assigned during the ethereum/EIPs PR. Added: - eips/magicians-post-draft.md — forum pitch + 6 open questions. - eips/SUBMISSION-CHECKLIST.md — fork, eip-XXXX naming, editor number assignment, eipw lint gates, what an editor will/won't gate on. Hardening from stale branch eip/native-eth-a2a-escrow (commit 63c7377) was already folded into main (drafts were byte-identical); this commit builds on it and reconciles the spec with the actual reference contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 69c5004 commit 4aa2228

3 files changed

Lines changed: 270 additions & 25 deletions

File tree

eips/SUBMISSION-CHECKLIST.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# ethereum/EIPs submission checklist
2+
3+
How to take `eips/draft-native-eth-a2a-escrow.md` from this repo to a numbered, merged
4+
Draft EIP in [`ethereum/EIPs`](https://github.com/ethereum/EIPs). Follow [EIP-1] for the
5+
authoritative process; this is the condensed operational path.
6+
7+
[EIP-1]: https://eips.ethereum.org/EIPS/eip-1
8+
9+
---
10+
11+
## 0. Preconditions (do these first)
12+
13+
- [ ] **GitHub handles in `author:` are real and resolve.** EIP-1 requires every `@handle` to be a
14+
valid GitHub account. Confirm `@abhicris`, `@Pattermesh`, `@kcolbchain` all exist. If an author
15+
prefers name-only, use `Name <email>` form instead — but at least one author must be reachable.
16+
- [ ] **Open the ethereum-magicians thread** (see `eips/magicians-post-draft.md`) and let it gather a
17+
little discussion. The EIP bot/editors expect a real `discussions-to:` URL.
18+
- [ ] **Reference implementation is public and stable.** `contracts/AgentEscrow.sol` must be reachable
19+
at a permanent URL. EIP body links into this repo are fine, but editors prefer the spec be
20+
self-contained — the normative interface lives in the EIP text, not only in the repo.
21+
- [ ] **Close the two known conformance gaps** (or keep them as clearly-flagged TODOs in the
22+
Reference Implementation section, which the current draft does): add ERC-165 `supportsInterface`
23+
to the reference contract and a `test_supportsInterface` asserting `0x5c3738e9`.
24+
25+
## 1. Fork and branch
26+
27+
- [ ] Fork [`ethereum/EIPs`](https://github.com/ethereum/EIPs) to your account.
28+
- [ ] `git clone` your fork; `git checkout -b eip-native-eth-a2a-escrow`.
29+
- [ ] Read `ethereum/EIPs`'s `CONTRIBUTING` and the EIP template once more — the repo CI
30+
(`eipw` linter + HTML preview build) is strict and will reject on format alone.
31+
32+
## 2. File naming and number
33+
34+
- [ ] **Do NOT pick your own number.** New EIPs are submitted as `EIPS/eip-draft_<slug>.md` or you
35+
may use a placeholder; an **EIP editor assigns the number** during review. In practice the
36+
common path is: name the file `EIPS/eip-XXXX.md` (literal `XXXX`) and set `eip: XXXX`, or use a
37+
descriptive `eip-draft_native_eth_a2a_escrow.md`. The editor renames it to the assigned number
38+
(`eip-7xxx.md`) on merge.
39+
- [ ] Copy `eips/draft-native-eth-a2a-escrow.md` into `EIPS/` in the fork under that name.
40+
- [ ] Place asset files (diagrams, if any are externalized) under `assets/eip-XXXX/`. The current
41+
draft uses only inline ASCII, so no assets are needed.
42+
43+
## 3. Frontmatter cleanup (the two placeholders)
44+
45+
- [ ] Set `discussions-to:` to the actual ethereum-magicians topic URL.
46+
- [ ] Set `eip:` to match the filename. If using the `XXXX` placeholder, leave both as `XXXX`; the
47+
editor fills them. **Remove the `EDITOR NOTE` HTML comment block** at the top of the draft.
48+
- [ ] Confirm the rest of the frontmatter is exactly the EIP-1 set in the right order:
49+
`eip, title, description, author, discussions-to, status, type, category, created, requires`.
50+
- `status: Draft`
51+
- `type: Standards Track`
52+
- `category: ERC`
53+
- `requires: 165`
54+
- `title` ≤ 44 chars, `description` ≤ 140 chars, neither ending in a period, neither
55+
repeating the word "standard"/"EIP" (eipw enforces these).
56+
57+
## 4. Body conformance (what the linter and editors check)
58+
59+
- [ ] Required sections present and in EIP-1 order: **Abstract, Motivation, Specification, Rationale,
60+
Backwards Compatibility, Reference Implementation, Security Considerations, Copyright**
61+
(plus optional Test Cases — present here). All eight required headers are `##` level.
62+
- [ ] **RFC-2119 keywords** boilerplate paragraph present in Specification (it is).
63+
- [ ] **Copyright** is exactly the CC0 waiver line EIP-1 mandates (it is).
64+
- [ ] Internal links to other EIPs use the relative `./eip-N.md` form (the draft links ERC-165 as
65+
`./eip-165.md` — correct).
66+
- [ ] No broken external links; `eipw` checks reachability of some.
67+
- [ ] Markdown passes the repo's `markdownlint` config (line length is not capped, but tables and
68+
headers must be well-formed).
69+
70+
## 5. Open the PR
71+
72+
- [ ] Push the branch to your fork; open a PR into `ethereum/EIPs:master`.
73+
- [ ] PR title convention: `Add EIP: Native-ETH Agent-to-Agent Escrow` (or
74+
`Add ERC: ...`). The bot comments with the assigned number and lint results within minutes.
75+
- [ ] Address every `eipw` / preview-CI failure. Re-push; CI re-runs.
76+
- [ ] An **EIP editor** reviews for format + EIP-1 conformance (NOT for whether the idea is good —
77+
that's the magicians thread's job). Once it conforms, an editor assigns the number, may rename
78+
the file, and merges it as **Draft**.
79+
80+
## 6. After merge
81+
82+
- [ ] Update the `discussions-to` thread title and this repo's `eips/draft-native-eth-a2a-escrow.md`
83+
frontmatter to reference the assigned `ERC-XXXX` number, and link the merged EIP from issue #50
84+
and PR #51.
85+
- [ ] Advancing **Draft → Review → Last Call → Final** is a later, separate process driven by author
86+
readiness + editor sign-off + the discussion thread; it is out of scope for initial submission.
87+
88+
---
89+
90+
### Quick reference: what an editor will and won't gate on
91+
92+
| Editor gates on (must fix to merge) | Editor does NOT gate on |
93+
|---|---|
94+
| Frontmatter format, field order, char limits | Whether the primitive is a good idea |
95+
| All required sections present, correct order | Whether the community agrees with the design |
96+
| Valid author handles, real `discussions-to` | Adoption / deployment count |
97+
| CC0 copyright line verbatim | Test coverage depth |
98+
| Relative EIP links, lint pass | The bikeshed in §Rationale |

eips/draft-native-eth-a2a-escrow.md

Lines changed: 100 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
---
2-
eip: <TBD — request from EIP editors>
2+
eip: <to be assigned by an EIP editor on PR>
33
title: Native-ETH Agent-to-Agent Escrow
44
description: A minimal payable escrow primitive for autonomous agent-to-agent payments using native ETH, with timeout-based refund and explicit challenge period.
55
author: Abhishek Krishna (@abhicris), Pattermesh (@Pattermesh), kcolbchain (@kcolbchain)
6-
discussions-to: https://ethereum-magicians.org/<TBD>
6+
discussions-to: <ethereum-magicians.org thread URL — to be filled before opening the ethereum/EIPs PR>
77
status: Draft
88
type: Standards Track
99
category: ERC
1010
created: 2026-05-24
1111
requires: 165
1212
---
1313

14+
<!--
15+
EDITOR NOTE (remove before submitting to ethereum/EIPs):
16+
Two frontmatter fields are intentionally placeholders because the EIP process
17+
forbids self-assigning them:
18+
- `eip:` is assigned by an EIP editor when the submission PR is opened
19+
(file is named `eip-XXXX.md`); ethereum/EIPs CI normally fills it.
20+
- `discussions-to:` MUST be a live ethereum-magicians.org thread. Open the
21+
thread first (see eips/magicians-post-draft.md), then paste the URL here.
22+
Everything else in this document is final and editor-ready.
23+
-->
24+
25+
1426
## Abstract
1527

1628
This standard defines a minimal escrow contract interface for autonomous agent-to-agent (A2A) payments using native ETH. A compliant contract accepts `msg.value` directly on `createPayment`, holds it under a string-keyed mapping, and resolves it on one of three terminal transitions: `confirmPayment` by the payer, `requestRefund` by the payer after a timeout plus challenge period, or `cancelPayment` by the payer while still locked.
@@ -98,7 +110,10 @@ interface IAgentEscrow {
98110
/// by the original payer. Returns the funds to the payer.
99111
function cancelPayment(string calldata requestId) external;
100112
101-
/// @notice Read the payment record.
113+
/// @notice Read the payment record. The return shape is illustrative;
114+
/// the ERC-165 selector depends only on the argument types
115+
/// (`string`), so a `returns (Payment)` struct form is
116+
/// equally conformant.
102117
function getPayment(string calldata requestId) external view returns (
103118
address payer,
104119
address payee,
@@ -114,9 +129,13 @@ interface IAgentEscrow {
114129
}
115130
```
116131

117-
The ERC-165 interface identifier is **`0x5c3738e9`**, computed as the XOR of the six selectors in `IAgentEscrow`:
132+
### ERC-165 interface identifier
133+
134+
The interface identifier of `IAgentEscrow` is **`0x5c3738e9`**.
118135

119-
| Function | Selector |
136+
It is the XOR of the [Solidity ABI](https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector) function selectors of the six member functions, per [ERC-165](./eip-165.md). A function selector is the first four bytes of `keccak256` of the canonical signature — the function name and the parenthesized argument types only. Return types, the `payable`/`view`/`external` modifiers, and `address` vs `address payable` do **not** affect the selector. The six signatures and selectors are:
137+
138+
| Function (canonical signature) | Selector |
120139
|---|---|
121140
| `createPayment(string,address,uint256,uint256)` | `0x8a5d6ff0` |
122141
| `confirmPayment(string)` | `0x912db0fb` |
@@ -125,7 +144,30 @@ The ERC-165 interface identifier is **`0x5c3738e9`**, computed as the XOR of the
125144
| `getPayment(string)` | `0xc69207a3` |
126145
| `isExpired(string)` | `0xc64fafbc` |
127146

128-
A compliant contract MUST return `true` from `supportsInterface(0x5c3738e9)`.
147+
Running XOR (left to right):
148+
149+
```
150+
0x8a5d6ff0
151+
^ 0x912db0fb = 0x1b70df0b
152+
^ 0xc38821fc = 0xd8f8fef7
153+
^ 0x84126e01 = 0x5cea90f6
154+
^ 0xc69207a3 = 0x9a789755
155+
^ 0xc64fafbc = 0x5c3738e9 ← interface id
156+
```
157+
158+
The computation is reproducible with Foundry:
159+
160+
```bash
161+
cast sig "createPayment(string,address,uint256,uint256)" # 0x8a5d6ff0
162+
cast sig "confirmPayment(string)" # 0x912db0fb
163+
cast sig "requestRefund(string)" # 0xc38821fc
164+
cast sig "cancelPayment(string)" # 0x84126e01
165+
cast sig "getPayment(string)" # 0xc69207a3
166+
cast sig "isExpired(string)" # 0xc64fafbc
167+
# XOR of all six = 0x5c3738e9
168+
```
169+
170+
A compliant contract MUST implement [ERC-165](./eip-165.md) and MUST return `true` from `supportsInterface(0x5c3738e9)` and from `supportsInterface(0x01ffc9a7)` (the ERC-165 identifier itself).
129171

130172
### Required events
131173

@@ -145,6 +187,35 @@ A compliant contract MUST emit `PaymentCreated` from `createPayment`, and exactl
145187

146188
The `requestId` field is `indexed` even though it is a `string`; per the ABI specification, the topic is `keccak256(requestId)`. Off-chain indexers SHOULD hash off-chain request ids to query the log.
147189

190+
### Errors
191+
192+
A compliant contract MUST revert (it MUST NOT silently no-op or return `false`) when a precondition is violated. The normative revert conditions are:
193+
194+
| Function | MUST revert when |
195+
|---|---|
196+
| `createPayment` | `bytes(requestId).length == 0`; `payee == address(0)`; `timeoutBlocks == 0`; `msg.value == 0`; or `requestId` is already in use in this contract instance |
197+
| `confirmPayment` | caller is not the payer; `state != Locked`; or `block.number >= createdAt + timeoutBlocks` (window closed) |
198+
| `requestRefund` | caller is not the payer; `state != Locked`; or `block.number < createdAt + timeoutBlocks + challengePeriod` (challenge window not elapsed) |
199+
| `cancelPayment` | caller is not the payer; or `state != Locked` |
200+
| any terminal transition | the ETH transfer to the recipient fails (the state change MUST be rolled back with the revert) |
201+
202+
The reason strings or [custom errors](https://docs.soliditylang.org/en/latest/contracts.html#errors-and-the-revert-statement) used are NOT normative — only the *fact* of reverting is. Implementations are RECOMMENDED to use named custom errors for cheaper reverts and machine-readable cause codes, for example:
203+
204+
```solidity
205+
error RequestIdInUse(string requestId);
206+
error EmptyRequestId();
207+
error ZeroPayee();
208+
error ZeroTimeout();
209+
error ZeroValue();
210+
error NotPayer(address caller);
211+
error NotLocked(string requestId);
212+
error WindowClosed(string requestId); // confirm after timeout
213+
error ChallengeNotElapsed(string requestId); // refund before challenge end
214+
error TransferFailed(address recipient, uint256 amount);
215+
```
216+
217+
The reference implementation currently uses `require` reason strings; the migration to custom errors is editorial and does not change the interface id.
218+
148219
### Checks, effects, interactions
149220

150221
All three terminal transitions MUST update the on-chain `state` field before transferring value. The reference implementation uses `(bool ok, ) = payee.call{value: amount}("")` so that smart-contract payees can receive ETH via their `receive` or `fallback`. If the external call fails, the transition MUST revert and the funds remain locked; the payer can retry, or after the challenge period, call `requestRefund`.
@@ -199,33 +270,37 @@ Implementations MUST NOT silently accept ERC-20 token transfers (`ERC-20::transf
199270

200271
## Reference Implementation
201272

202-
The reference implementation is [`contracts/AgentEscrow.sol`](../contracts/AgentEscrow.sol) in the `kcolbchain/switchboard` repository. The contract is ~180 lines of Solidity ^0.8.20, MIT-licensed, dependency-free. It has been running on Base Sepolia and Lux testnet since April 2026.
273+
The reference implementation is [`contracts/AgentEscrow.sol`](../contracts/AgentEscrow.sol) in the `kcolbchain/switchboard` repository. It is Solidity ^0.8.20, MIT-licensed, and has been running on Base Sepolia and Lux testnet since April 2026.
274+
275+
The reference contract is a *superset* of `IAgentEscrow`: it implements the full required interface (with `createPayment`/`confirmPayment`/`requestRefund`/`cancelPayment` returning `bool` and `getPayment` returning a `Payment` struct — neither return-shape difference changes the selectors, hence the interface id is unchanged), plus three non-normative extensions that are out of scope for this base standard:
276+
277+
- An owner-curated agent allowlist (`registerAgent` / `deregisterAgent`), and
278+
- An optional oracle-release path (`createPaymentWithPolicy`, `releaseByAttestation`) that consults an external [`IOracleAggregator`](../contracts/IOracleAggregator.sol). This is the concrete shape of the layered extension described in the Rationale; it composes onto the base state machine without altering it.
279+
280+
Two known gaps between the reference contract and this specification, to be closed before the contract is declared conformant (they do not affect the interface id):
203281

204-
Foundry test coverage is in [`tests/`](../tests/) and covers:
282+
1. The reference contract does not yet inherit ERC-165 / expose `supportsInterface`. A conformant deployment MUST add it and return `true` for `0x5c3738e9` and `0x01ffc9a7`.
283+
2. The reference enum is `{Created, Locked, Confirmed, Released, Refunded, Cancelled}`; the canonical `IAgentEscrow` enum is `{None, Locked, Released, Refunded, Cancelled}`. The on-chain observable state machine is identical (only `Locked` and the three terminals are reachable post-`createPayment`); the extra enum members are implementation detail.
205284

206-
- `createPayment` success path, value lock, event emission
207-
- All `createPayment` revert cases (duplicate request id, zero payee, zero timeout, zero value, empty id)
208-
- `confirmPayment` happy path, only-payer enforcement, expired-window revert
209-
- `requestRefund` happy path, only-payer, pre-challenge-period revert
210-
- `cancelPayment` happy path, only-payer, only-locked revert
211-
- Smart-contract payee receiving via `.call{value:}` + `receive()`
212-
- Reentrancy via checks-effects-interactions ordering
285+
Foundry tests live in [`test/AgentEscrow.t.sol`](../test/AgentEscrow.t.sol) (base primitive) and [`contracts/test/AgentEscrowOracle.t.sol`](../contracts/test/AgentEscrowOracle.t.sol) (allowlist + oracle extension), with a mock aggregator at [`contracts/mocks/MockOracleAggregator.sol`](../contracts/mocks/MockOracleAggregator.sol).
213286

214287
## Test Cases
215288

216-
The reference implementation's test suite is the canonical test set. Selected cases:
289+
The reference implementation's Foundry suite is the canonical test set. Selected cases (names as they appear in the suite):
217290

218291
| Test | What it asserts |
219292
|---|---|
220-
| `test_createPayment_locksValue` | `address(escrow).balance` increases by `msg.value`; `getPayment(id).state == Locked` |
221-
| `test_createPayment_revertsOnDuplicateId` | A second `createPayment` with the same `requestId` reverts |
222-
| `test_confirmPayment_onlyPayer` | `confirmPayment` called by non-payer reverts |
223-
| `test_confirmPayment_revertsAfterTimeout` | `confirmPayment` at `block.number == createdAt + timeoutBlocks` reverts |
224-
| `test_requestRefund_revertsBeforeChallenge` | `requestRefund` at `block.number == createdAt + timeoutBlocks + challengePeriod - 1` reverts |
225-
| `test_requestRefund_succeedsAfterChallenge` | At exactly `createdAt + timeoutBlocks + challengePeriod`, `requestRefund` returns funds |
226-
| `test_cancelPayment_onlyLocked` | `cancelPayment` on a terminal state reverts |
227-
| `test_smartContractPayee_receivesValue` | A payee with a `receive()` function receives ETH and emits its own event |
228-
| `test_supportsInterface` | `supportsInterface(0x5c3738e9) == true`; `supportsInterface(0x12345678) == false` |
293+
| `test_happyPath_createConfirmReleased` | After `createPayment`, state is `Locked`; after payer `confirmPayment`, state is `Released` and the payee balance increases by `amount` |
294+
| `test_timeoutRefund_path` | `requestRefund` reverts before `createdAt + timeoutBlocks + challengePeriod`; `isExpired` is true once `block.number >= createdAt + timeoutBlocks`; refund succeeds after the challenge window and state becomes `Refunded` |
295+
| `test_doubleConfirm_reverts` | A second `confirmPayment` on an already-`Released` request reverts |
296+
| `test_cancel_returnsFunds` | `cancelPayment` while `Locked` returns funds to the payer and sets state `Cancelled` |
297+
| `test_onlyPayerCanConfirm` | `confirmPayment` from a non-payer reverts |
298+
| `test_reentrancy_confirmPayment_reverts` | A malicious payee re-entering `confirmPayment` cannot trigger a second release (state already terminal + guard) |
299+
| `test_registerAgent_onlyOwner_strangerReverts` | The allowlist extension's `registerAgent` is owner-gated |
300+
| `test_releaseByAttestation_success` | Oracle-extension release succeeds when the aggregator verifies the attestation; permissionless submitter |
301+
| `test_releaseByAttestation_revertsAfterTimeout` | Oracle release reverts once the timeout window has closed (use refund instead) |
302+
303+
A `supportsInterface(0x5c3738e9) == true` test MUST be added alongside the ERC-165 implementation noted above; it does not yet exist in the reference suite.
229304

230305
## Security Considerations
231306

0 commit comments

Comments
 (0)