--- eip: 8001 title: Agent Coordination Framework description: Minimal, single-chain, multi-party agent coordination using EIP-712 attestations author: Kwame Bryan (@KBryan) discussions-to: https://ethereum-magicians.org/t/erc-8001-secure-intents-a-cryptographic-framework-for-autonomous-agent-coordination-draft-erc-8001/24989 status: Final type: Standards Track category: ERC created: 2025-08-02 requires: 712, 1271, 2098, 5267 --- ## Abstract [ERC-8001](./eip-8001.md) defines a minimal, single-chain primitive for **multi-party agent coordination**. An initiator posts an intent and each participant provides a verifiable acceptance attestation. Once the required set of acceptances is present and fresh, the intent is executable. The standard specifies typed data, lifecycle, mandatory events, and verification rules compatible with [EIP-712], [ERC-1271], [EIP-2098], and [EIP-5267]. [ERC-8001](./eip-8001.md) omits privacy, reputation, threshold policies, bonding, and cross-chain semantics. Those are expected as optional modules that reference this specification. ## Motivation Agents in DeFi/MEV/Web3 Gaming and Agentic Commerce often need to act together without a trusted coordinator. Existing intent standards (e.g., [ERC-7521](./eip-7521.md), [ERC-7683](./eip-7683.md)) define single-initiator flows and do not specify multi-party agreement. [ERC-8001](./eip-8001.md) specifies the smallest on-chain primitive for that gap: an initiator's [EIP-712](./eip-712.md) intent plus per-participant [EIP-712](./eip-712.md)/[EIP-1271](./eip-1271.md) acceptances. The intent becomes executable only when the required set of acceptances is present and unexpired. Canonical (sorted-unique) participant lists and standard typed data provide replay safety and wallet compatibility. Privacy, thresholds, bonding, and cross-chain are left to modules. ## Specification The keywords “MUST”, “SHOULD”, and “MAY” are to be interpreted as described in RFC 2119 and RFC 8174. Implementations MUST expose the following canonical status codes for `getCoordinationStatus`: ### Status Codes Implementations MUST use the canonical enum defined below: ```solidity enum Status { None, Proposed, Ready, Executed, Cancelled, Expired } ``` - `None` = default zero state (intent not found) - `Proposed` = intent proposed, not all acceptances yet - `Ready` = all participants have accepted, intent executable - `Executed` = intent successfully executed - `Cancelled` = intent explicitly cancelled - `Expired` = intent expired before execution ### Overview This ERC specifies: - A canonicalised EIP-712 domain for agent coordination, - Typed data structures (`AgentIntent`, `CoordinationPayload`, `AcceptanceAttestation`), - Deterministic hashing rules, - A standard interface (`IAgentCoordination`), - Lifecycle semantics (propose → accept → execute/cancel), - Error surface and status codes. ### EIP-712 Domain Implementations MUST use the following EIP-712 domain: ``` {name: "ERC-8001", version: "1", chainId, verifyingContract} ``` Implementations SHOULD expose the domain via [ERC-5267](./eip-5267.md). ### Primary Types ```solidity struct AgentIntent { bytes32 payloadHash; // keccak256(CoordinationPayload) uint64 expiry; // unix seconds; MUST be > block.timestamp at propose uint64 nonce; // per-agent nonce; MUST be > agentNonces[agentId] address agentId; // initiator and signer of the intent bytes32 coordinationType; // domain-specific type id, e.g. keccak256("MEV_SANDWICH_COORD_V1") uint256 coordinationValue; // informational in Core; modules MAY bind value address[] participants; // unique, ascending; MUST include agentId } struct CoordinationPayload { bytes32 version; // payload format id bytes32 coordinationType; // MUST equal AgentIntent.coordinationType bytes coordinationData; // opaque to Core bytes32 conditionsHash; // domain-specific uint256 timestamp; // creation time (informational) bytes metadata; // optional } struct AcceptanceAttestation { bytes32 intentHash; // getIntentHash(intent) address participant; // signer uint64 nonce; // optional in Core; see Nonces uint64 expiry; // acceptance validity; MUST be > now at accept and execute bytes32 conditionsHash; // participant constraints bytes signature; // ECDSA (65 or 64 bytes) or ERC-1271 } ``` ### Typed Data Hashes ```solidity bytes32 constant AGENT_INTENT_TYPEHASH = keccak256( "AgentIntent(bytes32 payloadHash,uint64 expiry,uint64 nonce,address agentId,bytes32 coordinationType,uint256 coordinationValue,address[] participants)" ); bytes32 constant ACCEPTANCE_TYPEHASH = keccak256( // Field names MUST exactly match the Solidity struct. "AcceptanceAttestation(bytes32 intentHash,address participant,uint64 nonce,uint64 expiry,bytes32 conditionsHash)" ); // participants MUST be unique and strictly ascending by uint160(address). function _participantsHash(address[] memory ps) internal pure returns (bytes32) { return keccak256(abi.encodePacked(ps)); } function _agentIntentStructHash(AgentIntent calldata i) internal pure returns (bytes32) { return keccak256(abi.encode( AGENT_INTENT_TYPEHASH, i.payloadHash, i.expiry, i.nonce, i.agentId, i.coordinationType, i.coordinationValue, _participantsHash(i.participants) )); } // Full EIP-712 digest for the initiator’s signature. function _agentIntentDigest(bytes32 domainSeparator, AgentIntent calldata i) internal pure returns (bytes32) { return keccak256(abi.encodePacked("\x19\x01", domainSeparator, _agentIntentStructHash(i))); } function _acceptanceStructHash(AcceptanceAttestation calldata a) internal pure returns (bytes32) { // a.intentHash MUST be the AgentIntent struct hash, not the digest. return keccak256(abi.encode( ACCEPTANCE_TYPEHASH, a.intentHash, a.participant, a.nonce, a.expiry, a.conditionsHash )); } function _acceptanceDigest(bytes32 domainSeparator, AcceptanceAttestation calldata a) internal pure returns (bytes32) { return keccak256(abi.encodePacked("\x19\x01", domainSeparator, _acceptanceStructHash(a))); } ``` Computation (normative): ```solidity participantsHash = keccak256(abi.encodePacked(participants)); // sorted unique intentStructHash = keccak256(abi.encode( AGENT_INTENT_TYPEHASH, payloadHash, expiry, nonce, agentId, coordinationType, coordinationValue, participantsHash )); intentDigest = keccak256("\x19\x01" || domainSeparator || intentStructHash); ``` Clarifications (normative): - `getIntentHash(intent)` MUST return `intentStructHash` (struct hash), not the full digest. - `AcceptanceAttestation.intentHash` MUST be that struct hash. - Each acceptance is signed over its own EIP-712 digest that includes this field. - `participants` MUST be strictly ascending by `uint160(address)` and deduplicated. ### Interface Implementations **MUST** expose the following interface and events. ```solidity interface IAgentCoordination { event CoordinationProposed(bytes32 indexed intentHash, address indexed proposer, bytes32 coordinationType, uint256 participantCount, uint256 coordinationValue); event CoordinationAccepted(bytes32 indexed intentHash, address indexed participant, bytes32 acceptanceHash, uint256 acceptedCount, uint256 requiredCount); event CoordinationExecuted(bytes32 indexed intentHash, address indexed executor, bool success, uint256 gasUsed, bytes result); event CoordinationCancelled(bytes32 indexed intentHash, address indexed canceller, string reason, uint8 finalStatus); function proposeCoordination(AgentIntent calldata intent, bytes calldata signature, CoordinationPayload calldata payload) external returns (bytes32 intentHash); function acceptCoordination(bytes32 intentHash, AcceptanceAttestation calldata attestation) external returns (bool allAccepted); function executeCoordination(bytes32 intentHash, CoordinationPayload calldata payload, bytes calldata executionData) external returns (bool success, bytes memory result); function cancelCoordination(bytes32 intentHash, string calldata reason) external; function getCoordinationStatus(bytes32 intentHash) external view returns (Status status, address proposer, address[] memory participants, address[] memory acceptedBy, uint256 expiry); function getRequiredAcceptances(bytes32 intentHash) external view returns (uint256); function getAgentNonce(address agent) external view returns (uint64); } ``` ### Semantics The functions defined in this specification MUST exhibit the following externally observable behaviours. This standard does NOT prescribe storage layout, execution model, or internal mechanisms. #### `proposeCoordination` `proposeCoordination` MUST revert if: - the signature does not validate the supplied `AgentIntent` under the ERC-8001 EIP-712 domain; - `intent.expiry <= block.timestamp`; - `intent.nonce` is not strictly greater than `getAgentNonce(intent.agentId)`; - `participants` is not strictly ascending and unique; - `intent.agentId` is not included in the participants list. If valid: - `CoordinationProposed` MUST be emitted; - `getCoordinationStatus` MUST report `Proposed`; - `getAgentNonce(intent.agentId)` MUST equal the supplied nonce; - `getRequiredAcceptances(intentHash)` MUST equal the number of participants. #### `acceptCoordination` `acceptCoordination` MUST revert if: - the intent does not exist or has expired; - the caller is not listed as a participant; - the participant has already accepted; - the attestation signature does not validate under the ERC-8001 domain; - `attestation.expiry <= block.timestamp`. If valid: - `CoordinationAccepted` MUST be emitted; - the participant MUST appear in the `acceptedBy` list returned by `getCoordinationStatus`; - if all participants have accepted: - the function MUST return `true`; - status MUST be `Ready`. Otherwise the function MUST return `false`. #### `executeCoordination` `executeCoordination` MUST revert if: - the intent is not in `Ready` state; - `intent.expiry <= block.timestamp`; - any acceptance has expired; - the supplied payload does not hash to `payloadHash`. If valid: - the implementation MUST attempt execution of the behaviour represented by `executionData`; - the function MUST return `(success, result)`; - `CoordinationExecuted` MUST be emitted; - `getCoordinationStatus` MUST report `Executed`. #### `cancelCoordination` - If the intent has not expired, only the proposer MUST be permitted to cancel. - After expiry, any caller MUST be permitted to cancel. On success: - `CoordinationCancelled` MUST be emitted; - status MUST be `Cancelled`. #### `getCoordinationStatus` `getCoordinationStatus(intentHash)` MUST return: - `None` if the intent does not exist; - `Proposed` if not all participants have accepted and the intent has not expired; - `Ready` if all participants have accepted and expiries have not elapsed; - `Executed` if execution has occurred; - `Cancelled` if cancellation has occurred; - `Expired` if the intent has expired and was not executed or cancelled. #### Nonces - `getAgentNonce(agent)` MUST increase for every valid new intent. - `proposeCoordination` MUST reject nonces not strictly greater than the stored nonce. - Acceptance-level nonces MAY be implemented; if so, they MUST be strictly monotonic per participant. ### Errors Implementations SHOULD revert with descriptive custom errors (or equivalent revert strings) for the following baseline conditions, and MAY define additional errors for domain-specific modules (e.g. slashing, reputation, or privacy conditions): - Expired intent - Bad signature - Non-participant - Duplicate acceptance - Acceptance expired at execute - Payload hash mismatch ```solidity error ERC8001_NotProposer(); error ERC8001_ExpiredIntent(); error ERC8001_ExpiredAcceptance(address participant); error ERC8001_BadSignature(); error ERC8001_NotParticipant(); error ERC8001_DuplicateAcceptance(); error ERC8001_ParticipantsNotCanonical(); error ERC8001_NonceTooLow(); error ERC8001_PayloadHashMismatch(); error ERC8001_NotReady(); ``` ## Rationale - Sorted participant lists remove hash malleability and allow off-chain deduplication. - Separation of intent and acceptance allows off-chain collation and a single on-chain check. - Keeping [ERC-8001](./eip-8001.md) single-chain avoids coupling to bridge semantics and keeps the primitive audit-friendly. - Wallet friendliness: EIP-712 arrays let signers see actual participant addresses. ## Backwards Compatibility [ERC-8001](./eip-8001.md) introduces a new interface. It is compatible with EOA and contract wallets via ECDSA and ERC-1271. It does not modify existing standards. ## Reference Implementation A permissive reference implementation is provided in [`contracts/AgentCoordination.sol`](../assets/eip-8001/contracts/AgentCoordination.sol). It uses a minimal ECDSA helper and supports ERC-1271 signers. It enforces participant canonicalisation, intent nonces, acceptance freshness, and all-participants policy. ## Security Considerations - **Replay**: EIP-712 domain binding and monotonic nonces prevent cross-contract replay. - **Malleability**: Low-s enforcement and 64/65-byte signature support are required. - **Equivocation**: A participant can sign conflicting intents. Mitigate with module-level slashing or reputation. - **Liveness**: Enforce TTL on both intent and acceptances. Executors should ensure enough time remains. - **MEV**: If `coordinationData` reveals strategy, use a Privacy module with commit-reveal or encryption. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). [EIP-712]: ./eip-712.md [ERC-1271]: ./eip-1271.md [EIP-2098]: ./eip-2098.md [EIP-5267]: ./eip-5267.md