Skip to content
Closed
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
2 changes: 2 additions & 0 deletions contracts/registry/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ pub enum RegistryError {
/// Metadata is empty, exceeds the byte bound, or contains invalid
/// (non-visible-ASCII / control / whitespace-delimited) encodings (code 10).
InvalidMetadata = 10,
/// No pending admin nomination exists to accept or cancel (code 11).
NoPendingAdmin = 11,
}
15 changes: 15 additions & 0 deletions contracts/registry/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ pub fn event_version_v1(env: &Env) -> Symbol {
Symbol::new(env, "callora.v1")
}

/// Returns the Symbol for the `"admin_nominated"` event topic.
pub fn event_admin_nominated(env: &Env) -> Symbol {
Symbol::new(env, "admin_nominated")
}

/// Returns the Symbol for the `"admin_accepted"` event topic.
pub fn event_admin_accepted(env: &Env) -> Symbol {
Symbol::new(env, "admin_accepted")
}

/// Returns the Symbol for the `"admin_cancelled"` event topic.
pub fn event_admin_cancelled(env: &Env) -> Symbol {
Symbol::new(env, "admin_cancelled")
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
69 changes: 69 additions & 0 deletions contracts/registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub const INSTANCE_TTL_EXTENSION: u32 = INSTANCE_TTL_THRESHOLD;
#[derive(Clone, Debug, PartialEq)]
pub enum StorageKey {
Admin,
PendingAdmin,
Catalog,
RegisteredCount,
Offering(String),
Expand Down Expand Up @@ -296,4 +297,72 @@ impl CalloraRegistry {
Self::extend_offering_ttl(&env, &key);
Ok(record)
}

/// Return the current admin address.
pub fn get_admin(env: Env) -> Result<Address, RegistryError> {
Self::extend_instance_ttl(&env);
Self::admin(&env)
}

/// Return the pending nominated admin address, if any.
pub fn get_pending_admin(env: Env) -> Option<Address> {
Self::extend_instance_ttl(&env);
env.storage().instance().get(&StorageKey::PendingAdmin)
}

/// Nominate a new admin for two-step admin rotation.
pub fn set_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), RegistryError> {
Self::extend_instance_ttl(&env);
caller.require_auth();
let admin = Self::admin(&env)?;
if caller != admin {
return Err(RegistryError::Unauthorized);
}
env.storage()
.instance()
.set(&StorageKey::PendingAdmin, &new_admin);
env.events()
.publish((events::event_admin_nominated(&env), caller), new_admin);
Ok(())
}

/// Accept a pending admin nomination. Must be authorized by the nominee.
pub fn accept_admin(env: Env, caller: Address) -> Result<(), RegistryError> {
Self::extend_instance_ttl(&env);
caller.require_auth();
let old_admin = Self::admin(&env)?;
let pending: Address = env
.storage()
.instance()
.get(&StorageKey::PendingAdmin)
.ok_or(RegistryError::NoPendingAdmin)?;
if caller != pending {
return Err(RegistryError::Unauthorized);
}
let inst = env.storage().instance();
inst.set(&StorageKey::Admin, &pending);
inst.remove(&StorageKey::PendingAdmin);
env.events()
.publish((events::event_admin_accepted(&env), old_admin), pending);
Ok(())
}

/// Cancel an active admin nomination. Must be authorized by the current admin.
pub fn cancel_admin_transfer(env: Env, caller: Address) -> Result<(), RegistryError> {
Self::extend_instance_ttl(&env);
caller.require_auth();
let admin = Self::admin(&env)?;
if caller != admin {
return Err(RegistryError::Unauthorized);
}
let cancelled: Address = env
.storage()
.instance()
.get(&StorageKey::PendingAdmin)
.ok_or(RegistryError::NoPendingAdmin)?;
env.storage().instance().remove(&StorageKey::PendingAdmin);
env.events()
.publish((events::event_admin_cancelled(&env), caller), cancelled);
Ok(())
}
}
60 changes: 59 additions & 1 deletion contracts/registry/tests/auth_snap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

extern crate std;

use callora_registry::{CalloraRegistry, CalloraRegistryClient};
use callora_registry::{CalloraRegistry, CalloraRegistryClient, RegistryError};
use soroban_sdk::testutils::Address as _;
use soroban_sdk::{Address, Env, String};

Expand Down Expand Up @@ -87,3 +87,61 @@ fn registered_count_does_not_require_auth() {
let count = client.registered_count();
assert_eq!(count, 0);
}

#[test]
fn set_accept_and_cancel_admin_require_auth() {
let env = Env::default();
let (admin, _catalog, client) = setup(&env);
let nominee = Address::generate(&env);

env.set_auths(&[]);
assert!(client.try_set_admin(&admin, &nominee).is_err());

env.mock_all_auths();
client.set_admin(&admin, &nominee);

env.set_auths(&[]);
assert!(client.try_accept_admin(&nominee).is_err());
assert!(client.try_cancel_admin_transfer(&admin).is_err());
}

#[test]
fn nominate_accept_and_cancel_admin_transfer_lifecycle() {
let env = Env::default();
let (admin, _catalog, client) = setup(&env);
let nominee = Address::generate(&env);
let outsider = Address::generate(&env);

client.set_admin(&admin, &nominee);
assert_eq!(client.get_pending_admin(), Some(nominee.clone()));

// Outsider cannot accept or cancel
assert_eq!(
client.try_accept_admin(&outsider),
Err(Ok(RegistryError::Unauthorized))
);
assert_eq!(
client.try_cancel_admin_transfer(&outsider),
Err(Ok(RegistryError::Unauthorized))
);

// Current admin can cancel
client.cancel_admin_transfer(&admin);
assert_eq!(client.get_pending_admin(), None);
assert_eq!(
client.try_accept_admin(&nominee),
Err(Ok(RegistryError::NoPendingAdmin))
);

// Re-nominate and accept as nominee
client.set_admin(&admin, &nominee);
client.accept_admin(&nominee);
assert_eq!(client.get_admin(), nominee);
assert_eq!(client.get_pending_admin(), None);

// Old admin is no longer authorized
assert_eq!(
client.try_set_admin(&admin, &outsider),
Err(Ok(RegistryError::Unauthorized))
);
}