// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; /** * @title AereIdentityRegistry8004V2 — ERC-8004 Identity Registry, to the letter of the draft. * * @notice Every function, event, argument order and event indexing here is transcribed from the * ERC-8004 draft text (eips.ethereum.org/EIPS/eip-8004, "Identity Registry" section, read * 2026-09-02), not from our earlier adapter. The adapter deployed in Wave A * (AereIdentityRegistry8004, 0x5C65105E) predates the draft's ERC-721 form and fails 10 of * 10 identity checks of our own conformance suite; this contract is the one meant to pass * them, and the suite can be run against a local deployment of it before anyone deploys. * * @dev Surface (ERC-721 + URIStorage, plus the ERC-8004 additions): * register(string,(string,bytes)[]) / register(string) / register() * setAgentURI(uint256,string) -> URIUpdated * getMetadata(uint256,string) / setMetadata(uint256,string,bytes) -> MetadataSet * setAgentWallet(uint256,address,uint256,bytes) / getAgentWallet / unsetAgentWallet * The reserved key `agentWallet` is initialised to the owner at registration, cannot be set * through setMetadata or the register metadata array, is cleared on transfer, and can only * be changed with an EIP-712 signature of the NEW wallet (EOA) or an ERC-1271 approval * (contract wallet), bound to (agentId, newWallet, owner, deadline) under this registry's * domain. `agentCount` is kept for the operational check POP-1 of the suite. * Agent ids start at 1 and never repeat. No owner, no admin, no upgrade. */ contract AereIdentityRegistry8004V2 is ERC721URIStorage, EIP712 { struct MetadataEntry { string metadataKey; bytes metadataValue; } event Registered(uint256 indexed agentId, string agentURI, address indexed owner); event URIUpdated(uint256 indexed agentId, string newURI, address indexed updatedBy); event MetadataSet(uint256 indexed agentId, string indexed indexedMetadataKey, string metadataKey, bytes metadataValue); error NotAgentOwnerOrOperator(uint256 agentId, address caller); error ReservedMetadataKey(); error DeadlinePassed(uint256 deadline, uint256 nowTs); error InvalidWalletSignature(address newWallet); error ZeroWallet(); bytes32 private constant AGENT_WALLET_KEY_HASH = keccak256("agentWallet"); string private constant AGENT_WALLET_KEY = "agentWallet"; /// @dev EIP-712 type the NEW wallet signs to prove control; `owner` binds it to the current holder. bytes32 public constant AGENT_WALLET_TYPEHASH = keccak256("AgentWalletSet(uint256 agentId,address newWallet,address owner,uint256 deadline)"); uint256 private _nextId = 1; uint256 public agentCount; mapping(uint256 => mapping(bytes32 => bytes)) private _metadata; mapping(uint256 => address) private _agentWallet; constructor() ERC721("AERE Agent Identity", "AGENT") EIP712("ERC8004IdentityRegistry", "1") {} // ------------------------------------------------------------------ registration function register(string calldata agentURI, MetadataEntry[] calldata metadata) external returns (uint256 agentId) { agentId = _register(agentURI); for (uint256 i = 0; i < metadata.length; i++) { _setMetadata(agentId, metadata[i].metadataKey, metadata[i].metadataValue); } emit Registered(agentId, agentURI, msg.sender); } function register(string calldata agentURI) external returns (uint256 agentId) { agentId = _register(agentURI); emit Registered(agentId, agentURI, msg.sender); } function register() external returns (uint256 agentId) { agentId = _register(""); emit Registered(agentId, "", msg.sender); } function _register(string memory agentURI) internal returns (uint256 agentId) { agentId = _nextId++; agentCount += 1; _safeMint(msg.sender, agentId); if (bytes(agentURI).length != 0) _setTokenURI(agentId, agentURI); // the reserved key is initialised to the owner's address (spec: "initially set to the owner's address") _agentWallet[agentId] = msg.sender; emit MetadataSet(agentId, AGENT_WALLET_KEY, AGENT_WALLET_KEY, abi.encode(msg.sender)); } // ------------------------------------------------------------------ agentURI function setAgentURI(uint256 agentId, string calldata newURI) external { _requireOwnerOrOperator(agentId); _setTokenURI(agentId, newURI); emit URIUpdated(agentId, newURI, msg.sender); } // ------------------------------------------------------------------ metadata function getMetadata(uint256 agentId, string memory metadataKey) external view returns (bytes memory) { if (keccak256(bytes(metadataKey)) == AGENT_WALLET_KEY_HASH) return abi.encode(_agentWallet[agentId]); return _metadata[agentId][keccak256(bytes(metadataKey))]; } function setMetadata(uint256 agentId, string memory metadataKey, bytes memory metadataValue) external { _requireOwnerOrOperator(agentId); _setMetadata(agentId, metadataKey, metadataValue); } function _setMetadata(uint256 agentId, string memory metadataKey, bytes memory metadataValue) internal { bytes32 k = keccak256(bytes(metadataKey)); if (k == AGENT_WALLET_KEY_HASH) revert ReservedMetadataKey(); _metadata[agentId][k] = metadataValue; emit MetadataSet(agentId, metadataKey, metadataKey, metadataValue); } // ------------------------------------------------------------------ agent wallet function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes calldata signature) external { _requireOwnerOrOperator(agentId); if (newWallet == address(0)) revert ZeroWallet(); if (block.timestamp > deadline) revert DeadlinePassed(deadline, block.timestamp); bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(AGENT_WALLET_TYPEHASH, agentId, newWallet, ownerOf(agentId), deadline))); if (!SignatureChecker.isValidSignatureNow(newWallet, digest, signature)) revert InvalidWalletSignature(newWallet); _agentWallet[agentId] = newWallet; emit MetadataSet(agentId, AGENT_WALLET_KEY, AGENT_WALLET_KEY, abi.encode(newWallet)); } function getAgentWallet(uint256 agentId) external view returns (address) { return _agentWallet[agentId]; } function unsetAgentWallet(uint256 agentId) external { _requireOwnerOrOperator(agentId); _clearWallet(agentId); } function _clearWallet(uint256 agentId) internal { _agentWallet[agentId] = address(0); emit MetadataSet(agentId, AGENT_WALLET_KEY, AGENT_WALLET_KEY, abi.encode(address(0))); } /// @dev spec: "When the agent is transferred, agentWallet is automatically cleared". function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal override { super._afterTokenTransfer(from, to, firstTokenId, batchSize); if (from != address(0) && to != address(0) && from != to) _clearWallet(firstTokenId); } // ------------------------------------------------------------------ helpers function _requireOwnerOrOperator(uint256 agentId) internal view { if (!_isApprovedOrOwner(msg.sender, agentId)) revert NotAgentOwnerOrOperator(agentId, msg.sender); } /// @notice True when `who` is the owner, the approved address, or an operator of `agentId`. function isOwnerOrOperator(uint256 agentId, address who) external view returns (bool) { return _isApprovedOrOwner(who, agentId); } /// @notice EIP-712 domain separator, for wallets that build the AgentWalletSet signature. function domainSeparator() external view returns (bytes32) { return _domainSeparatorV4(); } }