Skip to content
Merged
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
87 changes: 80 additions & 7 deletions contracts/predictify-hybrid/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1657,30 +1657,103 @@ impl AdminManager {
// ===== MULTISIG MANAGER =====

/// Manages multisig/threshold approval for sensitive admin operations
pub const THRESHOLD_ROTATION_DELAY: u64 = 86400; // 24 hours

/// Pending threshold update for two-phase commit
#[derive(Clone, Debug)]
#[contracttype]
pub struct PendingThresholdUpdate {
pub new_threshold: u32,
pub proposed_at: u64,
pub proposed_by: Address,
}

pub struct MultisigManager;

impl MultisigManager {
/// Set the multisig threshold (M-of-N)
pub fn set_threshold(env: &Env, admin: &Address, threshold: u32) -> Result<(), Error> {
/// Propose a new multisig threshold (M-of-N)
pub fn propose_threshold(env: &Env, admin: &Address, new_threshold: u32) -> Result<(), Error> {
AdminAccessControl::validate_permission(env, admin, &AdminPermission::Emergency)?;

let total_admins = Self::count_active_admins(env);
if threshold == 0 || threshold > total_admins {
if new_threshold == 0 || new_threshold > total_admins {
return Err(Error::InvalidInput);
}

let config = MultisigConfig {
threshold,
total_admins,
enabled: threshold > 1,
let pending = PendingThresholdUpdate {
new_threshold,
proposed_at: env.ledger().timestamp(),
proposed_by: admin.clone(),
};

env.storage()
.persistent()
.set(&Symbol::new(env, "PendingThreshold"), &pending);

let config = Self::get_config(env);
EventEmitter::emit_threshold_proposed(
env,
admin,
config.threshold,
new_threshold,
env.ledger().timestamp() + THRESHOLD_ROTATION_DELAY,
);

Ok(())
}

/// Confirm a pending threshold update after the delay
pub fn confirm_threshold(env: &Env) -> Result<(), Error> {
let pending: PendingThresholdUpdate = env
.storage()
.persistent()
.get(&Symbol::new(env, "PendingThreshold"))
.ok_or(Error::InvalidState)?;

if env.ledger().timestamp() < pending.proposed_at + THRESHOLD_ROTATION_DELAY {
return Err(Error::InvalidState);
}

let total_admins = Self::count_active_admins(env);
if pending.new_threshold == 0 || pending.new_threshold > total_admins {
env.storage().persistent().remove(&Symbol::new(env, "PendingThreshold"));
return Err(Error::InvalidInput);
}

let mut config = Self::get_config(env);
let old_threshold = config.threshold;
config.threshold = pending.new_threshold;
config.enabled = pending.new_threshold > 1;
config.total_admins = total_admins;

env.storage()
.persistent()
.set(&Symbol::new(env, "MultisigConfig"), &config);

env.storage().persistent().remove(&Symbol::new(env, "PendingThreshold"));

EventEmitter::emit_threshold_confirmed(
env,
&pending.proposed_by,
old_threshold,
pending.new_threshold,
);

Ok(())
}

/// Cancel a pending threshold proposal
pub fn cancel_threshold_proposal(env: &Env, admin: &Address) -> Result<(), Error> {
AdminAccessControl::validate_permission(env, admin, &AdminPermission::Emergency)?;

if env.storage().persistent().has(&Symbol::new(env, "PendingThreshold")) {
env.storage().persistent().remove(&Symbol::new(env, "PendingThreshold"));
Ok(())
} else {
Err(Error::InvalidState)
}
}

/// Get current multisig configuration
pub fn get_config(env: &Env) -> MultisigConfig {
env.storage()
Expand Down
61 changes: 61 additions & 0 deletions contracts/predictify-hybrid/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,25 @@ pub struct FeeWithdrawnEvent {
pub timestamp: u64,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultisigThresholdProposedEvent {
pub admin: Address,
pub old_threshold: u32,
pub new_threshold: u32,
pub confirm_after: u64,
pub timestamp: u64,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MultisigThresholdConfirmedEvent {
pub admin: Address,
pub old_threshold: u32,
pub new_threshold: u32,
pub timestamp: u64,
}

// ===== ORACLE RESULT VERIFICATION EVENTS =====

/// Event emitted when oracle result verification is initiated for a market.
Expand Down Expand Up @@ -4282,6 +4301,48 @@ pub fn emit_manual_resolution_required(env: &Env, market_id: &Symbol, reason: &S
}

impl EventEmitter {
pub fn emit_threshold_proposed(
env: &Env,
admin: &Address,
old_threshold: u32,
new_threshold: u32,
confirm_after: u64,
) {
let event = MultisigThresholdProposedEvent {
admin: admin.clone(),
old_threshold,
new_threshold,
confirm_after,
timestamp: env.ledger().timestamp(),
};

Self::store_event(env, &symbol_short!("thld_prop"), &event);
env.events().publish(
(symbol_short!("thld_prop"), admin.clone()),
event,
);
}

pub fn emit_threshold_confirmed(
env: &Env,
admin: &Address,
old_threshold: u32,
new_threshold: u32,
) {
let event = MultisigThresholdConfirmedEvent {
admin: admin.clone(),
old_threshold,
new_threshold,
timestamp: env.ledger().timestamp(),
};

Self::store_event(env, &symbol_short!("thld_conf"), &event);
env.events().publish(
(symbol_short!("thld_conf"), admin.clone()),
event,
);
}

/// Emit oracle callback event for oracle data updates
pub fn emit_oracle_callback(
env: &Env,
Expand Down
Loading
Loading