Skip to content

feat(stellar): on-chain multisig quorum rotation for signers (#104)#141

Open
EmperorNexus wants to merge 1 commit into
wraith-protocol:developfrom
EmperorNexus:feat/104-multisig-quorum-rotation
Open

feat(stellar): on-chain multisig quorum rotation for signers (#104)#141
EmperorNexus wants to merge 1 commit into
wraith-protocol:developfrom
EmperorNexus:feat/104-multisig-quorum-rotation

Conversation

@EmperorNexus

Copy link
Copy Markdown

Summary

Closes #104. Adds an on-chain rotate_signers flow - gated by quorum-of-current-signers plus a 7-day timelock - to the two contracts GOVERNANCE.md designates Timelock + Multisig Upgradable: stealth-sender and wraith-names.

Why both contracts

The issue's suggested path (contracts/*/src/multisig.rs) is a glob; this repo has no single admin-gated contract to attach it to - today's admin is a plain Stellar account address, multisig'd off-chain (see MULTISIG.md), with zero on-chain visibility into signers/threshold from any contract. GOVERNANCE.md designates both stealth-sender and wraith-names as Timelock + Multisig Upgradable with the same 3-of-5 / 7-day model, so both got a symmetric src/multisig.rs module rather than picking one arbitrarily.

Design

Each contract tracks its own governance signer set independently (DataKey::MultisigSigners / MultisigThreshold / PendingRotation) - this is a second, independent layer from the account-level Stellar multisig: the account multisig controls who can submit transactions at all; this controls quorum + timelock for signer rotation specifically, enforced by the contract regardless of which account submitted the call.

Flow: init_multisig (one-shot bootstrap) -> propose_rotate_signers (any current signer; auto-approves; only one rotation pending at a time) -> approve_rotate_signers (remaining signers, asynchronously - matches the existing off-chain MULTISIG.md 'collect M signatures over time' UX rather than requiring one atomic co-signed transaction) -> execute_rotate_signers (once approvals >= current threshold AND 7 days have elapsed since proposal; emits SignersRotated(new_signers, old_threshold, new_threshold)) -> cancel_rotate_signers (any current signer, any time before execution; fully clears proposal state so a fresh rotation can be proposed immediately).

InvalidThreshold is a dedicated error rejecting threshold 0 or threshold > new_signers.len(), at both init_multisig and propose_rotate_signers.

Acceptance criteria

  • Rotation requires quorum + timelock - test_rotate_signers_requires_quorum_and_timelock (both contracts): asserts QuorumNotMet with insufficient approvals, TimelockNotElapsed after quorum but before the delay, success after both.
  • Invalid thresholds rejected with dedicated error - test_init_multisig_rejects_invalid_threshold, test_propose_rotate_signers_rejects_invalid_threshold.
  • Test covers cancelled-mid-rotation state cleanup - test_cancelled_rotation_clears_state: cancel mid-approval, confirms proposal/approvals are fully cleared, original signers/threshold untouched, a stale approve/execute/cancel against the cleared proposal fails with NoPendingRotation, and a fresh proposal can be made immediately.
  • Bonus: test_non_signer_cannot_propose_or_approve.
  • MULTISIG.md updated with the on-chain rotation runbook (CLI examples for every step plus a state-inspection section).
  • MULTISIG.md runbook rehearsed on futurenet - not done. This requires a live futurenet deployment and a maintainer with deploy access, which I do not have. Called out explicitly in MULTISIG.md's new 'Futurenet rehearsal' section as not yet rehearsed, with the exact walkthrough steps a maintainer should follow (propose -> approve x2 -> advance past 7-day timelock -> execute, and separately propose -> approve -> cancel -> propose again) before relying on this runbook for a real mainnet rotation.

Test location note

Tests are inline in each contract's existing #[cfg(test)] mod test in src/lib.rs, matching how every existing test in both stealth-sender and wraith-names is already organized (there is no per-contract tests/ directory convention in use, and the repo's shared integration-tests crate is currently non-functional - its src is a stray file containing a misnamed Cargo.toml, no real harness - so a new file there wouldn't compile without unrelated scaffolding repair).

Test plan

  • cd stellar && cargo test -p stealth-sender -p wraith-names (I could not run this locally - this sandbox has no MSVC linker installed and the available MinGW toolchain fails to link due to a space in its own install path, unrelated to this repo. Please run in CI / locally before merge.)
  • Manual review of every call against soroban-sdk 22 APIs already used elsewhere in both files (Vec::contains, env.ledger().timestamp(), env.storage().instance(), Symbol::new).

Add rotate_signers flow to stealth-sender and wraith-names (the two
Timelock + Multisig Upgradable contracts per GOVERNANCE.md), gated by
quorum approval from current signers plus a 7-day timelock matching
the existing upgrade timelock. Invalid thresholds (zero, or greater
than the proposed signer count) are rejected with a dedicated error.
Execution emits SignersRotated; cancellation fully clears pending
proposal state so a new rotation can be proposed immediately.

Update MULTISIG.md with the on-chain rotation runbook. Futurenet
rehearsal is called out as a follow-up since it requires a live
deployment.

Closes wraith-protocol#104
@drips-wave

drips-wave Bot commented Jul 24, 2026

Copy link
Copy Markdown

@EmperorNexus Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-sig quorum rotation flow

2 participants