Skip to content

fix: block NFT transfer for seized/active-loan holders (closes #12)#25

Open
iyanumajekodunmi756 wants to merge 1 commit into
LabsCrypt:mainfrom
iyanumajekodunmi756:fix/transfer-seized-active-loan
Open

fix: block NFT transfer for seized/active-loan holders (closes #12)#25
iyanumajekodunmi756 wants to merge 1 commit into
LabsCrypt:mainfrom
iyanumajekodunmi756:fix/transfer-seized-active-loan

Conversation

@iyanumajekodunmi756

Copy link
Copy Markdown

Summary

Fixes the reputation-laundering bug described in #12.

A seized borrower could call transfer() to move their NFT to a clean
address, shedding the Seized and DefaultCount flags. Similarly, a
borrower with an active approved loan could transfer the NFT out from
under the loan, decoupling credit identity from outstanding debt.


Changes

remittance_nft/src/lib.rs

New error variants

Code Name Meaning
19 TransferSeized Sender's collateral is seized
20 TransferActiveLoan Sender has an outstanding approved loan
21 NotLoanManager Caller is not the registered loan manager

New DataKey variants

  • ActiveLoanHolder(Address) — per-address active-loan lock
  • LoanManager — the contract address authorised to set/clear locks

New public functions

  • set_loan_manager(loan_manager) — admin-only, registers the loan manager
  • get_loan_manager() — read-only accessor
  • set_active_loan_holder(holder) — loan-manager-only, locks NFT transfer
  • clear_active_loan_holder(holder) — loan-manager-only, removes the lock
  • has_active_loan(holder) — read-only query

transfer() guard order (after cooldown check, before state mutations):

  1. Reject if Seized(from) is set → TransferSeized
  2. Reject if ActiveLoanHolder(from) is set → TransferActiveLoan

Removed the now-unreachable Seized flag copy block (a seized sender
never reaches that code path; the guard returns early).

Docs — extended seize_collateral doc comment with the full transfer
eligibility rules.


loan_manager/src/lib.rs

  • Extended NftClient trait with set_active_loan_holder and clear_active_loan_holder.
  • approve_loan: calls nft.set_active_loan_holder(borrower) after the token transfer.
  • repay (full repayment): calls nft.clear_active_loan_holder(borrower).
  • check_default: calls nft.clear_active_loan_holder(borrower).
  • liquidate: calls nft.clear_active_loan_holder(borrower).

Tests

Test What it verifies
test_transfer_blocked_for_seized_user Seized holder cannot transfer; NFT stays put
test_transfer_blocked_with_active_loan Active-loan lock blocks transfer; clearing the lock re-enables it
test_set_active_loan_holder_requires_loan_manager Panics if no loan manager is registered
test_clear_active_loan_holder_requires_loan_manager Same
test_transfer_moves_identity_state_to_new_wallet (updated) Non-seized, no-active-loan transfer still works correctly

All 69 remittance_nft tests and 84 loan_manager tests pass (cargo test clean).


Deployment note

After deploying both updated contracts, the NFT admin must call
set_loan_manager(<loan_manager_address>) once to register the loan
manager. This wires up the bidirectional lock mechanism.

Closes #12

## Problem
A seized borrower could call transfer() to move their NFT to a clean
address, shedding the Seized and DefaultCount flags (reputation
laundering). Similarly, a borrower with an active approved loan could
transfer the NFT out from under the loan, decoupling credit identity
from outstanding debt.

## Fix

### remittance_nft/src/lib.rs
- Added NftError::TransferSeized (19) and NftError::TransferActiveLoan (20)
  and NftError::NotLoanManager (21).
- Added DataKey::ActiveLoanHolder(Address) and DataKey::LoanManager
  to track the per-address active-loan lock and the authorised loan
  manager contract.
- Added public functions:
  - set_loan_manager(loan_manager): admin-only, registers the loan manager
  - get_loan_manager(): read-only accessor
  - set_active_loan_holder(holder): loan-manager-only, sets the lock
  - clear_active_loan_holder(holder): loan-manager-only, clears the lock
  - has_active_loan(holder): read-only query
- transfer() now rejects:
  1. Seized senders → NftError::TransferSeized
  2. Active-loan holders → NftError::TransferActiveLoan
- Removed the now-unreachable Seized flag copy block from transfer()
  (a seized user can never reach that code path).
- Extended seize_collateral doc comment with transfer eligibility rules.

### loan_manager/src/lib.rs
- Extended NftClient trait with set_active_loan_holder and
  clear_active_loan_holder.
- approve_loan: calls nft.set_active_loan_holder(borrower) after the
  token transfer to lock the NFT.
- repay (full repayment): calls nft.clear_active_loan_holder(borrower).
- check_default: calls nft.clear_active_loan_holder(borrower) after
  recording the default.
- liquidate: calls nft.clear_active_loan_holder(borrower) after
  liquidation completes.

### Tests
- test_transfer_blocked_for_seized_user: verifies seized holders are
  blocked and NFT stays put.
- test_transfer_blocked_with_active_loan: verifies active-loan lock
  blocks transfer, and that clearing the lock re-enables transfer.
- test_set_active_loan_holder_requires_loan_manager: panics if no loan
  manager is registered.
- test_clear_active_loan_holder_requires_loan_manager: same.
- Updated test_transfer_moves_identity_state_to_new_wallet to use a
  non-seized sender (the old version was testing the laundering bug).

Closes LabsCrypt#12

@ogazboiz ogazboiz left a comment

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.

nice work on this, the core of issue #12 is solid. i pulled the branch and ran it locally: cargo fmt --check, clippy with -D warnings, and the full suite all pass clean (69 remittance_nft tests, 84 loan_manager tests). the seized-transfer block is exactly right: the guard reads the same DataKey::Seized key that seize_collateral writes, returns a typed TransferSeized error, and you removed the old seized-state copy so it can't be laundered to a fresh address. test_transfer_blocked_for_seized_user genuinely proves a seized holder's transfer is rejected, and the eligibility doc comment is a good touch.

a couple of things i'd like to see before merge:

  1. the active-loan lock is a single boolean per borrower (DataKey::ActiveLoanHolder(Address)), but max loans per borrower defaults to 3. approve_loan sets the bool and repay/check_default/liquidate each just remove it, so a borrower with two active loans who closes one gets the lock lifted while the other loan is still outstanding, which re-opens the transfer bypass. could you make it a reference count (increment on approve, decrement on close, clear at zero) so it holds with concurrent loans?

  2. if the admin forgets to call set_loan_manager after deploy, approve_loan will revert because set_active_loan_holder returns NotLoanManager and the client call panics. that is a hard dependency, so either guard it or add a test that documents the failure mode, and make the deploy step very visible in the pr.

  3. small one: the description and the seize_collateral doc say the lock is cleared on "cancelled" loans, but cancel/reject only touch Pending loans, which never set the lock. just tidy the wording so it does not imply a path that cannot happen.

the snapshot json churn is expected, so no concern there, the real surface is just the four .rs files.

if you want to keep contributing, join us on Telegram: https://t.me/+DOylgFv1jyJlNzM0

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.

[Contracts] RemittanceNFT transfer moves seized/default state to recipient, enabling reputation laundering

2 participants