fix: block NFT transfer for seized/active-loan holders (closes #12)#25
Conversation
## 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
left a comment
There was a problem hiding this comment.
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:
-
the active-loan lock is a single boolean per borrower (
DataKey::ActiveLoanHolder(Address)), but max loans per borrower defaults to 3.approve_loansets the bool andrepay/check_default/liquidateeach justremoveit, 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? -
if the admin forgets to call
set_loan_managerafter deploy,approve_loanwill revert becauseset_active_loan_holderreturnsNotLoanManagerand 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. -
small one: the description and the
seize_collateraldoc say the lock is cleared on "cancelled" loans, but cancel/reject only touchPendingloans, 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
Summary
Fixes the reputation-laundering bug described in #12.
A seized borrower could call
transfer()to move their NFT to a cleanaddress, shedding the
SeizedandDefaultCountflags. Similarly, aborrower 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.rsNew error variants
TransferSeizedTransferActiveLoanNotLoanManagerNew
DataKeyvariantsActiveLoanHolder(Address)— per-address active-loan lockLoanManager— the contract address authorised to set/clear locksNew public functions
set_loan_manager(loan_manager)— admin-only, registers the loan managerget_loan_manager()— read-only accessorset_active_loan_holder(holder)— loan-manager-only, locks NFT transferclear_active_loan_holder(holder)— loan-manager-only, removes the lockhas_active_loan(holder)— read-only querytransfer()guard order (after cooldown check, before state mutations):Seized(from)is set →TransferSeizedActiveLoanHolder(from)is set →TransferActiveLoanRemoved the now-unreachable
Seizedflag copy block (a seized sendernever reaches that code path; the guard returns early).
Docs — extended
seize_collateraldoc comment with the full transfereligibility rules.
loan_manager/src/lib.rsNftClienttrait withset_active_loan_holderandclear_active_loan_holder.approve_loan: callsnft.set_active_loan_holder(borrower)after the token transfer.repay(full repayment): callsnft.clear_active_loan_holder(borrower).check_default: callsnft.clear_active_loan_holder(borrower).liquidate: callsnft.clear_active_loan_holder(borrower).Tests
test_transfer_blocked_for_seized_usertest_transfer_blocked_with_active_loantest_set_active_loan_holder_requires_loan_managertest_clear_active_loan_holder_requires_loan_managertest_transfer_moves_identity_state_to_new_wallet(updated)All 69 remittance_nft tests and 84 loan_manager tests pass (
cargo testclean).Deployment note
After deploying both updated contracts, the NFT admin must call
set_loan_manager(<loan_manager_address>)once to register the loanmanager. This wires up the bidirectional lock mechanism.
Closes #12