Files
2026-02-25 15:18:58 +02:00

1031 lines
41 KiB
Solidity

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.27;
import { ClaimIssuerHelper } from "../helpers/ClaimIssuerHelper.sol";
import { ClaimSignerHelper } from "../helpers/ClaimSignerHelper.sol";
import { IdentityHelper } from "../helpers/IdentityHelper.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { ClaimIssuer } from "contracts/ClaimIssuer.sol";
import { Identity } from "contracts/Identity.sol";
import { IdentityUtilities } from "contracts/IdentityUtilities.sol";
import { IIdentityUtilities } from "contracts/interface/IIdentityUtilities.sol";
import { KeyPurposes } from "contracts/libraries/KeyPurposes.sol";
import { KeyTypes } from "contracts/libraries/KeyTypes.sol";
import { IdentityUtilitiesProxy } from "contracts/proxy/IdentityUtilitiesProxy.sol";
import { Test } from "forge-std/Test.sol";
import { Test as TestContract } from "test/mocks/Test.sol";
import { TestIdentityUtilities } from "test/mocks/TestIdentityUtilities.sol";
/// @notice Test suite for IdentityUtilities topic schema registry
contract IdentityUtilitiesTest is Test {
IdentityUtilities internal utilities;
address internal admin;
uint256 internal adminPk;
address internal user;
function setUp() public {
(admin, adminPk) = makeAddrAndKey("utilAdmin");
user = makeAddr("utilUser");
IdentityUtilities impl = new IdentityUtilities();
IdentityUtilitiesProxy proxy =
new IdentityUtilitiesProxy(address(impl), abi.encodeCall(IdentityUtilities.initialize, (admin)));
utilities = IdentityUtilities(address(proxy));
}
// ---- internal helpers ----
function _encodeNames(string[] memory names) internal pure returns (bytes memory) {
return abi.encode(names);
}
function _encodeTypes(string[] memory types) internal pure returns (bytes memory) {
return abi.encode(types);
}
function _singleStringArray(string memory s) internal pure returns (string[] memory arr) {
arr = new string[](1);
arr[0] = s;
}
function _addDefaultTopic(
uint256 topicId,
string memory name,
string[] memory fieldNames,
string[] memory fieldTypes
) internal {
vm.prank(admin);
utilities.addTopic(topicId, name, _encodeNames(fieldNames), _encodeTypes(fieldTypes));
}
// =========================================================================
// Topic schema examples from AssetID spec
// =========================================================================
function test_addAndRetrieveNAVPerShare() public {
uint256 topicId = 1000003;
string[] memory fieldNames = new string[](3);
fieldNames[0] = "value";
fieldNames[1] = "decimals";
fieldNames[2] = "timestamp";
string[] memory fieldTypes = new string[](3);
fieldTypes[0] = "uint256";
fieldTypes[1] = "uint256";
fieldTypes[2] = "uint256";
bytes memory encodedNames = _encodeNames(fieldNames);
bytes memory encodedTypes = _encodeTypes(fieldTypes);
vm.prank(admin);
vm.expectEmit(true, true, true, true, address(utilities));
emit IIdentityUtilities.TopicAdded(topicId, "NAV Per Share", encodedNames, encodedTypes);
utilities.addTopic(topicId, "NAV Per Share", encodedNames, encodedTypes);
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(topicId);
assertEq(retNames.length, 3);
assertEq(retNames[0], "value");
assertEq(retNames[1], "decimals");
assertEq(retNames[2], "timestamp");
assertEq(retTypes[0], "uint256");
assertEq(retTypes[1], "uint256");
assertEq(retTypes[2], "uint256");
}
function test_addISIN() public {
bytes memory encodedNames = _encodeNames(_singleStringArray("isin"));
bytes memory encodedTypes = _encodeTypes(_singleStringArray("string"));
vm.prank(admin);
utilities.addTopic(1000001, "ISIN", encodedNames, encodedTypes);
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(1000001);
assertEq(retNames[0], "isin");
assertEq(retTypes[0], "string");
}
function test_addLEI() public {
_addDefaultTopic(1000002, "LEI", _singleStringArray("lei"), _singleStringArray("string"));
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(1000002);
assertEq(retNames[0], "lei");
assertEq(retTypes[0], "string");
}
function test_addNAVGlobal() public {
string[] memory fieldNames = new string[](3);
fieldNames[0] = "value";
fieldNames[1] = "decimals";
fieldNames[2] = "timestamp";
string[] memory fieldTypes = new string[](3);
fieldTypes[0] = "uint256";
fieldTypes[1] = "uint256";
fieldTypes[2] = "uint256";
_addDefaultTopic(1000004, "NAV Global", fieldNames, fieldTypes);
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(1000004);
assertEq(retNames.length, 3);
assertEq(retNames[0], "value");
assertEq(retTypes[2], "uint256");
}
function test_addBaseCurrency() public {
_addDefaultTopic(1000005, "Base Currency", _singleStringArray("currencyCode"), _singleStringArray("uint16"));
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(1000005);
assertEq(retNames[0], "currencyCode");
assertEq(retTypes[0], "uint16");
}
function test_addQualificationURL() public {
_addDefaultTopic(1000006, "Qualification URL", _singleStringArray("urls"), _singleStringArray("string[]"));
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(1000006);
assertEq(retNames[0], "urls");
assertEq(retTypes[0], "string[]");
}
function test_addERC3643Certificate() public {
_addDefaultTopic(1000007, "ERC3643 Certificate", _singleStringArray("issuer"), _singleStringArray("address"));
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(1000007);
assertEq(retNames[0], "issuer");
assertEq(retTypes[0], "address");
}
// =========================================================================
// Validation and permissioning
// =========================================================================
function test_revertMismatchedNameTypes() public {
string[] memory names = _singleStringArray("field1");
string[] memory types = new string[](2);
types[0] = "uint256";
types[1] = "uint8";
vm.prank(admin);
vm.expectRevert(bytes("Field name/type count mismatch"));
utilities.addTopic(1234, "BrokenTopic", _encodeNames(names), _encodeTypes(types));
}
function test_revertNonTopicManagerCannotAdd() public {
vm.prank(user);
vm.expectRevert();
utilities.addTopic(
1000002, "LEI", _encodeNames(_singleStringArray("lei")), _encodeTypes(_singleStringArray("string"))
);
}
// =========================================================================
// addTopic - additional validation
// =========================================================================
function test_addTopic_revertEmptyName() public {
vm.prank(admin);
vm.expectRevert(bytes("Empty topic name"));
utilities.addTopic(
1001, "", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray("string"))
);
}
function test_addTopic_revertAlreadyExists() public {
_addDefaultTopic(1001, "Test", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(admin);
vm.expectRevert(bytes("Topic already exists"));
utilities.addTopic(
1001, "Test", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray("string"))
);
}
function test_addTopic_revertEmptyFieldNamesArrayMismatch() public {
string[] memory names = new string[](0);
string[] memory types = _singleStringArray("string");
vm.prank(admin);
vm.expectRevert(bytes("Field name/type count mismatch"));
utilities.addTopic(1001, "Test", _encodeNames(names), _encodeTypes(types));
}
function test_addTopic_revertEmptyFieldName() public {
vm.prank(admin);
vm.expectRevert(bytes("Empty field name"));
utilities.addTopic(
1001, "Test", _encodeNames(_singleStringArray("")), _encodeTypes(_singleStringArray("string"))
);
}
function test_addTopic_revertEmptyFieldType() public {
vm.prank(admin);
vm.expectRevert(bytes("Empty field type"));
utilities.addTopic(
1001, "Test", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray(""))
);
}
function test_addTopic_emptyArraysBothSides() public {
string[] memory names = new string[](0);
string[] memory types = new string[](0);
bytes memory encodedNames = _encodeNames(names);
bytes memory encodedTypes = _encodeTypes(types);
vm.prank(admin);
vm.expectEmit(true, true, true, true, address(utilities));
emit IIdentityUtilities.TopicAdded(1001, "Empty Arrays Topic", encodedNames, encodedTypes);
utilities.addTopic(1001, "Empty Arrays Topic", encodedNames, encodedTypes);
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(1001);
assertEq(topic.name, "Empty Arrays Topic");
assertEq(topic.encodedFieldNames, encodedNames);
assertEq(topic.encodedFieldTypes, encodedTypes);
}
function test_addTopic_complexFieldTypes() public {
string[] memory names = new string[](5);
names[0] = "address";
names[1] = "uint256";
names[2] = "bool";
names[3] = "string";
names[4] = "bytes";
string[] memory types = new string[](5);
types[0] = "address";
types[1] = "uint256";
types[2] = "bool";
types[3] = "string";
types[4] = "bytes";
bytes memory encodedNames = _encodeNames(names);
bytes memory encodedTypes = _encodeTypes(types);
vm.prank(admin);
vm.expectEmit(true, true, true, true, address(utilities));
emit IIdentityUtilities.TopicAdded(1001, "Complex Topic", encodedNames, encodedTypes);
utilities.addTopic(1001, "Complex Topic", encodedNames, encodedTypes);
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(1001);
assertEq(topic.name, "Complex Topic");
assertEq(topic.encodedFieldNames, encodedNames);
assertEq(topic.encodedFieldTypes, encodedTypes);
}
// =========================================================================
// updateTopic
// =========================================================================
function test_updateTopic_success() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
string[] memory newNames = new string[](2);
newNames[0] = "field1";
newNames[1] = "field2";
string[] memory newTypes = new string[](2);
newTypes[0] = "string";
newTypes[1] = "uint256";
bytes memory encodedNames = _encodeNames(newNames);
bytes memory encodedTypes = _encodeTypes(newTypes);
vm.prank(admin);
vm.expectEmit(true, true, true, true, address(utilities));
emit IIdentityUtilities.TopicUpdated(1001, "Updated", encodedNames, encodedTypes);
utilities.updateTopic(1001, "Updated", encodedNames, encodedTypes);
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(1001);
assertEq(topic.name, "Updated");
assertEq(topic.encodedFieldNames, encodedNames);
assertEq(topic.encodedFieldTypes, encodedTypes);
}
function test_updateTopic_revertNonExistent() public {
vm.prank(admin);
vm.expectRevert(bytes("Topic does not exist"));
utilities.updateTopic(
9999, "Updated", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray("string"))
);
}
function test_updateTopic_revertEmptyName() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(admin);
vm.expectRevert(bytes("Empty topic name"));
utilities.updateTopic(
1001, "", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray("string"))
);
}
function test_updateTopic_revertMismatch() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
string[] memory names = _singleStringArray("field1");
string[] memory types = new string[](2);
types[0] = "string";
types[1] = "uint256";
vm.prank(admin);
vm.expectRevert(bytes("Field name/type count mismatch"));
utilities.updateTopic(1001, "Updated", _encodeNames(names), _encodeTypes(types));
}
function test_updateTopic_revertEmptyFieldName() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(admin);
vm.expectRevert(bytes("Empty field name"));
utilities.updateTopic(
1001, "Updated", _encodeNames(_singleStringArray("")), _encodeTypes(_singleStringArray("string"))
);
}
function test_updateTopic_revertEmptyFieldType() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(admin);
vm.expectRevert(bytes("Empty field type"));
utilities.updateTopic(
1001, "Updated", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray(""))
);
}
function test_updateTopic_revertNonManager() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(user);
vm.expectRevert();
utilities.updateTopic(
1001, "Updated", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray("string"))
);
}
// =========================================================================
// removeTopic
// =========================================================================
function test_removeTopic_success() public {
_addDefaultTopic(1001, "Test", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(admin);
vm.expectEmit(true, true, true, true, address(utilities));
emit IIdentityUtilities.TopicRemoved(1001);
utilities.removeTopic(1001);
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(1001);
assertEq(topic.name, "");
assertEq(topic.encodedFieldNames, bytes(""));
assertEq(topic.encodedFieldTypes, bytes(""));
}
function test_removeTopic_revertNonExistent() public {
vm.prank(admin);
vm.expectRevert(bytes("Topic does not exist"));
utilities.removeTopic(9999);
}
function test_removeTopic_revertNonManager() public {
_addDefaultTopic(1001, "Test", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(user);
vm.expectRevert();
utilities.removeTopic(1001);
}
function test_removeTopic_thenReadd() public {
bytes memory encodedNames = _encodeNames(_singleStringArray("field1"));
bytes memory encodedTypes = _encodeTypes(_singleStringArray("string"));
vm.startPrank(admin);
utilities.addTopic(1001, "Test", encodedNames, encodedTypes);
utilities.removeTopic(1001);
vm.expectEmit(true, true, true, true, address(utilities));
emit IIdentityUtilities.TopicAdded(1001, "Test", encodedNames, encodedTypes);
utilities.addTopic(1001, "Test", encodedNames, encodedTypes);
vm.stopPrank();
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(1001);
assertEq(topic.name, "Test");
assertEq(topic.encodedFieldNames, encodedNames);
assertEq(topic.encodedFieldTypes, encodedTypes);
}
// =========================================================================
// getTopic
// =========================================================================
function test_getTopic_nonExistent() public view {
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(9999);
assertEq(topic.name, "");
assertEq(topic.encodedFieldNames, bytes(""));
assertEq(topic.encodedFieldTypes, bytes(""));
}
function test_getTopic_existing() public {
string[] memory names = new string[](3);
names[0] = "field1";
names[1] = "field2";
names[2] = "field3";
string[] memory types = new string[](3);
types[0] = "string";
types[1] = "uint256";
types[2] = "bool";
bytes memory encodedNames = _encodeNames(names);
bytes memory encodedTypes = _encodeTypes(types);
_addDefaultTopic(1001, "Test", names, types);
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(1001);
assertEq(topic.name, "Test");
assertEq(topic.encodedFieldNames, encodedNames);
assertEq(topic.encodedFieldTypes, encodedTypes);
}
function test_getTopic_afterUpdate() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
string[] memory newNames = new string[](2);
newNames[0] = "field1";
newNames[1] = "field2";
string[] memory newTypes = new string[](2);
newTypes[0] = "string";
newTypes[1] = "uint256";
bytes memory newEncodedNames = _encodeNames(newNames);
bytes memory newEncodedTypes = _encodeTypes(newTypes);
vm.prank(admin);
utilities.updateTopic(1001, "Updated", newEncodedNames, newEncodedTypes);
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(1001);
assertEq(topic.name, "Updated");
assertEq(topic.encodedFieldNames, newEncodedNames);
assertEq(topic.encodedFieldTypes, newEncodedTypes);
}
// =========================================================================
// getFieldNames
// =========================================================================
function test_getFieldNames_nonExistent() public view {
string[] memory names = utilities.getFieldNames(9999);
assertEq(names.length, 0);
}
function test_getFieldNames_existing() public {
string[] memory names = new string[](3);
names[0] = "field1";
names[1] = "field2";
names[2] = "field3";
string[] memory types = new string[](3);
types[0] = "string";
types[1] = "uint256";
types[2] = "bool";
_addDefaultTopic(1001, "Test", names, types);
string[] memory retNames = utilities.getFieldNames(1001);
assertEq(retNames.length, 3);
assertEq(retNames[0], "field1");
assertEq(retNames[1], "field2");
assertEq(retNames[2], "field3");
}
function test_getFieldNames_afterUpdate() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
string[] memory newNames = new string[](2);
newNames[0] = "newField1";
newNames[1] = "newField2";
string[] memory newTypes = new string[](2);
newTypes[0] = "string";
newTypes[1] = "uint256";
vm.prank(admin);
utilities.updateTopic(1001, "Updated", _encodeNames(newNames), _encodeTypes(newTypes));
string[] memory retNames = utilities.getFieldNames(1001);
assertEq(retNames.length, 2);
assertEq(retNames[0], "newField1");
assertEq(retNames[1], "newField2");
}
function test_getFieldNames_afterRemove() public {
_addDefaultTopic(1001, "Test", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(admin);
utilities.removeTopic(1001);
string[] memory retNames = utilities.getFieldNames(1001);
assertEq(retNames.length, 0);
}
// =========================================================================
// getFieldTypes
// =========================================================================
function test_getFieldTypes_nonExistent() public view {
string[] memory types = utilities.getFieldTypes(9999);
assertEq(types.length, 0);
}
function test_getFieldTypes_existing() public {
string[] memory names = new string[](3);
names[0] = "field1";
names[1] = "field2";
names[2] = "field3";
string[] memory types = new string[](3);
types[0] = "string";
types[1] = "uint256";
types[2] = "bool";
_addDefaultTopic(1001, "Test", names, types);
string[] memory retTypes = utilities.getFieldTypes(1001);
assertEq(retTypes.length, 3);
assertEq(retTypes[0], "string");
assertEq(retTypes[1], "uint256");
assertEq(retTypes[2], "bool");
}
function test_getFieldTypes_afterUpdate() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
string[] memory newNames = new string[](2);
newNames[0] = "newField1";
newNames[1] = "newField2";
string[] memory newTypes = new string[](2);
newTypes[0] = "string";
newTypes[1] = "uint256";
vm.prank(admin);
utilities.updateTopic(1001, "Updated", _encodeNames(newNames), _encodeTypes(newTypes));
string[] memory retTypes = utilities.getFieldTypes(1001);
assertEq(retTypes.length, 2);
assertEq(retTypes[0], "string");
assertEq(retTypes[1], "uint256");
}
function test_getFieldTypes_afterRemove() public {
_addDefaultTopic(1001, "Test", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(admin);
utilities.removeTopic(1001);
string[] memory retTypes = utilities.getFieldTypes(1001);
assertEq(retTypes.length, 0);
}
// =========================================================================
// getSchema
// =========================================================================
function test_getSchema_nonExistent() public view {
(string[] memory names, string[] memory types) = utilities.getSchema(9999);
assertEq(names.length, 0);
assertEq(types.length, 0);
}
function test_getSchema_existing() public {
string[] memory names = new string[](3);
names[0] = "field1";
names[1] = "field2";
names[2] = "field3";
string[] memory types = new string[](3);
types[0] = "string";
types[1] = "uint256";
types[2] = "bool";
_addDefaultTopic(1001, "Test", names, types);
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(1001);
assertEq(retNames.length, 3);
assertEq(retTypes.length, 3);
assertEq(retNames[0], "field1");
assertEq(retNames[1], "field2");
assertEq(retNames[2], "field3");
assertEq(retTypes[0], "string");
assertEq(retTypes[1], "uint256");
assertEq(retTypes[2], "bool");
}
function test_getSchema_afterUpdate() public {
_addDefaultTopic(1001, "Initial", _singleStringArray("field1"), _singleStringArray("string"));
string[] memory newNames = new string[](2);
newNames[0] = "newField1";
newNames[1] = "newField2";
string[] memory newTypes = new string[](2);
newTypes[0] = "string";
newTypes[1] = "uint256";
vm.prank(admin);
utilities.updateTopic(1001, "Updated", _encodeNames(newNames), _encodeTypes(newTypes));
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(1001);
assertEq(retNames.length, 2);
assertEq(retTypes.length, 2);
assertEq(retNames[0], "newField1");
assertEq(retNames[1], "newField2");
assertEq(retTypes[0], "string");
assertEq(retTypes[1], "uint256");
}
function test_getSchema_afterRemove() public {
_addDefaultTopic(1001, "Test", _singleStringArray("field1"), _singleStringArray("string"));
vm.prank(admin);
utilities.removeTopic(1001);
(string[] memory retNames, string[] memory retTypes) = utilities.getSchema(1001);
assertEq(retNames.length, 0);
assertEq(retTypes.length, 0);
}
// =========================================================================
// getTopicInfos
// =========================================================================
function test_getTopicInfos_emptyInput() public view {
uint256[] memory ids = new uint256[](0);
IIdentityUtilities.TopicInfo[] memory result = utilities.getTopicInfos(ids);
assertEq(result.length, 0);
}
function test_getTopicInfos_singleExisting() public {
bytes memory encodedNames = _encodeNames(_singleStringArray("field1"));
bytes memory encodedTypes = _encodeTypes(_singleStringArray("string"));
_addDefaultTopic(1001, "Test", _singleStringArray("field1"), _singleStringArray("string"));
uint256[] memory ids = new uint256[](1);
ids[0] = 1001;
IIdentityUtilities.TopicInfo[] memory result = utilities.getTopicInfos(ids);
assertEq(result.length, 1);
assertEq(result[0].name, "Test");
assertEq(result[0].encodedFieldNames, encodedNames);
assertEq(result[0].encodedFieldTypes, encodedTypes);
}
function test_getTopicInfos_multiple() public {
vm.startPrank(admin);
utilities.addTopic(10, "A", _encodeNames(_singleStringArray("f1")), _encodeTypes(_singleStringArray("string")));
utilities.addTopic(20, "B", _encodeNames(_singleStringArray("f2")), _encodeTypes(_singleStringArray("uint256")));
vm.stopPrank();
uint256[] memory ids = new uint256[](2);
ids[0] = 10;
ids[1] = 20;
IIdentityUtilities.TopicInfo[] memory result = utilities.getTopicInfos(ids);
assertEq(result.length, 2);
assertEq(result[0].name, "A");
assertEq(result[1].name, "B");
assertEq(result[0].encodedFieldNames, _encodeNames(_singleStringArray("f1")));
assertEq(result[1].encodedFieldTypes, _encodeTypes(_singleStringArray("uint256")));
}
function test_getTopicInfos_multipleThreeTopics() public {
vm.startPrank(admin);
utilities.addTopic(
1001, "Topic 1", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray("string"))
);
utilities.addTopic(
1002, "Topic 2", _encodeNames(_singleStringArray("field2")), _encodeTypes(_singleStringArray("uint256"))
);
utilities.addTopic(
1003, "Topic 3", _encodeNames(_singleStringArray("field3")), _encodeTypes(_singleStringArray("bool"))
);
vm.stopPrank();
uint256[] memory ids = new uint256[](3);
ids[0] = 1001;
ids[1] = 1002;
ids[2] = 1003;
IIdentityUtilities.TopicInfo[] memory result = utilities.getTopicInfos(ids);
assertEq(result.length, 3);
assertEq(result[0].name, "Topic 1");
assertEq(result[1].name, "Topic 2");
assertEq(result[2].name, "Topic 3");
assertEq(result[0].encodedFieldNames, _encodeNames(_singleStringArray("field1")));
assertEq(result[1].encodedFieldTypes, _encodeTypes(_singleStringArray("uint256")));
assertEq(result[2].encodedFieldTypes, _encodeTypes(_singleStringArray("bool")));
}
function test_getTopicInfos_nonExistent() public view {
uint256[] memory ids = new uint256[](3);
ids[0] = 9999;
ids[1] = 8888;
ids[2] = 7777;
IIdentityUtilities.TopicInfo[] memory result = utilities.getTopicInfos(ids);
assertEq(result.length, 3);
assertEq(result[0].name, "");
assertEq(result[1].name, "");
assertEq(result[2].name, "");
assertEq(result[0].encodedFieldNames, bytes(""));
assertEq(result[1].encodedFieldNames, bytes(""));
assertEq(result[2].encodedFieldNames, bytes(""));
}
function test_getTopicInfos_mixed() public {
bytes memory encodedNames = _encodeNames(_singleStringArray("field1"));
bytes memory encodedTypes = _encodeTypes(_singleStringArray("string"));
_addDefaultTopic(1001, "Test", _singleStringArray("field1"), _singleStringArray("string"));
uint256[] memory ids = new uint256[](2);
ids[0] = 1001;
ids[1] = 9999;
IIdentityUtilities.TopicInfo[] memory result = utilities.getTopicInfos(ids);
assertEq(result.length, 2);
assertEq(result[0].name, "Test");
assertEq(result[0].encodedFieldNames, encodedNames);
assertEq(result[0].encodedFieldTypes, encodedTypes);
assertEq(result[1].name, "");
assertEq(result[1].encodedFieldNames, bytes(""));
assertEq(result[1].encodedFieldTypes, bytes(""));
}
// =========================================================================
// Access control - full CRUD by admin
// =========================================================================
function test_accessControl_fullCRUD() public {
bytes memory encodedNames = _encodeNames(_singleStringArray("field1"));
bytes memory encodedTypes = _encodeTypes(_singleStringArray("string"));
vm.startPrank(admin);
// Add
vm.expectEmit(true, true, true, true, address(utilities));
emit IIdentityUtilities.TopicAdded(1001, "Test", encodedNames, encodedTypes);
utilities.addTopic(1001, "Test", encodedNames, encodedTypes);
// Update
vm.expectEmit(true, true, true, true, address(utilities));
emit IIdentityUtilities.TopicUpdated(1001, "Updated", encodedNames, encodedTypes);
utilities.updateTopic(1001, "Updated", encodedNames, encodedTypes);
// Remove
vm.expectEmit(true, true, true, true, address(utilities));
emit IIdentityUtilities.TopicRemoved(1001);
utilities.removeTopic(1001);
vm.stopPrank();
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(1001);
assertEq(topic.name, "");
}
// =========================================================================
// Edge cases and stress testing
// =========================================================================
function test_topicWithManyFields() public {
string[] memory names = new string[](10);
string[] memory types = new string[](10);
for (uint256 i = 0; i < 10; i++) {
names[i] = string(abi.encodePacked("field", vm.toString(i)));
types[i] = i % 2 == 0 ? "string" : "uint256";
}
_addDefaultTopic(1001, "Large Topic", names, types);
string[] memory retNames = utilities.getFieldNames(1001);
string[] memory retTypes = utilities.getFieldTypes(1001);
assertEq(retNames.length, 10);
assertEq(retTypes.length, 10);
assertEq(retNames[0], "field0");
assertEq(retNames[9], "field9");
assertEq(retTypes[0], "string");
assertEq(retTypes[1], "uint256");
}
function test_topicWithLongNames() public {
string memory longName = "this_is_a_very_long_field_name_that_might_be_used_in_real_world_scenarios";
string memory longType = "this_is_a_very_long_field_type_that_might_be_used_in_real_world_scenarios";
_addDefaultTopic(1001, "Long Names Topic", _singleStringArray(longName), _singleStringArray(longType));
string[] memory retNames = utilities.getFieldNames(1001);
string[] memory retTypes = utilities.getFieldTypes(1001);
assertEq(retNames[0], longName);
assertEq(retTypes[0], longType);
}
function test_rapidAddUpdateRemove() public {
bytes memory encodedNames = _encodeNames(_singleStringArray("field1"));
bytes memory encodedTypes = _encodeTypes(_singleStringArray("string"));
vm.startPrank(admin);
utilities.addTopic(1001, "Test", encodedNames, encodedTypes);
utilities.updateTopic(1001, "Updated", encodedNames, encodedTypes);
utilities.removeTopic(1001);
vm.stopPrank();
IIdentityUtilities.TopicInfo memory topic = utilities.getTopic(1001);
assertEq(topic.name, "");
assertEq(topic.encodedFieldNames, bytes(""));
assertEq(topic.encodedFieldTypes, bytes(""));
}
function test_multipleTopicsSameFieldNamesDifferentTypes() public {
vm.startPrank(admin);
utilities.addTopic(
1001, "Topic 1", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray("string"))
);
utilities.addTopic(
1002, "Topic 2", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray("uint256"))
);
utilities.addTopic(
1003, "Topic 3", _encodeNames(_singleStringArray("field1")), _encodeTypes(_singleStringArray("bool"))
);
vm.stopPrank();
string[] memory types1 = utilities.getFieldTypes(1001);
string[] memory types2 = utilities.getFieldTypes(1002);
string[] memory types3 = utilities.getFieldTypes(1003);
assertEq(types1[0], "string");
assertEq(types2[0], "uint256");
assertEq(types3[0], "bool");
string[] memory names1 = utilities.getFieldNames(1001);
string[] memory names2 = utilities.getFieldNames(1002);
string[] memory names3 = utilities.getFieldNames(1003);
assertEq(names1[0], "field1");
assertEq(names2[0], "field1");
assertEq(names3[0], "field1");
}
// =========================================================================
// getClaimsWithTopicInfo
// =========================================================================
function test_getClaimsWithTopicInfo() public {
// Add topics
vm.startPrank(admin);
utilities.addTopic(
1001, "KYC", _encodeNames(_singleStringArray("status")), _encodeTypes(_singleStringArray("string"))
);
utilities.addTopic(
1002, "AML", _encodeNames(_singleStringArray("level")), _encodeTypes(_singleStringArray("uint8"))
);
vm.stopPrank();
// Deploy ClaimIssuer and Identity
(address claimIssuerOwner, uint256 claimIssuerOwnerPk) = makeAddrAndKey("ciOwner");
address identityOwner = makeAddr("idOwner");
(address claimSigner,) = makeAddrAndKey("claimSigner");
ClaimIssuer ci = ClaimIssuerHelper.deployWithProxy(claimIssuerOwner);
Identity identity = IdentityHelper.deployIdentityWithProxy(identityOwner);
// Add CLAIM_SIGNER key to claim issuer for the claimIssuerOwner
vm.prank(claimIssuerOwner);
ci.addKey(ClaimSignerHelper.addressToKey(claimIssuerOwner), KeyPurposes.CLAIM_SIGNER, KeyTypes.ECDSA);
// Add CLAIM_SIGNER key to identity for claimSigner
vm.prank(identityOwner);
identity.addKey(ClaimSignerHelper.addressToKey(claimSigner), KeyPurposes.CLAIM_SIGNER, KeyTypes.ECDSA);
// Build claims
bytes memory claimData1 = abi.encode("verified");
bytes memory claimData2 = abi.encode(uint8(2));
bytes memory sig1 = ClaimSignerHelper.signClaim(claimIssuerOwnerPk, address(identity), 1001, claimData1);
bytes memory sig2 = ClaimSignerHelper.signClaim(claimIssuerOwnerPk, address(identity), 1002, claimData2);
// Add claims to identity via claimSigner (has CLAIM_SIGNER key)
vm.startPrank(claimSigner);
identity.addClaim(1001, 1, address(ci), sig1, claimData1, "https://example.com/kyc");
identity.addClaim(1002, 1, address(ci), sig2, claimData2, "https://example.com/aml");
vm.stopPrank();
// Query
uint256[] memory topicIds = new uint256[](2);
topicIds[0] = 1001;
topicIds[1] = 1002;
IIdentityUtilities.ClaimInfo[] memory result = utilities.getClaimsWithTopicInfo(address(identity), topicIds);
assertEq(result.length, 2);
// Verify KYC claim
assertEq(result[0].scheme, 1);
assertEq(result[0].issuer, address(ci));
assertTrue(result[0].isValid);
assertEq(result[0].data, claimData1);
assertEq(result[0].uri, "https://example.com/kyc");
assertEq(result[0].topic.name, "KYC");
assertEq(result[0].topic.encodedFieldNames, _encodeNames(_singleStringArray("status")));
assertEq(result[0].topic.encodedFieldTypes, _encodeTypes(_singleStringArray("string")));
// Verify AML claim
assertEq(result[1].scheme, 1);
assertEq(result[1].issuer, address(ci));
assertTrue(result[1].isValid);
assertEq(result[1].data, claimData2);
assertEq(result[1].uri, "https://example.com/aml");
assertEq(result[1].topic.name, "AML");
assertEq(result[1].topic.encodedFieldNames, _encodeNames(_singleStringArray("level")));
assertEq(result[1].topic.encodedFieldTypes, _encodeTypes(_singleStringArray("uint8")));
// Decode and verify claim data
string memory decodedKyc = abi.decode(result[0].data, (string));
assertEq(decodedKyc, "verified");
uint8 decodedAml = abi.decode(result[1].data, (uint8));
assertEq(decodedAml, 2);
}
function test_getClaimsWithTopicInfo_selfAttestedClaim() public {
// Add topic
_addDefaultTopic(3004, "Test Topic", _singleStringArray("name"), _singleStringArray("string"));
// Deploy Identity
Identity identity = IdentityHelper.deployIdentityWithProxy(admin);
// Add CLAIM_SIGNER key for admin on the identity
vm.prank(admin);
identity.addKey(ClaimSignerHelper.addressToKey(admin), KeyPurposes.CLAIM_SIGNER, KeyTypes.ECDSA);
// Sign claim properly for self-attested claim
bytes memory claimData = hex"";
bytes memory signature = ClaimSignerHelper.signClaim(adminPk, address(identity), 3004, claimData);
// Add a self-attested claim with valid signature
vm.prank(admin);
identity.addClaim(3004, 1, address(identity), signature, claimData, "https://example.com/claim");
// Query
uint256[] memory topicIds = new uint256[](1);
topicIds[0] = 3004;
IIdentityUtilities.ClaimInfo[] memory result = utilities.getClaimsWithTopicInfo(address(identity), topicIds);
assertGt(result.length, 0);
assertEq(result[0].issuer, address(identity));
}
// =========================================================================
// TestIdentityUtilities - _isClaimValid coverage
// =========================================================================
function test_isClaimValid_zeroAddressIssuer() public {
TestIdentityUtilities testUtil = new TestIdentityUtilities();
Identity identity = IdentityHelper.deployIdentityWithProxy(admin);
bool result = testUtil.checkIsClaimValid(address(identity), 3007, address(0), hex"", hex"");
assertFalse(result);
}
function test_isClaimValid_invalidContractIssuer() public {
TestIdentityUtilities testUtil = new TestIdentityUtilities();
Identity identity = IdentityHelper.deployIdentityWithProxy(admin);
// Deploy a contract that does not implement isClaimValid (catches and returns false)
TestContract invalidContract = new TestContract();
bool result = testUtil.checkIsClaimValid(address(identity), 3008, address(invalidContract), hex"", hex"");
assertFalse(result);
}
// =========================================================================
// UUPS upgrade
// =========================================================================
function test_upgrade_revertNonAdmin() public {
address nonAdmin = makeAddr("nonAdmin");
IdentityUtilities newImpl = new IdentityUtilities();
vm.prank(nonAdmin);
vm.expectRevert();
utilities.upgradeToAndCall(address(newImpl), bytes(""));
}
function test_upgrade_successAsAdmin() public {
IdentityUtilities newImpl = new IdentityUtilities();
vm.prank(admin);
utilities.upgradeToAndCall(address(newImpl), bytes(""));
}
// =========================================================================
// ERC1967 proxy deployment variant
// =========================================================================
function test_upgrade_viaERC1967Proxy_revertNonAdmin() public {
address deployer = makeAddr("erc1967deployer");
address nonAdmin = makeAddr("erc1967nonAdmin");
IdentityUtilities impl = new IdentityUtilities();
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), abi.encodeCall(IdentityUtilities.initialize, (deployer)));
IdentityUtilities proxyUtil = IdentityUtilities(address(proxy));
IdentityUtilities newImpl = new IdentityUtilities();
vm.prank(nonAdmin);
vm.expectRevert();
proxyUtil.upgradeToAndCall(address(newImpl), bytes(""));
}
}