Skip to content

feat: implement identity recovery and backup wallet logic#75

Open
dhruvi-16-me wants to merge 1 commit into
StabilityNexus:mainfrom
dhruvi-16-me:identity-recovery-backup-wallet
Open

feat: implement identity recovery and backup wallet logic#75
dhruvi-16-me wants to merge 1 commit into
StabilityNexus:mainfrom
dhruvi-16-me:identity-recovery-backup-wallet

Conversation

@dhruvi-16-me
Copy link
Copy Markdown
Contributor

@dhruvi-16-me dhruvi-16-me commented Mar 25, 2026

Addressed Issues:

Fixes #48

Screenshots/Recordings:

image image

Additional Notes:

This PR implements a secure identity recovery and backup wallet mechanism for IdentityToken, enabling users to recover ownership of their identity if their primary wallet is lost or compromised.

The implementation introduces a timelocked backup wallet update flow, compromise flagging, and a controlled recovery process while preserving the soulbound (non-transferable) nature of the token.

🔧 Changes Made

Backup Wallet Management

  • Added support for assigning a backupWallet with a 7-day timelock

  • Implemented:

    • initiateBackupUpdate(uint256 tokenId, address newBackup) — stores pending backup and unlock timestamp
    • finalizeBackupUpdate(uint256 tokenId) — finalizes backup assignment after timelock
  • Emits:

    • BackupUpdateInitiated
    • BackupUpdated

Compromise Flagging

  • Added flagCompromised(uint256 tokenId) callable by:

    • Token owner
    • Assigned backup wallet
  • Sets isCompromised = true, restricting protocol actions via existing guards

  • Emits IdentityCompromised

Identity Recovery

  • Implemented recoverIdentity(uint256 tokenId, address newOwner):

    • Restricted via onlyBackupWallet
    • Transfers ERC-721 ownership to newOwner
    • Updates ownerToTokenId mapping
    • Resets isCompromised to false
  • Emits IdentityRecovered

Transfer Safety

  • Updated _update to enforce non-transferability:

    • Blocks transfers unless minting, burning, or recovery is in progress
  • Introduced _recovering flag to safely allow transfers only during recovery

Testing

Added 14 comprehensive test cases in IdentityToken.t.sol covering:

Backup Wallet Management

  • Pending backup state creation and validation
  • Timelock enforcement
  • Event emission checks
  • Access control and revert scenarios

Compromise Flagging

  • Authorization for owner and backup wallet
  • State updates and event emission
  • Enforcement of restricted actions when compromised

Identity Recovery

  • Ownership transfer and mapping updates
  • Reset of compromised state
  • Event emission validation
  • Reverts for unauthorized access and invalid conditions
  • Recovery flow validation with and without prior compromise flagging

All tests pass and cover both expected behavior and edge cases.

AI Usage Disclosure:

We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact. AI slop is strongly discouraged and may lead to banning and blocking. Do not spam our repos with AI slop.

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.
  • I have filled this PR template completely and carefully, and I understand that my PR may be closed without review otherwise.

Summary by CodeRabbit

  • New Features

    • Introduced backup wallet recovery flow with timelock protection for secure wallet updates
    • Added ability to flag compromised identities and recover ownership to a new address
    • Implemented endorsement revocation capability
  • Tests

    • Comprehensive test coverage for backup wallet operations and identity recovery workflows

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 25, 2026

Walkthrough

This pull request implements a secure backup wallet recovery system for identity tokens. It introduces a timelock mechanism for backup wallet updates, allows compromise flagging and identity recovery workflows, and adds endorsement revocation functionality. The token transfer logic is modified to permit operations during explicit recovery scenarios.

Changes

Cohort / File(s) Summary
Identity Recovery & Backup Flow
src/IdentityToken.sol
Added BACKUP_TIMELOCK constant, onlyBackupWallet modifier, and modified _update to permit transfers during recovery. Introduced initiateBackupUpdate, finalizeBackupUpdate (with 7-day timelock), flagCompromised (callable by owner/backup wallet), and recoverIdentity (transfers token to new owner, clears compromise flag).
Endorsement Management
src/IdentityToken.sol
Added revokeEndorsement function to allow token owners to revoke endorsement entries by index with bounds checking and revocation status validation.
Backup Wallet Recovery Tests
test/IdentityToken.t.sol
Added test actor carol and comprehensive test suite covering backup wallet workflow (initiate, timelock, finalize), negative cases for unauthorized access and premature finalization, and tests validating state transitions and event emissions.
Compromise & Recovery Tests
test/IdentityToken.t.sol
Added tests for flagCompromised (owner/backup wallet callable, freezes attribute changes), recoverIdentity (transfers ownership, resets compromise flag, enables new owner modifications), and negative cases for invalid callers and state conditions.

Sequence Diagram(s)

sequenceDiagram
    participant Owner
    participant IdentityToken as IdentityToken<br/>(Contract)
    participant Timelock as 7-Day<br/>Timelock
    
    Owner->>IdentityToken: initiateBackupUpdate(tokenId, newBackup)
    activate IdentityToken
    IdentityToken->>IdentityToken: Set pendingBackupWallet<br/>Set backupUnlockTime
    IdentityToken->>Owner: emit BackupUpdateInitiated
    deactivate IdentityToken
    
    Owner->>Timelock: Wait 7 days
    Timelock->>Owner: Timelock expires
    
    Owner->>IdentityToken: finalizeBackupUpdate(tokenId)
    activate IdentityToken
    IdentityToken->>IdentityToken: Verify timelock passed
    IdentityToken->>IdentityToken: Commit pendingBackupWallet<br/>Clear pending fields
    IdentityToken->>Owner: emit BackupUpdated
    deactivate IdentityToken
Loading
sequenceDiagram
    participant Owner as Owner/<br/>BackupWallet
    participant IdentityToken as IdentityToken<br/>(Contract)
    participant NewOwner
    
    Owner->>IdentityToken: flagCompromised(tokenId)
    activate IdentityToken
    IdentityToken->>IdentityToken: Set isCompromised = true
    IdentityToken->>Owner: emit IdentityCompromised
    deactivate IdentityToken
    
    Note over IdentityToken: Token operations frozen<br/>until recovery
    
    BackupWallet->>IdentityToken: recoverIdentity(tokenId, newOwner)
    activate IdentityToken
    IdentityToken->>IdentityToken: Verify caller is backupWallet
    IdentityToken->>IdentityToken: Transfer ERC-721 ownership<br/>Update ownerToTokenId mapping
    IdentityToken->>IdentityToken: Set isCompromised = false
    IdentityToken->>NewOwner: emit IdentityRecovered
    deactivate IdentityToken
    
    Note over IdentityToken: NewOwner can now<br/>modify attributes
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Solidity Lang

Poem

🐰 A backup in the warren, a safety plan so wise,
When wallets go missing, there's no surprise,
Seven days to think it through, then recover with care,
A new owner springs forth—hop, skip, and fare! 🌱

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly summarizes the main change: implementing identity recovery and backup wallet logic, which matches the core functionality added.
Linked Issues check ✅ Passed All objectives from issue #48 are met: backup wallet assignment with timelock (initiateBackupUpdate/finalizeBackupUpdate), compromise flagging (flagCompromised), identity recovery (recoverIdentity), and required modifiers/access controls are implemented.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #48 objectives: backup wallet management, compromise flagging, identity recovery, and related test coverage. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/IdentityToken.t.sol (1)

1-828: ⚠️ Potential issue | 🟠 Major

Missing test coverage for revokeEndorsement.

The new revokeEndorsement function in IdentityToken.sol has no corresponding tests. Per coding guidelines, security-sensitive logic changes require adequate test coverage. The following scenarios should be tested:

  • Successful revocation by endorser
  • Event emission (EndorsementRevoked)
  • Revert when index is out of bounds (IndexOutOfBounds)
  • Revert when caller is not the endorser (NotEndorser)
  • Revert when endorsement is already revoked (AlreadyRevoked)
  • Verify revokedAt timestamp is set correctly
  • Verify revoked endorsement no longer counts as active in isVerified

Do you want me to generate the test cases for revokeEndorsement?

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

In `@test/IdentityToken.t.sol` around lines 1 - 828, Add unit tests for
revokeEndorsement covering: (1) successful revocation by the endorser — mint two
tokens, call endorse(endorserId, recipientId,...), then vm.prank(endorser) call
revokeEndorsement(recipientId, index) and assert endorsements(recipientId,
index).revokedAt > 0 and isVerified(recipientId) updates accordingly; (2) event
emission — wrap the revoke call with vm.expectEmit and assert
Events.EndorsementRevoked(recipientId, endorserId, index); (3) index out of
bounds revert — call revokeEndorsement with an invalid index and
vm.expectRevert(Errors.IndexOutOfBounds.selector); (4) unauthorized caller
revert — have a non-endorser call revokeEndorsement and
vm.expectRevert(Errors.NotEndorser.selector); and (5) already revoked revert —
revoke once, then attempt to revoke again and
vm.expectRevert(Errors.AlreadyRevoked.selector). Use identityToken.endorse,
identityToken.revokeEndorsement, the endorsements(recipientId, index) accessor,
vm.prank for callers, vm.warp if checking timestamp comparisons, and
Events.EndorsementRevoked / Errors.* selectors to locate assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/IdentityToken.sol`:
- Around line 259-261: The recovery pattern using the _recovering flag should be
hardened by adding OpenZeppelin's ReentrancyGuard: import
"@openzeppelin/contracts/security/ReentrancyGuard.sol", have the contract
inherit ReentrancyGuard, and add the nonReentrant modifier to the recovery
function that currently toggles _recovering and calls _transfer (the function
that sets _recovering = true; _transfer(currentOwner, newOwner, tokenId);
_recovering = false). This preserves the existing flag logic while providing
defense-in-depth against future reentrancy risks.
- Around line 206-214: In initiateBackupUpdate, prevent the owner from setting
their own address as the backup by validating that newBackup is not equal to the
token owner; retrieve the owner (e.g. via ownerOf(tokenId) or currentTokenOwner
lookup used by onlyTokenOwner) and add a require/revert if newBackup == owner,
then proceed to set pendingBackupWallet and backupUnlockTime and emit
Events.BackupUpdateInitiated; reference the function initiateBackupUpdate and
the DataTypes.IdentityState.pendingBackupWallet field when making this change.
- Around line 240-247: Add a guard in flagCompromised to avoid redundant work
and duplicate events by checking identityStates[tokenId].isCompromised before
setting it; if already true, revert with a descriptive error (e.g.,
Errors.AlreadyCompromised()) or make the call a no-op, then only set
state.isCompromised = true and emit Events.IdentityCompromised(tokenId) when the
token was not previously compromised; keep existing ownership check (ownerOf and
state.backupWallet) as-is.
- Around line 277-292: Add unit tests covering revokeEndorsement: implement
tests for (1) successful revoke — set up an endorsement (create tokens/users,
set ownerToTokenId for endorser, add endorsement to
endorsements[targetTokenId]), call revokeEndorsement and assert
endorsements[targetTokenId][index].revokedAt is nonzero and
Events.EndorsementRevoked was emitted with (endorserTokenId, targetTokenId,
index); (2) IndexOutOfBounds — call revokeEndorsement with an out-of-range index
and assert it reverts with Errors.IndexOutOfBounds; (3) NotEndorser — call as a
caller without ownerToTokenId or with mismatched endorserTokenId and assert
revert with Errors.NotEndorser; (4) AlreadyRevoked — revoke once then call again
and assert revert with Errors.AlreadyRevoked. Use the contract functions/state
names from the diff (revokeEndorsement, endorsements, ownerToTokenId,
Events.EndorsementRevoked, Errors.*) to locate and exercise the logic.
- Around line 254-266: Add a zero-address check in recoverIdentity to prevent
recovering an identity to address(0): inside the recoverIdentity function
(alongside the existing balanceOf and onlyBackupWallet checks) require newOwner
!= address(0) and revert with a defined error from Errors.sol; either add a new
InvalidRecipient error to Errors.sol and use that, or reuse the existing
TargetInvalid error (update the revert call in recoverIdentity accordingly).
Ensure the change references recoverIdentity and Errors.sol only, and keep the
remainder of the function logic (setting _recovering, _transfer, clearing
isCompromised, and emitting Events.IdentityRecovered) unchanged.

---

Outside diff comments:
In `@test/IdentityToken.t.sol`:
- Around line 1-828: Add unit tests for revokeEndorsement covering: (1)
successful revocation by the endorser — mint two tokens, call
endorse(endorserId, recipientId,...), then vm.prank(endorser) call
revokeEndorsement(recipientId, index) and assert endorsements(recipientId,
index).revokedAt > 0 and isVerified(recipientId) updates accordingly; (2) event
emission — wrap the revoke call with vm.expectEmit and assert
Events.EndorsementRevoked(recipientId, endorserId, index); (3) index out of
bounds revert — call revokeEndorsement with an invalid index and
vm.expectRevert(Errors.IndexOutOfBounds.selector); (4) unauthorized caller
revert — have a non-endorser call revokeEndorsement and
vm.expectRevert(Errors.NotEndorser.selector); and (5) already revoked revert —
revoke once, then attempt to revoke again and
vm.expectRevert(Errors.AlreadyRevoked.selector). Use identityToken.endorse,
identityToken.revokeEndorsement, the endorsements(recipientId, index) accessor,
vm.prank for callers, vm.warp if checking timestamp comparisons, and
Events.EndorsementRevoked / Errors.* selectors to locate assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 40372957-75ac-48f7-8e22-5ce4e7dc1c77

📥 Commits

Reviewing files that changed from the base of the PR and between 22e681b and 63d9502.

📒 Files selected for processing (2)
  • src/IdentityToken.sol
  • test/IdentityToken.t.sol

Comment thread src/IdentityToken.sol
Comment thread src/IdentityToken.sol
Comment thread src/IdentityToken.sol
Comment thread src/IdentityToken.sol
Comment thread src/IdentityToken.sol
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements an identity backup wallet + recovery mechanism for IdentityToken to support account loss/compromise scenarios while keeping the token generally non-transferable.

Changes:

  • Added timelocked backup wallet update flow (initiate/finalize) plus compromise flagging.
  • Added backup-wallet-controlled identity recovery path and updated transfer restrictions to allow recovery transfers.
  • Added Foundry tests covering backup wallet management, compromise flagging, and recovery flows.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/IdentityToken.sol Adds backup wallet timelock logic, compromised/recovery flows, and endorsement revocation; updates _update transfer restrictions for recovery.
test/IdentityToken.t.sol Adds test cases for backup wallet state transitions, access control, compromise freezing, and recovery behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/IdentityToken.sol
_;
}

modifier onlyBackupWallet(uint256 tokenId) {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

onlyBackupWallet checks identityStates[tokenId].backupWallet before verifying the token exists. For a nonexistent tokenId, recoverIdentity will revert with NotBackupWallet rather 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) or ownerOf(tokenId) inside the modifier) before checking the backup wallet.

Suggested change
modifier onlyBackupWallet(uint256 tokenId) {
modifier onlyBackupWallet(uint256 tokenId) {
_requireOwned(tokenId);

Copilot uses AI. Check for mistakes.
Comment thread src/IdentityToken.sol
Comment on lines +254 to +261
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;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

recoverIdentity currently allows a backup wallet to transfer the identity even when it is not marked compromised. That creates a general-purpose transfer path (owner can set a backup wallet, wait the timelock, and have it “recover” to any address), which weakens the intended soulbound / non-transferable guarantees. If recovery is meant to be an emergency-only path, require identityStates[tokenId].isCompromised == true (or another explicit recovery-initiated flag) before allowing the transfer.

Copilot uses AI. Check for mistakes.
Comment thread src/IdentityToken.sol
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;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

After recoverIdentity, the previous backupWallet remains set. This means the same backup wallet can immediately “recover” again (and the new owner can only change the backup via a 7-day timelock), which is a strong and possibly unintended power over the recovered identity. Consider clearing backupWallet/pending fields on recovery, or updating the backup wallet as part of the recovery flow so the new owner isn’t stuck with the prior guardian.

Suggested change
* 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 uses AI. Check for mistakes.
Comment thread src/IdentityToken.sol
DataTypes.Endorsement storage e = list[index];

uint256 callerTokenId = ownerToTokenId[msg.sender];
if (callerTokenId == 0 || e.endorserTokenId != callerTokenId) revert Errors.NotEndorser();
Copy link

Copilot AI Mar 27, 2026

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.

Suggested change
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 uses AI. Check for mistakes.
Comment thread src/IdentityToken.sol
Comment on lines +277 to +292
function revokeEndorsement(uint256 targetTokenId, uint256 index) external {
DataTypes.Endorsement[] storage list = endorsements[targetTokenId];

if (index >= list.length) revert Errors.IndexOutOfBounds();

DataTypes.Endorsement storage e = list[index];

uint256 callerTokenId = ownerToTokenId[msg.sender];
if (callerTokenId == 0 || e.endorserTokenId != callerTokenId) revert Errors.NotEndorser();

if (e.revokedAt != 0) revert Errors.AlreadyRevoked();

e.revokedAt = block.timestamp;

emit Events.EndorsementRevoked(e.endorserTokenId, targetTokenId, index);
}
Copy link

Copilot AI Mar 27, 2026

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.

Copilot uses AI. Check for mistakes.
@KanishkSogani
Copy link
Copy Markdown
Member

@dhruvi-16-me please resolve the issues pointed out by copilot and coderabbit above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Implement Identity Recovery and Backup Wallet Logic

3 participants