Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions packages/identity/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# ERC-8004 Identity Package

This package implements the [ERC-8004: Trustless Agents](https://eips.ethereum.org/EIPS/eip-8004) standard as three upgradeable UUPS-proxy contracts on Arbitrum Sepolia.

## Contracts

### `AgentIdentityRegistry`

**Path:** `contracts/core/AgentIdentityRegistry.sol`

- ERC-721 + URIStorage for agent registration
- `register(agentURI, MetadataEntry[]) → agentId`
- `setAgentURI(agentId, newURI)`
- `setMetadata(agentId, key, value)` / `getMetadata(agentId, key)`
- `setAgentWallet(agentId, newWallet, deadline, signature)` — EIP-712 / ERC-1271 verification
- `getAgentWallet(agentId)` / `unsetAgentWallet(agentId)`
- Wallet auto-clears on token transfer (transferable by default; soulbound decision deferred)
- `__gap[50]`

### `AgentReputationRegistry`

**Path:** `contracts/core/AgentReputationRegistry.sol`

- `giveFeedback(agentId, value, valueDecimals, tag1, tag2, endpoint, feedbackURI, feedbackHash)`
- Only non-owners can submit feedback
- `value` is int128, `valueDecimals` 0–18
- Emits `NewFeedback` event
- `revokeFeedback(agentId, feedbackIndex)` — author-only
- `appendResponse(agentId, clientAddress, feedbackIndex, responseURI, responseHash)` — counter-evidence
- `readFeedback`, `getLastIndex`, `getResponseCount`, `getClients`
- `__gap[50]`

### `AgentValidationRegistry`

**Path:** `contracts/core/AgentValidationRegistry.sol`

- `validationRequest(validator, agentId, requestURI, requestHash)` — agent-owner only
- `validationResponse(requestHash, response, responseURI, responseHash, tag)` — validator-only, 0–100
- Can be called multiple times for progressive validation
- `getValidationStatus`, `getAgentValidations`, `getValidatorRequests`
- `__gap[50]`

## Architecture

All three contracts inherit from `TestnetCoreBase` (`@reineira-os/shared`), which provides:

- UUPS upgradeability
- `OwnableUpgradeable`
- `ReentrancyGuardUpgradeable`
- ERC-2771 meta-transaction support

## Dependencies

- `@openzeppelin/contracts ~5.2.0`
- `@openzeppelin/contracts-upgradeable ~5.2.0`
- `@reineira-os/shared workspace:*`

## Deployment

```bash
# Install
pnpm install

# Compile
forge build

# Test
forge test -vv

# Coverage
forge coverage

# Deploy to Arb Sepolia (set PRIVATE_KEY and optionally TRUSTED_FORWARDER)
forge script script/Deploy.s.sol --rpc-url $ARB_SEPOLIA_RPC --broadcast --verify
```

## Decisions Deferred

1. **Soulbound vs transferable `agentId`**: Currently fully transferable ERC-721. Soulbound can be enforced later via `_update` override.
2. **Encrypted-reputation variant**: Not implemented; plain-text reputation only.

## Tests

- `test/unit/AgentIdentityRegistry.t.sol` — registration, URI, metadata, wallet verification, transfers
- `test/unit/AgentReputationRegistry.t.sol` — feedback, revocation, responses, access control
- `test/unit/AgentValidationRegistry.t.sol` — requests, responses, progressive updates, view functions

All tests use Foundry with standard `Test` base (no FHE required for identity contracts).
214 changes: 214 additions & 0 deletions packages/identity/contracts/core/AgentIdentityRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2026 Reineira Labs Limited All rights reserved.
pragma solidity ^0.8.24;

import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol";
import {ERC2771ContextUpgradeable} from "@openzeppelin/contracts-upgradeable/metatx/ERC2771ContextUpgradeable.sol";
import {ERC721Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
import {ERC721URIStorageUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import {TestnetCoreBase} from "@reineira-os/shared/contracts/common/TestnetCoreBase.sol";
import {IAgentIdentityRegistry} from "../interfaces/core/IAgentIdentityRegistry.sol";

contract AgentIdentityRegistry is
IAgentIdentityRegistry,
TestnetCoreBase,
ERC721Upgradeable,
ERC721URIStorageUpgradeable,
EIP712Upgradeable
{
using ECDSA for bytes32;

bytes32 private constant SET_AGENT_WALLET_TYPEHASH =
keccak256("SetAgentWallet(uint256 agentId,address newWallet,uint256 nonce,uint256 deadline)");

uint256 private _nextAgentId;
mapping(uint256 => address) private _agentWallets;
mapping(uint256 => uint256) private _walletNonces;
mapping(uint256 => mapping(string => bytes)) private _metadata;

uint256[50] private __gap;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address trustedForwarder_) TestnetCoreBase(trustedForwarder_) {
_disableInitializers();
}

function initialize(address owner_) external initializer {
if (owner_ == address(0)) revert ZeroAddress();
__TestnetCoreBase_init(owner_);
__ERC721_init("Reineira Agent Identity", "RAI");
__ERC721URIStorage_init();
__EIP712_init("Reineira Agent Identity", "1");
emit CoreInitialized(owner_);
}

// --- ERC-721 + URIStorage overrides ---

function tokenURI(
uint256 tokenId
) public view override(ERC721Upgradeable, ERC721URIStorageUpgradeable) returns (string memory) {
return super.tokenURI(tokenId);
}

function supportsInterface(
bytes4 interfaceId
) public view override(ERC721Upgradeable, ERC721URIStorageUpgradeable) returns (bool) {
return super.supportsInterface(interfaceId);
}

function _msgSender() internal view virtual override(ContextUpgradeable, TestnetCoreBase) returns (address) {
return ERC2771ContextUpgradeable._msgSender();
}

function _msgData() internal view virtual override(ContextUpgradeable, TestnetCoreBase) returns (bytes calldata) {
return ERC2771ContextUpgradeable._msgData();
}

function _contextSuffixLength()
internal
view
virtual
override(ContextUpgradeable, TestnetCoreBase)
returns (uint256)
{
return ERC2771ContextUpgradeable._contextSuffixLength();
}

function _update(address to, uint256 tokenId, address auth) internal override(ERC721Upgradeable) returns (address) {
address from = super._update(to, tokenId, auth);
// Clear agentWallet on any transfer (including mint/burn)
if (from != address(0) && to != address(0) && _agentWallets[tokenId] != address(0)) {
delete _agentWallets[tokenId];
emit AgentWalletCleared(tokenId);
}
return from;
}

// --- registration ---

function register(string calldata agentURI, MetadataEntry[] calldata metadata) external returns (uint256 agentId) {
agentId = _nextAgentId++;
address caller = _msgSender();
_safeMint(caller, agentId);
_setTokenURI(agentId, agentURI);

// Emit reserved agentWallet metadata as owner address
emit MetadataSet(agentId, "agentWallet", "agentWallet", abi.encode(caller));

for (uint256 i = 0; i < metadata.length; i++) {
_setMetadata(agentId, metadata[i].metadataKey, metadata[i].metadataValue);
}

emit Registered(agentId, agentURI, caller);
}

function register(string calldata agentURI) external returns (uint256 agentId) {
agentId = _nextAgentId++;
address caller = _msgSender();
_safeMint(caller, agentId);
_setTokenURI(agentId, agentURI);

emit MetadataSet(agentId, "agentWallet", "agentWallet", abi.encode(caller));
emit Registered(agentId, agentURI, caller);
}

function register() external returns (uint256 agentId) {
agentId = _nextAgentId++;
address caller = _msgSender();
_safeMint(caller, agentId);

emit MetadataSet(agentId, "agentWallet", "agentWallet", abi.encode(caller));
emit Registered(agentId, "", caller);
}

// --- URI ---

function setAgentURI(uint256 agentId, string calldata newURI) external {
if (!_exists(agentId)) revert AgentNotFound();
if (ownerOf(agentId) != _msgSender()) revert NotAgentOwner();

_setTokenURI(agentId, newURI);
emit URIUpdated(agentId, newURI, _msgSender());
}

// --- metadata ---

function setMetadata(uint256 agentId, string calldata metadataKey, bytes calldata metadataValue) external {
if (!_exists(agentId)) revert AgentNotFound();
if (ownerOf(agentId) != _msgSender()) revert NotAgentOwner();
_setMetadata(agentId, metadataKey, metadataValue);
}

function getMetadata(uint256 agentId, string calldata metadataKey) external view returns (bytes memory) {
if (!_exists(agentId)) revert AgentNotFound();
return _metadata[agentId][metadataKey];
}

// --- agent wallet ---

function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes calldata signature) external {
if (!_exists(agentId)) revert AgentNotFound();
if (ownerOf(agentId) != _msgSender()) revert NotAgentOwner();
if (newWallet == address(0)) revert InvalidSignature();
if (block.timestamp > deadline) revert SignatureExpired();
if (_agentWallets[agentId] == newWallet) revert WalletAlreadySet();

uint256 nonce = _walletNonces[agentId]++;
bytes32 structHash = keccak256(abi.encode(SET_AGENT_WALLET_TYPEHASH, agentId, newWallet, nonce, deadline));
bytes32 digest = _hashTypedDataV4(structHash);

if (!_verifySignature(newWallet, digest, signature)) revert InvalidSignature();

_agentWallets[agentId] = newWallet;
emit AgentWalletSet(agentId, newWallet);
}

function getAgentWallet(uint256 agentId) external view returns (address) {
if (!_exists(agentId)) revert AgentNotFound();
return _agentWallets[agentId];
}

function unsetAgentWallet(uint256 agentId) external {
if (!_exists(agentId)) revert AgentNotFound();
if (ownerOf(agentId) != _msgSender()) revert NotAgentOwner();
if (_agentWallets[agentId] == address(0)) revert WalletNotSet();

delete _agentWallets[agentId];
emit AgentWalletCleared(agentId);
}

// --- views ---

function agentCount() external view returns (uint256) {
return _nextAgentId;
}

// --- internal ---

function _setMetadata(uint256 agentId, string calldata metadataKey, bytes calldata metadataValue) internal {
if (keccak256(bytes(metadataKey)) == keccak256(bytes("agentWallet"))) revert ReservedMetadataKey();
_metadata[agentId][metadataKey] = metadataValue;
emit MetadataSet(agentId, metadataKey, metadataKey, metadataValue);
}

function _verifySignature(address signer, bytes32 digest, bytes calldata signature) internal view returns (bool) {
if (signature.length == 65) {
(address recovered, ECDSA.RecoverError error, ) = ECDSA.tryRecover(digest, signature);
return error == ECDSA.RecoverError.NoError && recovered == signer;
}

// ERC-1271 smart contract wallet
try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 magicValue) {
return magicValue == IERC1271.isValidSignature.selector;
} catch {
return false;
}
}

function _exists(uint256 tokenId) internal view returns (bool) {
return _ownerOf(tokenId) != address(0);
}
}
Loading
Loading