diff --git a/contracts/identity-registry-contract/src/contract.rs b/contracts/identity-registry-contract/src/contract.rs index 6939685..c241478 100644 --- a/contracts/identity-registry-contract/src/contract.rs +++ b/contracts/identity-registry-contract/src/contract.rs @@ -61,10 +61,37 @@ pub fn batch_ban_experts(env: Env, experts: Vec
) -> Result<(), Registry Ok(()) } -pub fn verify_expert(env: &Env, expert: &Address, data_uri: String) -> Result<(), RegistryError> { +/// Add a moderator (Admin only) +pub fn add_moderator(env: &Env, moderator: &Address) -> Result<(), RegistryError> { let admin = storage::get_admin(env).ok_or(RegistryError::NotInitialized)?; + admin.require_auth(); + storage::set_moderator(env, moderator); + Ok(()) +} +/// Remove a moderator (Admin only) +pub fn remove_moderator(env: &Env, moderator: &Address) -> Result<(), RegistryError> { + let admin = storage::get_admin(env).ok_or(RegistryError::NotInitialized)?; admin.require_auth(); + storage::remove_moderator(env, moderator); + Ok(()) +} + +pub fn verify_expert( + env: &Env, + caller: &Address, + expert: &Address, + data_uri: String, +) -> Result<(), RegistryError> { + let admin = storage::get_admin(env).ok_or(RegistryError::NotInitialized)?; + + if caller == &admin { + admin.require_auth(); + } else if storage::is_moderator(env, caller) { + caller.require_auth(); + } else { + return Err(RegistryError::Unauthorized); + } let current_status = storage::get_expert_status(env, expert); @@ -85,16 +112,23 @@ pub fn verify_expert(env: &Env, expert: &Address, data_uri: String) -> Result<() expert.clone(), current_status, ExpertStatus::Verified, - admin, + caller.clone(), ); Ok(()) } -/// Ban an expert by setting their status to Banned (Admin only) -pub fn ban_expert(env: &Env, expert: &Address) -> Result<(), RegistryError> { +/// Ban an expert by setting their status to Banned (Admin or Moderator) +pub fn ban_expert(env: &Env, caller: &Address, expert: &Address) -> Result<(), RegistryError> { let admin = storage::get_admin(env).ok_or(RegistryError::NotInitialized)?; - admin.require_auth(); + + if caller == &admin { + admin.require_auth(); + } else if storage::is_moderator(env, caller) { + caller.require_auth(); + } else { + return Err(RegistryError::Unauthorized); + } let current_status = storage::get_expert_status(env, expert); @@ -111,7 +145,7 @@ pub fn ban_expert(env: &Env, expert: &Address) -> Result<(), RegistryError> { expert.clone(), current_status, ExpertStatus::Banned, - admin, + caller.clone(), ); Ok(()) diff --git a/contracts/identity-registry-contract/src/error.rs b/contracts/identity-registry-contract/src/error.rs index 5ba9d39..20bcc52 100644 --- a/contracts/identity-registry-contract/src/error.rs +++ b/contracts/identity-registry-contract/src/error.rs @@ -19,4 +19,5 @@ pub enum RegistryError { NotVerified = 8, UriTooLong = 9, NotBanned = 10, + Unauthorized = 11, } diff --git a/contracts/identity-registry-contract/src/lib.rs b/contracts/identity-registry-contract/src/lib.rs index 3504ccb..4963b6c 100644 --- a/contracts/identity-registry-contract/src/lib.rs +++ b/contracts/identity-registry-contract/src/lib.rs @@ -22,6 +22,16 @@ impl IdentityRegistryContract { contract::initialize_registry(&env, &admin) } + /// Add a moderator (Admin only) + pub fn add_moderator(env: Env, moderator: Address) -> Result<(), RegistryError> { + contract::add_moderator(&env, &moderator) + } + + /// Remove a moderator (Admin only) + pub fn remove_moderator(env: Env, moderator: Address) -> Result<(), RegistryError> { + contract::remove_moderator(&env, &moderator) + } + /// Batch Add an expert to the whitelist (Admin only) pub fn batch_add_experts(env: Env, experts: Vec
) -> Result<(), RegistryError> { contract::batch_add_experts(env, experts) @@ -32,15 +42,20 @@ impl IdentityRegistryContract { contract::batch_ban_experts(env, experts) } - /// Add an expert to the whitelist (Admin only) + /// Add an expert to the whitelist (Admin or Moderator) /// Also saves a profile data_uri reference (e.g., ipfs://...) - pub fn add_expert(env: Env, expert: Address, data_uri: String) -> Result<(), RegistryError> { - contract::verify_expert(&env, &expert, data_uri) + pub fn add_expert( + env: Env, + caller: Address, + expert: Address, + data_uri: String, + ) -> Result<(), RegistryError> { + contract::verify_expert(&env, &caller, &expert, data_uri) } - /// Ban an expert and revoke their verification status (Admin only) - pub fn ban_expert(env: Env, expert: Address) -> Result<(), RegistryError> { - contract::ban_expert(&env, &expert) + /// Ban an expert and revoke their verification status (Admin or Moderator) + pub fn ban_expert(env: Env, caller: Address, expert: Address) -> Result<(), RegistryError> { + contract::ban_expert(&env, &caller, &expert) } /// Unban an expert and restore their verification status (Admin only) diff --git a/contracts/identity-registry-contract/src/storage.rs b/contracts/identity-registry-contract/src/storage.rs index 53a1222..c3cc222 100644 --- a/contracts/identity-registry-contract/src/storage.rs +++ b/contracts/identity-registry-contract/src/storage.rs @@ -9,6 +9,7 @@ pub enum DataKey { Expert(Address), VerifiedExpertIndex(u64), TotalVerifiedCount, + Moderator(Address), } // Constants for TTL (Time To Live) @@ -40,6 +41,30 @@ pub fn get_admin(env: &Env) -> Option
{ env.storage().instance().get(&DataKey::Admin) } +// ... [Moderator Helpers] ... + +/// Check if an address is a moderator +pub fn is_moderator(env: &Env, address: &Address) -> bool { + env.storage() + .instance() + .get(&DataKey::Moderator(address.clone())) + .unwrap_or(false) +} + +/// Set an address as a moderator +pub fn set_moderator(env: &Env, address: &Address) { + env.storage() + .instance() + .set(&DataKey::Moderator(address.clone()), &true); +} + +/// Remove an address from moderators +pub fn remove_moderator(env: &Env, address: &Address) { + env.storage() + .instance() + .remove(&DataKey::Moderator(address.clone())); +} + // ... [Expert Helpers] ... /// Set the expert record with status, data_uri and timestamp diff --git a/contracts/identity-registry-contract/src/test.rs b/contracts/identity-registry-contract/src/test.rs index b153af2..7f491d1 100644 --- a/contracts/identity-registry-contract/src/test.rs +++ b/contracts/identity-registry-contract/src/test.rs @@ -41,7 +41,7 @@ fn test_data_uri_persisted_on_verify() { let uri = String::from_str(&env, "ipfs://persisted"); client.init(&admin); - client.add_expert(&expert, &uri); + client.add_expert(&admin, &expert, &uri); // Read storage as contract and assert data_uri persisted env.as_contract(&contract_id, || { @@ -64,7 +64,7 @@ fn test_update_profile_updates_uri_and_emits_event() { let uri2 = String::from_str(&env, "ipfs://updated"); client.init(&admin); - client.add_expert(&expert, &uri1); + client.add_expert(&admin, &expert, &uri1); // Update profile URI client.update_profile(&expert, &uri2); @@ -98,7 +98,7 @@ fn test_update_profile_rejections() { // Verify then try overlong uri let expert = Address::generate(&env); let ok_uri = String::from_str(&env, "ipfs://ok"); - client.add_expert(&expert, &ok_uri); + client.add_expert(&admin, &expert, &ok_uri); // Build >64 length string let long_str = "a".repeat(65); @@ -239,7 +239,7 @@ fn test_add_expert() { client.init(&admin); let data_uri = String::from_str(&env, "ipfs://profile1"); - let res = client.try_add_expert(&expert, &data_uri); + let res = client.try_add_expert(&admin, &expert, &data_uri); assert!(res.is_ok()); assert_eq!( @@ -250,7 +250,7 @@ fn test_add_expert() { function: AuthorizedFunction::Contract(( contract_id.clone(), Symbol::new(&env, "add_expert"), - (expert.clone(), data_uri.clone()).into_val(&env) + (admin.clone(), expert.clone(), data_uri.clone()).into_val(&env) )), sub_invocations: std::vec![] } @@ -271,7 +271,8 @@ fn test_add_expert_unauthorized() { client.init(&admin); let data_uri = String::from_str(&env, "ipfs://unauth"); - client.add_expert(&expert, &data_uri); + // admin is the caller but no auth is mocked — should panic + client.add_expert(&admin, &expert, &data_uri); } #[test] @@ -287,7 +288,7 @@ fn test_expert_status_changed_event() { client.init(&admin); let data_uri = String::from_str(&env, "ipfs://event"); - client.add_expert(&expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri); let events = env.events().all(); let event = events.last().unwrap(); @@ -313,21 +314,21 @@ fn test_ban_expert() { // Verify the expert first env.mock_all_auths(); let data_uri = String::from_str(&env, "ipfs://ban"); - client.add_expert(&expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri); // Verify status is Verified let status = client.get_status(&expert); assert_eq!(status, ExpertStatus::Verified); // Ban the expert (should succeed) - client.ban_expert(&expert); + client.ban_expert(&admin, &expert); // Check that status is now Banned let status = client.get_status(&expert); assert_eq!(status, ExpertStatus::Banned); // Test: Try to ban again (should fail with AlreadyBanned) - let result = client.try_ban_expert(&expert); + let result = client.try_ban_expert(&admin, &expert); assert_eq!(result, Err(Ok(RegistryError::AlreadyBanned))); } @@ -345,13 +346,14 @@ fn test_ban_expert_unauthorized() { env.mock_all_auths(); let data_uri = String::from_str(&env, "ipfs://ban-unauth"); - client.add_expert(&expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri); env.mock_all_auths_allowing_non_root_auth(); env.mock_auths(&[]); - client.ban_expert(&expert); + // admin is the caller but no auth is mocked — should panic + client.ban_expert(&admin, &expert); } #[test] @@ -372,7 +374,7 @@ fn test_ban_unverified_expert() { // Ban an expert who was never verified (should still succeed) env.mock_all_auths(); - client.ban_expert(&expert); + client.ban_expert(&admin, &expert); // Status should be Banned now let status = client.get_status(&expert); @@ -399,9 +401,9 @@ fn test_ban_expert_workflow() { let uri1 = String::from_str(&env, "ipfs://u1"); let uri2 = String::from_str(&env, "ipfs://u2"); let uri3 = String::from_str(&env, "ipfs://u3"); - client.add_expert(&expert1, &uri1); - client.add_expert(&expert2, &uri2); - client.add_expert(&expert3, &uri3); + client.add_expert(&admin, &expert1, &uri1); + client.add_expert(&admin, &expert2, &uri2); + client.add_expert(&admin, &expert3, &uri3); // Check all are verified assert_eq!(client.get_status(&expert1), ExpertStatus::Verified); @@ -409,7 +411,7 @@ fn test_ban_expert_workflow() { assert_eq!(client.get_status(&expert3), ExpertStatus::Verified); // Ban expert2 - client.ban_expert(&expert2); + client.ban_expert(&admin, &expert2); // Verify expert2 is banned, others remain verified assert_eq!(client.get_status(&expert1), ExpertStatus::Verified); @@ -417,7 +419,7 @@ fn test_ban_expert_workflow() { assert_eq!(client.get_status(&expert3), ExpertStatus::Verified); // Ban expert1 - client.ban_expert(&expert1); + client.ban_expert(&admin, &expert1); // Verify expert1 is now banned assert_eq!(client.get_status(&expert1), ExpertStatus::Banned); @@ -432,11 +434,12 @@ fn test_ban_before_contract_initialized() { let client = IdentityRegistryContractClient::new(&env, &contract_id); let expert = Address::generate(&env); + let caller = Address::generate(&env); env.mock_all_auths(); // Try to ban without initializing (should fail) - let result = client.try_ban_expert(&expert); + let result = client.try_ban_expert(&caller, &expert); assert_eq!(result, Err(Ok(RegistryError::NotInitialized))); } @@ -459,11 +462,11 @@ fn test_complete_expert_lifecycle() { // 2. Verify the expert let data_uri = String::from_str(&env, "ipfs://life"); - client.add_expert(&expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri); assert_eq!(client.get_status(&expert), ExpertStatus::Verified); // 3. Ban the expert - client.ban_expert(&expert); + client.ban_expert(&admin, &expert); assert_eq!(client.get_status(&expert), ExpertStatus::Banned); } @@ -486,12 +489,12 @@ fn test_getters() { // Test 2: Verify an expert and check is_verified (should be true) let expert = Address::generate(&env); let data_uri = String::from_str(&env, "ipfs://getters"); - client.add_expert(&expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri); assert_eq!(client.is_verified(&expert), true); assert_eq!(client.get_status(&expert), ExpertStatus::Verified); // Test 3: Ban the expert and check is_verified (should be false) - client.ban_expert(&expert); + client.ban_expert(&admin, &expert); assert_eq!(client.is_verified(&expert), false); assert_eq!(client.get_status(&expert), ExpertStatus::Banned); } @@ -515,9 +518,9 @@ fn test_expert_directory_enumeration() { let uri1 = String::from_str(&env, "ipfs://e1"); let uri2 = String::from_str(&env, "ipfs://e2"); let uri3 = String::from_str(&env, "ipfs://e3"); - client.add_expert(&expert1, &uri1); - client.add_expert(&expert2, &uri2); - client.add_expert(&expert3, &uri3); + client.add_expert(&admin, &expert1, &uri1); + client.add_expert(&admin, &expert2, &uri2); + client.add_expert(&admin, &expert3, &uri3); // Total should be 3 assert_eq!(client.get_total_experts(), 3u64); @@ -542,13 +545,13 @@ fn test_expert_directory_no_duplicates_on_reverify() { client.init(&admin); let uri = String::from_str(&env, "ipfs://expert"); - client.add_expert(&expert, &uri); + client.add_expert(&admin, &expert, &uri); // Total is 1 assert_eq!(client.get_total_experts(), 1u64); // Re-verifying an already verified expert returns AlreadyVerified - let result = client.try_add_expert(&expert, &uri); + let result = client.try_add_expert(&admin, &expert, &uri); assert_eq!(result, Err(Ok(RegistryError::AlreadyVerified))); // Total remains 1 — no duplicate in the index @@ -606,11 +609,11 @@ fn test_batch_update_profiles() { let uri4 = String::from_str(&env, "ipfs://original4"); let uri5 = String::from_str(&env, "ipfs://original5"); - client.add_expert(&expert1, &uri1); - client.add_expert(&expert2, &uri2); - client.add_expert(&expert3, &uri3); - client.add_expert(&expert4, &uri4); - client.add_expert(&expert5, &uri5); + client.add_expert(&admin, &expert1, &uri1); + client.add_expert(&admin, &expert2, &uri2); + client.add_expert(&admin, &expert3, &uri3); + client.add_expert(&admin, &expert4, &uri4); + client.add_expert(&admin, &expert5, &uri5); // Prepare batch updates with new URIs let new_uri1 = String::from_str(&env, "ipfs://updated1"); @@ -691,7 +694,7 @@ fn test_batch_update_profiles_uri_too_long() { let expert = Address::generate(&env); let uri = String::from_str(&env, "ipfs://initial"); - client.add_expert(&expert, &uri); + client.add_expert(&admin, &expert, &uri); // Create update with URI that's too long (>64 chars) let long_str = "a".repeat(65); @@ -720,7 +723,7 @@ fn test_expert_pagination() { for _ in 0..15 { let expert = Address::generate(&env); let uri = String::from_str(&env, "ipfs://expert"); - client.add_expert(&expert, &uri); + client.add_expert(&admin, &expert, &uri); experts.push_back(expert); } @@ -759,14 +762,14 @@ fn test_unban_expert() { env.mock_all_auths(); let data_uri = String::from_str(&env, "ipfs://unban"); - client.add_expert(&expert, &data_uri); + client.add_expert(&admin, &expert, &data_uri); // Initial status: Verified assert_eq!(client.get_status(&expert), ExpertStatus::Verified); let initial_total = client.get_total_experts(); // Ban the expert - client.ban_expert(&expert); + client.ban_expert(&admin, &expert); assert_eq!(client.get_status(&expert), ExpertStatus::Banned); // Unban the expert @@ -787,3 +790,102 @@ fn test_unban_expert() { assert_eq!(result, Err(Ok(RegistryError::NotBanned))); } +#[test] +fn test_moderator_can_verify_expert() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let moderator = Address::generate(&env); + let expert = Address::generate(&env); + + client.init(&admin); + + // Admin adds a moderator + client.add_moderator(&moderator); + + // Moderator verifies an expert (should succeed) + let uri = String::from_str(&env, "ipfs://mod-verify"); + let res = client.try_add_expert(&moderator, &expert, &uri); + assert!(res.is_ok()); + + assert_eq!(client.get_status(&expert), ExpertStatus::Verified); +} + +#[test] +fn test_moderator_can_ban_expert() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let moderator = Address::generate(&env); + let expert = Address::generate(&env); + + client.init(&admin); + client.add_moderator(&moderator); + + // Verify expert first (by admin) + let uri = String::from_str(&env, "ipfs://mod-ban"); + client.add_expert(&admin, &expert, &uri); + + // Moderator bans the expert (should succeed) + let res = client.try_ban_expert(&moderator, &expert); + assert!(res.is_ok()); + + assert_eq!(client.get_status(&expert), ExpertStatus::Banned); +} + +#[test] +fn test_non_moderator_cannot_verify_expert() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let random = Address::generate(&env); + let expert = Address::generate(&env); + + client.init(&admin); + + // Random address (not admin, not moderator) tries to verify — should fail with Unauthorized + let uri = String::from_str(&env, "ipfs://unauth"); + let res = client.try_add_expert(&random, &expert, &uri); + assert_eq!(res, Err(Ok(RegistryError::Unauthorized))); +} + +#[test] +fn test_remove_moderator() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register(IdentityRegistryContract, ()); + let client = IdentityRegistryContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let moderator = Address::generate(&env); + let expert = Address::generate(&env); + + client.init(&admin); + client.add_moderator(&moderator); + + // Verify moderator is set + env.as_contract(&contract_id, || { + assert!(storage::is_moderator(&env, &moderator)); + }); + + // Remove the moderator + client.remove_moderator(&moderator); + + // After removal, moderator can no longer verify experts + let uri = String::from_str(&env, "ipfs://removed-mod"); + let res = client.try_add_expert(&moderator, &expert, &uri); + assert_eq!(res, Err(Ok(RegistryError::Unauthorized))); +}