Skip to content

Commit 7f7ded3

Browse files
authored
feat: implement reputation registry system with feedback functionality (#4507)
### Description This PR introduces a new Reputation Registry system for agent feedback management. The system allows clients to provide ratings and feedback for agents, with support for tagging, revocation, and responses to feedback. ### Changes - Removed unused `_verifyStake()` function from `IdentityRegistryFacet.sol` - Added new `IReputationRegistry.sol` interface defining the reputation system contract methods - Implemented `ReputationRegistryBase.sol` with core reputation functionality: - Feedback submission with ratings and tags - Feedback revocation - Response management for feedback - Comprehensive query methods for feedback data - Created `ReputationRegistryFacet.sol` that implements the interface and extends the base functionality - Added `ReputationRegistryStorage.sol` for managing the reputation system's persistent state ### Checklist - [ ] Tests added where required - [ ] Documentation updated where applicable - [ ] Changes adhere to the repository's contribution guidelines
1 parent 4c5bc74 commit 7f7ded3

File tree

11 files changed

+1681
-17
lines changed

11 files changed

+1681
-17
lines changed

packages/contracts/scripts/deployments/diamonds/DeployAppRegistry.s.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {DeployAppFactoryFacet} from "../facets/DeployAppFactoryFacet.s.sol";
1818
import {DeploySpaceFactory} from "../diamonds/DeploySpaceFactory.s.sol";
1919
import {DeploySimpleAppBeacon} from "../diamonds/DeploySimpleAppBeacon.s.sol";
2020
import {DeployIdentityRegistry} from "../facets/DeployIdentityRegistry.s.sol";
21+
import {DeployReputationRegistry} from "../facets/DeployReputationRegistry.s.sol";
2122

2223
// contracts
2324
import {Diamond} from "@towns-protocol/diamond/src/Diamond.sol";
@@ -37,6 +38,11 @@ contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
3738
DeploySimpleAppBeacon private deploySimpleAppBeacon = new DeploySimpleAppBeacon();
3839

3940
string internal constant APP_REGISTRY_SCHEMA = "address app, address client";
41+
string internal constant FEEDBACK_SCHEMA =
42+
"uint256 agentId, uint8 score, bytes32 tag1, bytes32 tag2, string feedbackUri, bytes32 feedbackHash";
43+
string internal constant RESPONSE_SCHEMA =
44+
"uint256 agentId, address reviewerAddress, uint64 feedbackIndex, string responseUri, bytes32 responseHash";
45+
4046
address internal spaceFactory;
4147
address internal simpleAppBeacon;
4248

@@ -103,6 +109,7 @@ contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
103109
facetHelper.add("AppInstallerFacet");
104110
facetHelper.add("AppFactoryFacet");
105111
facetHelper.add("IdentityRegistryFacet");
112+
facetHelper.add("ReputationRegistryFacet");
106113

107114
facetHelper.deployBatch(deployer);
108115

@@ -140,6 +147,13 @@ contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
140147
DeployIdentityRegistry.makeInitData()
141148
);
142149

150+
facet = facetHelper.getDeployedAddress("ReputationRegistryFacet");
151+
addFacet(
152+
makeCut(facet, FacetCutAction.Add, DeployReputationRegistry.selectors()),
153+
facet,
154+
DeployReputationRegistry.makeInitData(FEEDBACK_SCHEMA, RESPONSE_SCHEMA)
155+
);
156+
143157
address multiInit = facetHelper.getDeployedAddress("MultiInit");
144158

145159
return
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
// interfaces
5+
import {IDiamond} from "@towns-protocol/diamond/src/IDiamond.sol";
6+
7+
// libraries
8+
import {LibDeploy} from "@towns-protocol/diamond/src/utils/LibDeploy.sol";
9+
import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol";
10+
11+
// contracts
12+
import {ReputationRegistryFacet} from "src/apps/facets/reputation/ReputationRegistryFacet.sol";
13+
14+
library DeployReputationRegistry {
15+
using DynamicArrayLib for DynamicArrayLib.DynamicArray;
16+
17+
function selectors() internal pure returns (bytes4[] memory res) {
18+
uint256 selectorsCount = 10;
19+
DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(selectorsCount);
20+
arr.p(ReputationRegistryFacet.giveFeedback.selector);
21+
arr.p(ReputationRegistryFacet.revokeFeedback.selector);
22+
arr.p(ReputationRegistryFacet.appendResponse.selector);
23+
arr.p(ReputationRegistryFacet.getIdentityRegistry.selector);
24+
arr.p(ReputationRegistryFacet.getSummary.selector);
25+
arr.p(ReputationRegistryFacet.readFeedback.selector);
26+
arr.p(ReputationRegistryFacet.readAllFeedback.selector);
27+
arr.p(ReputationRegistryFacet.getResponseCount.selector);
28+
arr.p(ReputationRegistryFacet.getClients.selector);
29+
arr.p(ReputationRegistryFacet.getLastIndex.selector);
30+
31+
bytes32[] memory selectors_ = arr.asBytes32Array();
32+
33+
assembly ("memory-safe") {
34+
res := selectors_
35+
}
36+
}
37+
38+
function makeCut(
39+
address facetAddress,
40+
IDiamond.FacetCutAction action
41+
) internal pure returns (IDiamond.FacetCut memory) {
42+
return
43+
IDiamond.FacetCut({
44+
action: action,
45+
facetAddress: facetAddress,
46+
functionSelectors: selectors()
47+
});
48+
}
49+
50+
function makeInitData(
51+
string memory feedbackSchema,
52+
string memory responseSchema
53+
) internal pure returns (bytes memory) {
54+
return
55+
abi.encodeCall(
56+
ReputationRegistryFacet.__ReputationRegistry_init,
57+
(feedbackSchema, responseSchema)
58+
);
59+
}
60+
61+
function deploy() internal returns (address) {
62+
return LibDeploy.deployCode("ReputationRegistryFacet.sol", "");
63+
}
64+
}

packages/contracts/src/apps/facets/identity/IIdentityRegistry.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ interface IIdentityRegistryBase {
5959

6060
/// @notice Thrown when attempting to promote or register an agent that already has a promoted identity
6161
error IdentityRegistry__AgentAlreadyPromoted();
62+
63+
/// @notice Thrown when attempting to register an agent with too many metadata entries
64+
error IdentityRegistry__TooManyMetadataEntries();
6265
}
6366

6467
/// @title IIdentityRegistry

packages/contracts/src/apps/facets/identity/IdentityRegistryFacet.sol

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ contract IdentityRegistryFacet is
2121
{
2222
using CustomRevert for bytes4;
2323

24+
uint256 internal constant MAX_METADATA_ENTRIES = 10;
25+
2426
function __IdentityRegistryFacet_init() external onlyInitializing {
2527
__IdentityRegistryFacet_init_unchained();
2628
__ERC721A_init_unchained("Towns Bot Agent", "TBA");
@@ -52,15 +54,17 @@ contract IdentityRegistryFacet is
5254
string calldata agentUri,
5355
MetadataEntry[] calldata metadata
5456
) external returns (uint256 agentId) {
57+
_verifyMetadataLength(metadata);
5558
_verifyAgent();
5659
agentId = _nextTokenId();
5760
_mint(msg.sender, 1);
5861
_setAgentUri(agentId, agentUri);
59-
emit Registered(agentId, agentUri, msg.sender);
6062

6163
for (uint256 i; i < metadata.length; ++i) {
6264
_setMetadata(agentId, metadata[i].metadataKey, metadata[i].metadataValue);
6365
}
66+
67+
emit Registered(agentId, agentUri, msg.sender);
6468
}
6569

6670
/// @inheritdoc IIdentityRegistry
@@ -100,6 +104,12 @@ contract IdentityRegistryFacet is
100104
/* INTERNAL FUNCTIONS */
101105
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
102106

107+
function _verifyMetadataLength(MetadataEntry[] calldata metadata) internal pure {
108+
uint256 metadataLength = metadata.length;
109+
if (metadataLength > MAX_METADATA_ENTRIES)
110+
IdentityRegistry__TooManyMetadataEntries.selector.revertWith();
111+
}
112+
103113
function _verifyAgent() internal view {
104114
if (_balanceOf(msg.sender) > 0)
105115
IdentityRegistry__AgentAlreadyPromoted.selector.revertWith();
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.23;
3+
4+
// interfaces
5+
6+
// libraries
7+
8+
// contracts
9+
10+
/// @title Reputation Registry Base - ERC-8004 Data Structures
11+
/// @notice Core data structures and events for the ERC-8004 reputation system
12+
interface IReputationRegistryBase {
13+
struct Feedback {
14+
uint8 rating;
15+
bytes32 tag1;
16+
bytes32 tag2;
17+
}
18+
19+
struct FeedbackAuth {
20+
uint256 agentId;
21+
address reviewerAddress;
22+
uint64 indexLimit;
23+
uint256 expiry;
24+
uint256 chainId;
25+
address identityRegistry;
26+
address signerAddress;
27+
}
28+
29+
event NewFeedback(
30+
uint256 indexed agentId,
31+
address indexed reviewerAddress,
32+
uint8 score,
33+
bytes32 indexed tag1,
34+
bytes32 tag2,
35+
string feedbackUri,
36+
bytes32 feedbackHash
37+
);
38+
39+
event FeedbackRevoked(
40+
uint256 indexed agentId,
41+
address indexed reviewerAddress,
42+
uint64 indexed feedbackIndex
43+
);
44+
45+
event ResponseAppended(
46+
uint256 indexed agentId,
47+
address indexed reviewerAddress,
48+
uint64 feedbackIndex,
49+
address indexed responder,
50+
string responseUri,
51+
bytes32 responseHash
52+
);
53+
}
54+
55+
/// @title Reputation Registry - ERC-8004 Implementation
56+
/// @notice A reputation system for AI agents enabling feedback collection, scoring, and trust establishment
57+
/// @dev This contract implements the ERC-8004 Reputation Registry specification, which enables
58+
/// discovering and trusting agents across organizational boundaries through reputation signals.
59+
interface IReputationRegistry is IReputationRegistryBase {
60+
/// @notice Returns the address of the identity registry where agents are registered as NFTs
61+
/// @dev The identity registry is an ERC-721 contract where each agent has a unique token ID.
62+
/// This registry links reputation data to agent identities defined in the ERC-8004 Identity Registry.
63+
/// @return The address of the identity registry contract
64+
function getIdentityRegistry() external view returns (address);
65+
66+
/// @notice Posts feedback for an agent after completing a task or interaction
67+
/// @param agentId The NFT token ID of the agent being reviewed
68+
/// @param score The rating from 0 (worst) to 100 (best)
69+
/// @param tag1 First tag for categorization and filtering (use bytes32(0) for none)
70+
/// @param tag2 Second tag for categorization and filtering (use bytes32(0) for none)
71+
/// @param feedbackUri URI pointing to detailed off-chain feedback data (IPFS recommended)
72+
/// @param feedbackHash KECCAK-256 hash of off-chain data for integrity verification
73+
function giveFeedback(
74+
uint256 agentId,
75+
uint8 score,
76+
bytes32 tag1,
77+
bytes32 tag2,
78+
string calldata feedbackUri,
79+
bytes32 feedbackHash
80+
) external;
81+
82+
/// @notice Revokes previously submitted feedback
83+
/// @dev Only the original reviewer can revoke their feedback. Revoked feedback is excluded
84+
/// from getSummary() calculations but remains visible in readAllFeedback() with the
85+
/// revoked status flag. This maintains audit trail integrity per ERC-8004 principles.
86+
/// @param agentId The NFT token ID of the agent
87+
/// @param feedbackIndex The 1-based index of the feedback to revoke (must be > 0)
88+
function revokeFeedback(uint256 agentId, uint64 feedbackIndex) external;
89+
90+
/// @notice Appends a response to existing feedback
91+
/// @param agentId The NFT token ID of the agent
92+
/// @param reviewerAddress The address that gave the original feedback
93+
/// @param feedbackIndex The 1-based index of the feedback being responded to
94+
/// @param responseUri URI pointing to the response content (IPFS recommended)
95+
/// @param responseHash KECCAK-256 hash of response data for integrity verification
96+
function appendResponse(
97+
uint256 agentId,
98+
address reviewerAddress,
99+
uint64 feedbackIndex,
100+
string calldata responseUri,
101+
bytes32 responseHash
102+
) external;
103+
104+
/// @notice Retrieves aggregated reputation statistics for an agent with optional filtering
105+
/// @param agentId The NFT token ID of the agent
106+
/// @param clientAddresses Optional filter for specific reviewers (empty = all reviewers)
107+
/// @param tag1 Optional filter for first tag (bytes32(0) = no filter)
108+
/// @param tag2 Optional filter for second tag (bytes32(0) = no filter)
109+
/// @return count Number of matching feedback entries (excludes revoked)
110+
/// @return averageScore Mean score 0-100 (returns 0 if no feedback matches)
111+
function getSummary(
112+
uint256 agentId,
113+
address[] calldata clientAddresses,
114+
bytes32 tag1,
115+
bytes32 tag2
116+
) external view returns (uint64 count, uint8 averageScore);
117+
118+
/// @notice Reads a specific feedback entry by agent, reviewer, and index
119+
/// @param agentId The NFT token ID of the agent
120+
/// @param reviewerAddress The address that gave the feedback
121+
/// @param index The 1-based feedback index (must be > 0 and <= lastIndex)
122+
/// @return score The rating from 0 to 100
123+
/// @return tag1 The first categorization tag
124+
/// @return tag2 The second categorization tag
125+
/// @return isRevoked True if feedback has been revoked, false otherwise
126+
function readFeedback(
127+
uint256 agentId,
128+
address reviewerAddress,
129+
uint64 index
130+
) external view returns (uint8 score, bytes32 tag1, bytes32 tag2, bool isRevoked);
131+
132+
/// @notice Reads all feedback for an agent with flexible filtering options
133+
/// @param agentId The NFT token ID of the agent
134+
/// @param clientAddresses Optional filter for specific reviewers (empty = all reviewers)
135+
/// @param tag1 Optional filter for first tag (bytes32(0) = no filter)
136+
/// @param tag2 Optional filter for second tag (bytes32(0) = no filter)
137+
/// @param includeRevoked Whether to include revoked feedback in results
138+
/// @return clients Array of reviewer addresses (one per feedback entry)
139+
/// @return scores Array of ratings 0-100 (parallel to clients array)
140+
/// @return tag1s Array of first tags (parallel to clients array)
141+
/// @return tag2s Array of second tags (parallel to clients array)
142+
/// @return revokedStatuses Array of revocation flags (parallel to clients array)
143+
function readAllFeedback(
144+
uint256 agentId,
145+
address[] calldata clientAddresses,
146+
bytes32 tag1,
147+
bytes32 tag2,
148+
bool includeRevoked
149+
)
150+
external
151+
view
152+
returns (
153+
address[] memory clients,
154+
uint8[] memory scores,
155+
bytes32[] memory tag1s,
156+
bytes32[] memory tag2s,
157+
bool[] memory revokedStatuses
158+
);
159+
160+
/// @notice Gets the total count of responses with flexible filtering
161+
/// @param agentId The NFT token ID of the agent
162+
/// @param reviewerAddress Filter by reviewer (address(0) = all reviewers)
163+
/// @param feedbackIndex Filter by specific feedback index (0 = all feedback)
164+
/// @param responders Filter by specific responders (empty = all responders)
165+
/// @return Total count of responses matching the filter criteria
166+
function getResponseCount(
167+
uint256 agentId,
168+
address reviewerAddress,
169+
uint64 feedbackIndex,
170+
address[] calldata responders
171+
) external view returns (uint256);
172+
173+
/// @notice Returns all addresses that have given feedback to this agent
174+
/// @param agentId The NFT token ID of the agent
175+
/// @return clients Array of all reviewer addresses (in order of first feedback)
176+
function getClients(uint256 agentId) external view returns (address[] memory clients);
177+
178+
/// @notice Gets the highest feedback index for a specific reviewer-agent pair
179+
/// @param agentId The NFT token ID of the agent
180+
/// @param reviewerAddress The address of the reviewer
181+
/// @return lastIndex The highest assigned index (0 if no feedback exists)
182+
function getLastIndex(
183+
uint256 agentId,
184+
address reviewerAddress
185+
) external view returns (uint64 lastIndex);
186+
}

0 commit comments

Comments
 (0)