Skip to content

Commit 9d116d6

Browse files
committed
Merge main and resolve payout escrow conflict
2 parents 4210a56 + 7d73e6e commit 9d116d6

11 files changed

Lines changed: 2249 additions & 13 deletions

File tree

contracts/tholos-v2/src/lib.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ pub struct PauseUpdated {
211211
pub paused: bool,
212212
}
213213

214+
#[contractevent]
215+
pub struct AdminUpdated {
216+
pub old_admin: Address,
217+
pub new_admin: Address,
218+
}
219+
214220
#[contractevent]
215221
pub struct RoundCancelled {
216222
#[topic]
@@ -733,9 +739,7 @@ impl TholosV2 {
733739
env.storage().instance().set(&DataKey::Policy, &policy);
734740
env.storage().instance().set(&DataKey::NextId, &0u64);
735741
env.storage().instance().set(&DataKey::Paused, &false);
736-
env.storage()
737-
.instance()
738-
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);
742+
Self::touch_instance_ttl(&env);
739743

740744
Ok(())
741745
}
@@ -750,6 +754,29 @@ impl TholosV2 {
750754
.ok_or(Error::NotInitialized)
751755
}
752756

757+
/// Replaces the deployment admin. Only the current admin may authorize
758+
/// the change. The old admin loses authority as soon as this call
759+
/// succeeds. Fails with `NotInitialized` before `initialize` and emits
760+
/// `AdminUpdated` on success.
761+
pub fn set_admin(env: Env, new_admin: Address) -> Result<(), Error> {
762+
let old_admin: Address = env
763+
.storage()
764+
.instance()
765+
.get(&DataKey::Admin)
766+
.ok_or(Error::NotInitialized)?;
767+
old_admin.require_auth();
768+
769+
env.storage().instance().set(&DataKey::Admin, &new_admin);
770+
Self::touch_instance_ttl(&env);
771+
AdminUpdated {
772+
old_admin,
773+
new_admin,
774+
}
775+
.publish(&env);
776+
777+
Ok(())
778+
}
779+
753780
/// Blocks or unblocks new `assert_outcome` calls. Only callable by the
754781
/// admin set at `initialize`. Does not affect any already-active
755782
/// round: registration, reveal, `resolve_outcome`, `settle`, and
@@ -765,6 +792,7 @@ impl TholosV2 {
765792
.ok_or(Error::NotInitialized)?;
766793
admin.require_auth();
767794

795+
Self::touch_instance_ttl(&env);
768796
env.storage().instance().set(&DataKey::Paused, &paused);
769797
PauseUpdated { paused }.publish(&env);
770798

@@ -780,6 +808,16 @@ impl TholosV2 {
780808
.ok_or(Error::AssertionNotFound)
781809
}
782810

811+
/// Renews instance storage after a state-changing call that uses the
812+
/// deployment-wide instance entries. Keeping this in one helper prevents
813+
/// an admin or pause operation from leaving those entries to expire while
814+
/// the contract is still in use.
815+
fn touch_instance_ttl(env: &Env) {
816+
env.storage()
817+
.instance()
818+
.extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);
819+
}
820+
783821
/// TTL bump `(threshold, amount)`, in ledgers, sized to cover one
784822
/// dispute's full worst-case active-phase horizon (registration through
785823
/// reveal, per this specific assertion's own pinned policy) plus

contracts/tholos-v2/src/test.rs

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#![cfg(test)]
22

33
use super::*;
4-
use soroban_sdk::testutils::storage::Persistent as _;
5-
use soroban_sdk::testutils::{Address as _, Ledger};
4+
use soroban_sdk::testutils::storage::{Instance as _, Persistent as _};
5+
use soroban_sdk::testutils::{Address as _, Ledger, MockAuth, MockAuthInvoke};
6+
use soroban_sdk::IntoVal;
67

78
const DEFAULT_BOND: i128 = 100;
89
const DEFAULT_CHALLENGE_WINDOW: u64 = 3600;
@@ -2863,6 +2864,122 @@ fn test_set_paused_v2_blocks_new_assertions() {
28632864
f.client.assert_outcome(&asserter, &true);
28642865
}
28652866

2867+
#[test]
2868+
fn test_admin_state_changes_renew_instance_storage_ttl() {
2869+
let f = Fixture::new();
2870+
2871+
let instance_ttl = || {
2872+
f.env
2873+
.as_contract(&f.client.address, || f.env.storage().instance().get_ttl())
2874+
};
2875+
2876+
assert_eq!(instance_ttl(), INSTANCE_BUMP_AMOUNT);
2877+
2878+
f.env
2879+
.ledger()
2880+
.with_mut(|l| l.sequence_number += INSTANCE_BUMP_AMOUNT - 10);
2881+
f.client.set_paused_v2(&true);
2882+
assert_eq!(instance_ttl(), INSTANCE_BUMP_AMOUNT);
2883+
2884+
f.env
2885+
.ledger()
2886+
.with_mut(|l| l.sequence_number += INSTANCE_BUMP_AMOUNT - 10);
2887+
f.client.set_admin(&f.generate());
2888+
assert_eq!(instance_ttl(), INSTANCE_BUMP_AMOUNT);
2889+
}
2890+
2891+
#[test]
2892+
fn test_admin_rotation_updates_authority() {
2893+
let env = Env::default();
2894+
let token_id = setup(&env);
2895+
let contract_id = env.register(TholosV2, ());
2896+
let client = TholosV2Client::new(&env, &contract_id);
2897+
let old_admin = Address::generate(&env);
2898+
let new_admin = Address::generate(&env);
2899+
let arbitrary = Address::generate(&env);
2900+
2901+
env.mock_auths(&[MockAuth {
2902+
address: &old_admin,
2903+
invoke: &MockAuthInvoke {
2904+
contract: &contract_id,
2905+
fn_name: "initialize",
2906+
args: (
2907+
old_admin.clone(),
2908+
token_id.clone(),
2909+
DEFAULT_BOND,
2910+
DEFAULT_CHALLENGE_WINDOW,
2911+
DEFAULT_FINALIZE_REWARD_BPS,
2912+
DEFAULT_REGISTRATION_SECS,
2913+
DEFAULT_ANTI_SNIPE_EXT_SECS,
2914+
DEFAULT_ANTI_SNIPE_HARD_MAX_SECS,
2915+
DEFAULT_REVEAL_SECS,
2916+
DEFAULT_MAX_POSITION,
2917+
DEFAULT_MAX_TOTAL_WEIGHT,
2918+
)
2919+
.into_val(&env),
2920+
sub_invokes: &[],
2921+
},
2922+
}]);
2923+
init(
2924+
&client,
2925+
&old_admin,
2926+
&token_id,
2927+
DEFAULT_BOND,
2928+
DEFAULT_CHALLENGE_WINDOW,
2929+
DEFAULT_FINALIZE_REWARD_BPS,
2930+
)
2931+
.unwrap()
2932+
.unwrap();
2933+
2934+
// An arbitrary address cannot authorize a rotation: set_admin always
2935+
// requires the admin currently stored by the contract.
2936+
env.mock_auths(&[MockAuth {
2937+
address: &arbitrary,
2938+
invoke: &MockAuthInvoke {
2939+
contract: &contract_id,
2940+
fn_name: "set_admin",
2941+
args: (new_admin.clone(),).into_val(&env),
2942+
sub_invokes: &[],
2943+
},
2944+
}]);
2945+
assert!(client.try_set_admin(&new_admin).is_err());
2946+
2947+
env.mock_auths(&[MockAuth {
2948+
address: &old_admin,
2949+
invoke: &MockAuthInvoke {
2950+
contract: &contract_id,
2951+
fn_name: "set_admin",
2952+
args: (new_admin.clone(),).into_val(&env),
2953+
sub_invokes: &[],
2954+
},
2955+
}]);
2956+
client.set_admin(&new_admin);
2957+
2958+
// Rotation is immediate: the previous admin can no longer use an
2959+
// admin-only entrypoint, while the new admin can.
2960+
env.mock_auths(&[MockAuth {
2961+
address: &old_admin,
2962+
invoke: &MockAuthInvoke {
2963+
contract: &contract_id,
2964+
fn_name: "set_paused_v2",
2965+
args: (true,).into_val(&env),
2966+
sub_invokes: &[],
2967+
},
2968+
}]);
2969+
assert!(client.try_set_paused_v2(&true).is_err());
2970+
2971+
env.mock_auths(&[MockAuth {
2972+
address: &new_admin,
2973+
invoke: &MockAuthInvoke {
2974+
contract: &contract_id,
2975+
fn_name: "set_paused_v2",
2976+
args: (true,).into_val(&env),
2977+
sub_invokes: &[],
2978+
},
2979+
}]);
2980+
client.set_paused_v2(&true);
2981+
}
2982+
28662983
#[test]
28672984
fn test_set_paused_v2_does_not_block_existing_round() {
28682985
// The narrower v2 pause only ever gates assert_outcome: an

0 commit comments

Comments
 (0)