Skip to content

Commit 19be8c4

Browse files
Merge pull request Predictify-org#365 from Jagadeeshftw/resolution
feat: implement contract pause and admin transfer functionality
2 parents bab7cbc + d40cdad commit 19be8c4

13 files changed

Lines changed: 388 additions & 13 deletions

contracts/predictify-hybrid/src/admin.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,93 @@ impl AdminAccessControl {
564564

565565
Ok(())
566566
}
567+
}
568+
569+
// ===== CONTRACT PAUSE AND ADMIN TRANSFER =====
570+
571+
const CONTRACT_PAUSED_KEY: &str = "ContractPaused";
572+
573+
/// Contract-level pause and primary admin transfer.
574+
pub struct ContractPauseManager;
575+
576+
impl ContractPauseManager {
577+
/// Returns true if the contract is currently paused.
578+
pub fn is_contract_paused(env: &Env) -> bool {
579+
env.storage()
580+
.persistent()
581+
.get(&Symbol::new(env, CONTRACT_PAUSED_KEY))
582+
.unwrap_or(false)
583+
}
567584

585+
/// Pause contract operations. Caller must be the current primary admin.
586+
pub fn pause(env: &Env, admin: &Address) -> Result<(), Error> {
587+
admin.require_auth();
588+
let stored: Address = env
589+
.storage()
590+
.persistent()
591+
.get(&Symbol::new(env, "Admin"))
592+
.ok_or(Error::AdminNotSet)?;
593+
if admin != &stored {
594+
return Err(Error::Unauthorized);
595+
}
596+
env.storage()
597+
.persistent()
598+
.set(&Symbol::new(env, CONTRACT_PAUSED_KEY), &true);
599+
EventEmitter::emit_contract_paused(env, admin);
600+
Ok(())
601+
}
602+
603+
/// Unpause contract operations. Caller must be the current primary admin.
604+
pub fn unpause(env: &Env, admin: &Address) -> Result<(), Error> {
605+
admin.require_auth();
606+
let stored: Address = env
607+
.storage()
608+
.persistent()
609+
.get(&Symbol::new(env, "Admin"))
610+
.ok_or(Error::AdminNotSet)?;
611+
if admin != &stored {
612+
return Err(Error::Unauthorized);
613+
}
614+
env.storage()
615+
.persistent()
616+
.set(&Symbol::new(env, CONTRACT_PAUSED_KEY), &false);
617+
EventEmitter::emit_contract_unpaused(env, admin);
618+
Ok(())
619+
}
620+
621+
/// Require that the contract is not paused; return Error::InvalidState otherwise.
622+
pub fn require_not_paused(env: &Env) -> Result<(), Error> {
623+
if Self::is_contract_paused(env) {
624+
return Err(Error::InvalidState);
625+
}
626+
Ok(())
627+
}
628+
629+
/// Transfer the primary admin role to a new address. Caller must be the current primary admin.
630+
/// New admin must not be the zero/invalid address.
631+
pub fn transfer_admin(env: &Env, current_admin: &Address, new_admin: &Address) -> Result<(), Error> {
632+
current_admin.require_auth();
633+
let stored: Address = env
634+
.storage()
635+
.persistent()
636+
.get(&Symbol::new(env, "Admin"))
637+
.ok_or(Error::AdminNotSet)?;
638+
if current_admin != &stored {
639+
return Err(Error::Unauthorized);
640+
}
641+
if new_admin == current_admin {
642+
return Err(Error::InvalidInput);
643+
}
644+
AdminValidator::validate_admin_address(env, new_admin)?;
645+
env.storage()
646+
.persistent()
647+
.set(&Symbol::new(env, "Admin"), new_admin);
648+
EventEmitter::emit_admin_transferred(env, current_admin, new_admin);
649+
Ok(())
650+
}
651+
}
652+
653+
impl AdminAccessControl {
568654
/// Validates admin authentication and permissions for a specific action.
569655
///
570656
/// This comprehensive validation function combines authentication and

contracts/predictify-hybrid/src/bet_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ impl BetTestSetup {
116116
&None,
117117
&86400u64,
118118
&None,
119+
&None,
120+
&None,
119121
)
120122
}
121123

contracts/predictify-hybrid/src/bets.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,16 @@ impl BetValidator {
798798
return Err(Error::MarketClosed);
799799
}
800800

801+
// Bet deadline: no bets after deadline (0 = use end_time)
802+
let deadline = if market.bet_deadline > 0 {
803+
market.bet_deadline
804+
} else {
805+
market.end_time
806+
};
807+
if current_time >= deadline {
808+
return Err(Error::MarketClosed);
809+
}
810+
801811
// Check if market is not already resolved
802812
if market.winning_outcomes.is_some() {
803813
return Err(Error::MarketResolved);

contracts/predictify-hybrid/src/category_tags_tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ fn create_test_market(
4848
&None,
4949
&86400u64,
5050
&None,
51+
&None,
52+
&None,
5153
)
5254
}
5355

@@ -265,6 +267,8 @@ impl TokenTestSetup {
265267
&None,
266268
&86400u64,
267269
&None,
270+
&None,
271+
&None,
268272
);
269273

270274
Self { env, contract_id, admin, user1, user2, token_id, market_id }

contracts/predictify-hybrid/src/event_creation_tests.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ fn test_create_market_success() {
105105
&None,
106106
&0,
107107
&None,
108+
&None,
109+
&None,
108110
);
109111

110112
assert!(client.get_market(&market_id).is_some());
@@ -237,6 +239,9 @@ fn test_create_event_limit_enforced() {
237239
&oracle_config,
238240
&None,
239241
&0,
242+
&None,
243+
&None,
244+
&None,
240245
);
241246
}
242247
}
@@ -272,6 +277,9 @@ fn test_decrement_on_cancel_frees_slot() {
272277
&oracle_config,
273278
&None,
274279
&0,
280+
&None,
281+
&None,
282+
&None,
275283
));
276284
}
277285

@@ -290,5 +298,91 @@ fn test_decrement_on_cancel_frees_slot() {
290298
&oracle_config,
291299
&None,
292300
&0,
301+
&None,
302+
&None,
303+
&None,
304+
);
305+
}
306+
307+
#[test]
308+
fn test_event_id_unique() {
309+
let setup = TestSetup::new();
310+
let client = PredictifyHybridClient::new(&setup.env, &setup.contract_id);
311+
312+
let description = String::from_str(&setup.env, "Will this be a unique event A?");
313+
let outcomes = vec![
314+
&setup.env,
315+
String::from_str(&setup.env, "Yes"),
316+
String::from_str(&setup.env, "No"),
317+
];
318+
let end_time = setup.env.ledger().timestamp() + 3600;
319+
let oracle_config = OracleConfig {
320+
provider: OracleProvider::Reflector,
321+
oracle_address: Address::generate(&setup.env),
322+
feed_id: String::from_str(&setup.env, "BTC/USD"),
323+
threshold: 50000,
324+
comparison: String::from_str(&setup.env, "gt"),
325+
};
326+
327+
let event_id_1 = client.create_event(
328+
&setup.admin,
329+
&description,
330+
&outcomes,
331+
&end_time,
332+
&oracle_config,
333+
&None,
334+
&0,
335+
&None,
293336
);
337+
let desc_b = String::from_str(&setup.env, "Will this be a unique event B?");
338+
let event_id_2 = client.create_event(
339+
&setup.admin,
340+
&desc_b,
341+
&outcomes,
342+
&end_time,
343+
&oracle_config,
344+
&None,
345+
&0,
346+
&None,
347+
);
348+
349+
assert_ne!(event_id_1, event_id_2, "Event IDs must be unique");
350+
}
351+
352+
#[test]
353+
fn test_event_storage_consistency() {
354+
let setup = TestSetup::new();
355+
let client = PredictifyHybridClient::new(&setup.env, &setup.contract_id);
356+
357+
let description = String::from_str(&setup.env, "Stored event?");
358+
let outcomes = vec![
359+
&setup.env,
360+
String::from_str(&setup.env, "Yes"),
361+
String::from_str(&setup.env, "No"),
362+
];
363+
let end_time = setup.env.ledger().timestamp() + 7200;
364+
let oracle_config = OracleConfig {
365+
provider: OracleProvider::Reflector,
366+
oracle_address: Address::generate(&setup.env),
367+
feed_id: String::from_str(&setup.env, "BTC/USD"),
368+
threshold: 50000,
369+
comparison: String::from_str(&setup.env, "gt"),
370+
};
371+
372+
let event_id = client.create_event(
373+
&setup.admin,
374+
&description,
375+
&outcomes,
376+
&end_time,
377+
&oracle_config,
378+
&None,
379+
&0,
380+
&None,
381+
);
382+
383+
let stored = client.get_event(&event_id).unwrap();
384+
assert_eq!(stored.description, description);
385+
assert_eq!(stored.end_time, end_time);
386+
assert_eq!(stored.outcomes.len(), outcomes.len());
387+
assert_eq!(stored.id, event_id);
294388
}

contracts/predictify-hybrid/src/event_management_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ impl TestSetup {
7373
&None,
7474
&86400u64,
7575
&None,
76+
&None,
77+
&None,
7678
)
7779
}
7880
}

contracts/predictify-hybrid/src/events.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,31 @@ pub struct AdminInitializedEvent {
10881088
pub timestamp: u64,
10891089
}
10901090

1091+
/// Event emitted when the contract admin is transferred to a new address.
1092+
#[contracttype]
1093+
#[derive(Clone, Debug, Eq, PartialEq)]
1094+
pub struct AdminTransferredEvent {
1095+
pub previous_admin: Address,
1096+
pub new_admin: Address,
1097+
pub timestamp: u64,
1098+
}
1099+
1100+
/// Event emitted when the contract is paused by admin.
1101+
#[contracttype]
1102+
#[derive(Clone, Debug, Eq, PartialEq)]
1103+
pub struct ContractPausedEvent {
1104+
pub admin: Address,
1105+
pub timestamp: u64,
1106+
}
1107+
1108+
/// Event emitted when the contract is unpaused by admin.
1109+
#[contracttype]
1110+
#[derive(Clone, Debug, Eq, PartialEq)]
1111+
pub struct ContractUnpausedEvent {
1112+
pub admin: Address,
1113+
pub timestamp: u64,
1114+
}
1115+
10911116
#[contracttype]
10921117
#[derive(Clone, Debug, Eq, PartialEq)]
10931118
pub struct ContractInitializedEvent {
@@ -2308,6 +2333,34 @@ impl EventEmitter {
23082333
Self::store_event(env, &symbol_short!("adm_init"), &event);
23092334
}
23102335

2336+
/// Emit admin transferred event (primary admin role transferred to new address).
2337+
pub fn emit_admin_transferred(env: &Env, previous_admin: &Address, new_admin: &Address) {
2338+
let event = AdminTransferredEvent {
2339+
previous_admin: previous_admin.clone(),
2340+
new_admin: new_admin.clone(),
2341+
timestamp: env.ledger().timestamp(),
2342+
};
2343+
Self::store_event(env, &symbol_short!("adm_xfer"), &event);
2344+
}
2345+
2346+
/// Emit contract paused event.
2347+
pub fn emit_contract_paused(env: &Env, admin: &Address) {
2348+
let event = ContractPausedEvent {
2349+
admin: admin.clone(),
2350+
timestamp: env.ledger().timestamp(),
2351+
};
2352+
Self::store_event(env, &symbol_short!("ctr_pause"), &event);
2353+
}
2354+
2355+
/// Emit contract unpaused event.
2356+
pub fn emit_contract_unpaused(env: &Env, admin: &Address) {
2357+
let event = ContractUnpausedEvent {
2358+
admin: admin.clone(),
2359+
timestamp: env.ledger().timestamp(),
2360+
};
2361+
Self::store_event(env, &symbol_short!("ctr_unp"), &event);
2362+
}
2363+
23112364
/// Emit contract initialized event (full initialization with platform fee)
23122365
pub fn emit_contract_initialized(env: &Env, admin: &Address, fee: i128) {
23132366
let event = ContractInitializedEvent {

contracts/predictify-hybrid/src/integration_test.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ impl IntegrationTestSuite {
9999
&None,
100100
&0,
101101
&None,
102+
&None,
103+
&None,
102104
);
103105

104106
self.market_ids.push_back(market_id.clone());

0 commit comments

Comments
 (0)