aere-contracts/contracts/erc8004/v2/AereIdentityRegistry8004V2.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

164 lines
8.2 KiB
Solidity

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