From e0c72a4df901f87f1aa39c570cc44c4ea8bb8a5d Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 29 Mar 2026 15:18:24 +0100 Subject: [PATCH 1/4] fix: emit ProposalApproved event with proposal_id and add coverage tests --- contracts/multisig_governance/src/lib.rs | 10 +-- contracts/multisig_governance/src/test.rs | 100 ++++++++++++++++++++++ 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/contracts/multisig_governance/src/lib.rs b/contracts/multisig_governance/src/lib.rs index 7c4bfbd4..6d0465fa 100644 --- a/contracts/multisig_governance/src/lib.rs +++ b/contracts/multisig_governance/src/lib.rs @@ -106,6 +106,7 @@ pub struct ProposalCancelledEvent { #[contracttype] #[derive(Clone, Debug)] pub struct TransferApprovedEvent { + pub proposal_id: u32, pub signer: Address, pub approvals_so_far: u32, pub threshold: u32, @@ -308,13 +309,8 @@ impl GovernanceContract { env.storage().instance().set(&KEY_PENDING, &pending); env.events().publish( - (symbol_short!("GovAppr"), signer.clone()), - TransferApprovedEvent { - signer, - approvals_so_far, - threshold, - timestamp: env.ledger().timestamp(), - }, + (Symbol::new(&env, "ProposalApproved"),), + (pending.id, signer.clone(), approvals_so_far, threshold), ); } diff --git a/contracts/multisig_governance/src/test.rs b/contracts/multisig_governance/src/test.rs index 861269ba..8276922a 100644 --- a/contracts/multisig_governance/src/test.rs +++ b/contracts/multisig_governance/src/test.rs @@ -620,3 +620,103 @@ fn propose_rejects_too_many_signers() { } client.propose_admin_transfer(&Address::generate(&env), &addrs, &1, &MIN_TIMELOCK_SECONDS); } + +#[test] +#[should_panic(expected = "threshold must be >= 1")] +fn propose_rejects_zero_threshold() { + let (env, client, _, _) = setup(); + let signers = Vec::from_slice(&env, &[Address::generate(&env)]); + client.propose_admin_transfer(&Address::generate(&env), &signers, &0, &MIN_TIMELOCK_SECONDS); +} + +#[test] +fn propose_succeeds_after_cancelled_proposal() { + // Covers the branch: pending exists but status != Active + let (env, client, _, _) = setup(); + let s = Address::generate(&env); + let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); + + set_ts(&env, 1000); + client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + client.cancel_admin_transfer(); + + set_ts(&env, 1000 + REPROPOSAL_COOLDOWN_SECONDS + 1); + let proposed2 = Address::generate(&env); + client.propose_admin_transfer(&proposed2, &signers, &1, &MIN_TIMELOCK_SECONDS); + assert_eq!(client.get_pending_transfer().proposed_admin, proposed2); +} + +#[test] +fn cancel_already_cancelled_is_noop() { + // Covers the early return in cancel_admin_transfer when already Cancelled + let (env, client, _, _) = setup(); + let s = Address::generate(&env); + let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); + + set_ts(&env, 1000); + client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + let proposal_id = client.get_pending_transfer().id; + client.emergency_cancel_proposal(&proposal_id, &None); + + // cancel_admin_transfer on an already-cancelled proposal should return early + client.cancel_admin_transfer(); + assert!(!client.has_pending_transfer()); +} + +#[test] +fn emergency_cancel_already_cancelled_is_noop() { + // Covers the early return in emergency_cancel_proposal when already Cancelled + let (env, client, _, _) = setup(); + let s = Address::generate(&env); + let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); + + set_ts(&env, 1000); + client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + let proposal_id = client.get_pending_transfer().id; + client.emergency_cancel_proposal(&proposal_id, &None); + + // Second call should be a no-op (early return) + client.emergency_cancel_proposal(&proposal_id, &None); + assert!(!client.has_pending_transfer()); +} + +#[test] +#[should_panic(expected = "proposal ID mismatch")] +fn emergency_cancel_wrong_id_panics() { + let (env, client, _, _) = setup(); + let s = Address::generate(&env); + let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); + + set_ts(&env, 1000); + client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + client.emergency_cancel_proposal(&9999, &None); +} + +#[test] +#[should_panic(expected = "proposal is not active")] +fn expire_cancelled_proposal_panics() { + let (env, client, _, _) = setup(); + let s = Address::generate(&env); + let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); + + set_ts(&env, 1000); + client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + let proposal_id = client.get_pending_transfer().id; + client.emergency_cancel_proposal(&proposal_id, &None); + + set_ts(&env, 1000 + PROPOSAL_TTL_SECONDS + 1); + client.expire_proposal(&Address::generate(&env)); +} + +#[test] +fn timelock_remaining_returns_zero_with_no_pending() { + // Covers the None branch in get_timelock_remaining + let (_env, client, _, _) = setup(); + assert_eq!(client.get_timelock_remaining(), 0); +} + +#[test] +fn has_pending_transfer_false_with_no_proposal() { + let (_env, client, _, _) = setup(); + assert!(!client.has_pending_transfer()); +} From 4af1763ea8a2056f2969cdae6d54fae6f5c8a239 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 29 Mar 2026 17:43:08 +0100 Subject: [PATCH 2/4] style: apply cargo fmt formatting --- contracts/multisig_governance/src/test.rs | 42 +++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/contracts/multisig_governance/src/test.rs b/contracts/multisig_governance/src/test.rs index 8276922a..032252aa 100644 --- a/contracts/multisig_governance/src/test.rs +++ b/contracts/multisig_governance/src/test.rs @@ -626,7 +626,12 @@ fn propose_rejects_too_many_signers() { fn propose_rejects_zero_threshold() { let (env, client, _, _) = setup(); let signers = Vec::from_slice(&env, &[Address::generate(&env)]); - client.propose_admin_transfer(&Address::generate(&env), &signers, &0, &MIN_TIMELOCK_SECONDS); + client.propose_admin_transfer( + &Address::generate(&env), + &signers, + &0, + &MIN_TIMELOCK_SECONDS, + ); } #[test] @@ -637,7 +642,12 @@ fn propose_succeeds_after_cancelled_proposal() { let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); set_ts(&env, 1000); - client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + client.propose_admin_transfer( + &Address::generate(&env), + &signers, + &1, + &MIN_TIMELOCK_SECONDS, + ); client.cancel_admin_transfer(); set_ts(&env, 1000 + REPROPOSAL_COOLDOWN_SECONDS + 1); @@ -654,7 +664,12 @@ fn cancel_already_cancelled_is_noop() { let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); set_ts(&env, 1000); - client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + client.propose_admin_transfer( + &Address::generate(&env), + &signers, + &1, + &MIN_TIMELOCK_SECONDS, + ); let proposal_id = client.get_pending_transfer().id; client.emergency_cancel_proposal(&proposal_id, &None); @@ -671,7 +686,12 @@ fn emergency_cancel_already_cancelled_is_noop() { let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); set_ts(&env, 1000); - client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + client.propose_admin_transfer( + &Address::generate(&env), + &signers, + &1, + &MIN_TIMELOCK_SECONDS, + ); let proposal_id = client.get_pending_transfer().id; client.emergency_cancel_proposal(&proposal_id, &None); @@ -688,7 +708,12 @@ fn emergency_cancel_wrong_id_panics() { let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); set_ts(&env, 1000); - client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + client.propose_admin_transfer( + &Address::generate(&env), + &signers, + &1, + &MIN_TIMELOCK_SECONDS, + ); client.emergency_cancel_proposal(&9999, &None); } @@ -700,7 +725,12 @@ fn expire_cancelled_proposal_panics() { let signers = Vec::from_slice(&env, core::slice::from_ref(&s)); set_ts(&env, 1000); - client.propose_admin_transfer(&Address::generate(&env), &signers, &1, &MIN_TIMELOCK_SECONDS); + client.propose_admin_transfer( + &Address::generate(&env), + &signers, + &1, + &MIN_TIMELOCK_SECONDS, + ); let proposal_id = client.get_pending_transfer().id; client.emergency_cancel_proposal(&proposal_id, &None); From 0036d2b0832963b9dce21c22f26646a7ce13c5ec Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 29 Mar 2026 17:58:01 +0100 Subject: [PATCH 3/4] ci: trigger CI checks From 8fd14eb8c70581627eda65046708369e076b0950 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 5 Apr 2026 15:32:24 +0100 Subject: [PATCH 4/4] fix: remove unused TransferApprovedEvent struct and trigger CI --- contracts/multisig_governance/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/multisig_governance/src/lib.rs b/contracts/multisig_governance/src/lib.rs index 6d0465fa..ae24f0a8 100644 --- a/contracts/multisig_governance/src/lib.rs +++ b/contracts/multisig_governance/src/lib.rs @@ -310,7 +310,13 @@ impl GovernanceContract { env.events().publish( (Symbol::new(&env, "ProposalApproved"),), - (pending.id, signer.clone(), approvals_so_far, threshold), + TransferApprovedEvent { + proposal_id: pending.id, + signer, + approvals_so_far, + threshold, + timestamp: env.ledger().timestamp(), + }, ); }