diff --git a/contracts/payment-vault-contract/src/contract.rs b/contracts/payment-vault-contract/src/contract.rs index 544b62f..b226166 100644 --- a/contracts/payment-vault-contract/src/contract.rs +++ b/contracts/payment-vault-contract/src/contract.rs @@ -215,6 +215,23 @@ pub fn reclaim_stale_session(env: &Env, user: &Address, booking_id: u64) -> Resu Ok(()) } +pub fn transfer_admin(env: &Env, new_admin: &Address) -> Result<(), VaultError> { + let current_admin = storage::get_admin(env).ok_or(VaultError::NotInitialized)?; + current_admin.require_auth(); + storage::set_admin(env, new_admin); + events::admin_transferred(env, ¤t_admin, new_admin); + Ok(()) +} + +pub fn set_oracle(env: &Env, new_oracle: &Address) -> Result<(), VaultError> { + let admin = storage::get_admin(env).ok_or(VaultError::NotInitialized)?; + admin.require_auth(); + let old_oracle = storage::get_oracle(env); + storage::set_oracle(env, new_oracle); + events::oracle_updated(env, &old_oracle, new_oracle); + Ok(()) +} + pub fn reject_session(env: &Env, expert: &Address, booking_id: u64) -> Result<(), VaultError> { if storage::is_paused(env) { return Err(VaultError::ContractPaused); diff --git a/contracts/payment-vault-contract/src/events.rs b/contracts/payment-vault-contract/src/events.rs index 3d20162..f72da0e 100644 --- a/contracts/payment-vault-contract/src/events.rs +++ b/contracts/payment-vault-contract/src/events.rs @@ -42,3 +42,17 @@ pub fn expert_rate_updated(env: &Env, expert: &Address, rate: i128) { let topics = (symbol_short!("rate_upd"), expert.clone()); env.events().publish(topics, rate); } + +/// Emitted when admin is transferred to a new address +pub fn admin_transferred(env: &Env, old_admin: &Address, new_admin: &Address) { + let topics = (symbol_short!("adm_xfer"),); + env.events() + .publish(topics, (old_admin.clone(), new_admin.clone())); +} + +/// Emitted when the oracle address is updated +pub fn oracle_updated(env: &Env, old_oracle: &Address, new_oracle: &Address) { + let topics = (symbol_short!("orc_upd"),); + env.events() + .publish(topics, (old_oracle.clone(), new_oracle.clone())); +} diff --git a/contracts/payment-vault-contract/src/lib.rs b/contracts/payment-vault-contract/src/lib.rs index 651c2c4..ea1c915 100644 --- a/contracts/payment-vault-contract/src/lib.rs +++ b/contracts/payment-vault-contract/src/lib.rs @@ -39,6 +39,18 @@ impl PaymentVaultContract { contract::unpause(&env) } + /// Transfer admin rights to a new address (Admin-only) + /// Old admin instantly loses all privileges + pub fn transfer_admin(env: Env, new_admin: Address) -> Result<(), VaultError> { + contract::transfer_admin(&env, &new_admin) + } + + /// Update the oracle address (Admin-only) + /// Old oracle instantly loses authorization to finalize sessions + pub fn set_oracle(env: Env, new_oracle: Address) -> Result<(), VaultError> { + contract::set_oracle(&env, &new_oracle) + } + /// Set an expert's own rate per second pub fn set_my_rate(env: Env, expert: Address, rate_per_second: i128) -> Result<(), VaultError> { contract::set_my_rate(&env, &expert, rate_per_second) diff --git a/contracts/payment-vault-contract/src/test.rs b/contracts/payment-vault-contract/src/test.rs index ea0e77d..fb50915 100644 --- a/contracts/payment-vault-contract/src/test.rs +++ b/contracts/payment-vault-contract/src/test.rs @@ -697,6 +697,139 @@ fn test_reject_nonexistent_booking() { assert!(result.is_err()); } +// ==================== Key Rotation Tests ==================== + +#[test] +fn test_transfer_admin_success() { + let env = Env::default(); + env.mock_all_auths(); + + let admin_a = Address::generate(&env); + let admin_b = Address::generate(&env); + let token = Address::generate(&env); + let oracle = Address::generate(&env); + + let client = create_client(&env); + client.init(&admin_a, &token, &oracle); + + // Admin A transfers to Admin B + let result = client.try_transfer_admin(&admin_b); + assert!(result.is_ok()); +} + +#[test] +fn test_new_admin_can_pause_after_transfer() { + let env = Env::default(); + env.mock_all_auths(); + + let admin_a = Address::generate(&env); + let admin_b = Address::generate(&env); + let token = Address::generate(&env); + let oracle = Address::generate(&env); + + let client = create_client(&env); + client.init(&admin_a, &token, &oracle); + client.transfer_admin(&admin_b); + + // New admin B can pause and unpause + assert!(client.try_pause().is_ok()); + assert!(client.try_unpause().is_ok()); +} + +#[test] +fn test_old_admin_loses_privileges_after_transfer() { + let env = Env::default(); + env.mock_all_auths(); + + let admin_a = Address::generate(&env); + let admin_b = Address::generate(&env); + let token = Address::generate(&env); + let oracle = Address::generate(&env); + + let client = create_client(&env); + client.init(&admin_a, &token, &oracle); + client.transfer_admin(&admin_b); + + // Remove all mocked auths — now only explicit auth will pass + env.set_auths(&[]); + + // Without any valid auth for admin_b, pause should fail + // (admin_b is now the required auth, but no auth is mocked) + let result = client.try_pause(); + assert!(result.is_err()); +} + +#[test] +fn test_set_oracle_success() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let token = Address::generate(&env); + let oracle_old = Address::generate(&env); + let oracle_new = Address::generate(&env); + + let token_admin = Address::generate(&env); + let token_contract = create_token_contract(&env, &token_admin); + let user = Address::generate(&env); + let expert = Address::generate(&env); + token_contract.mint(&user, &10_000); + + let client = create_client(&env); + client.init(&admin, &token_contract.address, &oracle_old); + + // Book a session + client.set_my_rate(&expert, &10_i128); + let booking_id = client.book_session(&user, &expert, &100); + + // Rotate oracle to new address + let result = client.try_set_oracle(&oracle_new); + assert!(result.is_ok()); + + // New oracle can finalize + let result = client.try_finalize_session(&booking_id, &50); + assert!(result.is_ok()); +} + +#[test] +fn test_non_admin_cannot_transfer_admin() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let token = Address::generate(&env); + let oracle = Address::generate(&env); + + let client = create_client(&env); + client.init(&admin, &token, &oracle); + + // Clear auths so attacker has no authorization + env.set_auths(&[]); + + let result = client.try_transfer_admin(&attacker); + assert!(result.is_err()); +} + +#[test] +fn test_non_admin_cannot_set_oracle() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let attacker = Address::generate(&env); + let token = Address::generate(&env); + let oracle = Address::generate(&env); + + let client = create_client(&env); + client.init(&admin, &token, &oracle); + + env.set_auths(&[]); + + let result = client.try_set_oracle(&attacker); + assert!(result.is_err()); +} + // ==================== Expert Rate Tests ==================== #[test]