Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions stellar/stealth-registry/AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Security Audit: Stealth Registry Soroban Contract

**Date**: 2026-05-29
**Auditor**: thebabalola
**Status**: Completed (with implemented fixes)

## 1. Executive Summary
The `stealth-registry` contract is a core component of the Wraith Protocol, mapping user addresses to their 64-byte stealth meta-addresses. The initial implementation was functional but had opportunities for improvement in storage efficiency, privacy, and robustness.

## 2. Findings & Improvements

### 2.1 Storage Type Efficiency (Medium)
- **Original**: Used `instance()` storage for meta-addresses.
- **Risk**: `instance()` storage has a limited footprint size. As more users register, the contract would eventually hit the size limit and fail.
- **Fix**: Migrated to `persistent()` storage. This allows the registry to scale to a virtually unlimited number of users without affecting the contract's instance footprint.

### 2.2 User Privacy: Missing Deletion (Low)
- **Original**: No way for a user to remove their stealth keys once registered.
- **Risk**: Users might want to opt-out or rotate keys completely without leaving old data on-chain.
- **Fix**: Added `remove_keys(registrant, scheme_id)` function with `require_auth()` verification.

### 2.3 Event Consistency (Low)
- **Improvement**: Added a `remove` event to complement the `register` event, ensuring indexers can stay in sync with the registry state.

## 3. Implementation Details

### 3.1 Changes to `lib.rs`
- Switched `env.storage().instance()` to `env.storage().persistent()` in `register_keys` and `stealth_meta_address_of`.
- Implemented `remove_keys` function.
- Updated tests to cover `remove_keys` and verify storage behavior.

## 4. Conclusion
The contract is now more robust and scalable. The use of `persistent` storage is a critical fix for long-term viability.
83 changes: 80 additions & 3 deletions stellar/stealth-registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ impl StealthRegistryContract {
return Err(RegistryError::InvalidMetaAddressLength);
}

// Persist.
// Persist using persistent storage to handle large number of users.
let key = DataKey::MetaAddress(registrant.clone(), scheme_id);
env.storage().instance().set(&key, &stealth_meta_address);
env.storage().persistent().set(&key, &stealth_meta_address);

// Emit event.
env.events().publish(
Expand All @@ -62,6 +62,35 @@ impl StealthRegistryContract {
Ok(())
}

/// Remove a previously registered stealth meta-address.
///
/// # Arguments
/// * `registrant` - The address whose meta-address is being removed (must authorise).
/// * `scheme_id` - The stealth address scheme identifier.
pub fn remove_keys(
env: Env,
registrant: Address,
scheme_id: u32,
) -> Result<(), RegistryError> {
// Require authorisation from the registrant.
registrant.require_auth();

let key = DataKey::MetaAddress(registrant.clone(), scheme_id);
if !env.storage().persistent().has(&key) {
return Err(RegistryError::NotRegistered);
}

env.storage().persistent().remove(&key);

// Emit event.
env.events().publish(
(symbol_short!("remove"), registrant, scheme_id),
(),
);

Ok(())
}

/// Look up a previously registered stealth meta-address.
///
/// # Arguments
Expand All @@ -74,7 +103,7 @@ impl StealthRegistryContract {
) -> Result<Bytes, RegistryError> {
let key = DataKey::MetaAddress(registrant, scheme_id);
env.storage()
.instance()
.persistent()
.get(&key)
.ok_or(RegistryError::NotRegistered)
}
Expand Down Expand Up @@ -176,4 +205,52 @@ mod test {
meta_v2
);
}

#[test]
fn test_remove_keys() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register(StealthRegistryContract, ());
let client = StealthRegistryContractClient::new(&env, &contract_id);

let registrant = Address::generate(&env);
let scheme_id: u32 = 1;
let meta_address = Bytes::from_slice(&env, &[42u8; 64]);

client.register_keys(&registrant, &scheme_id, &meta_address);
assert_eq!(client.stealth_meta_address_of(&registrant, &scheme_id), meta_address);

client.remove_keys(&registrant, &scheme_id);

// Verify event was emitted
let events = env.events().all();
let event = events.last().unwrap();
let expected_topics: soroban_sdk::Vec<Val> = vec![
&env,
symbol_short!("remove").into_val(&env),
registrant.clone().into_val(&env),
scheme_id.into_val(&env),
];
assert_eq!(event.1, expected_topics);

// Verify it's gone
let result = client.try_stealth_meta_address_of(&registrant, &scheme_id);
assert_eq!(result, Err(Ok(RegistryError::NotRegistered)));
}

#[test]
fn test_remove_not_registered() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register(StealthRegistryContract, ());
let client = StealthRegistryContractClient::new(&env, &contract_id);

let registrant = Address::generate(&env);
let scheme_id: u32 = 1;

let result = client.try_remove_keys(&registrant, &scheme_id);
assert_eq!(result, Err(Ok(RegistryError::NotRegistered)));
}
}