diff --git a/README.md b/README.md index b4da467..f86c518 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ Soroban smart contracts for the AgentPay protocol: escrow, usage recording, and - **escrow** — Records usage and supports settlement logic for machine-to-machine payments. +### Admin proposal validation + +`propose_admin_transfer` rejects proposing the current admin as the new admin +(panics with `InvalidAdminProposal`). This surfaces no-op handovers as caller +mistakes rather than silently storing a pending entry equal to the active admin. + ## Prerequisites - [Rust](https://rustup.rs/) (stable, with `rustfmt`) diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 1811bed..015af46 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -114,6 +114,10 @@ pub enum EscrowError { MigrationVersionMismatch = 11, /// `record_usage` referenced a service that has been disabled. ServiceDisabled = 12, + /// `propose_admin_transfer` was called with the current admin as the + /// proposed new admin — a no-op handover that is rejected to surface + /// caller mistakes early. + InvalidAdminProposal = 13, } #[contracttype] @@ -596,6 +600,9 @@ impl Escrow { .get(&DataKey::Admin) .unwrap_or_else(|| panic_with_error!(&env, EscrowError::NotInitialized)); admin.require_auth(); + if new_admin == admin { + panic_with_error!(&env, EscrowError::InvalidAdminProposal); + } env.storage() .persistent() .set(&DataKey::PendingAdmin, &new_admin); diff --git a/contracts/escrow/src/test.rs b/contracts/escrow/src/test.rs index a233a9d..71626cc 100644 --- a/contracts/escrow/src/test.rs +++ b/contracts/escrow/src/test.rs @@ -248,3 +248,30 @@ fn test_record_usage_rejects_zero_requests() { let service_id = Symbol::new(&env, "weather_api"); client.record_usage(&agent, &service_id, &0u32); } + +#[test] +#[should_panic(expected = "Error(Contract, #13)")] +fn test_propose_admin_transfer_rejects_self_target() { + let env = Env::default(); + let (client, admin) = setup_initialized(&env); + client.propose_admin_transfer(&admin); +} + +#[test] +fn test_propose_admin_transfer_accepts_distinct_address() { + let env = Env::default(); + let (client, _admin) = setup_initialized(&env); + let next = Address::generate(&env); + client.propose_admin_transfer(&next); + assert_eq!(client.get_pending_admin(), Some(next)); +} + +#[test] +fn test_accept_admin_transfer_clears_pending() { + let env = Env::default(); + let (client, _admin) = setup_initialized(&env); + let next = Address::generate(&env); + client.propose_admin_transfer(&next); + client.accept_admin_transfer(&next); + assert_eq!(client.get_pending_admin(), None); +}