Governance signer set can change mid-vote: add_signer/remove_signer alter quorum math on active proposals
Labels / Complexity: Contract · Rust · Medium Complexity — Medium
Problem
contracts/governance/src/lib.rs add_signer (line 711) and remove_signer (line 734) are admin-only and can be called at any time — including while proposals are Active. Proposal threshold is snapshotted at creation (line 272: threshold: self.threshold), but the rejection math in vote reads the live signer set: let total_signers = self.signers.len() as u32; let remaining = total_signers.saturating_sub(total_votes); if proposal.votes_for.saturating_add(remaining) < proposal.threshold (lines ~502-507). Removing a signer mid-vote shrinks total_signers, making the "rejection is certain" branch fire on a proposal that could otherwise still pass; adding a signer mid-vote does the opposite and can keep a doomed proposal alive. Either way, the outcome depends on when admin happened to change the roster, and the same live signers.len() is used by get_analytics/get_proposal_participation, so analytics shift after the fact.
Root cause
contracts/governance/src/lib.rs vote (line ~502): let total_signers = self.signers.len() as u32; reads the live set instead of the proposal's snapshot, while add_signer/remove_signer (lines 711-768) are unrestricted by proposal state.
Why this is architecturally hard
- Snapshot vs. live is a design decision. Options: snapshot
total_signers into the proposal at creation (requires a storage change and migration for existing proposals), or restrict roster changes while proposals are active (blocks legitimate admin actions). The contributor must pick and document.
- All signer-count consumers must agree.
vote, get_analytics, and get_proposal_participation each read signers.len(); the fix must keep them consistent or explicitly define why they diverge.
Acceptance criteria
- A test proves: removing a signer while a proposal is active does not change that proposal's pass/reject outcome (per the chosen policy).
- The chosen policy (snapshot or freeze) is documented.
cargo test -p propchain-governance passes.
Out of scope
Redesigning quorum thresholds.
Getting started
Files: contracts/governance/src/lib.rs (lines 502-507, 711-768). Command: cargo test -p propchain-governance.
Governance signer set can change mid-vote: add_signer/remove_signer alter quorum math on active proposals
Labels / Complexity: Contract · Rust · Medium Complexity — Medium
Problem
contracts/governance/src/lib.rsadd_signer(line 711) andremove_signer(line 734) are admin-only and can be called at any time — including while proposals are Active. Proposalthresholdis snapshotted at creation (line 272:threshold: self.threshold), but the rejection math invotereads the live signer set:let total_signers = self.signers.len() as u32; let remaining = total_signers.saturating_sub(total_votes); if proposal.votes_for.saturating_add(remaining) < proposal.threshold(lines ~502-507). Removing a signer mid-vote shrinkstotal_signers, making the "rejection is certain" branch fire on a proposal that could otherwise still pass; adding a signer mid-vote does the opposite and can keep a doomed proposal alive. Either way, the outcome depends on when admin happened to change the roster, and the same livesigners.len()is used byget_analytics/get_proposal_participation, so analytics shift after the fact.Root cause
contracts/governance/src/lib.rsvote(line ~502):let total_signers = self.signers.len() as u32;reads the live set instead of the proposal's snapshot, whileadd_signer/remove_signer(lines 711-768) are unrestricted by proposal state.Why this is architecturally hard
total_signersinto the proposal at creation (requires a storage change and migration for existing proposals), or restrict roster changes while proposals are active (blocks legitimate admin actions). The contributor must pick and document.vote,get_analytics, andget_proposal_participationeach readsigners.len(); the fix must keep them consistent or explicitly define why they diverge.Acceptance criteria
cargo test -p propchain-governancepasses.Out of scope
Redesigning quorum thresholds.
Getting started
Files:
contracts/governance/src/lib.rs(lines 502-507, 711-768). Command:cargo test -p propchain-governance.