From c94db59c7da2dab94513d74037e78bae6a5a1bc1 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Tue, 1 Sep 2026 23:32:32 +0500 Subject: [PATCH 1/2] feat: add v2 admin rotation --- contracts/tholos-v2/src/lib.rs | 31 + contracts/tholos-v2/src/test.rs | 95 ++- ...st_admin_rotation_updates_authority.1.json | 651 ++++++++++++++++++ docs/src/V2_RESOLUTION.md | 6 + 4 files changed, 782 insertions(+), 1 deletion(-) create mode 100644 contracts/tholos-v2/test_snapshots/test/test_admin_rotation_updates_authority.1.json diff --git a/contracts/tholos-v2/src/lib.rs b/contracts/tholos-v2/src/lib.rs index 008a6c0..585592b 100644 --- a/contracts/tholos-v2/src/lib.rs +++ b/contracts/tholos-v2/src/lib.rs @@ -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] @@ -750,6 +756,31 @@ impl TholosV2 { .ok_or(Error::NotInitialized) } + /// Replaces the deployment admin. Only the current admin may authorize + /// the change. The old admin loses authority as soon as this call + /// succeeds. Fails with `NotInitialized` before `initialize` and emits + /// `AdminUpdated` on success. + pub fn set_admin(env: Env, new_admin: Address) -> Result<(), Error> { + let old_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(Error::NotInitialized)?; + old_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, + new_admin, + } + .publish(&env); + + Ok(()) + } + /// 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 diff --git a/contracts/tholos-v2/src/test.rs b/contracts/tholos-v2/src/test.rs index ade1ffa..d0d71ec 100644 --- a/contracts/tholos-v2/src/test.rs +++ b/contracts/tholos-v2/src/test.rs @@ -2,7 +2,8 @@ use super::*; use soroban_sdk::testutils::storage::Persistent as _; -use soroban_sdk::testutils::{Address as _, Ledger}; +use soroban_sdk::testutils::{Address as _, Ledger, MockAuth, MockAuthInvoke}; +use soroban_sdk::IntoVal; const DEFAULT_BOND: i128 = 100; const DEFAULT_CHALLENGE_WINDOW: u64 = 3600; @@ -2863,6 +2864,98 @@ fn test_set_paused_v2_blocks_new_assertions() { f.client.assert_outcome(&asserter, &true); } +#[test] +fn test_admin_rotation_updates_authority() { + let env = Env::default(); + let token_id = setup(&env); + let contract_id = env.register(TholosV2, ()); + let client = TholosV2Client::new(&env, &contract_id); + let old_admin = Address::generate(&env); + let new_admin = Address::generate(&env); + let arbitrary = Address::generate(&env); + + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "initialize", + args: ( + old_admin.clone(), + token_id.clone(), + DEFAULT_BOND, + DEFAULT_CHALLENGE_WINDOW, + DEFAULT_FINALIZE_REWARD_BPS, + DEFAULT_REGISTRATION_SECS, + DEFAULT_ANTI_SNIPE_EXT_SECS, + DEFAULT_ANTI_SNIPE_HARD_MAX_SECS, + DEFAULT_REVEAL_SECS, + DEFAULT_MAX_POSITION, + DEFAULT_MAX_TOTAL_WEIGHT, + ) + .into_val(&env), + sub_invokes: &[], + }, + }]); + init( + &client, + &old_admin, + &token_id, + DEFAULT_BOND, + DEFAULT_CHALLENGE_WINDOW, + DEFAULT_FINALIZE_REWARD_BPS, + ) + .unwrap() + .unwrap(); + + // An arbitrary address cannot authorize a rotation: set_admin always + // requires the admin currently stored by the contract. + env.mock_auths(&[MockAuth { + address: &arbitrary, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "set_admin", + args: (new_admin.clone(),).into_val(&env), + sub_invokes: &[], + }, + }]); + assert!(client.try_set_admin(&new_admin).is_err()); + + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "set_admin", + args: (new_admin.clone(),).into_val(&env), + sub_invokes: &[], + }, + }]); + client.set_admin(&new_admin); + + // Rotation is immediate: the previous admin can no longer use an + // admin-only entrypoint, while the new admin can. + env.mock_auths(&[MockAuth { + address: &old_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "set_paused_v2", + args: (true,).into_val(&env), + sub_invokes: &[], + }, + }]); + assert!(client.try_set_paused_v2(&true).is_err()); + + env.mock_auths(&[MockAuth { + address: &new_admin, + invoke: &MockAuthInvoke { + contract: &contract_id, + fn_name: "set_paused_v2", + args: (true,).into_val(&env), + sub_invokes: &[], + }, + }]); + client.set_paused_v2(&true); +} + #[test] fn test_set_paused_v2_does_not_block_existing_round() { // The narrower v2 pause only ever gates assert_outcome: an diff --git a/contracts/tholos-v2/test_snapshots/test/test_admin_rotation_updates_authority.1.json b/contracts/tholos-v2/test_snapshots/test/test_admin_rotation_updates_authority.1.json new file mode 100644 index 0000000..5f7c39a --- /dev/null +++ b/contracts/tholos-v2/test_snapshots/test/test_admin_rotation_updates_authority.1.json @@ -0,0 +1,651 @@ +{ + "generators": { + "address": 6, + "nonce": 5, + "mux_id": 0 + }, + "auth": [ + [], + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + { + "function": { + "contract_fn": { + "contract_address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL" + }, + { + "i128": "100" + }, + { + "u64": "3600" + }, + { + "u32": 0 + }, + { + "u64": "3600" + }, + { + "u64": "300" + }, + { + "u64": "3900" + }, + { + "u64": "3600" + }, + { + "i128": "1000000" + }, + { + "i128": "10000000" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "set_paused_v2", + "args": [ + { + "bool": true + } + ] + } + }, + "sub_invocations": [] + } + ] + ] + ], + "ledger": { + "protocol_version": 26, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "balance": "0", + "seq_num": "0", + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + "live_until": null + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": true + } + }, + { + "key": { + "vec": [ + { + "symbol": "Policy" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "anti_snipe_extension_secs" + }, + "val": { + "u64": "300" + } + }, + { + "key": { + "symbol": "anti_snipe_hard_max_secs" + }, + "val": { + "u64": "3900" + } + }, + { + "key": { + "symbol": "base_bond" + }, + "val": { + "i128": "100" + } + }, + { + "key": { + "symbol": "challenge_window_secs" + }, + "val": { + "u64": "3600" + } + }, + { + "key": { + "symbol": "finalize_reward_bps" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "max_position" + }, + "val": { + "i128": "1000000" + } + }, + { + "key": { + "symbol": "max_total_weight" + }, + "val": { + "i128": "10000000" + } + }, + { + "key": { + "symbol": "min_resolution_bond" + }, + "val": { + "i128": "100" + } + }, + { + "key": { + "symbol": "payout_rule" + }, + "val": { + "vec": [ + { + "symbol": "ProRataV1" + } + ] + } + }, + { + "key": { + "symbol": "registration_duration_secs" + }, + "val": { + "u64": "3600" + } + }, + { + "key": { + "symbol": "reveal_duration_secs" + }, + "val": { + "u64": "3600" + } + }, + { + "key": { + "symbol": "timeout_default" + }, + "val": { + "vec": [ + { + "symbol": "AssertedOutcomeStands" + } + ] + } + }, + { + "key": { + "symbol": "token" + }, + "val": { + "address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL" + } + }, + { + "key": { + "symbol": "weight_rule" + }, + "val": { + "vec": [ + { + "symbol": "LinearStakeV1" + } + ] + } + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 518400 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "1" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "3" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": "5" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": null + } + } + } + }, + "ext": "v0" + }, + "live_until": 4095 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000002" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 120960 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 518400 + } + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "pause_updated" + } + ], + "data": { + "map": [ + { + "key": { + "symbol": "paused" + }, + "val": { + "bool": true + } + } + ] + } + } + } + }, + "failed_call": false + } + ] +} \ No newline at end of file diff --git a/docs/src/V2_RESOLUTION.md b/docs/src/V2_RESOLUTION.md index 5a11e35..130e3b6 100644 --- a/docs/src/V2_RESOLUTION.md +++ b/docs/src/V2_RESOLUTION.md @@ -463,6 +463,12 @@ V2 has no global resolver membership and no `update_resolvers` equivalent for v2 assertions. The admin cannot insert a voter, remove one, alter weight, or change a pinned policy. +The admin address itself can be rotated through `set_admin(new_admin)`. The call +requires the currently stored admin's signature, takes effect immediately, and +emits `AdminUpdated` with the old and new addresses. Rotation changes only who may +invoke the existing administrative controls; it cannot alter any assertion's +pinned policy, weight, deadline, or outcome. + An emergency pause must not selectively censor a time-critical step of an already funded assertion. In particular, blocking `dispute` while allowing `finalize`, or blocking `reveal` while its deadline continues, can choose a winner From fec363379d599edc03421d986ebba16e57598132 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 4 Sep 2026 21:40:54 +0500 Subject: [PATCH 2/2] fix(v2): renew instance TTL for admin state changes --- contracts/tholos-v2/src/lib.rs | 19 +- contracts/tholos-v2/src/test.rs | 26 +- ..._changes_renew_instance_storage_ttl.1.json | 548 ++++++++++++++++++ docs/src/DEPLOYMENT.md | 16 + 4 files changed, 602 insertions(+), 7 deletions(-) create mode 100644 contracts/tholos-v2/test_snapshots/test/test_admin_state_changes_renew_instance_storage_ttl.1.json diff --git a/contracts/tholos-v2/src/lib.rs b/contracts/tholos-v2/src/lib.rs index 585592b..5bc5faa 100644 --- a/contracts/tholos-v2/src/lib.rs +++ b/contracts/tholos-v2/src/lib.rs @@ -739,9 +739,7 @@ impl TholosV2 { env.storage().instance().set(&DataKey::Policy, &policy); env.storage().instance().set(&DataKey::NextId, &0u64); env.storage().instance().set(&DataKey::Paused, &false); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + Self::touch_instance_ttl(&env); Ok(()) } @@ -769,9 +767,7 @@ impl TholosV2 { old_admin.require_auth(); env.storage().instance().set(&DataKey::Admin, &new_admin); - env.storage() - .instance() - .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + Self::touch_instance_ttl(&env); AdminUpdated { old_admin, new_admin, @@ -796,6 +792,7 @@ impl TholosV2 { .ok_or(Error::NotInitialized)?; admin.require_auth(); + Self::touch_instance_ttl(&env); env.storage().instance().set(&DataKey::Paused, &paused); PauseUpdated { paused }.publish(&env); @@ -811,6 +808,16 @@ impl TholosV2 { .ok_or(Error::AssertionNotFound) } + /// Renews instance storage after a state-changing call that uses the + /// deployment-wide instance entries. Keeping this in one helper prevents + /// an admin or pause operation from leaving those entries to expire while + /// the contract is still in use. + fn touch_instance_ttl(env: &Env) { + env.storage() + .instance() + .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); + } + /// TTL bump `(threshold, amount)`, in ledgers, sized to cover one /// dispute's full worst-case active-phase horizon (registration through /// reveal, per this specific assertion's own pinned policy) plus diff --git a/contracts/tholos-v2/src/test.rs b/contracts/tholos-v2/src/test.rs index d0d71ec..94ac69e 100644 --- a/contracts/tholos-v2/src/test.rs +++ b/contracts/tholos-v2/src/test.rs @@ -1,7 +1,7 @@ #![cfg(test)] use super::*; -use soroban_sdk::testutils::storage::Persistent as _; +use soroban_sdk::testutils::storage::{Instance as _, Persistent as _}; use soroban_sdk::testutils::{Address as _, Ledger, MockAuth, MockAuthInvoke}; use soroban_sdk::IntoVal; @@ -2864,6 +2864,30 @@ fn test_set_paused_v2_blocks_new_assertions() { f.client.assert_outcome(&asserter, &true); } +#[test] +fn test_admin_state_changes_renew_instance_storage_ttl() { + let f = Fixture::new(); + + 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_paused_v2(&true); + assert_eq!(instance_ttl(), INSTANCE_BUMP_AMOUNT); + + f.env + .ledger() + .with_mut(|l| l.sequence_number += INSTANCE_BUMP_AMOUNT - 10); + f.client.set_admin(&f.generate()); + assert_eq!(instance_ttl(), INSTANCE_BUMP_AMOUNT); +} + #[test] fn test_admin_rotation_updates_authority() { let env = Env::default(); diff --git a/contracts/tholos-v2/test_snapshots/test/test_admin_state_changes_renew_instance_storage_ttl.1.json b/contracts/tholos-v2/test_snapshots/test/test_admin_state_changes_renew_instance_storage_ttl.1.json new file mode 100644 index 0000000..a2bd687 --- /dev/null +++ b/contracts/tholos-v2/test_snapshots/test/test_admin_state_changes_renew_instance_storage_ttl.1.json @@ -0,0 +1,548 @@ +{ + "generators": { + "address": 5, + "nonce": 0, + "mux_id": 0 + }, + "auth": [ + [], + [ + [ + "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + { + "function": { + "contract_fn": { + "contract_address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "initialize", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL" + }, + { + "i128": "100" + }, + { + "u64": "3600" + }, + { + "u32": 0 + }, + { + "u64": "3600" + }, + { + "u64": "300" + }, + { + "u64": "3900" + }, + { + "u64": "3600" + }, + { + "i128": "1000000" + }, + { + "i128": "10000000" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "set_paused_v2", + "args": [ + { + "bool": true + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "function_name": "set_admin", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 26, + "sequence_number": 1036780, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "account": { + "account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "balance": "0", + "seq_num": "0", + "num_sub_entries": 0, + "inflation_dest": null, + "flags": 0, + "home_domain": "", + "thresholds": "01010101", + "signers": [], + "ext": "v0" + } + }, + "ext": "v0" + }, + "live_until": null + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF", + "key": { + "ledger_key_nonce": { + "nonce": "801925984706572462" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "NextId" + } + ] + }, + "val": { + "u64": "0" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": true + } + }, + { + "key": { + "vec": [ + { + "symbol": "Policy" + } + ] + }, + "val": { + "map": [ + { + "key": { + "symbol": "anti_snipe_extension_secs" + }, + "val": { + "u64": "300" + } + }, + { + "key": { + "symbol": "anti_snipe_hard_max_secs" + }, + "val": { + "u64": "3900" + } + }, + { + "key": { + "symbol": "base_bond" + }, + "val": { + "i128": "100" + } + }, + { + "key": { + "symbol": "challenge_window_secs" + }, + "val": { + "u64": "3600" + } + }, + { + "key": { + "symbol": "finalize_reward_bps" + }, + "val": { + "u32": 0 + } + }, + { + "key": { + "symbol": "max_position" + }, + "val": { + "i128": "1000000" + } + }, + { + "key": { + "symbol": "max_total_weight" + }, + "val": { + "i128": "10000000" + } + }, + { + "key": { + "symbol": "min_resolution_bond" + }, + "val": { + "i128": "100" + } + }, + { + "key": { + "symbol": "payout_rule" + }, + "val": { + "vec": [ + { + "symbol": "ProRataV1" + } + ] + } + }, + { + "key": { + "symbol": "registration_duration_secs" + }, + "val": { + "u64": "3600" + } + }, + { + "key": { + "symbol": "reveal_duration_secs" + }, + "val": { + "u64": "3600" + } + }, + { + "key": { + "symbol": "timeout_default" + }, + "val": { + "vec": [ + { + "symbol": "AssertedOutcomeStands" + } + ] + } + }, + { + "key": { + "symbol": "token" + }, + "val": { + "address": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL" + } + }, + { + "key": { + "symbol": "weight_rule" + }, + "val": { + "vec": [ + { + "symbol": "LinearStakeV1" + } + ] + } + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 1555180 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "1033654523790656264" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6830389 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "4837995959683129791" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 7348779 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4", + "key": { + "ledger_key_nonce": { + "nonce": "5541220902715666415" + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + "live_until": 6311999 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CBEPDNVYXQGWB5YUBXKJWYJA7OXTZW5LFLNO5JRRGE6Z6C5OSUZPCCEL", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": "stellar_asset", + "storage": [ + { + "key": { + "symbol": "METADATA" + }, + "val": { + "map": [ + { + "key": { + "symbol": "decimal" + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "symbol": "name" + }, + "val": { + "string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGWF" + } + }, + { + "key": { + "symbol": "symbol" + }, + "val": { + "string": "aaa" + } + } + ] + } + }, + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM" + } + }, + { + "key": { + "vec": [ + { + "symbol": "AssetInfo" + } + ] + }, + "val": { + "vec": [ + { + "symbol": "AlphaNum4" + }, + { + "map": [ + { + "key": { + "symbol": "asset_code" + }, + "val": { + "string": "aaa\\0" + } + }, + { + "key": { + "symbol": "issuer" + }, + "val": { + "bytes": "0000000000000000000000000000000000000000000000000000000000000002" + } + } + ] + } + ] + } + } + ] + } + } + } + }, + "ext": "v0" + }, + "live_until": 120960 + }, + { + "entry": { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + "live_until": 1555180 + } + ] + }, + "events": [] +} \ No newline at end of file diff --git a/docs/src/DEPLOYMENT.md b/docs/src/DEPLOYMENT.md index 5c7fdd3..440fc83 100644 --- a/docs/src/DEPLOYMENT.md +++ b/docs/src/DEPLOYMENT.md @@ -66,6 +66,22 @@ handing the contract id to anyone. ## Admin runbook +### Rotating the admin key + +`set_admin` is authorized by the current admin and takes effect immediately. +The old signer loses authority as soon as the transaction succeeds, so verify +the new address before signing and keep the new signer available: + +```sh +# Current admin performs the rotation. +stellar contract invoke --id "$CONTRACT" --source admin --network testnet -- set_admin \ + --new_admin "$NEW_ADMIN" +``` + +The successful call emits `AdminUpdated` with both addresses. Afterward, use +the new signer for `set_paused_v2`, `update_resolvers`, and future admin +rotations. + ### Pausing during an incident If something looks wrong (a bug is found, a resolver key looks compromised, vote