Make GOVERNANCE_QUORUM relative to signer-set size and block remove_signer from dropping below quorum.
The governance contract previously hardcoded GOVERNANCE_QUORUM: u32 = 2 independent of how many signers were actually registered. This caused two critical issues: (1) remove_signer performed no check that the remaining signer count stays >= quorum, allowing the admin to shrink the signer set to a single signer while quorum still claimed to require 2 — permanently bricking all future proposals. (2) A fixed quorum of 2 does not scale; with 7 signers, 2-of-7 is far weaker than the multisig intends.
This PR replaces the fixed quorum with a configurable threshold stored at init, enforces the invariant 1 <= threshold <= signers.len() on every signer-set mutation, and snapshots the threshold at quorum time so in-flight proposals are immune to mid-flight threshold changes.
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
- Documentation update
- Test coverage improvement
- Refactoring (no functional changes)
Closes #621
-
contracts/governance/src/lib.rs:- Removed hardcoded
GOVERNANCE_QUORUM: u32 = 2constant - Added
QuorumInfostruct to snapshot(reached_at, threshold)when quorum is first reached - Added
DataKey::Thresholdfor instance-stored configurable threshold - Added
GovernanceError::InvalidThreshold(15) for init-time validation failures - Added
GovernanceError::QuorumWouldBreak(16) for removal that would violate the invariant - Added
get_threshold()storage helper - Updated
init(admin, signers, threshold)— validates1 <= threshold <= signers.len() - Updated
remove_signer()— rejects removal ifsigners.len() - 1 < threshold - Updated
approve()— uses stored threshold; snapshots threshold inQuorumInfowhen quorum reached - Updated
execute()— reads threshold fromQuorumInfosnapshot (immune to threshold changes) - Updated
quorum()view — returns stored threshold
- Removed hardcoded
-
contracts/stream/tests/governance_integration.rs:- Updated all existing tests to pass
thresholdparameter toinit() - Added 5 new integration tests: zero threshold rejection, threshold above signers rejection, removal below threshold errors, execution with exactly threshold approvals, threshold unchanged after add_signer
- Updated all existing tests to pass
-
docs/governance.md:- Replaced fixed
GOVERNANCE_QUORUMdocs with configurable threshold model - Documented
init(admin, signers, threshold)entrypoint - Documented
remove_signerrejection withQuorumWouldBreak - Added
quorum()entrypoint doc returning stored threshold - Added 3 new security considerations (bricking prevention, threshold snapshotting, init validation)
- Updated storage layout table to include
ThresholdandQuorumInfotypes - Updated test coverage list
- Replaced fixed
- Yes - snapshot files were updated (explain below)
- No - no snapshot changes
N/A — no snapshot files were present in the repository for the governance contract.
- All governance unit tests pass locally:
cargo test -p fluxora_governance - All tests pass locally:
cargo test -p fluxora_stream - New tests added for new functionality
- Existing tests updated for changed functionality
- Test coverage remains above 95%
- Tested on local environment
- Tested edge cases
- Tested error conditions
Edge cases covered:
- Init with threshold = 0 →
InvalidThreshold - Init with threshold > signers →
InvalidThreshold - Init with threshold = signers count → succeeds
- Init with threshold = 1 → succeeds
- Remove signer down to threshold boundary → succeeds
- Remove signer below threshold →
QuorumWouldBreak - Remove non-existent signer → no-op (ok)
- Execute with exactly threshold approvals → succeeds
- Threshold unchanged after adding more signers
- Quorum snapshot protects against mid-flight threshold changes
- Code comments added/updated
- Documentation updated (if behavior changed)
- README updated (if needed)
- Snapshot test documentation reviewed
- No new security concerns introduced
- Authorization boundaries verified
- Input validation added/verified
- Error handling reviewed
Key security properties:
- Governance bricking prevented:
remove_signerenforcessigners.len() - 1 >= threshold, so the signer set can never shrink below quorum. - In-flight proposal protection: When quorum is first reached, the current threshold is snapshotted in
QuorumInfo. Execution uses this snapshot rather than the live threshold, so an admin cannot raise the threshold after quorum to block execution, nor lower it to let a proposal with fewer approvals through. - Init bounds enforced:
thresholdmust be>= 1and<= signers.len(), preventing degenerate configurations at deployment.
- My code follows the project's style guidelines
- I have performed a self-review of my code
- I have commented my code, particularly in hard-to-understand areas
- I have made corresponding changes to the documentation
- My changes generate no new warnings
- I have added tests that prove my fix is effective or that my feature works
- New and existing unit tests pass locally with my changes
- Any dependent changes have been merged and published
The stream contract (fluxora_stream) has pre-existing compilation errors unrelated to this PR (duplicate type definitions, missing struct fields, etc.), which prevented running the integration tests in contracts/stream/tests/governance_integration.rs. The governance unit tests (27 tests, 10 new) all pass successfully, and cargo build --target wasm32-unknown-unknown -p fluxora_governance completes without errors.
- Code quality and style
- Test coverage adequate
- Documentation complete
- Snapshot changes justified and correct
- Security implications reviewed
- Breaking changes documented