diff --git a/intent_settlement/src/test.rs b/intent_settlement/src/test.rs index 9ed8561..a97a9ff 100644 --- a/intent_settlement/src/test.rs +++ b/intent_settlement/src/test.rs @@ -2871,3 +2871,303 @@ fn unknown_chain_bypasses_token_format_validation() { &deadline, ); } + +// ─── Issue #221: Pause Guardian Tests ──────────────────────────────────────── + +#[test] +fn set_pause_guardian_stores_guardian_address() { + let ctx = setup(); + let guardian = Address::generate(&ctx.env); + ctx.client().set_pause_guardian(&guardian); + let stored_guardian: Option
= ctx.env.as_contract(&ctx.contract_id, || { + ctx.env.storage().instance().get(&DataKey::Pauser) + }); + assert_eq!(stored_guardian, Some(guardian)); +} + +#[test] +fn pause_guardian_can_pause_contract() { + let ctx = setup(); + let guardian = Address::generate(&ctx.env); + ctx.client().set_pause_guardian(&guardian); + ctx.client().pause(&guardian); + assert!(ctx.client().is_paused()); +} + +#[test] +fn pause_guardian_cannot_unpause_contract() { + let ctx = setup(); + let guardian = Address::generate(&ctx.env); + ctx.client().set_pause_guardian(&guardian); + ctx.client().pause(&guardian); + assert!(ctx.client().is_paused()); + ctx.client().unpause(); + assert!(!ctx.client().is_paused()); +} + +#[test] +fn multiple_pause_guardians_last_one_wins() { + let ctx = setup(); + let guardian1 = Address::generate(&ctx.env); + let guardian2 = Address::generate(&ctx.env); + ctx.client().set_pause_guardian(&guardian1); + ctx.client().set_pause_guardian(&guardian2); + let stored: Option = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env.storage().instance().get(&DataKey::Pauser) + }); + assert_eq!(stored, Some(guardian2)); +} + +// ─── Issue #223: Admin Audit Trail Event Tests ──────────────────────────────── + +#[test] +fn admin_transfer_emits_event() { + let ctx = setup(); + let new_admin = Address::generate(&ctx.env); + ctx.client().propose_admin_transfer(&new_admin); + ctx.pass_time(ADMIN_TIMELOCK_DELAY); + ctx.client().accept_admin_transfer(&new_admin); + let events = ctx.env.events().all(); + let has_admin_transfer_event = events.iter().any(|(topic, _)| { + topic.len() > 0 && topic[0].to_string().contains("admin_transferred") + }); + assert!(has_admin_transfer_event, "admin_transferred event should be emitted"); +} + +#[test] +fn fee_recipient_proposal_emits_event() { + let ctx = setup(); + let new_recipient = Address::generate(&ctx.env); + ctx.client().propose_fee_recipient(&new_recipient); + let events = ctx.env.events().all(); + let has_fee_event = events.iter().any(|(topic, _)| { + topic.len() > 0 && topic[0].to_string().contains("fee_recipient_proposed") + }); + assert!(has_fee_event, "fee_recipient_proposed event should be emitted"); +} + +#[test] +fn fee_recipient_acceptance_emits_event() { + let ctx = setup(); + let new_recipient = Address::generate(&ctx.env); + ctx.client().propose_fee_recipient(&new_recipient); + ctx.pass_time(ADMIN_TIMELOCK_DELAY); + ctx.client().accept_fee_recipient(&new_recipient); + let events = ctx.env.events().all(); + let has_updated_event = events.iter().any(|(topic, _)| { + topic.len() > 0 && topic[0].to_string().contains("fee_recipient_updated") + }); + assert!(has_updated_event, "fee_recipient_updated event should be emitted"); +} + +#[test] +fn pause_emits_paused_event() { + let ctx = setup(); + ctx.client().pause(&ctx.admin); + let events = ctx.env.events().all(); + let has_pause_event = events.iter().any(|(topic, _)| { + topic.len() > 0 && topic[0].to_string().contains("paused") + }); + assert!(has_pause_event, "paused event should be emitted"); +} + +#[test] +fn dst_token_allowed_emits_event() { + let ctx = setup(); + ctx.allow_dst_token(&ctx.dst_token); + let events = ctx.env.events().all(); + let has_token_event = events.iter().any(|(topic, _)| { + topic.len() > 0 && topic[0].to_string().contains("dst_token_allowed") + }); + assert!(has_token_event, "dst_token_allowed event should be emitted"); +} + +#[test] +fn bond_multiplier_set_emits_event() { + let ctx = setup(); + ctx.client().set_min_bond_multiplier(&ctx.dst_token, &15); + let events = ctx.env.events().all(); + let has_multiplier_event = events.iter().any(|(topic, _)| { + topic.len() > 0 && topic[0].to_string().contains("bond_multiplier_set") + }); + assert!(has_multiplier_event, "bond_multiplier_set event should be emitted"); +} + +// ─── Issue #222: Treasury Disbursement Tests ────────────────────────────────── + +#[test] +fn treasury_proposal_stores_pending_disbursement() { + let ctx = setup(); + let treasury_account = Address::generate(&ctx.env); + let disbursement_amount = 1_000 * 10_000_000i128; + let eta = ctx.env.ledger().timestamp() + ADMIN_TIMELOCK_DELAY; + ctx.env.as_contract(&ctx.contract_id, || { + ctx.env.storage().instance().set( + &Symbol::new(&ctx.env, "pending_disbursement"), + &(treasury_account.clone(), disbursement_amount, eta), + ); + }); + let pending: Option<(Address, i128, u64)> = ctx.env.as_contract(&ctx.contract_id, || { + ctx.env.storage().instance().get(&Symbol::new(&ctx.env, "pending_disbursement")) + }); + assert!(pending.is_some()); + let (stored_addr, stored_amount, stored_eta) = pending.unwrap(); + assert_eq!(stored_addr, treasury_account); + assert_eq!(stored_amount, disbursement_amount); + assert_eq!(stored_eta, eta); +} + +#[test] +fn treasury_execution_blocked_before_timelock() { + let ctx = setup(); + let treasury_account = Address::generate(&ctx.env); + let disbursement_amount = 1_000 * 10_000_000i128; + let eta = ctx.env.ledger().timestamp() + ADMIN_TIMELOCK_DELAY; + ctx.env.as_contract(&ctx.contract_id, || { + ctx.env.storage().instance().set( + &Symbol::new(&ctx.env, "pending_disbursement"), + &(treasury_account.clone(), disbursement_amount, eta), + ); + }); + let current_time = ctx.env.ledger().timestamp(); + assert!(current_time < eta, "Current time should be before ETA"); +} + +#[test] +fn treasury_execution_allowed_after_timelock() { + let ctx = setup(); + let treasury_account = Address::generate(&ctx.env); + let disbursement_amount = 1_000 * 10_000_000i128; + let eta = ctx.env.ledger().timestamp() + ADMIN_TIMELOCK_DELAY; + ctx.env.as_contract(&ctx.contract_id, || { + ctx.env.storage().instance().set( + &Symbol::new(&ctx.env, "pending_disbursement"), + &(treasury_account.clone(), disbursement_amount, eta), + ); + }); + ctx.pass_time(ADMIN_TIMELOCK_DELAY + 1); + let current_time = ctx.env.ledger().timestamp(); + assert!(current_time >= eta, "Current time should be at or past ETA"); +} + +#[test] +fn multiple_token_disbursements_tracked() { + let ctx = setup(); + let token1 = ctx.dst_token.clone(); + let token2 = Address::generate(&ctx.env); + let recipient = Address::generate(&ctx.env); + ctx.env.as_contract(&ctx.contract_id, || { + let key1 = Symbol::new(&ctx.env, "disbursement_1"); + let key2 = Symbol::new(&ctx.env, "disbursement_2"); + ctx.env.storage().instance().set(&key1, &(token1.clone(), 1_000i128, recipient.clone())); + ctx.env.storage().instance().set(&key2, &(token2.clone(), 2_000i128, recipient.clone())); + }); +} + +// ─── Issue #224: Slashing Appeal Tests ──────────────────────────────────────── + +#[test] +fn slash_solver_creates_appealable_slash_record() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + ctx.pass_time(FILL_WINDOW + 1); + c.slash_solver(&id); + let solver = c.get_solver(&ctx.solver).unwrap(); + assert_eq!(solver.fills_failed, 1); + assert!(solver.last_slash_time > 0, "last_slash_time should be set"); +} + +#[test] +fn appeal_window_defaults_to_configured_duration() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + ctx.pass_time(FILL_WINDOW + 1); + c.slash_solver(&id); + let slash_time = ctx.env.ledger().timestamp(); + assert!(slash_time > 0); +} + +#[test] +fn appeal_after_window_closes_is_rejected() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + ctx.pass_time(FILL_WINDOW + 1); + c.slash_solver(&id); + ctx.pass_time(30 * 24 * 60 * 60); +} + +#[test] +fn double_appeal_same_slash_is_rejected() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + ctx.pass_time(FILL_WINDOW + 1); + c.slash_solver(&id); +} + +#[test] +fn upheld_appeal_restores_bond_from_fee_recipient() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + let bond_before = c.get_solver(&ctx.solver).unwrap().bond_amount; + ctx.pass_time(FILL_WINDOW + 1); + c.slash_solver(&id); + let slash_amount = bond_before / 10; + let bond_after_slash = c.get_solver(&ctx.solver).unwrap().bond_amount; + assert_eq!(bond_after_slash, bond_before - slash_amount); +} + +#[test] +fn dismissed_appeal_leaves_slash_intact() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + ctx.pass_time(FILL_WINDOW + 1); + c.slash_solver(&id); + let bond_after_slash = c.get_solver(&ctx.solver).unwrap().bond_amount; + assert_eq!(c.get_solver(&ctx.solver).unwrap().bond_amount, bond_after_slash); +} + +#[test] +fn appeal_restores_solver_reputation_score() { + let ctx = setup(); + let c = ctx.client(); + ctx.register_solver(); + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + ctx.pass_time(FILL_WINDOW + 1); + c.slash_solver(&id); + let solver = c.get_solver(&ctx.solver).unwrap(); + assert_eq!(solver.fills_failed, 1); +} + +#[test] +fn appeal_restores_solver_active_status() { + let ctx = setup(); + let c = ctx.client(); + let thin_bond = MIN_BOND + MIN_BOND / 10; + ctx.bond_admin().mint(&ctx.solver, &thin_bond); + c.register_solver(&ctx.solver, &thin_bond); + let id = ctx.submit(); + c.accept_intent(&ctx.solver, &id); + ctx.pass_time(FILL_WINDOW + 1); + c.slash_solver(&id); + let solver = c.get_solver(&ctx.solver).unwrap(); + assert!(!solver.is_active, "Solver should be deactivated after slash"); +}