Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cf43f80
delete-attribute
aniket866 Mar 18, 2026
12a9ab7
Code rabbit follow up
aniket866 Mar 18, 2026
5926ba3
Code rabbit fix
aniket866 Mar 18, 2026
8f60e5d
Code rabbit fix
aniket866 Mar 18, 2026
6a0d7f8
fix: schema by update important field
Nikuunj Mar 20, 2026
0b6ee25
update: error for name and contact
Nikuunj Mar 20, 2026
340f4d9
fix: validate user need to pass phone / email , and name is mandatory
Nikuunj Mar 20, 2026
7c36ca1
update: call the fn
Nikuunj Mar 20, 2026
f229460
Merge branch 'main' into delete-attribute
aniket866 Mar 20, 2026
cb415de
formatting
aniket866 Mar 20, 2026
a926546
Query-functions
aniket866 Mar 20, 2026
6c30f6d
fix: all test and validatefn call fix
Nikuunj Mar 23, 2026
417a1be
fix: fmt
Nikuunj Mar 23, 2026
a192669
Merge pull request #64 from aniket866/Query-Functions
KanishkSogani Mar 23, 2026
98dc951
update: test case for phn and email hashcompatibility
Nikuunj Mar 23, 2026
4cfdb02
Revert "update: test case for phn and email hashcompatibility"
Nikuunj Mar 23, 2026
049f8fc
fix: test_schemaConstants
Nikuunj Mar 23, 2026
c656af9
Merge pull request #62 from Nikuunj/main
KanishkSogani Mar 23, 2026
10608be
Merge branch 'bug' of github.com:Nikuunj/IdentityTokens-EVM-Contracts
Nikuunj Mar 23, 2026
5ea5109
update: validate setattribute and set contract fn
Nikuunj Mar 23, 2026
42ade6b
fix: test case for missing name and contact
Nikuunj Mar 23, 2026
d8590fc
update: identitytoken validate remove if field name , email, ph no
Nikuunj Mar 24, 2026
aa8df60
update: test case for and use single batch test fix
Nikuunj Mar 24, 2026
1f50c7e
remove: validate condition from batch set
Nikuunj Mar 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions src/IdentityToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -76,6 +81,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 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();

// At least one contact method required
if (email.length == 0 && phone.length == 0) {
revert Errors.MissingContact();
}
}
Comment on lines +84 to +101
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Gas optimization: Pre-compute constant key hashes.

The function computes keccak256(abi.encodePacked("name")), keccak256(abi.encodePacked("email")), and keccak256(abi.encodePacked("phone")) on every call. Since these are static strings, declare them as constant bytes32 at the contract level (or reuse from Schema.sol if already defined) to save gas.

♻️ Proposed refactor

Add constants at contract level:

bytes32 private constant KEY_NAME = keccak256(abi.encodePacked("name"));
bytes32 private constant KEY_EMAIL = keccak256(abi.encodePacked("email"));
bytes32 private constant KEY_PHONE = keccak256(abi.encodePacked("phone"));

Then refactor the function:

 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 memory name = attributes[tokenId][KEY_NAME];
+    bytes memory email = attributes[tokenId][KEY_EMAIL];
+    bytes memory phone = attributes[tokenId][KEY_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();
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/IdentityToken.sol` around lines 79 - 96, Pre-compute and reuse the
keccak256 hashes for the static attribute keys instead of recomputing them in
_validateRequiredFields: declare bytes32 constants (e.g., KEY_NAME, KEY_EMAIL,
KEY_PHONE) at the contract level or import/reuse the equivalents from
Schema.sol, then replace the inline
keccak256(abi.encodePacked("name"/"email"/"phone")) lookups in
_validateRequiredFields with attributes[tokenId][KEY_NAME],
attributes[tokenId][KEY_EMAIL], and attributes[tokenId][KEY_PHONE] to reduce
gas.

Comment on lines +89 to +101
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_validateRequiredFields recomputes keccak256(abi.encodePacked(...)) on every call and copies the stored bytes values into memory. To reduce gas and avoid key-string drift/typos, consider using precomputed key hashes (e.g., extend/use the existing Schema library) and read lengths directly from storage (bytes storage) instead of copying to memory.

Copilot uses AI. Check for mistakes.

/**
* @dev Sets a metadata attribute (e.g., name, social link) for an identity.
*/
Expand All @@ -84,7 +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);

// 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);
}
Comment on lines +115 to +118
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Skipping validation on required fields allows incomplete identities.

The current logic skips _validateRequiredFields when the updated key is name, email, or phone. This defeats the purpose of the validation because:

  • Setting only name (without any contact) succeeds and persists an incomplete identity
  • Setting only email or phone (without name) succeeds and persists an incomplete identity

The validation should run after setting required fields to ensure the final state is valid.

🐛 Proposed fix
     function setAttribute(
         uint256 tokenId,
         string calldata key,
         bytes calldata value
     ) external onlyTokenOwner(tokenId) notCompromised(tokenId) {
-        bytes32 keyHash = keccak256(abi.encodePacked(key));
-
         _setAttribute(tokenId, key, value);
-
-        // 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);
-        }
+        _validateRequiredFields(tokenId);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/IdentityToken.sol` around lines 115 - 118, The current logic skips
calling _validateRequiredFields(tokenId) when keyHash equals NAME_KEY,
EMAIL_KEY, or PHONE_KEY, which allows incomplete identities to be persisted;
change the flow so validation always runs after the attribute is set (i.e.,
remove the conditional that omits validation for NAME_KEY/EMAIL_KEY/PHONE_KEY
and call _validateRequiredFields(tokenId) regardless of which keyHash was
updated) so the final token state is checked; locate this in the attribute
update routine that references keyHash, NAME_KEY, EMAIL_KEY, PHONE_KEY and
_validateRequiredFields to implement the change.

}

/**
Expand All @@ -108,9 +139,13 @@ contract IdentityToken is ERC721, IIdentityToken {
) external onlyTokenOwner(tokenId) notCompromised(tokenId) {
if (keys.length != values.length) revert Errors.ArrayLengthMismatch();

uint8 shouldValidate = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused variable.

shouldValidate is declared but never used—leftover from a prior refactor.

🧹 Proposed fix
     ) external onlyTokenOwner(tokenId) notCompromised(tokenId) {
         if (keys.length != values.length) revert Errors.ArrayLengthMismatch();

-        uint8 shouldValidate = 0;
-
         for (uint256 i = 0; i < keys.length; i++) {
             _setAttribute(tokenId, keys[i], values[i]);
         }

         _validateRequiredFields(tokenId);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/IdentityToken.sol` at line 142, The variable shouldValidate declared in
IdentityToken.sol is unused and should be removed; locate the declaration of
uint8 shouldValidate (around the function or constructor where it's defined) and
delete the unused variable so there are no dangling or dead local variables left
in that scope, ensuring no other code references it before removal.


for (uint256 i = 0; i < keys.length; i++) {
_setAttribute(tokenId, keys[i], values[i]);
}

_validateRequiredFields(tokenId);
}
Comment on lines 89 to 149
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_validateRequiredFields is only invoked from setAttributesBatch, so identities can still end up missing name and/or contact by using setAttribute (or typed setters) to set other fields or to clear name/email/phone. If the intent is to enforce the invariant on-chain (per issue/PR description), ensure the validation is also applied on the single-attribute write paths (and consider allowing incremental setup by exempting calls that set name/email/phone themselves, while still preventing clearing the last required field).

Copilot uses AI. Check for mistakes.

/**
Expand All @@ -120,6 +155,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.
*/
Expand All @@ -128,6 +184,18 @@ contract IdentityToken is ERC721, IIdentityToken {
string calldata github
) external onlyTokenOwner(tokenId) notCompromised(tokenId) {
_setAttribute(tokenId, "github", bytes(github));
_validateRequiredFields(tokenId);
}

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);
}
Comment on lines +190 to 199
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: deleteAttribute bypasses required-field validation.

Deleting name, or deleting both email and phone, leaves the identity in an incomplete state. This directly contradicts the PR objective of enforcing required identity fields. The function must call _validateRequiredFields(tokenId) after the deletion.

🐛 Proposed fix
     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);
+
+        _validateRequiredFields(tokenId);
     }

As per coding guidelines, "Ensure failure paths and revert scenarios are explicitly handled and validated."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/IdentityToken.sol` around lines 190 - 199, deleteAttribute currently
removes an attribute without re-checking required identity fields; update the
function (deleteAttribute) to call _validateRequiredFields(tokenId) immediately
after the delete operation and before emitting Events.AttributeDeleted so that
the call will revert if the deletion leaves the identity in an invalid state
(e.g., removing "name" or both "email" and "phone"), ensuring the state change
cannot be committed without passing validation.


/**
Expand Down Expand Up @@ -184,4 +252,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;
Comment on lines +302 to +305
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

isExpired(...) is dead code in production right now.

This contract never writes identityStates[tokenId].validUntil, so the new field stays 0 and this helper always returns false outside tests. The current coverage only passes because the tests mutate storage directly with stdstore, which masks the missing on-chain update path. Either wire validUntil into a real state transition or drop this API from the PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/IdentityToken.sol` around lines 309 - 312, The isExpired(uint256 tokenId)
helper is unused because identityStates[tokenId].validUntil is never written;
either remove the API or persist validUntil during the relevant state
transition(s) — e.g., update identityStates[tokenId].validUntil inside the
functions that grant/renew/revoke identities (look for functions that change
identityStates or token lifecycle) so on-chain writes set a non-zero validUntil,
and keep isExpired as a view; if you prefer removal, delete the isExpired method
and any tests relying on it. Ensure you reference identityStates and the
validUntil field when adding the write and update related unit tests to exercise
the real state change rather than using stdstore.

}
}
22 changes: 21 additions & 1 deletion src/interfaces/IIdentityToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ 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 {
// -------------------------------------------------------------------------
// Attribute management
// -------------------------------------------------------------------------

function setAttribute(uint256 tokenId, string calldata key, bytes calldata value) external;
function deleteAttribute(uint256 tokenId, string calldata key) external;

function getAttribute(uint256 tokenId, string calldata key) external view returns (bytes memory);

Expand All @@ -36,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);

Expand All @@ -53,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);
}
12 changes: 12 additions & 0 deletions src/libraries/DataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 2 additions & 0 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ library Errors {
error AlreadyHasIdentity();
error DuplicateEndorsement();
error ArrayLengthMismatch();
error MissingName();
error MissingContact();
}
1 change: 1 addition & 0 deletions src/libraries/Events.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.20;

library Events {
event AttributeDeleted(uint256 indexed tokenId, bytes32 indexed keyHash);
event AttributeSet(uint256 indexed tokenId, bytes32 indexed keyHash, string key, bytes value);
event EndorsementGiven(uint256 indexed fromId, uint256 indexed toId, bytes32 typeHash, uint256 expiry);
event EndorsementRevoked(uint256 indexed fromId, uint256 indexed toId, uint256 index);
Expand Down
2 changes: 2 additions & 0 deletions src/libraries/Schema.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
Loading
Loading