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
193 lines
11 KiB
Solidity
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);
|
|
}
|
|
}
|