-
-
Notifications
You must be signed in to change notification settings - Fork 17
feat: implement identity recovery and backup wallet logic #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,8 +11,12 @@ import { IIdentityToken } from "./interfaces/IIdentityToken.sol"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contract IdentityToken is ERC721, IIdentityToken { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error NonTransferable(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uint256 private constant BACKUP_TIMELOCK = 7 days; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uint256 private _nextTokenId = 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool private _recovering; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // wallet => tokenId (enforce one identity per wallet) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mapping(address => uint256) public ownerToTokenId; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -35,6 +39,11 @@ contract IdentityToken is ERC721, IIdentityToken { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modifier onlyBackupWallet(uint256 tokenId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (identityStates[tokenId].backupWallet != msg.sender) revert Errors.NotBackupWallet(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor() ERC721("IdentityToken", "IDT") {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function supportsInterface(bytes4 interfaceId) public view override(ERC721, IERC165) returns (bool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -44,8 +53,8 @@ contract IdentityToken is ERC721, IIdentityToken { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function _update(address to, uint256 tokenId, address auth) internal override returns (address) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address from = _ownerOf(tokenId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // prevent transfers (only mint or burn allowed) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (from != address(0) && to != address(0)) revert NonTransferable(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // prevent transfers (only mint, burn, or explicit recovery allowed) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!_recovering && from != address(0) && to != address(0)) revert NonTransferable(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address prevOwner = super._update(to, tokenId, auth); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -185,6 +194,103 @@ contract IdentityToken is ERC721, IIdentityToken { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emit Events.EndorsementGiven(fromTokenId, toTokenId, connectionType, validUntil); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Backup Wallet Management | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @dev Initiates a timelocked backup wallet change. The pending address is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * stored; the caller must call finalizeBackupUpdate after BACKUP_TIMELOCK | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * has elapsed to commit the change. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function initiateBackupUpdate( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uint256 tokenId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address newBackup | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) external onlyTokenOwner(tokenId) notCompromised(tokenId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DataTypes.IdentityState storage state = identityStates[tokenId]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.pendingBackupWallet = newBackup; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.backupUnlockTime = block.timestamp + BACKUP_TIMELOCK; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emit Events.BackupUpdateInitiated(tokenId, newBackup, state.backupUnlockTime); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
dhruvi-16-me marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @dev Finalizes a pending backup wallet change after the timelock has passed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function finalizeBackupUpdate(uint256 tokenId) external onlyTokenOwner(tokenId) notCompromised(tokenId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DataTypes.IdentityState storage state = identityStates[tokenId]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (state.pendingBackupWallet == address(0)) revert Errors.NoPendingUpdate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (block.timestamp < state.backupUnlockTime) revert Errors.TimelockActive(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address newBackup = state.pendingBackupWallet; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.backupWallet = newBackup; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.pendingBackupWallet = address(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.backupUnlockTime = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emit Events.BackupUpdated(tokenId, newBackup); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Compromise & Recovery | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @dev Marks a token as compromised, freezing all attribute and endorsement | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * operations. Callable by the token owner or its registered backup wallet. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function flagCompromised(uint256 tokenId) external { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DataTypes.IdentityState storage state = identityStates[tokenId]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ownerOf(tokenId) != msg.sender && state.backupWallet != msg.sender) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| revert Errors.NotTokenOwner(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.isCompromised = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emit Events.IdentityCompromised(tokenId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
dhruvi-16-me marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @dev Recovers a compromised (or otherwise inaccessible) identity by | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * transferring it to a new owner. Only the registered backup wallet | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * may call this. Clears the isCompromised flag post-transfer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function recoverIdentity(uint256 tokenId, address newOwner) external onlyBackupWallet(tokenId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (balanceOf(newOwner) != 0) revert Errors.AlreadyHasIdentity(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| address currentOwner = ownerOf(tokenId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _recovering = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _transfer(currentOwner, newOwner, tokenId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _recovering = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
dhruvi-16-me marked this conversation as resolved.
Comment on lines
+254
to
+261
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identityStates[tokenId].isCompromised = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+252
to
+263
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * may call this. Clears the isCompromised flag post-transfer. | |
| */ | |
| function recoverIdentity(uint256 tokenId, address newOwner) external onlyBackupWallet(tokenId) { | |
| if (balanceOf(newOwner) != 0) revert Errors.AlreadyHasIdentity(); | |
| address currentOwner = ownerOf(tokenId); | |
| _recovering = true; | |
| _transfer(currentOwner, newOwner, tokenId); | |
| _recovering = false; | |
| identityStates[tokenId].isCompromised = false; | |
| * may call this. Clears the isCompromised flag post-transfer and | |
| * removes the existing backup wallet to avoid repeated recoveries | |
| * by the previous guardian. | |
| */ | |
| function recoverIdentity(uint256 tokenId, address newOwner) external onlyBackupWallet(tokenId) { | |
| if (balanceOf(newOwner) != 0) revert Errors.AlreadyHasIdentity(); | |
| address currentOwner = ownerOf(tokenId); | |
| DataTypes.IdentityState storage state = identityStates[tokenId]; | |
| _recovering = true; | |
| _transfer(currentOwner, newOwner, tokenId); | |
| _recovering = false; | |
| state.isCompromised = false; | |
| state.backupWallet = address(0); |
Copilot
AI
Mar 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
flagCompromised’s docstring says it freezes “all attribute and endorsement operations,” but revokeEndorsement has no notCompromised check for the caller’s identity. Either add a compromised-state guard for revocations (e.g., require the caller’s tokenId is not compromised) or adjust the documentation/expectations so it’s clear revocation remains allowed while compromised.
| if (callerTokenId == 0 || e.endorserTokenId != callerTokenId) revert Errors.NotEndorser(); | |
| if (callerTokenId == 0 || e.endorserTokenId != callerTokenId) revert Errors.NotEndorser(); | |
| if (identityStates[callerTokenId].isCompromised) revert Errors.CompromisedIdentity(); |
Copilot
AI
Mar 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
revokeEndorsement is newly introduced behavior but there are no tests covering the happy path (revoking an active endorsement), the authorization checks (NotEndorser), and the double-revoke case (AlreadyRevoked). Adding targeted tests would help ensure revocation semantics stay correct and don’t regress.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onlyBackupWalletchecksidentityStates[tokenId].backupWalletbefore verifying the token exists. For a nonexistenttokenId,recoverIdentitywill revert withNotBackupWalletrather than the standard ERC721 nonexistent-token revert, which is misleading and can break callers relying on that behavior. Consider validating ownership/existence first (e.g.,_requireOwned(tokenId)orownerOf(tokenId)inside the modifier) before checking the backup wallet.