// 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); } }