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
15 changes: 13 additions & 2 deletions swaptrade-contracts/counter/src/governance/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,10 @@ pub struct MultiSigProposal {
pub approvals: HashSet<String>,
pub executed: bool,
pub rejected: bool,
/// Minimum signatures required for execution
pub required_signatures: usize,
/// Total authorized signers at time of creation
pub total_signers: usize,
}

impl MultiSigProposal {
Expand All @@ -276,7 +280,7 @@ impl MultiSigProposal {
}

pub fn is_approved(&self) -> bool {
self.approvals.len() >= MULTISIG_THRESHOLD
self.approvals.len() >= self.required_signatures
}
}

Expand Down Expand Up @@ -330,6 +334,8 @@ impl MultiSigCoordinator {
approvals,
executed: false,
rejected: false,
required_signatures: MULTISIG_THRESHOLD,
total_signers: self.authorized_signers.len(),
});

Ok(proposal_id)
Expand All @@ -347,6 +353,11 @@ impl MultiSigCoordinator {
if proposal.executed { return Err("Already executed".into()); }
if proposal.rejected { return Err("Proposal rejected".into()); }

// Prevent duplicate signatures
if proposal.approvals.contains(&signer) {
return Err(format!("'{}' has already approved this proposal", signer));
}

proposal.approvals.insert(signer);
Ok(proposal.approvals.len())
}
Expand All @@ -359,7 +370,7 @@ impl MultiSigCoordinator {
if proposal.rejected { return Err("Proposal rejected".into()); }
if !proposal.is_approved() {
return Err(format!(
"Insufficient approvals: {}/{}", proposal.approval_count(), MULTISIG_THRESHOLD
"Insufficient approvals: {}/{}", proposal.approval_count(), proposal.required_signatures
));
}

Expand Down
18 changes: 18 additions & 0 deletions swaptrade-contracts/counter/src/governance/voting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub enum ProposalStatus {
Passed,
Rejected,
Expired,
Executed,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -86,6 +87,18 @@ impl Proposal {
self.status = ProposalStatus::Rejected;
}
}

/// Execute a passed proposal. Prevents double execution.
pub fn execute(&mut self) -> Result<(), String> {
if self.status == ProposalStatus::Executed {
return Err("Proposal has already been executed".to_string());
}
if self.status != ProposalStatus::Passed {
return Err(format!("Proposal cannot be executed, current status: {:?}", self.status));
}
self.status = ProposalStatus::Executed;
Ok(())
}
}

pub struct GovernanceVoting {
Expand Down Expand Up @@ -127,6 +140,11 @@ impl GovernanceVoting {
Ok(proposal.status.clone())
}

pub fn execute_proposal(&mut self, proposal_id: u64) -> Result<(), String> {
let proposal = self.proposals.get_mut(&proposal_id).ok_or("proposal not found")?;
proposal.execute()
}

pub fn get(&self, proposal_id: u64) -> Option<&Proposal> {
self.proposals.get(&proposal_id)
}
Expand Down
192 changes: 192 additions & 0 deletions swaptrade-contracts/counter/src/governance_multisig_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// tests/governance_multisig_tests.rs
//! Tests for multi-signature governance execution (Issue #166)
//! and prevention of double execution (Issue #167)

#[cfg(test)]
mod tests {
use crate::governance::{
MultiSigCoordinator, MultiSigProposal, MULTISIG_THRESHOLD, MULTISIG_TOTAL,
};

#[test]
fn test_multisig_requires_minimum_signatures() {
let signers: Vec<String> = (0..MULTISIG_TOTAL)
.map(|i| format!("signer_{}", i))
.collect();
let mut coordinator = MultiSigCoordinator::new(signers.clone());

// Create a proposal
let payload = b"test payload";
let proposal_id = coordinator
.propose("signer_0", "Test proposal", payload)
.unwrap();

// Proposal should not be approved yet (only 1 signature from proposer)
let proposal = coordinator.proposals.get(&proposal_id).unwrap();
assert!(!proposal.is_approved());
assert_eq!(proposal.approval_count(), 1);

// Add more signatures
coordinator.approve(&proposal_id, "signer_1").unwrap();
coordinator.approve(&proposal_id, "signer_2").unwrap();

// Now should be approved (3 signatures)
let proposal = coordinator.proposals.get(&proposal_id).unwrap();
assert!(proposal.is_approved());
assert_eq!(proposal.approval_count(), MULTISIG_THRESHOLD);
}

#[test]
fn test_multisig_fails_without_quorum() {
let signers: Vec<String> = (0..MULTISIG_TOTAL)
.map(|i| format!("signer_{}", i))
.collect();
let mut coordinator = MultiSigCoordinator::new(signers.clone());

let payload = b"test payload";
let proposal_id = coordinator
.propose("signer_0", "Test proposal", payload)
.unwrap();

// Only add 1 more signature (total 2, need 3)
coordinator.approve(&proposal_id, "signer_1").unwrap();

// Try to execute - should fail
let result = coordinator.execute(&proposal_id, payload);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Insufficient approvals"));
}

#[test]
fn test_multisig_succeeds_with_required_signatures() {
let signers: Vec<String> = (0..MULTISIG_TOTAL)
.map(|i| format!("signer_{}", i))
.collect();
let mut coordinator = MultiSigCoordinator::new(signers.clone());

let payload = b"test payload";
let proposal_id = coordinator
.propose("signer_0", "Test proposal", payload)
.unwrap();

// Add required signatures
coordinator.approve(&proposal_id, "signer_1").unwrap();
coordinator.approve(&proposal_id, "signer_2").unwrap();

// Execute should succeed
let result = coordinator.execute(&proposal_id, payload);
assert!(result.is_ok());

// Verify proposal is marked as executed
let proposal = coordinator.proposals.get(&proposal_id).unwrap();
assert!(proposal.executed);
}

#[test]
fn test_multisig_prevents_duplicate_signatures() {
let signers: Vec<String> = (0..MULTISIG_TOTAL)
.map(|i| format!("signer_{}", i))
.collect();
let mut coordinator = MultiSigCoordinator::new(signers.clone());

let payload = b"test payload";
let proposal_id = coordinator
.propose("signer_0", "Test proposal", payload)
.unwrap();

// Try to approve twice with same signer
let result1 = coordinator.approve(&proposal_id, "signer_1");
assert!(result1.is_ok());

let result2 = coordinator.approve(&proposal_id, "signer_1");
assert!(result2.is_err());
assert!(result2
.unwrap_err()
.contains("already approved"));
}

#[test]
fn test_multisig_rejects_unauthorized_signer() {
let signers: Vec<String> = (0..MULTISIG_TOTAL)
.map(|i| format!("signer_{}", i))
.collect();
let mut coordinator = MultiSigCoordinator::new(signers);

let payload = b"test payload";
let proposal_id = coordinator
.propose("signer_0", "Test proposal", payload)
.unwrap();

// Unauthorized signer tries to approve
let result = coordinator.approve(&proposal_id, "unauthorized_signer");
assert!(result.is_err());
assert!(result.unwrap_err().contains("not an authorized signer"));
}

#[test]
fn test_multisig_tracks_approval_count_correctly() {
let signers: Vec<String> = (0..MULTISIG_TOTAL)
.map(|i| format!("signer_{}", i))
.collect();
let mut coordinator = MultiSigCoordinator::new(signers.clone());

let payload = b"test payload";
let proposal_id = coordinator
.propose("signer_0", "Test proposal", payload)
.unwrap();

// Check initial count (proposer auto-approves)
let proposal = coordinator.proposals.get(&proposal_id).unwrap();
assert_eq!(proposal.approval_count(), 1);

// Add signatures one by one
for i in 1..MULTISIG_TOTAL {
coordinator
.approve(&proposal_id, &format!("signer_{}", i))
.unwrap();
let proposal = coordinator.proposals.get(&proposal_id).unwrap();
assert_eq!(proposal.approval_count(), i + 1);
}
}

#[test]
fn test_multisig_execution_prevented_after_rejection() {
let signers: Vec<String> = (0..MULTISIG_TOTAL)
.map(|i| format!("signer_{}", i))
.collect();
let mut coordinator = MultiSigCoordinator::new(signers.clone());

let payload = b"test payload";
let proposal_id = coordinator
.propose("signer_0", "Test proposal", payload)
.unwrap();

// Mark as rejected
let proposal = coordinator.proposals.get_mut(&proposal_id).unwrap();
proposal.rejected = true;

// Try to execute
let result = coordinator.execute(&proposal_id, payload);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Proposal rejected"));
}

#[test]
fn test_multisig_stores_total_signers_at_creation() {
let signers: Vec<String> = (0..MULTISIG_TOTAL)
.map(|i| format!("signer_{}", i))
.collect();
let mut coordinator = MultiSigCoordinator::new(signers.clone());

let payload = b"test payload";
let proposal_id = coordinator
.propose("signer_0", "Test proposal", payload)
.unwrap();

let proposal = coordinator.proposals.get(&proposal_id).unwrap();
assert_eq!(proposal.total_signers, MULTISIG_TOTAL);
assert_eq!(proposal.required_signatures, MULTISIG_THRESHOLD);
}
}
Loading
Loading