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
164 lines
8.2 KiB
Solidity
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();
|
|
}
|
|
}
|