Skip to content

Commit bcaf079

Browse files
aladuru001claude
andauthored
fix(user_registry): keep Privy DID to wallet mapping unique on update and recovery (#1075)
update_privy_did and recover_privy_did could move a DID onto a wallet already linked to another DID, overwriting its reverse index. Reject that case, matching register_privy_did. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7a66578 commit bcaf079

2 files changed

Lines changed: 37 additions & 0 deletions

File tree

‎contracts/contracts/user_registry/src/lib.rs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,14 @@ impl UserRegistryContract {
455455
if stored_wallet != old_wallet {
456456
panic!("unauthorized: old wallet does not match registered wallet");
457457
}
458+
// One wallet -> one DID: the new wallet must not already be linked.
459+
if env
460+
.storage()
461+
.persistent()
462+
.has(&DataKey::WalletDid(new_wallet.clone()))
463+
{
464+
panic!("wallet already has a DID linked");
465+
}
458466
// Remove old reverse mapping
459467
env.storage()
460468
.persistent()
@@ -480,6 +488,14 @@ impl UserRegistryContract {
480488
let admin = Self::require_admin(&env);
481489
admin.require_auth();
482490
let did_key = DataKey::PrivyDid(did.clone());
491+
// One wallet -> one DID: the new wallet must not already be linked.
492+
if env
493+
.storage()
494+
.persistent()
495+
.has(&DataKey::WalletDid(new_wallet.clone()))
496+
{
497+
panic!("wallet already has a DID linked");
498+
}
483499
// Remove old reverse mapping if present
484500
if let Some(old_wallet) = env.storage().persistent().get::<DataKey, Address>(&did_key) {
485501
env.storage()

‎contracts/contracts/user_registry/src/test.rs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,27 @@ fn test_recover_privy_did_as_admin() {
177177
assert_eq!(client.get_wallet_for_did(&did), new_wallet);
178178
}
179179

180+
/// Verify that update and recovery cannot move a DID onto a wallet that is
181+
/// already linked to another DID (one wallet -> one DID).
182+
#[test]
183+
#[ignore = "contract panics are non-unwinding under Soroban v20 testutils and abort the test process"]
184+
fn test_update_and_recover_reject_wallet_already_linked() {
185+
let (env, client, signing_key) = setup();
186+
let wallet_a = Address::generate(&env);
187+
let wallet_b = Address::generate(&env);
188+
let did_a = String::from_str(&env, "did:privy:a");
189+
let did_b = String::from_str(&env, "did:privy:b");
190+
let sig_a = sign_did_link(&env, &signing_key, &did_a, &wallet_a);
191+
let sig_b = sign_did_link(&env, &signing_key, &did_b, &wallet_b);
192+
client.register_privy_did(&did_a, &wallet_a, &sig_a);
193+
client.register_privy_did(&did_b, &wallet_b, &sig_b);
194+
195+
assert!(client.try_update_privy_did(&did_a, &wallet_a, &wallet_b).is_err());
196+
assert!(client.try_recover_privy_did(&did_a, &wallet_b).is_err());
197+
assert_eq!(client.get_wallet_for_did(&did_a), wallet_a);
198+
assert_eq!(client.get_did_for_wallet(&wallet_b), did_b);
199+
}
200+
180201
/// Verify that querying an unregistered DID returns an error.
181202
#[test]
182203
#[ignore = "contract panics are non-unwinding under Soroban v20 testutils and abort the test process"]

0 commit comments

Comments
 (0)