You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
description: A minimal payable escrow primitive for autonomous agent-to-agent payments using native ETH, with timeout-based refund and explicit challenge period.
discussions-to: <ethereum-magicians.org thread URL — to be filled before opening the ethereum/EIPs PR>
7
7
status: Draft
8
8
type: Standards Track
9
9
category: ERC
10
10
created: 2026-05-24
11
11
requires: 165
12
12
---
13
13
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
+
14
26
## Abstract
15
27
16
28
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 {
98
110
/// by the original payer. Returns the funds to the payer.
99
111
function cancelPayment(string calldata requestId) external;
100
112
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.
102
117
function getPayment(string calldata requestId) external view returns (
103
118
address payer,
104
119
address payee,
@@ -114,9 +129,13 @@ interface IAgentEscrow {
114
129
}
115
130
```
116
131
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`**.
118
135
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:
@@ -125,7 +144,30 @@ The ERC-165 interface identifier is **`0x5c3738e9`**, computed as the XOR of the
125
144
|`getPayment(string)`|`0xc69207a3`|
126
145
|`isExpired(string)`|`0xc64fafbc`|
127
146
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).
129
171
130
172
### Required events
131
173
@@ -145,6 +187,35 @@ A compliant contract MUST emit `PaymentCreated` from `createPayment`, and exactl
145
187
146
188
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.
147
189
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
The reference implementation currently uses `require` reason strings; the migration to custom errors is editorial and does not change the interface id.
218
+
148
219
### Checks, effects, interactions
149
220
150
221
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
199
270
200
271
## Reference Implementation
201
272
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):
203
281
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.
205
284
206
-
-`createPayment` success path, value lock, event emission
207
-
- All `createPayment` revert cases (duplicate request id, zero payee, zero timeout, zero value, empty id)
- 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).
213
286
214
287
## Test Cases
215
288
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):
217
290
218
291
| Test | What it asserts |
219
292
|---|---|
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 |
|`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.
0 commit comments