diff --git a/contracts/erc8004/v2/AereIdentityRegistry8004V2.sol b/contracts/erc8004/v2/AereIdentityRegistry8004V2.sol new file mode 100644 index 0000000..17c7cc8 --- /dev/null +++ b/contracts/erc8004/v2/AereIdentityRegistry8004V2.sol @@ -0,0 +1,163 @@ +// 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(); + } +} diff --git a/contracts/erc8004/v2/AereReputationRegistry8004V2.sol b/contracts/erc8004/v2/AereReputationRegistry8004V2.sol new file mode 100644 index 0000000..3dbc02c --- /dev/null +++ b/contracts/erc8004/v2/AereReputationRegistry8004V2.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/** + * @title AereReputationRegistry8004V2 — ERC-8004 Reputation Registry, to the letter of the draft. + * + * @notice Transcribed from the "Reputation Registry" section of the ERC-8004 draft (read + * 2026-09-02): giveFeedback / revokeFeedback / appendResponse and the read functions, with + * the exact argument orders and event indexing. Feedback value, valueDecimals, tag1, tag2 + * and isRevoked are stored; endpoint, feedbackURI and feedbackHash are only emitted, as the + * draft says. The feedback submitter must not be the agent's owner, approved address or + * operator (checked against the Identity Registry). + * + * @dev `initialize(address)` follows the draft's wording; it can run once. A constructor argument + * is offered for non-proxy deployments (pass address(0) to initialise later). No owner, no + * admin, no upgrade. getSummary normalises values to the largest valueDecimals among the + * counted feedback and returns their AVERAGE at that precision (the draft leaves the + * aggregation to implementations; this one is documented here and tested). + */ +contract AereReputationRegistry8004V2 { + struct Feedback { + int128 value; + uint8 valueDecimals; + string tag1; + string tag2; + bool isRevoked; + } + + event NewFeedback(uint256 indexed agentId, address indexed clientAddress, uint64 feedbackIndex, int128 value, uint8 valueDecimals, string indexed indexedTag1, string tag1, string tag2, string endpoint, string feedbackURI, bytes32 feedbackHash); + event FeedbackRevoked(uint256 indexed agentId, address indexed clientAddress, uint64 indexed feedbackIndex); + event ResponseAppended(uint256 indexed agentId, address indexed clientAddress, uint64 feedbackIndex, address indexed responder, string responseURI, bytes32 responseHash); + + error AlreadyInitialized(); + error NotInitialized(); + error ZeroAddress(); + error AgentNotRegistered(uint256 agentId); + error SelfFeedback(uint256 agentId, address caller); + error BadDecimals(uint8 valueDecimals); + error NoSuchFeedback(uint256 agentId, address clientAddress, uint64 feedbackIndex); + error AlreadyRevoked(uint256 agentId, address clientAddress, uint64 feedbackIndex); + error EmptyClientList(); + + address private _identityRegistry; + mapping(uint256 => mapping(address => Feedback[])) private _feedback; // feedbackIndex = position + 1 + mapping(uint256 => address[]) private _clients; + mapping(uint256 => mapping(address => bool)) private _isClient; + mapping(bytes32 => uint64) private _responses; // keccak(agentId, client, index, responder) + mapping(bytes32 => uint64) private _responsesTotal; // keccak(agentId, client, index) + + constructor(address identityRegistry_) { + if (identityRegistry_ != address(0)) _identityRegistry = identityRegistry_; + } + + function initialize(address identityRegistry_) external { + if (_identityRegistry != address(0)) revert AlreadyInitialized(); + if (identityRegistry_ == address(0)) revert ZeroAddress(); + _identityRegistry = identityRegistry_; + } + + function getIdentityRegistry() external view returns (address identityRegistry) { + return _identityRegistry; + } + + // ------------------------------------------------------------------ writes + + function giveFeedback(uint256 agentId, int128 value, uint8 valueDecimals, string calldata tag1, string calldata tag2, string calldata endpoint, string calldata feedbackURI, bytes32 feedbackHash) external { + if (valueDecimals > 18) revert BadDecimals(valueDecimals); + address owner = _ownerOf(agentId); + IERC721 idr = IERC721(_identityRegistry); + if (msg.sender == owner || idr.isApprovedForAll(owner, msg.sender) || idr.getApproved(agentId) == msg.sender) revert SelfFeedback(agentId, msg.sender); + Feedback[] storage list = _feedback[agentId][msg.sender]; + list.push(Feedback({ value: value, valueDecimals: valueDecimals, tag1: tag1, tag2: tag2, isRevoked: false })); + uint64 feedbackIndex = uint64(list.length); + if (!_isClient[agentId][msg.sender]) { _isClient[agentId][msg.sender] = true; _clients[agentId].push(msg.sender); } + emit NewFeedback(agentId, msg.sender, feedbackIndex, value, valueDecimals, tag1, tag1, tag2, endpoint, feedbackURI, feedbackHash); + } + + function revokeFeedback(uint256 agentId, uint64 feedbackIndex) external { + Feedback storage f = _get(agentId, msg.sender, feedbackIndex); + if (f.isRevoked) revert AlreadyRevoked(agentId, msg.sender, feedbackIndex); + f.isRevoked = true; + emit FeedbackRevoked(agentId, msg.sender, feedbackIndex); + } + + function appendResponse(uint256 agentId, address clientAddress, uint64 feedbackIndex, string calldata responseURI, bytes32 responseHash) external { + _get(agentId, clientAddress, feedbackIndex); // reverts when the feedback does not exist + _responses[keccak256(abi.encode(agentId, clientAddress, feedbackIndex, msg.sender))] += 1; + _responsesTotal[keccak256(abi.encode(agentId, clientAddress, feedbackIndex))] += 1; + emit ResponseAppended(agentId, clientAddress, feedbackIndex, msg.sender, responseURI, responseHash); + } + + // ------------------------------------------------------------------ reads + + function getSummary(uint256 agentId, address[] calldata clientAddresses, string calldata tag1, string calldata tag2) external view returns (uint64 count, int128 summaryValue, uint8 summaryValueDecimals) { + if (clientAddresses.length == 0) revert EmptyClientList(); + bytes32 h1 = keccak256(bytes(tag1)); bytes32 h2 = keccak256(bytes(tag2)); + bool f1 = bytes(tag1).length != 0; bool f2 = bytes(tag2).length != 0; + // first pass: the largest precision among the counted feedback + for (uint256 c = 0; c < clientAddresses.length; c++) { + Feedback[] storage list = _feedback[agentId][clientAddresses[c]]; + for (uint256 i = 0; i < list.length; i++) { + Feedback storage f = list[i]; + if (f.isRevoked) continue; + if (f1 && keccak256(bytes(f.tag1)) != h1) continue; + if (f2 && keccak256(bytes(f.tag2)) != h2) continue; + if (f.valueDecimals > summaryValueDecimals) summaryValueDecimals = f.valueDecimals; + count += 1; + } + } + if (count == 0) return (0, 0, 0); + int256 sum; + for (uint256 c = 0; c < clientAddresses.length; c++) { + Feedback[] storage list = _feedback[agentId][clientAddresses[c]]; + for (uint256 i = 0; i < list.length; i++) { + Feedback storage f = list[i]; + if (f.isRevoked) continue; + if (f1 && keccak256(bytes(f.tag1)) != h1) continue; + if (f2 && keccak256(bytes(f.tag2)) != h2) continue; + sum += int256(f.value) * int256(10 ** uint256(summaryValueDecimals - f.valueDecimals)); + } + } + summaryValue = int128(sum / int256(uint256(count))); + } + + function readFeedback(uint256 agentId, address clientAddress, uint64 feedbackIndex) external view returns (int128 value, uint8 valueDecimals, string memory tag1, string memory tag2, bool isRevoked) { + Feedback storage f = _get(agentId, clientAddress, feedbackIndex); + return (f.value, f.valueDecimals, f.tag1, f.tag2, f.isRevoked); + } + + function readAllFeedback(uint256 agentId, address[] calldata clientAddresses, string calldata tag1, string calldata tag2, bool includeRevoked) + external view + returns (address[] memory clients, uint64[] memory feedbackIndexes, int128[] memory values, uint8[] memory valueDecimals, string[] memory tag1s, string[] memory tag2s, bool[] memory revokedStatuses) + { + address[] memory who; + if (clientAddresses.length == 0) who = _clients[agentId]; + else who = clientAddresses; + bytes32 h1 = keccak256(bytes(tag1)); bytes32 h2 = keccak256(bytes(tag2)); + bool f1 = bytes(tag1).length != 0; bool f2 = bytes(tag2).length != 0; + uint256 n; + for (uint256 c = 0; c < who.length; c++) { + Feedback[] storage list = _feedback[agentId][who[c]]; + for (uint256 i = 0; i < list.length; i++) if (_matches(list[i], includeRevoked, f1, h1, f2, h2)) n++; + } + clients = new address[](n); feedbackIndexes = new uint64[](n); values = new int128[](n); valueDecimals = new uint8[](n); tag1s = new string[](n); tag2s = new string[](n); revokedStatuses = new bool[](n); + uint256 k; + for (uint256 c = 0; c < who.length; c++) { + Feedback[] storage list = _feedback[agentId][who[c]]; + for (uint256 i = 0; i < list.length; i++) { + Feedback storage f = list[i]; + if (!_matches(f, includeRevoked, f1, h1, f2, h2)) continue; + clients[k] = who[c]; feedbackIndexes[k] = uint64(i + 1); values[k] = f.value; valueDecimals[k] = f.valueDecimals; tag1s[k] = f.tag1; tag2s[k] = f.tag2; revokedStatuses[k] = f.isRevoked; + k++; + } + } + } + + function getResponseCount(uint256 agentId, address clientAddress, uint64 feedbackIndex, address[] calldata responders) external view returns (uint64 count) { + if (responders.length == 0) return _responsesTotal[keccak256(abi.encode(agentId, clientAddress, feedbackIndex))]; + for (uint256 i = 0; i < responders.length; i++) count += _responses[keccak256(abi.encode(agentId, clientAddress, feedbackIndex, responders[i]))]; + } + + function getClients(uint256 agentId) external view returns (address[] memory) { + return _clients[agentId]; + } + + function getLastIndex(uint256 agentId, address clientAddress) external view returns (uint64) { + return uint64(_feedback[agentId][clientAddress].length); + } + + // ------------------------------------------------------------------ internals + + function _matches(Feedback storage f, bool includeRevoked, bool f1, bytes32 h1, bool f2, bytes32 h2) internal view returns (bool) { + if (f.isRevoked && !includeRevoked) return false; + if (f1 && keccak256(bytes(f.tag1)) != h1) return false; + if (f2 && keccak256(bytes(f.tag2)) != h2) return false; + return true; + } + + function _get(uint256 agentId, address clientAddress, uint64 feedbackIndex) internal view returns (Feedback storage) { + Feedback[] storage list = _feedback[agentId][clientAddress]; + if (feedbackIndex == 0 || feedbackIndex > list.length) revert NoSuchFeedback(agentId, clientAddress, feedbackIndex); + return list[feedbackIndex - 1]; + } + + function _ownerOf(uint256 agentId) internal view returns (address owner) { + if (_identityRegistry == address(0)) revert NotInitialized(); + try IERC721(_identityRegistry).ownerOf(agentId) returns (address o) { owner = o; } catch { revert AgentNotRegistered(agentId); } + if (owner == address(0)) revert AgentNotRegistered(agentId); + } +} diff --git a/contracts/erc8004/v2/AereValidationRegistry8004V2.sol b/contracts/erc8004/v2/AereValidationRegistry8004V2.sol new file mode 100644 index 0000000..bd34e63 --- /dev/null +++ b/contracts/erc8004/v2/AereValidationRegistry8004V2.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +/** + * @title AereValidationRegistry8004V2 — ERC-8004 Validation Registry, to the letter of the draft. + * + * @notice Transcribed from the "Validation Registry" section of the ERC-8004 draft (read + * 2026-09-02): validationRequest (owner or operator of the agent), validationResponse (only + * the validator named in the request; response 0..100; may be called repeatedly, e.g. soft + * then hard finality via `tag`), and the four read functions, with the exact argument + * orders and event indexing. Stores requestHash, validatorAddress, agentId, response, + * responseHash, lastUpdate and tag, as the draft lists. + * + * @dev `initialize(address)` follows the draft's wording and can run once; a constructor + * argument is offered for non-proxy deployments. `requestCount` is kept for the operational + * check POP-1 of the conformance suite. No owner, no admin, no upgrade. + */ +contract AereValidationRegistry8004V2 { + struct Request { + address validatorAddress; + uint256 agentId; + uint8 response; + bytes32 responseHash; + string tag; + uint256 lastUpdate; + bool exists; + bool responded; + } + + event ValidationRequest(address indexed validatorAddress, uint256 indexed agentId, string requestURI, bytes32 indexed requestHash); + event ValidationResponse(address indexed validatorAddress, uint256 indexed agentId, bytes32 indexed requestHash, uint8 response, string responseURI, bytes32 responseHash, string tag); + + error AlreadyInitialized(); + error NotInitialized(); + error ZeroAddress(); + error NotAgentOwnerOrOperator(uint256 agentId, address caller); + error AgentNotRegistered(uint256 agentId); + error RequestExists(bytes32 requestHash); + error NoSuchRequest(bytes32 requestHash); + error NotTheValidator(bytes32 requestHash, address caller); + error BadResponse(uint8 response); + + address private _identityRegistry; + uint256 public requestCount; + mapping(bytes32 => Request) private _requests; + mapping(uint256 => bytes32[]) private _byAgent; + mapping(address => bytes32[]) private _byValidator; + + constructor(address identityRegistry_) { + if (identityRegistry_ != address(0)) _identityRegistry = identityRegistry_; + } + + function initialize(address identityRegistry_) external { + if (_identityRegistry != address(0)) revert AlreadyInitialized(); + if (identityRegistry_ == address(0)) revert ZeroAddress(); + _identityRegistry = identityRegistry_; + } + + function getIdentityRegistry() external view returns (address identityRegistry) { + return _identityRegistry; + } + + // ------------------------------------------------------------------ writes + + function validationRequest(address validatorAddress, uint256 agentId, string calldata requestURI, bytes32 requestHash) external { + if (validatorAddress == address(0)) revert ZeroAddress(); + _requireOwnerOrOperator(agentId); + if (_requests[requestHash].exists) revert RequestExists(requestHash); + _requests[requestHash] = Request({ validatorAddress: validatorAddress, agentId: agentId, response: 0, responseHash: bytes32(0), tag: "", lastUpdate: block.timestamp, exists: true, responded: false }); + _byAgent[agentId].push(requestHash); + _byValidator[validatorAddress].push(requestHash); + requestCount += 1; + emit ValidationRequest(validatorAddress, agentId, requestURI, requestHash); + } + + function validationResponse(bytes32 requestHash, uint8 response, string calldata responseURI, bytes32 responseHash, string calldata tag) external { + Request storage r = _requests[requestHash]; + if (!r.exists) revert NoSuchRequest(requestHash); + if (msg.sender != r.validatorAddress) revert NotTheValidator(requestHash, msg.sender); + if (response > 100) revert BadResponse(response); + r.response = response; + r.responseHash = responseHash; + r.tag = tag; + r.lastUpdate = block.timestamp; + r.responded = true; + emit ValidationResponse(r.validatorAddress, r.agentId, requestHash, response, responseURI, responseHash, tag); + } + + // ------------------------------------------------------------------ reads + + function getValidationStatus(bytes32 requestHash) external view returns (address validatorAddress, uint256 agentId, uint8 response, bytes32 responseHash, string memory tag, uint256 lastUpdate) { + Request storage r = _requests[requestHash]; + if (!r.exists) revert NoSuchRequest(requestHash); + return (r.validatorAddress, r.agentId, r.response, r.responseHash, r.tag, r.lastUpdate); + } + + function getSummary(uint256 agentId, address[] calldata validatorAddresses, string calldata tag) external view returns (uint64 count, uint8 averageResponse) { + bytes32[] storage hs = _byAgent[agentId]; + bytes32 ht = keccak256(bytes(tag)); bool ft = bytes(tag).length != 0; + uint256 sum; + for (uint256 i = 0; i < hs.length; i++) { + Request storage r = _requests[hs[i]]; + if (!r.responded) continue; + if (validatorAddresses.length != 0 && !_contains(validatorAddresses, r.validatorAddress)) continue; + if (ft && keccak256(bytes(r.tag)) != ht) continue; + sum += r.response; count += 1; + } + if (count != 0) averageResponse = uint8(sum / count); + } + + function getAgentValidations(uint256 agentId) external view returns (bytes32[] memory requestHashes) { + return _byAgent[agentId]; + } + + function getValidatorRequests(address validatorAddress) external view returns (bytes32[] memory requestHashes) { + return _byValidator[validatorAddress]; + } + + // ------------------------------------------------------------------ internals + + function _contains(address[] calldata xs, address x) internal pure returns (bool) { + for (uint256 i = 0; i < xs.length; i++) if (xs[i] == x) return true; + return false; + } + + function _requireOwnerOrOperator(uint256 agentId) internal view { + if (_identityRegistry == address(0)) revert NotInitialized(); + IERC721 idr = IERC721(_identityRegistry); + address owner; + try idr.ownerOf(agentId) returns (address o) { owner = o; } catch { revert AgentNotRegistered(agentId); } + if (msg.sender == owner || idr.isApprovedForAll(owner, msg.sender) || idr.getApproved(agentId) == msg.sender) return; + revert NotAgentOwnerOrOperator(agentId, msg.sender); + } +} diff --git a/contracts/mocks/ERC8004V2Mocks.sol b/contracts/mocks/ERC8004V2Mocks.sol new file mode 100644 index 0000000..51ba6ea --- /dev/null +++ b/contracts/mocks/ERC8004V2Mocks.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +/// @dev Test-only ERC-1271 wallet: a contract wallet whose approvals are ECDSA signatures of `owner`. +/// Used to prove that AereIdentityRegistry8004V2.setAgentWallet accepts contract wallets (ERC-1271) +/// and not only EOAs (EIP-712), as the ERC-8004 draft requires. +contract Mock1271Wallet is IERC1271 { + address public immutable owner; + constructor(address owner_) { owner = owner_; } + function isValidSignature(bytes32 hash, bytes memory signature) external view override returns (bytes4) { + (address rec, ECDSA.RecoverError err) = ECDSA.tryRecover(hash, signature); + if (err == ECDSA.RecoverError.NoError && rec == owner) return IERC1271.isValidSignature.selector; + return bytes4(0); + } +} diff --git a/contracts/confidential/AereConfidentialCompute.sol b/contracts/mpc/AereConfidentialCompute.sol similarity index 100% rename from contracts/confidential/AereConfidentialCompute.sol rename to contracts/mpc/AereConfidentialCompute.sol diff --git a/scripts/conformanta-locala-8004.js b/scripts/conformanta-locala-8004.js new file mode 100644 index 0000000..bb87db0 --- /dev/null +++ b/scripts/conformanta-locala-8004.js @@ -0,0 +1,35 @@ +// Desfasoara cele trei registre ERC-8004 V2 pe reteaua data (implicit: nodul hardhat local) si scrie fisierul +// de adrese pe care suita de conformitate il primeste prin AERE_CONFORMANCE_ADDR_FILE. Asa conformitatea +// CODULUI se masoara inainte sa desfasoare cineva ceva pe 2800 (care e semnatura fondatorului). +// npx hardhat run scripts/conformanta-locala-8004.js --network localhost +const { ethers, network } = require("hardhat"); +const fs = require("fs"); +const path = require("path"); + +async function main() { + const [deployer] = await ethers.getSigners(); + const idr = await (await ethers.getContractFactory("AereIdentityRegistry8004V2")).deploy(); + await idr.waitForDeployment(); + const rep = await (await ethers.getContractFactory("AereReputationRegistry8004V2")).deploy(await idr.getAddress()); + await rep.waitForDeployment(); + const val = await (await ethers.getContractFactory("AereValidationRegistry8004V2")).deploy(await idr.getAddress()); + await val.waitForDeployment(); + // un agent inregistrat si o cerere de validare, ca POP-1 sa aiba ce numara + await (await idr["register(string)"]("ipfs://agent-local")).wait(); + const h = ethers.keccak256(ethers.toUtf8Bytes("local-request")); + await (await val.validationRequest(deployer.address, 1, "ipfs://req", h)).wait(); + await (await val.validationResponse(h, 100, "", ethers.ZeroHash, "hard")).wait(); + const out = { + identity8004: await idr.getAddress(), + validation8004: await val.getAddress(), + reputation8004: await rep.getAddress(), + // primitivele vii ale lui 2800 nu exista pe reteaua locala; se lasa pe cele de mainnet doar ca sa aiba forma, + // iar verificarile lor (PQC, x402) NU sunt tinta rularii locale si raman cum ies + agentDID: await idr.getAddress(), pqcKeyRegistry: await idr.getAddress(), aiReputation: await rep.getAddress(), aere402V2: await val.getAddress(), + }; + const p = path.join(__dirname, "..", "reports", "adrese-conformanta-locala.json"); + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, JSON.stringify(out, null, 2)); + console.log(`retea ${network.name}: identity ${out.identity8004}, reputation ${out.reputation8004}, validation ${out.validation8004}; adrese in ${p}`); +} +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/test/ERC8004V2.test.js b/test/ERC8004V2.test.js new file mode 100644 index 0000000..3c10dd0 --- /dev/null +++ b/test/ERC8004V2.test.js @@ -0,0 +1,212 @@ +// ERC-8004 V2 registries (identity / reputation / validation), transcribed from the draft text. +// Every test names the draft sentence it enforces; negative cases are the draft's MUST NOTs. +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +const IFACE = { erc165: "0x01ffc9a7", erc721: "0x80ac58cd", erc721Metadata: "0x5b5e139f" }; + +describe("ERC-8004 V2 registries (draft-conformant)", function () { + let idr, rep, val, owner, other, client, validator, wallet2; + + beforeEach(async function () { + [owner, other, client, validator, wallet2] = await ethers.getSigners(); + idr = await (await ethers.getContractFactory("AereIdentityRegistry8004V2")).deploy(); + await idr.waitForDeployment(); + rep = await (await ethers.getContractFactory("AereReputationRegistry8004V2")).deploy(await idr.getAddress()); + val = await (await ethers.getContractFactory("AereValidationRegistry8004V2")).deploy(ethers.ZeroAddress); + await val.initialize(await idr.getAddress()); + }); + + describe("Identity Registry", function () { + it("is an ERC-721 with URIStorage: ERC-165 ids for 165, 721 and 721Metadata", async function () { + for (const id of Object.values(IFACE)) expect(await idr.supportsInterface(id)).to.equal(true); + expect(await idr.supportsInterface("0xffffffff")).to.equal(false); + }); + + it("register(string): mints to the caller, sets tokenURI, initialises agentWallet to the owner, emits Registered + MetadataSet(agentWallet)", async function () { + const tx = await idr.connect(owner)["register(string)"]("ipfs://agent-1"); + await expect(tx).to.emit(idr, "Registered").withArgs(1, "ipfs://agent-1", owner.address); + await expect(tx).to.emit(idr, "MetadataSet").withArgs(1, "agentWallet", "agentWallet", ethers.AbiCoder.defaultAbiCoder().encode(["address"], [owner.address])); + expect(await idr.ownerOf(1)).to.equal(owner.address); + expect(await idr.tokenURI(1)).to.equal("ipfs://agent-1"); + expect(await idr.getAgentWallet(1)).to.equal(owner.address); + expect(await idr.agentCount()).to.equal(1); + }); + + it("register() and register(string,(string,bytes)[]) overloads; metadata entries emit MetadataSet; ids never repeat", async function () { + await idr.connect(owner)["register()"](); + const tx = await idr.connect(other)["register(string,(string,bytes)[])"]("ipfs://a2", [{ metadataKey: "model", metadataValue: ethers.toUtf8Bytes("claude") }]); + await expect(tx).to.emit(idr, "MetadataSet").withArgs(2, "model", "model", ethers.hexlify(ethers.toUtf8Bytes("claude"))); + expect(await idr.getMetadata(2, "model")).to.equal(ethers.hexlify(ethers.toUtf8Bytes("claude"))); + expect(await idr.ownerOf(2)).to.equal(other.address); + expect(await idr.tokenURI(1)).to.equal(""); + }); + + it("the reserved key agentWallet cannot be set through setMetadata or the register metadata array", async function () { + await idr.connect(owner)["register()"](); + await expect(idr.connect(owner).setMetadata(1, "agentWallet", "0x01")).to.be.revertedWithCustomError(idr, "ReservedMetadataKey"); + await expect(idr.connect(owner)["register(string,(string,bytes)[])"]("", [{ metadataKey: "agentWallet", metadataValue: "0x01" }])).to.be.revertedWithCustomError(idr, "ReservedMetadataKey"); + }); + + it("setAgentURI and setMetadata are owner-or-operator only; URIUpdated carries updatedBy", async function () { + await idr.connect(owner)["register()"](); + await expect(idr.connect(other).setAgentURI(1, "x")).to.be.revertedWithCustomError(idr, "NotAgentOwnerOrOperator"); + await expect(idr.connect(owner).setAgentURI(1, "ipfs://new")).to.emit(idr, "URIUpdated").withArgs(1, "ipfs://new", owner.address); + await idr.connect(owner).setApprovalForAll(other.address, true); + await expect(idr.connect(other).setAgentURI(1, "ipfs://op")).to.emit(idr, "URIUpdated").withArgs(1, "ipfs://op", other.address); + await expect(idr.connect(client).setMetadata(1, "k", "0x02")).to.be.revertedWithCustomError(idr, "NotAgentOwnerOrOperator"); + }); + + async function walletSig(signer, agentId, newWallet, ownerAddr, deadline) { + const net = await ethers.provider.getNetwork(); + const domain = { name: "ERC8004IdentityRegistry", version: "1", chainId: net.chainId, verifyingContract: await idr.getAddress() }; + const types = { AgentWalletSet: [{ name: "agentId", type: "uint256" }, { name: "newWallet", type: "address" }, { name: "owner", type: "address" }, { name: "deadline", type: "uint256" }] }; + return signer.signTypedData(domain, types, { agentId, newWallet, owner: ownerAddr, deadline }); + } + + it("setAgentWallet: the NEW wallet proves control with an EIP-712 signature (EOA)", async function () { + await idr.connect(owner)["register()"](); + const deadline = Math.floor(Date.now() / 1000) + 3600; + const sig = await walletSig(wallet2, 1, wallet2.address, owner.address, deadline); + await expect(idr.connect(owner).setAgentWallet(1, wallet2.address, deadline, sig)).to.emit(idr, "MetadataSet"); + expect(await idr.getAgentWallet(1)).to.equal(wallet2.address); + expect(await idr.getMetadata(1, "agentWallet")).to.equal(ethers.AbiCoder.defaultAbiCoder().encode(["address"], [wallet2.address])); + }); + + it("setAgentWallet negatives: wrong signer, expired deadline, signature bound to another owner, non-owner caller", async function () { + await idr.connect(owner)["register()"](); + const deadline = Math.floor(Date.now() / 1000) + 3600; + const wrong = await walletSig(other, 1, wallet2.address, owner.address, deadline); // signed by someone else + await expect(idr.connect(owner).setAgentWallet(1, wallet2.address, deadline, wrong)).to.be.revertedWithCustomError(idr, "InvalidWalletSignature"); + const good = await walletSig(wallet2, 1, wallet2.address, owner.address, 1); // expired + await expect(idr.connect(owner).setAgentWallet(1, wallet2.address, 1, good)).to.be.revertedWithCustomError(idr, "DeadlinePassed"); + const forOther = await walletSig(wallet2, 1, wallet2.address, other.address, deadline); // bound to a different owner + await expect(idr.connect(owner).setAgentWallet(1, wallet2.address, deadline, forOther)).to.be.revertedWithCustomError(idr, "InvalidWalletSignature"); + const ok = await walletSig(wallet2, 1, wallet2.address, owner.address, deadline); + await expect(idr.connect(other).setAgentWallet(1, wallet2.address, deadline, ok)).to.be.revertedWithCustomError(idr, "NotAgentOwnerOrOperator"); + }); + + it("setAgentWallet accepts an ERC-1271 contract wallet", async function () { + await idr.connect(owner)["register()"](); + const cw = await (await ethers.getContractFactory("Mock1271Wallet")).deploy(wallet2.address); + const cwAddr = await cw.getAddress(); + const deadline = Math.floor(Date.now() / 1000) + 3600; + const sig = await walletSig(wallet2, 1, cwAddr, owner.address, deadline); // the contract wallet's owner signs + await idr.connect(owner).setAgentWallet(1, cwAddr, deadline, sig); + expect(await idr.getAgentWallet(1)).to.equal(cwAddr); + const bad = await walletSig(other, 1, cwAddr, owner.address, deadline); + await expect(idr.connect(owner).setAgentWallet(1, cwAddr, deadline, bad)).to.be.revertedWithCustomError(idr, "InvalidWalletSignature"); + }); + + it("transfer clears agentWallet (must be re-verified by the new owner); unsetAgentWallet clears it too", async function () { + await idr.connect(owner)["register()"](); + await expect(idr.connect(owner).transferFrom(owner.address, other.address, 1)).to.emit(idr, "MetadataSet").withArgs(1, "agentWallet", "agentWallet", ethers.AbiCoder.defaultAbiCoder().encode(["address"], [ethers.ZeroAddress])); + expect(await idr.getAgentWallet(1)).to.equal(ethers.ZeroAddress); + await idr.connect(other)["register()"](); + expect(await idr.getAgentWallet(2)).to.equal(other.address); + await idr.connect(other).unsetAgentWallet(2); + expect(await idr.getAgentWallet(2)).to.equal(ethers.ZeroAddress); + }); + }); + + describe("Reputation Registry", function () { + beforeEach(async function () { await idr.connect(owner)["register(string)"]("ipfs://a1"); }); + + it("getIdentityRegistry; initialize runs once", async function () { + expect(await rep.getIdentityRegistry()).to.equal(await idr.getAddress()); + await expect(rep.initialize(other.address)).to.be.revertedWithCustomError(rep, "AlreadyInitialized"); + const fresh = await (await ethers.getContractFactory("AereReputationRegistry8004V2")).deploy(ethers.ZeroAddress); + await fresh.initialize(await idr.getAddress()); + expect(await fresh.getIdentityRegistry()).to.equal(await idr.getAddress()); + }); + + it("giveFeedback stores value/decimals/tags, emits NewFeedback with the exact field order, indexes from 1", async function () { + const h = ethers.keccak256(ethers.toUtf8Bytes("file")); + await expect(rep.connect(client).giveFeedback(1, 87, 0, "starred", "", "https://a/x", "ipfs://f", h)) + .to.emit(rep, "NewFeedback").withArgs(1, client.address, 1, 87, 0, "starred", "starred", "", "https://a/x", "ipfs://f", h); + const r = await rep.readFeedback(1, client.address, 1); + expect(r.value).to.equal(87); expect(r.valueDecimals).to.equal(0); expect(r.tag1).to.equal("starred"); expect(r.isRevoked).to.equal(false); + expect(await rep.getLastIndex(1, client.address)).to.equal(1); + expect(await rep.getClients(1)).to.deep.equal([client.address]); + }); + + it("the submitter MUST NOT be the agent owner or an approved operator; valueDecimals MUST be 0..18; agent must exist", async function () { + await expect(rep.connect(owner).giveFeedback(1, 1, 0, "", "", "", "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "SelfFeedback"); + await idr.connect(owner).setApprovalForAll(other.address, true); + await expect(rep.connect(other).giveFeedback(1, 1, 0, "", "", "", "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "SelfFeedback"); + await expect(rep.connect(client).giveFeedback(1, 1, 19, "", "", "", "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "BadDecimals"); + await expect(rep.connect(client).giveFeedback(99, 1, 0, "", "", "", "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "AgentNotRegistered"); + }); + + it("revokeFeedback (own feedback only, once) and appendResponse (anyone) with their events and counts", async function () { + await rep.connect(client).giveFeedback(1, 5, 0, "", "", "", "", ethers.ZeroHash); + await expect(rep.connect(other).revokeFeedback(1, 1)).to.be.revertedWithCustomError(rep, "NoSuchFeedback"); + await expect(rep.connect(client).revokeFeedback(1, 1)).to.emit(rep, "FeedbackRevoked").withArgs(1, client.address, 1); + await expect(rep.connect(client).revokeFeedback(1, 1)).to.be.revertedWithCustomError(rep, "AlreadyRevoked"); + await expect(rep.connect(validator).appendResponse(1, client.address, 1, "ipfs://r", ethers.ZeroHash)).to.emit(rep, "ResponseAppended").withArgs(1, client.address, 1, validator.address, "ipfs://r", ethers.ZeroHash); + await rep.connect(other).appendResponse(1, client.address, 1, "", ethers.ZeroHash); + expect(await rep.getResponseCount(1, client.address, 1, [])).to.equal(2); + expect(await rep.getResponseCount(1, client.address, 1, [validator.address])).to.equal(1); + await expect(rep.connect(other).appendResponse(1, client.address, 7, "", ethers.ZeroHash)).to.be.revertedWithCustomError(rep, "NoSuchFeedback"); + }); + + it("getSummary averages at the largest precision, filters by client list and tags, ignores revoked; readAllFeedback honours includeRevoked", async function () { + await rep.connect(client).giveFeedback(1, 80, 0, "starred", "", "", "", ethers.ZeroHash); + await rep.connect(client).giveFeedback(1, 9977, 2, "uptime", "", "", "", ethers.ZeroHash); + await rep.connect(other).giveFeedback(1, 60, 0, "starred", "", "", "", ethers.ZeroHash); + await rep.connect(other).giveFeedback(1, 1, 0, "starred", "", "", "", ethers.ZeroHash); + await rep.connect(other).revokeFeedback(1, 2); + let s = await rep.getSummary(1, [client.address, other.address], "starred", ""); + expect(s.count).to.equal(2); expect(s.summaryValue).to.equal(70); expect(s.summaryValueDecimals).to.equal(0); + s = await rep.getSummary(1, [client.address], "", ""); + expect(s.count).to.equal(2); expect(s.summaryValueDecimals).to.equal(2); expect(s.summaryValue).to.equal(8988); // (8000 + 9977) / 2, impartire intreaga + await expect(rep.getSummary(1, [], "", "")).to.be.revertedWithCustomError(rep, "EmptyClientList"); + const all = await rep.readAllFeedback(1, [], "", "", false); + expect(all.clients.length).to.equal(3); + const withRevoked = await rep.readAllFeedback(1, [other.address], "starred", "", true); + expect(withRevoked.clients.length).to.equal(2); expect(withRevoked.revokedStatuses[1]).to.equal(true); + }); + }); + + describe("Validation Registry", function () { + const H = ethers.keccak256(ethers.toUtf8Bytes("req")); + beforeEach(async function () { await idr.connect(owner)["register(string)"]("ipfs://a1"); }); + + it("validationRequest: owner or operator only; unique requestHash; event with indexed validator/agent/requestHash", async function () { + await expect(val.connect(other).validationRequest(validator.address, 1, "ipfs://q", H)).to.be.revertedWithCustomError(val, "NotAgentOwnerOrOperator"); + await expect(val.connect(owner).validationRequest(validator.address, 1, "ipfs://q", H)).to.emit(val, "ValidationRequest").withArgs(validator.address, 1, "ipfs://q", H); + await expect(val.connect(owner).validationRequest(validator.address, 1, "ipfs://q", H)).to.be.revertedWithCustomError(val, "RequestExists"); + expect(await val.requestCount()).to.equal(1); + expect(await val.getAgentValidations(1)).to.deep.equal([H]); + expect(await val.getValidatorRequests(validator.address)).to.deep.equal([H]); + await idr.connect(owner).approve(other.address, 1); + const H2 = ethers.keccak256(ethers.toUtf8Bytes("req2")); + await val.connect(other).validationRequest(validator.address, 1, "", H2); + }); + + it("validationResponse: only the named validator, response 0..100, repeatable with tags (soft then hard finality), status readable", async function () { + await val.connect(owner).validationRequest(validator.address, 1, "ipfs://q", H); + await expect(val.connect(other).validationResponse(H, 100, "", ethers.ZeroHash, "")).to.be.revertedWithCustomError(val, "NotTheValidator"); + await expect(val.connect(validator).validationResponse(H, 101, "", ethers.ZeroHash, "")).to.be.revertedWithCustomError(val, "BadResponse"); + await expect(val.connect(validator).validationResponse(H, 60, "ipfs://r1", ethers.ZeroHash, "soft")).to.emit(val, "ValidationResponse").withArgs(validator.address, 1, H, 60, "ipfs://r1", ethers.ZeroHash, "soft"); + await val.connect(validator).validationResponse(H, 100, "ipfs://r2", ethers.ZeroHash, "hard"); + const st = await val.getValidationStatus(H); + expect(st.validatorAddress).to.equal(validator.address); expect(st.agentId).to.equal(1); expect(st.response).to.equal(100); expect(st.tag).to.equal("hard"); + await expect(val.getValidationStatus(ethers.ZeroHash)).to.be.revertedWithCustomError(val, "NoSuchRequest"); + }); + + it("getSummary counts responded requests, filtered by validators and tag", async function () { + const H2 = ethers.keccak256(ethers.toUtf8Bytes("req2")), H3 = ethers.keccak256(ethers.toUtf8Bytes("req3")); + await val.connect(owner).validationRequest(validator.address, 1, "", H); + await val.connect(owner).validationRequest(validator.address, 1, "", H2); + await val.connect(owner).validationRequest(other.address, 1, "", H3); + await val.connect(validator).validationResponse(H, 100, "", ethers.ZeroHash, "hard"); + await val.connect(validator).validationResponse(H2, 50, "", ethers.ZeroHash, "soft"); + await val.connect(other).validationResponse(H3, 0, "", ethers.ZeroHash, "hard"); + let s = await val.getSummary(1, [], ""); + expect(s.count).to.equal(3); expect(s.averageResponse).to.equal(50); + s = await val.getSummary(1, [validator.address], "hard"); + expect(s.count).to.equal(1); expect(s.averageResponse).to.equal(100); + }); + }); +}); diff --git a/test/fixtures/SURSE.md b/test/fixtures/SURSE.md new file mode 100644 index 0000000..dfda9a4 --- /dev/null +++ b/test/fixtures/SURSE.md @@ -0,0 +1,10 @@ +# SURSE: de unde vin fixturile din acest director + +Act de provenienta pentru poarta de secrete (`scripts/poarta-secrete.cjs`, clasa COD TERT). + +| fisier | ce e | de unde vine | +|---|---|---| +| `sphincs-sha2-128s-acvp-tg31.json` | vectori de proba SLH-DSA-SHA2-128s (FIPS 205) | NIST ACVP-Server, `gen-val/json-files/SLH-DSA-sigVer-FIPS205` (github.com/usnistgov/ACVP-Server); cheile publice si semnaturile sunt ale vectorilor NIST, nu ale nimanui de la Aere Network | +| `mldsa44-acvp-tg8.json` | vectori de proba ML-DSA-44 (FIPS 204) | NIST ACVP-Server, `gen-val/json-files/ML-DSA-sigVer-FIPS204` | +| `kzg-point-evaluation-kat.json` | vectori KZG (EIP-4844 point evaluation) | vectorii de referinta ai precompilei 0x0a | +| `aere-genesis-header.json`, `aere-halo2-cubic-proof.json`, `da/` | antetul de geneza al lantului 2800 si o dovada Halo2 de proba | generate de noi din lantul public; nu contin chei |