From fd8a5fb3aaa7212dc451e6460d6f79c8211424ec Mon Sep 17 00:00:00 2001 From: Anonfedora Date: Sat, 21 Feb 2026 12:34:00 +0100 Subject: [PATCH] feat: Renounce admin function --- contract/contract/src/base/events.rs | 5 ++ contract/contract/src/crowdfunding.rs | 15 +++- .../contract/src/interfaces/crowdfunding.rs | 2 + contract/contract/test/mod.rs | 1 + contract/contract/test/renounce_admin_test.rs | 89 +++++++++++++++++++ 5 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 contract/contract/test/renounce_admin_test.rs diff --git a/contract/contract/src/base/events.rs b/contract/contract/src/base/events.rs index f5a0da6..0148859 100644 --- a/contract/contract/src/base/events.rs +++ b/contract/contract/src/base/events.rs @@ -44,6 +44,11 @@ pub fn contract_unpaused(env: &Env, admin: Address, timestamp: u64) { env.events().publish(topics, timestamp); } +pub fn admin_renounced(env: &Env, admin: Address) { + let topics = (Symbol::new(env, "admin_renounced"), admin); + env.events().publish(topics, ()); +} + pub fn emergency_contact_updated(env: &Env, admin: Address, contact: Address) { let topics = (Symbol::new(env, "emergency_contact_updated"), admin); env.events().publish(topics, contact); diff --git a/contract/contract/src/crowdfunding.rs b/contract/contract/src/crowdfunding.rs index 18b9a27..35eedad 100644 --- a/contract/contract/src/crowdfunding.rs +++ b/contract/contract/src/crowdfunding.rs @@ -727,6 +727,19 @@ impl CrowdfundingTrait for CrowdfundingContract { Ok(()) } + fn renounce_admin(env: Env) -> Result<(), CrowdfundingError> { + let admin: Address = env + .storage() + .instance() + .get(&StorageKey::Admin) + .ok_or(CrowdfundingError::NotInitialized)?; + admin.require_auth(); + + env.storage().instance().remove(&StorageKey::Admin); + events::admin_renounced(&env, admin); + Ok(()) + } + fn is_paused(env: Env) -> bool { env.storage() .instance() @@ -906,7 +919,7 @@ impl CrowdfundingTrait for CrowdfundingContract { .storage() .instance() .get(&metrics_key) - .unwrap_or(PoolMetrics::new()); + .unwrap_or_default(); metrics.total_raised -= contribution.amount; // Note: We don't decrement contributor_count as the contributor may have other contributions diff --git a/contract/contract/src/interfaces/crowdfunding.rs b/contract/contract/src/interfaces/crowdfunding.rs index 71b9a45..1187a72 100644 --- a/contract/contract/src/interfaces/crowdfunding.rs +++ b/contract/contract/src/interfaces/crowdfunding.rs @@ -128,6 +128,8 @@ pub trait CrowdfundingTrait { fn is_closed(env: Env, pool_id: u64) -> Result; + fn renounce_admin(env: Env) -> Result<(), CrowdfundingError>; + fn get_active_campaign_count(env: Env) -> u32; fn verify_cause(env: Env, cause: Address) -> Result<(), CrowdfundingError>; diff --git a/contract/contract/test/mod.rs b/contract/contract/test/mod.rs index 1eb7cf3..1711cf2 100644 --- a/contract/contract/test/mod.rs +++ b/contract/contract/test/mod.rs @@ -1,4 +1,5 @@ mod close_pool_test; mod create_pool; mod crowdfunding_test; +mod renounce_admin_test; mod verify_cause; diff --git a/contract/contract/test/renounce_admin_test.rs b/contract/contract/test/renounce_admin_test.rs new file mode 100644 index 0000000..7b89c6b --- /dev/null +++ b/contract/contract/test/renounce_admin_test.rs @@ -0,0 +1,89 @@ +#![cfg(test)] + +use soroban_sdk::{ + testutils::{Address as _, MockAuth, MockAuthInvoke}, + Address, Env, IntoVal, +}; + +use crate::{ + base::errors::CrowdfundingError, + crowdfunding::{CrowdfundingContract, CrowdfundingContractClient}, +}; + +fn setup_test(env: &Env) -> (CrowdfundingContractClient<'_>, Address, Address) { + env.mock_all_auths(); + let contract_id = env.register(CrowdfundingContract, ()); + let client = CrowdfundingContractClient::new(env, &contract_id); + + let admin = Address::generate(env); + let token_admin = Address::generate(env); + let token_contract = env.register_stellar_asset_contract_v2(token_admin.clone()); + let token_address = token_contract.address(); + + client.initialize(&admin, &token_address, &0); + + (client, admin, token_address) +} + +#[test] +fn test_renounce_admin_success() { + let env = Env::default(); + let (client, _, _) = setup_test(&env); + + // Initial state: admin exists and can perform admin actions + assert!(!client.is_paused()); + client.pause(); + assert!(client.is_paused()); + client.unpause(); + assert!(!client.is_paused()); + + // Renounce admin + client.renounce_admin(); + + // Verify admin is removed by trying an admin action + let result = client.try_pause(); + assert_eq!(result, Err(Ok(CrowdfundingError::NotInitialized))); +} + +#[test] +fn test_renounce_admin_unauthorized() { + let env = Env::default(); + let (client, _, _) = setup_test(&env); + let _non_admin = Address::generate(&env); + + // unauthorized call is covered by require_auth in renounce_admin + // which is tested below via mock_auths +} + +#[test] +#[should_panic] +fn test_renounce_admin_requires_admin_auth() { + let env = Env::default(); + let (client, _, _) = setup_test(&env); + let non_admin = Address::generate(&env); + + // Use specific mock_auths to ensure non_admin is the one calling + client + .mock_auths(&[MockAuth { + address: &non_admin, + invoke: &MockAuthInvoke { + contract: &client.address, + fn_name: "renounce_admin", + args: ().into_val(&env), + sub_invokes: &[], + }, + }]) + .renounce_admin(); +} + +#[test] +fn test_renounce_admin_already_renounced() { + let env = Env::default(); + let (client, _, _) = setup_test(&env); + + client.renounce_admin(); + + // Try to renounce again + let result = client.try_renounce_admin(); + assert_eq!(result, Err(Ok(CrowdfundingError::NotInitialized))); +}