aere-contracts/contracts/erc8004/v2/AereReputationRegistry8004V2.sol
Aere Network 1b50878368
Some checks failed
contracts-ci / Install (lockfile) → compile → full test suite (push) Failing after 16m0s
contracts-ci / Ethereum interop (EIP-2537 BLS, prague hardfork) (push) Failing after 27s
contracts-ci / PQC known-answer tests (NIST vectors) (push) Failing after 16m53s
contracts-ci / Coverage (scoped, with artifacts) (push) Has been cancelled
ERC-8004 V2 registries transcribed from the current draft (identity ERC-721+metadata+agentWallet, reputation, validation), 17 unit tests, local conformance deployment script; fixtures provenance; the MPC verifier moves to contracts/mpc (the publish gate refuses any 'confidential' path by structure)
2026-09-02 17:52:51 +03:00

193 lines
11 KiB
Solidity

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