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
42 changes: 36 additions & 6 deletions contracts/tholos-v2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ pub struct PauseUpdated {
pub paused: bool,
}

#[contractevent]
pub struct AdminUpdated {
pub old_admin: Address,
pub new_admin: Address,
}

#[contractevent]
pub struct RoundCancelled {
#[topic]
Expand Down Expand Up @@ -751,12 +757,12 @@ impl TholosV2 {
}

/// Blocks or unblocks new `assert_outcome` calls. Only callable by the
/// admin set at `initialize`. Does not affect any already-active
/// round: registration, reveal, `resolve_outcome`, `settle`, and
/// `withdraw` all continue normally while paused, since blocking them
/// would strand capital already locked into a round rather than
/// protect it. `cancel_round` is the mechanism for protecting an
/// already-active round instead. Emits `PauseUpdated`.
/// current admin set at `initialize` or by the most recent `set_admin`
/// call. Does not affect any already-active round: registration, reveal,
/// `resolve_outcome`, `settle`, and `withdraw` all continue normally while
/// paused, since blocking them would strand capital already locked into a
/// round rather than protect it. `cancel_round` is the mechanism for
/// protecting an already-active round instead. Emits `PauseUpdated`.
pub fn set_paused_v2(env: Env, paused: bool) -> Result<(), Error> {
let admin: Address = env
.storage()
Expand All @@ -771,6 +777,30 @@ impl TholosV2 {
Ok(())
}

/// Rotates the contract admin. Only callable by the current admin set at
/// `initialize` or by the most recent `set_admin` call. Callable even
/// while paused. Emits `AdminUpdated`.
pub fn set_admin(env: Env, new_admin: Address) -> Result<(), Error> {
let admin: Address = env
.storage()
.instance()
.get(&DataKey::Admin)
.ok_or(Error::NotInitialized)?;
admin.require_auth();

env.storage().instance().set(&DataKey::Admin, &new_admin);
env.storage()
.instance()
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);
AdminUpdated {
old_admin: admin,
new_admin,
}
.publish(&env);

Ok(())
}

/// Read-only lookup of one assertion. Fails with `AssertionNotFound` if
/// the id doesn't exist.
pub fn get_assertion(env: Env, id: u64) -> Result<AssertionV2, Error> {
Expand Down
132 changes: 130 additions & 2 deletions contracts/tholos-v2/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![cfg(test)]

use super::*;
use soroban_sdk::testutils::storage::Persistent as _;
use soroban_sdk::testutils::{Address as _, Ledger};
use soroban_sdk::testutils::storage::{Instance as _, Persistent as _};
use soroban_sdk::testutils::{Address as _, Events as _, Ledger};

const DEFAULT_BOND: i128 = 100;
const DEFAULT_CHALLENGE_WINDOW: u64 = 3600;
Expand Down Expand Up @@ -3399,3 +3399,131 @@ fn test_initialize_accepts_anti_snipe_hard_max_at_max() {
);
assert_eq!(result, Ok(Ok(())));
}

#[test]
fn test_admin_can_rotate_admin() {
let f = Fixture::new();
let admin2 = f.generate();

f.client.set_admin(&admin2);

let auths = f.env.auths();
let admin1_authed = auths.iter().any(|(addr, _)| *addr == f.admin);
assert!(
admin1_authed,
"old admin require_auth was not invoked during set_admin"
);

let stored_admin: Address = f.env.as_contract(&f.client.address, || {
f.env.storage().instance().get(&DataKey::Admin).unwrap()
});
assert_eq!(stored_admin, admin2);
}

#[test]
fn test_old_admin_loses_authority_after_rotation() {
let f = Fixture::new();
let admin2 = f.generate();
let admin3 = f.generate();

f.client.set_admin(&admin2);

let auths = f.env.auths();
let admin1_authed = auths.iter().any(|(addr, _)| *addr == f.admin);
assert!(
admin1_authed,
"old admin require_auth was not invoked during set_admin"
);

// Now admin2 can pause and rotate
f.client.set_paused_v2(&true);

let auths = f.env.auths();
let admin2_authed = auths.iter().any(|(addr, _)| *addr == admin2);
assert!(
admin2_authed,
"new admin require_auth was not invoked during set_paused_v2"
);

let is_paused: bool = f.env.as_contract(&f.client.address, || {
f.env.storage().instance().get(&DataKey::Paused).unwrap()
});
assert!(is_paused);

f.client.set_admin(&admin3);
let stored_admin: Address = f.env.as_contract(&f.client.address, || {
f.env.storage().instance().get(&DataKey::Admin).unwrap()
});
assert_eq!(stored_admin, admin3);
}

#[test]
fn test_set_admin_emits_event() {
let f = Fixture::new();
let admin2 = f.generate();

f.client.set_admin(&admin2);

let filtered = f.env.events().all().filter_by_contract(&f.client.address);
let raw_events = filtered.events();
assert_eq!(raw_events.len(), 1);
assert_eq!(
raw_events[0].type_,
soroban_sdk::xdr::ContractEventType::Contract
);
let soroban_sdk::xdr::ContractEventBody::V0(v0) = &raw_events[0].body;
assert_eq!(v0.topics.len(), 1);
if let soroban_sdk::xdr::ScVal::Symbol(sym) = &v0.topics[0] {
assert_eq!(sym.to_utf8_string().unwrap(), "admin_updated");
} else {
panic!("expected symbol topic");
}
}

#[test]
fn test_set_admin_is_pause_exempt() {
let f = Fixture::new();
let admin2 = f.generate();

f.client.set_paused_v2(&true);
f.client.set_admin(&admin2);

let stored_admin: Address = f.env.as_contract(&f.client.address, || {
f.env.storage().instance().get(&DataKey::Admin).unwrap()
});
assert_eq!(stored_admin, admin2);
}

#[test]
fn test_cannot_set_admin_before_initialization() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register(TholosV2, ());
let client = TholosV2Client::new(&env, &contract_id);

let new_admin = Address::generate(&env);
assert_eq!(
client.try_set_admin(&new_admin),
Err(Ok(Error::NotInitialized))
);
}

#[test]
fn test_set_admin_extends_instance_ttl() {
let f = Fixture::new();
let admin2 = f.generate();

let instance_ttl = || {
f.env
.as_contract(&f.client.address, || f.env.storage().instance().get_ttl())
};

assert_eq!(instance_ttl(), INSTANCE_BUMP_AMOUNT);

f.env
.ledger()
.with_mut(|l| l.sequence_number += INSTANCE_BUMP_AMOUNT - 10);
f.client.set_admin(&admin2);
assert_eq!(instance_ttl(), INSTANCE_BUMP_AMOUNT);
}
Loading