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)
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

This commit is contained in:
Aere Network 2026-09-02 17:52:51 +03:00
parent d15d0d7e94
commit 1b50878368
8 changed files with 766 additions and 0 deletions

View File

@ -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();
}
}

View File

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

View File

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

View File

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

View File

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

212
test/ERC8004V2.test.js Normal file
View File

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

10
test/fixtures/SURSE.md vendored Normal file
View File

@ -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 |