From cf43f80e95a17af9f93e7089a444666ba03d31f1 Mon Sep 17 00:00:00 2001 From: aniket866 Date: Thu, 19 Mar 2026 01:27:30 +0530 Subject: [PATCH 01/20] delete-attribute --- src/IdentityToken.sol | 11 ++++ src/interfaces/IIdentityToken.sol | 1 + src/libraries/Events.sol | 1 + test/IdentityToken.t.sol | 92 +++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/src/IdentityToken.sol b/src/IdentityToken.sol index de81d60..245846f 100644 --- a/src/IdentityToken.sol +++ b/src/IdentityToken.sol @@ -91,6 +91,17 @@ contract IdentityToken is ERC721, IIdentityToken { emit Events.AttributeSet(tokenId, keyHash, value); } + function deleteAttribute( + uint256 tokenId, + string calldata key + ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { + bytes32 keyHash = keccak256(abi.encodePacked(key)); + + delete attributes[tokenId][keyHash]; + + emit Events.AttributeDeleted(tokenId, keyHash); + } + /** * @dev Allows an identity to endorse another identity. */ diff --git a/src/interfaces/IIdentityToken.sol b/src/interfaces/IIdentityToken.sol index f867243..bafe4b0 100644 --- a/src/interfaces/IIdentityToken.sol +++ b/src/interfaces/IIdentityToken.sol @@ -6,6 +6,7 @@ import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions interface IIdentityToken is IERC721, IERC721Metadata { function setAttribute(uint256 tokenId, string calldata key, bytes calldata value) external; + function deleteAttribute(uint256 tokenId, string calldata key) external; function endorse(uint256 fromTokenId, uint256 toTokenId, bytes32 connectionType, uint256 validUntil) external; diff --git a/src/libraries/Events.sol b/src/libraries/Events.sol index a67cacd..6132a17 100644 --- a/src/libraries/Events.sol +++ b/src/libraries/Events.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; library Events { event AttributeSet(uint256 indexed tokenId, bytes32 indexed keyHash, bytes value); + event AttributeDeleted(uint256 indexed tokenId, bytes32 indexed keyHash); event EndorsementGiven(uint256 indexed fromId, uint256 indexed toId, bytes32 typeHash, uint256 expiry); event EndorsementRevoked(uint256 indexed fromId, uint256 indexed toId, uint256 index); event IdentityCompromised(uint256 indexed tokenId); diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index da551b5..659e1e8 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -5,6 +5,7 @@ import { Test } from "forge-std/Test.sol"; import { IdentityToken } from "../src/IdentityToken.sol"; import { DataTypes } from "../src/libraries/DataTypes.sol"; import { Errors } from "../src/libraries/Errors.sol"; +import { Events } from "../src/libraries/Events.sol"; contract IdentityTokenTest is Test { IdentityToken public identityToken; @@ -77,4 +78,95 @@ contract IdentityTokenTest is Test { vm.expectRevert(Errors.NotTokenOwner.selector); identityToken.setAttribute(tokenId, "name", bytes("Hacker Bob")); } + + // --- deleteAttribute --- + + function test_DeleteAttribute() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + + vm.prank(alice); + identityToken.deleteAttribute(tokenId, "email"); + + bytes32 keyHash = keccak256(abi.encodePacked("email")); + bytes memory value = identityToken.attributes(tokenId, keyHash); + + assertEq(value.length, 0); + } + + function test_DeleteAttribute_EmitsEvent() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + + bytes32 keyHash = keccak256(abi.encodePacked("email")); + + vm.prank(alice); + vm.expectEmit(true, true, false, false); + emit Events.AttributeDeleted(tokenId, keyHash); + identityToken.deleteAttribute(tokenId, "email"); + } + + function test_RevertIf_NotOwnerDeletesAttribute() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + + vm.prank(bob); + vm.expectRevert(Errors.NotTokenOwner.selector); + identityToken.deleteAttribute(tokenId, "email"); + } + + function test_DeleteAttribute_NeverSet_DoesNotRevert() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + vm.prank(alice); + identityToken.deleteAttribute(tokenId, "nonexistent"); + + bytes32 keyHash = keccak256(abi.encodePacked("nonexistent")); + bytes memory value = identityToken.attributes(tokenId, keyHash); + + assertEq(value.length, 0); + } + + function test_DeleteAttribute_Twice_DoesNotRevert() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + + vm.prank(alice); + identityToken.deleteAttribute(tokenId, "email"); + + vm.prank(alice); + identityToken.deleteAttribute(tokenId, "email"); + } + + function test_DeleteAttribute_ThenReSet() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + + vm.prank(alice); + identityToken.deleteAttribute(tokenId, "email"); + + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("new@example.com")); + + bytes32 keyHash = keccak256(abi.encodePacked("email")); + bytes memory value = identityToken.attributes(tokenId, keyHash); + + assertEq(string(value), "new@example.com"); + } } From 12a9ab7509f0675c62489cd9a0f0dca80e3c4e0a Mon Sep 17 00:00:00 2001 From: Aniket Date: Thu, 19 Mar 2026 01:45:04 +0530 Subject: [PATCH 02/20] Code rabbit follow up Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- test/IdentityToken.t.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 659e1e8..10a51fa 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -169,4 +169,18 @@ contract IdentityTokenTest is Test { assertEq(string(value), "new@example.com"); } + + function test_RevertIf_CompromisedIdentityDeletesAttribute() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + // identityStates[tokenId].isCompromised = true + // slot index for identityStates mapping is 2 in current storage layout + bytes32 slot = keccak256(abi.encode(tokenId, uint256(2))); + vm.store(address(identityToken), slot, bytes32(uint256(1))); + + vm.prank(alice); + vm.expectRevert(Errors.IdentityCompromised.selector); + identityToken.deleteAttribute(tokenId, "email"); + } } From 5926ba387afefe471f7e17a28be98388f73973d9 Mon Sep 17 00:00:00 2001 From: aniket866 Date: Thu, 19 Mar 2026 01:57:14 +0530 Subject: [PATCH 03/20] Code rabbit fix --- test/IdentityToken.t.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 10a51fa..6960f2e 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -174,10 +174,12 @@ contract IdentityTokenTest is Test { vm.prank(alice); uint256 tokenId = identityToken.mint(); - // identityStates[tokenId].isCompromised = true - // slot index for identityStates mapping is 2 in current storage layout - bytes32 slot = keccak256(abi.encode(tokenId, uint256(2))); - vm.store(address(identityToken), slot, bytes32(uint256(1))); + // identityStates is at slot 2 in IdentityToken's storage layout: + // slot 0: _nextTokenId + // slot 1: ownerToTokenId + // slot 2: identityStates + bytes32 slot = keccak256(abi.encode(uint256(tokenId), uint256(2))); + vm.store(address(identityToken), slot, bytes32(uint256(1))); // isCompromised = true vm.prank(alice); vm.expectRevert(Errors.IdentityCompromised.selector); From 8f60e5d9eb17f00b856bbe7e3ad9ada7fdc1e328 Mon Sep 17 00:00:00 2001 From: aniket866 Date: Thu, 19 Mar 2026 02:02:44 +0530 Subject: [PATCH 04/20] Code rabbit fix --- test/IdentityToken.t.sol | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 6960f2e..6a83068 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -6,8 +6,10 @@ import { IdentityToken } from "../src/IdentityToken.sol"; import { DataTypes } from "../src/libraries/DataTypes.sol"; import { Errors } from "../src/libraries/Errors.sol"; import { Events } from "../src/libraries/Events.sol"; +import { StdStorage, stdStorage } from "forge-std/Test.sol"; contract IdentityTokenTest is Test { + using stdStorage for StdStorage; IdentityToken public identityToken; address public alice = address(0x1); address public bob = address(0x2); @@ -174,12 +176,12 @@ contract IdentityTokenTest is Test { vm.prank(alice); uint256 tokenId = identityToken.mint(); - // identityStates is at slot 2 in IdentityToken's storage layout: - // slot 0: _nextTokenId - // slot 1: ownerToTokenId - // slot 2: identityStates - bytes32 slot = keccak256(abi.encode(uint256(tokenId), uint256(2))); - vm.store(address(identityToken), slot, bytes32(uint256(1))); // isCompromised = true + // Use stdStorage to set isCompromised without hardcoding a slot + stdstore + .target(address(identityToken)) + .sig("identityStates(uint256)") + .with_key(tokenId) + .depth(0).checked_write(true); // isCompromised is the first field in IdentityState vm.prank(alice); vm.expectRevert(Errors.IdentityCompromised.selector); From 6a0d7f8d6ba80aca78fda5bf68f767cc638acb37 Mon Sep 17 00:00:00 2001 From: Nikunj Date: Fri, 20 Mar 2026 20:03:27 +0530 Subject: [PATCH 05/20] fix: schema by update important field --- src/libraries/Schema.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/Schema.sol b/src/libraries/Schema.sol index e17a33a..05f44b5 100644 --- a/src/libraries/Schema.sol +++ b/src/libraries/Schema.sol @@ -18,4 +18,6 @@ library Schema { bytes32 internal constant GITHUB = keccak256(abi.encodePacked("github")); bytes32 internal constant LINKEDIN = keccak256(abi.encodePacked("linkedin")); bytes32 internal constant TWITTER = keccak256(abi.encodePacked("twitter")); + bytes32 internal constant PHONE = keccak256(abi.encodePacked("phone")); + bytes32 internal constant EMAIL = keccak256(abi.encodePacked("email")); } From 0b6ee25d051942652c7068fb579e1fb5041f592a Mon Sep 17 00:00:00 2001 From: Nikunj Date: Fri, 20 Mar 2026 23:15:23 +0530 Subject: [PATCH 06/20] update: error for name and contact --- src/libraries/Errors.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 210c8cd..99e95d2 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -16,4 +16,6 @@ library Errors { error AlreadyHasIdentity(); error DuplicateEndorsement(); error ArrayLengthMismatch(); + error MissingName(); + error MissingContact(); } From 340f4d997bd337f8252cb16d3df1c3d5f5e7aa5a Mon Sep 17 00:00:00 2001 From: Nikunj Date: Fri, 20 Mar 2026 23:15:47 +0530 Subject: [PATCH 07/20] fix: validate user need to pass phone / email , and name is mandatory --- src/IdentityToken.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/IdentityToken.sol b/src/IdentityToken.sol index 101b251..03639b8 100644 --- a/src/IdentityToken.sol +++ b/src/IdentityToken.sol @@ -76,6 +76,25 @@ contract IdentityToken is ERC721, IIdentityToken { return tokenId; } + /** + * @dev Adds validation to ensure required identity fields are present. + * Name is mandatory and at least one contact method (email or phone) + * must be provided. + */ + function _validateRequiredFields(uint256 tokenId) internal view { + bytes memory name = attributes[tokenId][keccak256(abi.encodePacked("name"))]; + bytes memory email = attributes[tokenId][keccak256(abi.encodePacked("email"))]; + bytes memory phone = attributes[tokenId][keccak256(abi.encodePacked("phone"))]; + + // Name is mandatory + if (name.length == 0) revert Errors.MissingName(); + + // At least one contact method required + if (email.length == 0 && phone.length == 0) { + revert Errors.MissingContact(); + } + } + /** * @dev Sets a metadata attribute (e.g., name, social link) for an identity. */ From 7c36ca171cfec736a4e1a8a40072f9f28c1ce8fb Mon Sep 17 00:00:00 2001 From: Nikunj Date: Fri, 20 Mar 2026 23:16:40 +0530 Subject: [PATCH 08/20] update: call the fn --- src/IdentityToken.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/IdentityToken.sol b/src/IdentityToken.sol index 03639b8..291ad29 100644 --- a/src/IdentityToken.sol +++ b/src/IdentityToken.sol @@ -94,7 +94,7 @@ contract IdentityToken is ERC721, IIdentityToken { revert Errors.MissingContact(); } } - + /** * @dev Sets a metadata attribute (e.g., name, social link) for an identity. */ @@ -104,6 +104,8 @@ contract IdentityToken is ERC721, IIdentityToken { bytes calldata value ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { _setAttribute(tokenId, key, value); + + _validateRequiredFields(tokenId); } /** From cb415defcebc736859d38dff4c3a3dce8ab87656 Mon Sep 17 00:00:00 2001 From: aniket866 Date: Sat, 21 Mar 2026 00:13:33 +0530 Subject: [PATCH 09/20] formatting --- test/IdentityToken.t.sol | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index e409024..41f9d72 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -391,11 +391,9 @@ contract IdentityTokenTest is Test { uint256 tokenId = identityToken.mint(); // Use stdStorage to set isCompromised without hardcoding a slot - stdstore - .target(address(identityToken)) - .sig("identityStates(uint256)") - .with_key(tokenId) - .depth(0).checked_write(true); // isCompromised is the first field in IdentityState + stdstore.target(address(identityToken)).sig("identityStates(uint256)").with_key(tokenId).depth(0).checked_write( + true + ); // isCompromised is the first field in IdentityState vm.prank(alice); vm.expectRevert(Errors.IdentityCompromised.selector); From a926546e6973e8c2f2b51ea371457661289de3ba Mon Sep 17 00:00:00 2001 From: aniket866 Date: Sat, 21 Mar 2026 00:38:20 +0530 Subject: [PATCH 10/20] Query-functions --- src/IdentityToken.sol | 52 +++++++++++ src/interfaces/IIdentityToken.sol | 21 ++++- src/libraries/DataTypes.sol | 12 +++ test/IdentityToken.t.sol | 146 ++++++++++++++++++++++++++++-- 4 files changed, 221 insertions(+), 10 deletions(-) diff --git a/src/IdentityToken.sol b/src/IdentityToken.sol index ec5e058..1bc5c7a 100644 --- a/src/IdentityToken.sol +++ b/src/IdentityToken.sol @@ -195,4 +195,56 @@ contract IdentityToken is ERC721, IIdentityToken { emit Events.AttributeSet(tokenId, keyHash, key, value); } + + // Identity helpers + + /// @notice Returns true if the address owns any identity token. + function hasIdentity(address owner) external view returns (bool) { + return balanceOf(owner) > 0; + } + + /// @notice Returns full metadata for a given token. + function getIdentity(uint256 tokenId) external view returns (DataTypes.Identity memory) { + address owner = _requireOwned(tokenId); + DataTypes.IdentityState storage state = identityStates[tokenId]; + return + DataTypes.Identity({ + tokenId: tokenId, + owner: owner, + isCompromised: state.isCompromised, + backupWallet: state.backupWallet, + pendingBackupWallet: state.pendingBackupWallet, + backupUnlockTime: state.backupUnlockTime, + validUntil: state.validUntil, + endorsementCount: endorsements[tokenId].length + }); + } + + /// @notice Returns all token IDs owned by an address (0 or 1 given soulbound constraint). + function getIdentityByOwner(address owner) external view returns (uint256[] memory) { + uint256 tokenId = ownerToTokenId[owner]; + if (tokenId == 0) { + return new uint256[](0); + } + uint256[] memory result = new uint256[](1); + result[0] = tokenId; + return result; + } + + /// @notice Returns true if the token has at least one active (non-revoked, non-expired) endorsement. + function isVerified(uint256 tokenId) external view returns (bool) { + DataTypes.Endorsement[] storage list = endorsements[tokenId]; + for (uint256 i = 0; i < list.length; i++) { + DataTypes.Endorsement storage e = list[i]; + bool active = e.revokedAt == 0 && (e.validUntil == 0 || e.validUntil >= block.timestamp); + if (active) return true; + } + return false; + } + + /// @notice Returns true if the token's validUntil is set and has passed. + function isExpired(uint256 tokenId) external view returns (bool) { + uint256 validUntil = identityStates[tokenId].validUntil; + return validUntil != 0 && block.timestamp > validUntil; + } } diff --git a/src/interfaces/IIdentityToken.sol b/src/interfaces/IIdentityToken.sol index 20ad852..33e0ca8 100644 --- a/src/interfaces/IIdentityToken.sol +++ b/src/interfaces/IIdentityToken.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.24; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { DataTypes } from "../libraries/DataTypes.sol"; interface IIdentityToken is IERC721, IERC721Metadata { // ------------------------------------------------------------------------- @@ -37,7 +38,13 @@ interface IIdentityToken is IERC721, IERC721Metadata { ) external view - returns (bool isCompromised, address backupWallet, address pendingBackupWallet, uint256 backupUnlockTime); + returns ( + bool isCompromised, + address backupWallet, + address pendingBackupWallet, + uint256 backupUnlockTime, + uint256 validUntil + ); function attributes(uint256 tokenId, bytes32 keyHash) external view returns (bytes memory); @@ -54,4 +61,16 @@ interface IIdentityToken is IERC721, IERC721Metadata { uint256 validUntil, uint256 revokedAt ); + + // Identity helpers + + function hasIdentity(address owner) external view returns (bool); + + function getIdentity(uint256 tokenId) external view returns (DataTypes.Identity memory); + + function getIdentityByOwner(address owner) external view returns (uint256[] memory); + + function isVerified(uint256 tokenId) external view returns (bool); + + function isExpired(uint256 tokenId) external view returns (bool); } diff --git a/src/libraries/DataTypes.sol b/src/libraries/DataTypes.sol index c228658..6211cde 100644 --- a/src/libraries/DataTypes.sol +++ b/src/libraries/DataTypes.sol @@ -15,5 +15,17 @@ library DataTypes { address backupWallet; address pendingBackupWallet; uint256 backupUnlockTime; + uint256 validUntil; + } + + struct Identity { + uint256 tokenId; + address owner; + bool isCompromised; + address backupWallet; + address pendingBackupWallet; + uint256 backupUnlockTime; + uint256 validUntil; + uint256 endorsementCount; } } diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 41f9d72..545a800 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -286,15 +286,6 @@ contract IdentityTokenTest is Test { assertEq(revokedAt, 0); } - function test_RevertIf_NotOwnerSetsAttribute() public { - vm.prank(alice); - uint256 tokenId = identityToken.mint(); - - vm.prank(bob); - vm.expectRevert(Errors.NotTokenOwner.selector); - identityToken.setAttribute(tokenId, "name", bytes("Hacker Bob")); - } - // --- deleteAttribute --- function test_DeleteAttribute() public { @@ -399,4 +390,141 @@ contract IdentityTokenTest is Test { vm.expectRevert(Errors.IdentityCompromised.selector); identityToken.deleteAttribute(tokenId, "email"); } + // --- hasIdentity --- + + function test_HasIdentity_True() public { + vm.prank(alice); + identityToken.mint(); + assertTrue(identityToken.hasIdentity(alice)); + } + + function test_HasIdentity_False() public view { + assertFalse(identityToken.hasIdentity(alice)); + } + + // --- getIdentity --- + + function test_GetIdentity_ReturnsCorrectFields() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + DataTypes.Identity memory identity = identityToken.getIdentity(tokenId); + + assertEq(identity.tokenId, tokenId); + assertEq(identity.owner, alice); + assertFalse(identity.isCompromised); + assertEq(identity.validUntil, 0); + assertEq(identity.endorsementCount, 0); + } + + function test_GetIdentity_EndorsementCountUpdates() public { + vm.prank(alice); + uint256 aliceId = identityToken.mint(); + + vm.prank(bob); + uint256 bobId = identityToken.mint(); + + bytes32 connectionType = keccak256(abi.encodePacked("Colleague")); + vm.prank(alice); + identityToken.endorse(aliceId, bobId, connectionType, 0); + + DataTypes.Identity memory identity = identityToken.getIdentity(bobId); + assertEq(identity.endorsementCount, 1); + } + + function test_GetIdentity_RevertsForNonexistentToken() public { + vm.expectRevert(); + identityToken.getIdentity(999); + } + + // --- getIdentityByOwner --- + + function test_GetIdentityByOwner_ReturnsTokenId() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + uint256[] memory result = identityToken.getIdentityByOwner(alice); + + assertEq(result.length, 1); + assertEq(result[0], tokenId); + } + + function test_GetIdentityByOwner_ReturnsEmptyIfNoToken() public view { + uint256[] memory result = identityToken.getIdentityByOwner(alice); + assertEq(result.length, 0); + } + + // --- isVerified --- + + function test_IsVerified_FalseWithNoEndorsements() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + assertFalse(identityToken.isVerified(tokenId)); + } + + function test_IsVerified_TrueWithActiveEndorsement() public { + vm.prank(alice); + uint256 aliceId = identityToken.mint(); + + vm.prank(bob); + uint256 bobId = identityToken.mint(); + + bytes32 connectionType = keccak256(abi.encodePacked("Colleague")); + vm.prank(alice); + identityToken.endorse(aliceId, bobId, connectionType, 0); + + assertTrue(identityToken.isVerified(bobId)); + } + + function test_IsVerified_FalseWithExpiredEndorsement() public { + vm.prank(alice); + uint256 aliceId = identityToken.mint(); + + vm.prank(bob); + uint256 bobId = identityToken.mint(); + + bytes32 connectionType = keccak256(abi.encodePacked("Colleague")); + uint256 validUntil = block.timestamp + 1 days; + + vm.prank(alice); + identityToken.endorse(aliceId, bobId, connectionType, validUntil); + + vm.warp(block.timestamp + 2 days); + + assertFalse(identityToken.isVerified(bobId)); + } + + // --- isExpired --- + + function test_IsExpired_FalseWhenNoValidUntil() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + assertFalse(identityToken.isExpired(tokenId)); + } + + function test_IsExpired_FalseBeforeExpiry() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + stdstore.target(address(identityToken)).sig("identityStates(uint256)").with_key(tokenId).depth(4).checked_write( + block.timestamp + 1 days + ); + + assertFalse(identityToken.isExpired(tokenId)); + } + + function test_IsExpired_TrueAfterExpiry() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + uint256 expiry = block.timestamp + 1 days; + + stdstore.target(address(identityToken)).sig("identityStates(uint256)").with_key(tokenId).depth(4).checked_write( + expiry + ); + + vm.warp(expiry + 1); + + assertTrue(identityToken.isExpired(tokenId)); + } } From 6c30f6d5351052b1d9cf3c62120a11be8e69963e Mon Sep 17 00:00:00 2001 From: Nikunj Date: Mon, 23 Mar 2026 11:30:09 +0530 Subject: [PATCH 11/20] fix: all test and validatefn call fix --- src/IdentityToken.sol | 4 ++-- test/IdentityToken.t.sol | 52 +++++++++++++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/IdentityToken.sol b/src/IdentityToken.sol index 291ad29..afe6f57 100644 --- a/src/IdentityToken.sol +++ b/src/IdentityToken.sol @@ -104,8 +104,6 @@ contract IdentityToken is ERC721, IIdentityToken { bytes calldata value ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { _setAttribute(tokenId, key, value); - - _validateRequiredFields(tokenId); } /** @@ -132,6 +130,8 @@ contract IdentityToken is ERC721, IIdentityToken { for (uint256 i = 0; i < keys.length; i++) { _setAttribute(tokenId, keys[i], values[i]); } + + _validateRequiredFields(tokenId); } /** diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 8c27803..07bf481 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -37,8 +37,11 @@ contract IdentityTokenTest is Test { vm.prank(alice); uint256 tokenId = identityToken.mint(); + // Set name first, then email to satisfy validation vm.prank(alice); identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); bytes32 keyHash = keccak256(abi.encodePacked("name")); bytes memory retrievedValue = identityToken.attributes(tokenId, keyHash); @@ -52,6 +55,8 @@ contract IdentityTokenTest is Test { vm.prank(alice); identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); } @@ -60,6 +65,12 @@ contract IdentityTokenTest is Test { vm.prank(alice); uint256 tokenId = identityToken.mint(); + // Set required fields first + vm.prank(alice); + identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + vm.prank(alice); identityToken.setAttribute(tokenId, "github", bytes("https://github.com/alice")); @@ -73,6 +84,11 @@ contract IdentityTokenTest is Test { vm.prank(alice); uint256 tokenId = identityToken.mint(); + vm.prank(alice); + identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + vm.prank(alice); identityToken.setAttribute(tokenId, "github", bytes("https://github.com/alice")); vm.prank(alice); @@ -92,6 +108,8 @@ contract IdentityTokenTest is Test { vm.prank(alice); identityToken.setAttribute(tokenId, "name", bytes("Alice")); vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + vm.prank(alice); identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); @@ -102,15 +120,24 @@ contract IdentityTokenTest is Test { uint256 tokenId = identityToken.mint(); vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("")); - - assertEq(identityToken.getAttribute(tokenId, "name").length, 0); + identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + vm.prank(alice); + identityToken.setAttribute(tokenId, "github", bytes("")); + assertEq(identityToken.getAttribute(tokenId, "github").length, 0); } function test_SetAttribute_LongURL() public { vm.prank(alice); uint256 tokenId = identityToken.mint(); + // Required fields first + vm.prank(alice); + identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + string memory url = "https://www.linkedin.com/in/alice-nakamoto-very-long-profile-url-example-1234567890"; vm.prank(alice); identityToken.setAttribute(tokenId, "linkedin", bytes(url)); @@ -150,17 +177,19 @@ contract IdentityTokenTest is Test { vm.prank(alice); uint256 tokenId = identityToken.mint(); - string[] memory keys = new string[](4); + string[] memory keys = new string[](5); keys[0] = "name"; keys[1] = "github"; keys[2] = "nationality"; keys[3] = "residence"; + keys[4] = "email"; - bytes[] memory values = new bytes[](4); + bytes[] memory values = new bytes[](5); values[0] = bytes("Alice Nakamoto"); values[1] = bytes("https://github.com/alice"); values[2] = bytes("Japanese"); values[3] = bytes("Tokyo"); + values[4] = bytes("alice@example.com"); vm.prank(alice); identityToken.setAttributesBatch(tokenId, keys, values); @@ -169,17 +198,22 @@ contract IdentityTokenTest is Test { assertEq(string(identityToken.getAttribute(tokenId, "github")), "https://github.com/alice"); assertEq(string(identityToken.getAttribute(tokenId, "nationality")), "Japanese"); assertEq(string(identityToken.getAttribute(tokenId, "residence")), "Tokyo"); + assertEq(string(identityToken.getAttribute(tokenId, "email")), "alice@example.com"); } function test_SetAttributesBatch_SingleEntry() public { vm.prank(alice); uint256 tokenId = identityToken.mint(); - string[] memory keys = new string[](1); - keys[0] = "age"; + string[] memory keys = new string[](3); + keys[0] = "name"; + keys[1] = "email"; + keys[2] = "age"; - bytes[] memory values = new bytes[](1); - values[0] = bytes("30"); + bytes[] memory values = new bytes[](3); + values[0] = bytes("Alice Nakamoto"); + values[1] = bytes("alice@example.com"); + values[2] = bytes("30"); vm.prank(alice); identityToken.setAttributesBatch(tokenId, keys, values); From 417a1be5d37424ddab7cc642df99ba1c01bb6312 Mon Sep 17 00:00:00 2001 From: Nikunj Date: Mon, 23 Mar 2026 11:33:32 +0530 Subject: [PATCH 12/20] fix: fmt --- test/IdentityToken.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 07bf481..67009c2 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -182,7 +182,7 @@ contract IdentityTokenTest is Test { keys[1] = "github"; keys[2] = "nationality"; keys[3] = "residence"; - keys[4] = "email"; + keys[4] = "email"; bytes[] memory values = new bytes[](5); values[0] = bytes("Alice Nakamoto"); From 98dc951eef76a88219e15341f592e9cc234e33c2 Mon Sep 17 00:00:00 2001 From: Nikunj Date: Mon, 23 Mar 2026 21:21:44 +0530 Subject: [PATCH 13/20] update: test case for phn and email hashcompatibility --- test/IdentityToken.t.sol | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 8c27803..d2e04b5 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -8,6 +8,8 @@ import { Errors } from "../src/libraries/Errors.sol"; import { Schema } from "../src/libraries/Schema.sol"; contract IdentityTokenTest is Test { + event AttributeSet(uint256 indexed tokenId, bytes32 indexed keyHash, string key, bytes value); + IdentityToken public identityToken; address public alice = address(0x1); address public bob = address(0x2); @@ -69,6 +71,57 @@ contract IdentityTokenTest is Test { ); } + function test_SetAttribute_PhoneEmail_HashCompatibility() public { + vm.prank(alice); + uint256 tokenId = identityToken.mint(); + + // Phone set/get via raw key and hashed Schema key consistency + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit AttributeSet(tokenId, Schema.PHONE, "phone", bytes("1234567890")); + identityToken.setAttribute(tokenId, "phone", bytes("1234567890")); + + assertEq(string(identityToken.getAttribute(tokenId, "phone")), "1234567890"); + assertEq(string(identityToken.attributes(tokenId, Schema.PHONE)), "1234567890"); + + // Update phone value + vm.prank(alice); + identityToken.setAttribute(tokenId, "phone", bytes("0987654321")); + + assertEq(string(identityToken.getAttribute(tokenId, "phone")), "0987654321"); + assertEq(string(identityToken.attributes(tokenId, Schema.PHONE)), "0987654321"); + + // Clear phone value + vm.prank(alice); + identityToken.setAttribute(tokenId, "phone", bytes("")); + + assertEq(identityToken.getAttribute(tokenId, "phone").length, 0); + assertEq(identityToken.attributes(tokenId, Schema.PHONE).length, 0); + + // Email set/get via raw key and hashed Schema key consistency + vm.prank(alice); + vm.expectEmit(true, true, true, true); + emit AttributeSet(tokenId, Schema.EMAIL, "email", bytes("alice@example.com")); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + + assertEq(string(identityToken.getAttribute(tokenId, "email")), "alice@example.com"); + assertEq(string(identityToken.attributes(tokenId, Schema.EMAIL)), "alice@example.com"); + + // Update email value + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("alice+alias@example.com")); + + assertEq(string(identityToken.getAttribute(tokenId, "email")), "alice+alias@example.com"); + assertEq(string(identityToken.attributes(tokenId, Schema.EMAIL)), "alice+alias@example.com"); + + // Clear email value + vm.prank(alice); + identityToken.setAttribute(tokenId, "email", bytes("")); + + assertEq(identityToken.getAttribute(tokenId, "email").length, 0); + assertEq(identityToken.attributes(tokenId, Schema.EMAIL).length, 0); + } + function test_SetAttribute_SocialLinks() public { vm.prank(alice); uint256 tokenId = identityToken.mint(); @@ -250,6 +303,8 @@ contract IdentityTokenTest is Test { assertEq(Schema.GITHUB, keccak256(abi.encodePacked("github"))); assertEq(Schema.LINKEDIN, keccak256(abi.encodePacked("linkedin"))); assertEq(Schema.TWITTER, keccak256(abi.encodePacked("twitter"))); + assertEq(Schema.PHONE, keccak256(abi.encodePacked("phone"))); + assertEq(Schema.EMAIL, keccak256(abi.encodePacked("email"))); } // ------------------------------------------------------------------------- From 4cfdb02e431c3c9c9fa2102f429c3aa271dda299 Mon Sep 17 00:00:00 2001 From: Nikunj Date: Mon, 23 Mar 2026 21:27:54 +0530 Subject: [PATCH 14/20] Revert "update: test case for phn and email hashcompatibility" This reverts commit 98dc951eef76a88219e15341f592e9cc234e33c2. --- test/IdentityToken.t.sol | 55 ---------------------------------------- 1 file changed, 55 deletions(-) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index d2e04b5..8c27803 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -8,8 +8,6 @@ import { Errors } from "../src/libraries/Errors.sol"; import { Schema } from "../src/libraries/Schema.sol"; contract IdentityTokenTest is Test { - event AttributeSet(uint256 indexed tokenId, bytes32 indexed keyHash, string key, bytes value); - IdentityToken public identityToken; address public alice = address(0x1); address public bob = address(0x2); @@ -71,57 +69,6 @@ contract IdentityTokenTest is Test { ); } - function test_SetAttribute_PhoneEmail_HashCompatibility() public { - vm.prank(alice); - uint256 tokenId = identityToken.mint(); - - // Phone set/get via raw key and hashed Schema key consistency - vm.prank(alice); - vm.expectEmit(true, true, true, true); - emit AttributeSet(tokenId, Schema.PHONE, "phone", bytes("1234567890")); - identityToken.setAttribute(tokenId, "phone", bytes("1234567890")); - - assertEq(string(identityToken.getAttribute(tokenId, "phone")), "1234567890"); - assertEq(string(identityToken.attributes(tokenId, Schema.PHONE)), "1234567890"); - - // Update phone value - vm.prank(alice); - identityToken.setAttribute(tokenId, "phone", bytes("0987654321")); - - assertEq(string(identityToken.getAttribute(tokenId, "phone")), "0987654321"); - assertEq(string(identityToken.attributes(tokenId, Schema.PHONE)), "0987654321"); - - // Clear phone value - vm.prank(alice); - identityToken.setAttribute(tokenId, "phone", bytes("")); - - assertEq(identityToken.getAttribute(tokenId, "phone").length, 0); - assertEq(identityToken.attributes(tokenId, Schema.PHONE).length, 0); - - // Email set/get via raw key and hashed Schema key consistency - vm.prank(alice); - vm.expectEmit(true, true, true, true); - emit AttributeSet(tokenId, Schema.EMAIL, "email", bytes("alice@example.com")); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); - - assertEq(string(identityToken.getAttribute(tokenId, "email")), "alice@example.com"); - assertEq(string(identityToken.attributes(tokenId, Schema.EMAIL)), "alice@example.com"); - - // Update email value - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice+alias@example.com")); - - assertEq(string(identityToken.getAttribute(tokenId, "email")), "alice+alias@example.com"); - assertEq(string(identityToken.attributes(tokenId, Schema.EMAIL)), "alice+alias@example.com"); - - // Clear email value - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("")); - - assertEq(identityToken.getAttribute(tokenId, "email").length, 0); - assertEq(identityToken.attributes(tokenId, Schema.EMAIL).length, 0); - } - function test_SetAttribute_SocialLinks() public { vm.prank(alice); uint256 tokenId = identityToken.mint(); @@ -303,8 +250,6 @@ contract IdentityTokenTest is Test { assertEq(Schema.GITHUB, keccak256(abi.encodePacked("github"))); assertEq(Schema.LINKEDIN, keccak256(abi.encodePacked("linkedin"))); assertEq(Schema.TWITTER, keccak256(abi.encodePacked("twitter"))); - assertEq(Schema.PHONE, keccak256(abi.encodePacked("phone"))); - assertEq(Schema.EMAIL, keccak256(abi.encodePacked("email"))); } // ------------------------------------------------------------------------- From 049f8fceb7882e76070847d1fcb7921f01ac7be5 Mon Sep 17 00:00:00 2001 From: Nikunj Date: Mon, 23 Mar 2026 21:33:42 +0530 Subject: [PATCH 15/20] fix: test_schemaConstants --- test/IdentityToken.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 8c27803..5ff2ded 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -250,6 +250,8 @@ contract IdentityTokenTest is Test { assertEq(Schema.GITHUB, keccak256(abi.encodePacked("github"))); assertEq(Schema.LINKEDIN, keccak256(abi.encodePacked("linkedin"))); assertEq(Schema.TWITTER, keccak256(abi.encodePacked("twitter"))); + assertEq(Schema.PHONE, keccak256(abi.encodePacked("phone"))); + assertEq(Schema.EMAIL, keccak256(abi.encodePacked("email"))); } // ------------------------------------------------------------------------- From 5ea51090fd2e5dd2bc35267a8cf809651fe985fb Mon Sep 17 00:00:00 2001 From: Nikunj Date: Mon, 23 Mar 2026 23:57:59 +0530 Subject: [PATCH 16/20] update: validate setattribute and set contract fn --- src/IdentityToken.sol | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/IdentityToken.sol b/src/IdentityToken.sol index 3bc482c..a228fa2 100644 --- a/src/IdentityToken.sol +++ b/src/IdentityToken.sol @@ -104,6 +104,7 @@ contract IdentityToken is ERC721, IIdentityToken { bytes calldata value ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { _setAttribute(tokenId, key, value); + _validateRequiredFields(tokenId); } /** @@ -141,6 +142,27 @@ contract IdentityToken is ERC721, IIdentityToken { _setAttribute(tokenId, "name", bytes(name)); } + /** + * @dev Convenience setter for the "email" / "phone" attribute. + */ + function setContact( + uint256 tokenId, + string calldata email, + string calldata phone + ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { + if (bytes(email).length == 0 && bytes(phone).length == 0) { + revert Errors.MissingContact(); + } + + if (bytes(email).length != 0) { + _setAttribute(tokenId, "email", bytes(email)); + } + + if (bytes(phone).length != 0) { + _setAttribute(tokenId, "phone", bytes(phone)); + } + } + /** * @dev Convenience setter for the "github" attribute. */ @@ -149,6 +171,7 @@ contract IdentityToken is ERC721, IIdentityToken { string calldata github ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { _setAttribute(tokenId, "github", bytes(github)); + _validateRequiredFields(tokenId); } function deleteAttribute( From 42ade6b2357a33eda2bd846c8df2075d7838e0aa Mon Sep 17 00:00:00 2001 From: Nikunj Date: Tue, 24 Mar 2026 00:00:05 +0530 Subject: [PATCH 17/20] fix: test case for missing name and contact --- test/IdentityToken.t.sol | 134 +++++++++++++++------------------------ 1 file changed, 51 insertions(+), 83 deletions(-) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index dcfc085..4ad8bf7 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -24,7 +24,7 @@ contract IdentityTokenTest is Test { // ------------------------------------------------------------------------- function test_Mint() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); assertEq(tokenId, 1); @@ -37,45 +37,41 @@ contract IdentityTokenTest is Test { // ------------------------------------------------------------------------- function test_SetAttribute() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); // Set name first, then email to satisfy validation - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); + + identityToken.setAttribute(tokenId, "github", bytes("https://github.com/alice")); bytes32 keyHash = keccak256(abi.encodePacked("name")); bytes memory retrievedValue = identityToken.attributes(tokenId, keyHash); + assertEq(string(identityToken.getAttribute(tokenId, "github")), "https://github.com/alice"); assertEq(string(retrievedValue), "Alice Nakamoto"); } function test_GetAttribute() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); } function test_GetAttribute_MatchesRawMapping() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); // Set required fields first - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); - vm.prank(alice); - identityToken.setAttribute(tokenId, "github", bytes("https://github.com/alice")); + identityToken.setGithub(tokenId, "https://github.com/alice"); assertEq( string(identityToken.getAttribute(tokenId, "github")), @@ -84,19 +80,14 @@ contract IdentityTokenTest is Test { } function test_SetAttribute_SocialLinks() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); - vm.prank(alice); identityToken.setAttribute(tokenId, "github", bytes("https://github.com/alice")); - vm.prank(alice); identityToken.setAttribute(tokenId, "linkedin", bytes("https://linkedin.com/in/alice")); - vm.prank(alice); identityToken.setAttribute(tokenId, "twitter", bytes("https://twitter.com/alice")); assertEq(string(identityToken.getAttribute(tokenId, "github")), "https://github.com/alice"); @@ -105,44 +96,35 @@ contract IdentityTokenTest is Test { } function test_OverwriteAttribute() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("Alice")); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); - vm.prank(alice); + identityToken.setName(tokenId, "Alice"); + identityToken.setContact(tokenId, "alice@example.com", ""); identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); } function test_SetAttribute_EmptyValue() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); - vm.prank(alice); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); identityToken.setAttribute(tokenId, "github", bytes("")); assertEq(identityToken.getAttribute(tokenId, "github").length, 0); } function test_SetAttribute_LongURL() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); // Required fields first - vm.prank(alice); - identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); string memory url = "https://www.linkedin.com/in/alice-nakamoto-very-long-profile-url-example-1234567890"; - vm.prank(alice); identityToken.setAttribute(tokenId, "linkedin", bytes(url)); assertEq(string(identityToken.getAttribute(tokenId, "linkedin")), url); @@ -153,20 +135,20 @@ contract IdentityTokenTest is Test { // ------------------------------------------------------------------------- function test_SetName() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); identityToken.setName(tokenId, "Alice Nakamoto"); assertEq(string(identityToken.attributes(tokenId, Schema.NAME)), "Alice Nakamoto"); } function test_SetGithub() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); + identityToken.setName(tokenId, "Alice Nakamoto"); + identityToken.setContact(tokenId, "alice@example.com", ""); identityToken.setGithub(tokenId, "https://github.com/alice"); assertEq(string(identityToken.attributes(tokenId, Schema.GITHUB)), "https://github.com/alice"); @@ -177,7 +159,7 @@ contract IdentityTokenTest is Test { // ------------------------------------------------------------------------- function test_SetAttributesBatch() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); string[] memory keys = new string[](5); @@ -194,7 +176,6 @@ contract IdentityTokenTest is Test { values[3] = bytes("Tokyo"); values[4] = bytes("alice@example.com"); - vm.prank(alice); identityToken.setAttributesBatch(tokenId, keys, values); assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); @@ -205,7 +186,7 @@ contract IdentityTokenTest is Test { } function test_SetAttributesBatch_SingleEntry() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); string[] memory keys = new string[](3); @@ -218,7 +199,6 @@ contract IdentityTokenTest is Test { values[1] = bytes("alice@example.com"); values[2] = bytes("30"); - vm.prank(alice); identityToken.setAttributesBatch(tokenId, keys, values); assertEq(string(identityToken.getAttribute(tokenId, "age")), "30"); @@ -261,7 +241,7 @@ contract IdentityTokenTest is Test { } function test_RevertIf_BatchLengthMismatch() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); string[] memory keys = new string[](2); @@ -270,7 +250,6 @@ contract IdentityTokenTest is Test { bytes[] memory values = new bytes[](1); values[0] = bytes("Alice Nakamoto"); - vm.prank(alice); vm.expectRevert(Errors.ArrayLengthMismatch.selector); identityToken.setAttributesBatch(tokenId, keys, values); } @@ -325,13 +304,11 @@ contract IdentityTokenTest is Test { // --- deleteAttribute --- function test_DeleteAttribute() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + identityToken.setContact(tokenId, "alice@example.com", ""); - vm.prank(alice); identityToken.deleteAttribute(tokenId, "email"); bytes32 keyHash = keccak256(abi.encodePacked("email")); @@ -341,37 +318,35 @@ contract IdentityTokenTest is Test { } function test_DeleteAttribute_EmitsEvent() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + identityToken.setContact(tokenId, "alice@example.com", ""); bytes32 keyHash = keccak256(abi.encodePacked("email")); - vm.prank(alice); vm.expectEmit(true, true, false, false); emit Events.AttributeDeleted(tokenId, keyHash); identityToken.deleteAttribute(tokenId, "email"); } function test_RevertIf_NotOwnerDeletesAttribute() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + vm.startPrank(alice); + identityToken.setContact(tokenId, "alice@example.com", ""); + vm.stopPrank(); - vm.prank(bob); + vm.startPrank(bob); vm.expectRevert(Errors.NotTokenOwner.selector); identityToken.deleteAttribute(tokenId, "email"); } function test_DeleteAttribute_NeverSet_DoesNotRevert() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); identityToken.deleteAttribute(tokenId, "nonexistent"); bytes32 keyHash = keccak256(abi.encodePacked("nonexistent")); @@ -381,31 +356,25 @@ contract IdentityTokenTest is Test { } function test_DeleteAttribute_Twice_DoesNotRevert() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + identityToken.setContact(tokenId, "alice@example.com", ""); - vm.prank(alice); identityToken.deleteAttribute(tokenId, "email"); - vm.prank(alice); identityToken.deleteAttribute(tokenId, "email"); } function test_DeleteAttribute_ThenReSet() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); + identityToken.setContact(tokenId, "alice@example.com", ""); - vm.prank(alice); identityToken.deleteAttribute(tokenId, "email"); - vm.prank(alice); - identityToken.setAttribute(tokenId, "email", bytes("new@example.com")); + identityToken.setContact(tokenId, "new@example.com", ""); bytes32 keyHash = keccak256(abi.encodePacked("email")); bytes memory value = identityToken.attributes(tokenId, keyHash); @@ -414,7 +383,7 @@ contract IdentityTokenTest is Test { } function test_RevertIf_CompromisedIdentityDeletesAttribute() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); // Use stdStorage to set isCompromised without hardcoding a slot @@ -422,14 +391,13 @@ contract IdentityTokenTest is Test { true ); // isCompromised is the first field in IdentityState - vm.prank(alice); vm.expectRevert(Errors.IdentityCompromised.selector); identityToken.deleteAttribute(tokenId, "email"); } // --- hasIdentity --- function test_HasIdentity_True() public { - vm.prank(alice); + vm.startPrank(alice); identityToken.mint(); assertTrue(identityToken.hasIdentity(alice)); } @@ -441,7 +409,7 @@ contract IdentityTokenTest is Test { // --- getIdentity --- function test_GetIdentity_ReturnsCorrectFields() public { - vm.prank(alice); + vm.startPrank(alice); uint256 tokenId = identityToken.mint(); DataTypes.Identity memory identity = identityToken.getIdentity(tokenId); From d8590fc75537840c3a12c37ac2528e747d22c5c6 Mon Sep 17 00:00:00 2001 From: Nikunj Date: Tue, 24 Mar 2026 19:03:34 +0530 Subject: [PATCH 18/20] update: identitytoken validate remove if field name , email, ph no --- src/IdentityToken.sol | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/IdentityToken.sol b/src/IdentityToken.sol index a228fa2..bd42b92 100644 --- a/src/IdentityToken.sol +++ b/src/IdentityToken.sol @@ -22,6 +22,11 @@ contract IdentityToken is ERC721, IIdentityToken { // tokenId => attribute keyHash => attribute value mapping(uint256 => mapping(bytes32 => bytes)) public attributes; + // required attribute key hashes + bytes32 private constant NAME_KEY = keccak256(abi.encodePacked("name")); + bytes32 private constant EMAIL_KEY = keccak256(abi.encodePacked("email")); + bytes32 private constant PHONE_KEY = keccak256(abi.encodePacked("phone")); + // tokenId => array of Endorsements mapping(uint256 => DataTypes.Endorsement[]) public endorsements; @@ -82,9 +87,9 @@ contract IdentityToken is ERC721, IIdentityToken { * must be provided. */ function _validateRequiredFields(uint256 tokenId) internal view { - bytes memory name = attributes[tokenId][keccak256(abi.encodePacked("name"))]; - bytes memory email = attributes[tokenId][keccak256(abi.encodePacked("email"))]; - bytes memory phone = attributes[tokenId][keccak256(abi.encodePacked("phone"))]; + bytes storage name = attributes[tokenId][NAME_KEY]; + bytes storage email = attributes[tokenId][EMAIL_KEY]; + bytes storage phone = attributes[tokenId][PHONE_KEY]; // Name is mandatory if (name.length == 0) revert Errors.MissingName(); @@ -103,8 +108,14 @@ contract IdentityToken is ERC721, IIdentityToken { string calldata key, bytes calldata value ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { + bytes32 keyHash = keccak256(abi.encodePacked(key)); + _setAttribute(tokenId, key, value); - _validateRequiredFields(tokenId); + + // skip validation if the updated attribute is one of the required fields, since + if (keyHash != NAME_KEY && keyHash != EMAIL_KEY && keyHash != PHONE_KEY) { + _validateRequiredFields(tokenId); + } } /** @@ -128,11 +139,20 @@ contract IdentityToken is ERC721, IIdentityToken { ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { if (keys.length != values.length) revert Errors.ArrayLengthMismatch(); + bool shouldValidate = true; + for (uint256 i = 0; i < keys.length; i++) { + bytes32 keyHash = keccak256(abi.encodePacked(keys[i])); _setAttribute(tokenId, keys[i], values[i]); + + if (keyHash == NAME_KEY || keyHash == EMAIL_KEY || keyHash == PHONE_KEY) { + shouldValidate = false; + } } - _validateRequiredFields(tokenId); + if (shouldValidate) { + _validateRequiredFields(tokenId); + } } /** From aa8df60082210d6634bf8e139a8bd7361ebcc127 Mon Sep 17 00:00:00 2001 From: Nikunj Date: Tue, 24 Mar 2026 19:04:26 +0530 Subject: [PATCH 19/20] update: test case for and use single batch test fix --- test/IdentityToken.t.sol | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 4ad8bf7..73959dc 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -41,8 +41,8 @@ contract IdentityTokenTest is Test { uint256 tokenId = identityToken.mint(); // Set name first, then email to satisfy validation - identityToken.setName(tokenId, "Alice Nakamoto"); - identityToken.setContact(tokenId, "alice@example.com", ""); + identityToken.setAttribute(tokenId, "name", bytes("Alice Nakamoto")); + identityToken.setAttribute(tokenId, "email", bytes("alice@example.com")); identityToken.setAttribute(tokenId, "github", bytes("https://github.com/alice")); @@ -189,19 +189,15 @@ contract IdentityTokenTest is Test { vm.startPrank(alice); uint256 tokenId = identityToken.mint(); - string[] memory keys = new string[](3); + string[] memory keys = new string[](1); keys[0] = "name"; - keys[1] = "email"; - keys[2] = "age"; - bytes[] memory values = new bytes[](3); + bytes[] memory values = new bytes[](1); values[0] = bytes("Alice Nakamoto"); - values[1] = bytes("alice@example.com"); - values[2] = bytes("30"); identityToken.setAttributesBatch(tokenId, keys, values); - assertEq(string(identityToken.getAttribute(tokenId, "age")), "30"); + assertEq(string(identityToken.getAttribute(tokenId, "name")), "Alice Nakamoto"); } // ------------------------------------------------------------------------- From 1f50c7e7e3701b4eae07b7be6deb67c977fb87c1 Mon Sep 17 00:00:00 2001 From: Nikunj Date: Tue, 24 Mar 2026 19:24:04 +0530 Subject: [PATCH 20/20] remove: validate condition from batch set --- src/IdentityToken.sol | 11 ++--------- test/IdentityToken.t.sol | 2 ++ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/IdentityToken.sol b/src/IdentityToken.sol index bd42b92..7fb4054 100644 --- a/src/IdentityToken.sol +++ b/src/IdentityToken.sol @@ -139,20 +139,13 @@ contract IdentityToken is ERC721, IIdentityToken { ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { if (keys.length != values.length) revert Errors.ArrayLengthMismatch(); - bool shouldValidate = true; + uint8 shouldValidate = 0; for (uint256 i = 0; i < keys.length; i++) { - bytes32 keyHash = keccak256(abi.encodePacked(keys[i])); _setAttribute(tokenId, keys[i], values[i]); - - if (keyHash == NAME_KEY || keyHash == EMAIL_KEY || keyHash == PHONE_KEY) { - shouldValidate = false; - } } - if (shouldValidate) { - _validateRequiredFields(tokenId); - } + _validateRequiredFields(tokenId); } /** diff --git a/test/IdentityToken.t.sol b/test/IdentityToken.t.sol index 73959dc..adc0b44 100644 --- a/test/IdentityToken.t.sol +++ b/test/IdentityToken.t.sol @@ -189,6 +189,8 @@ contract IdentityTokenTest is Test { vm.startPrank(alice); uint256 tokenId = identityToken.mint(); + identityToken.setContact(tokenId, "alice@example.com", ""); + string[] memory keys = new string[](1); keys[0] = "name";