feat: Governance signer rotation for the upgrade guard (#27) - #32
Merged
meshackyaro merged 1 commit intoJul 24, 2026
Merged
Conversation
…abs#27) The governance-guard multi-sig previously froze its signer set and threshold at initialize, with no way to change them short of a full contract upgrade. This adds a timelocked, threshold-gated signer rotation flow (propose -> approve -> execute) to the shared guard and wires it into every host contract that consumes it. Security model: - Authorized by the current signer set at the current (upgrade) threshold -- rotation grants the set no power it lacked via upgrade. - Mandatory timelock between reaching threshold and execution gives a minority about to be removed a visible on-chain window: no silent, instant lockout (minority protection). - No unilateral veto; only one live rotation at a time, so a lone signer can neither reset a gathering rotation's approvals nor displace a scheduled one -- a compromised signer cannot block its own removal (majority protection). - Executing a rotation clears any pending upgrade, whose approvals came from the old signer set. Adds PendingRotation storage, RotationProposed/Approved/Scheduled/ Executed events, five rotation error variants, and 37 guard tests (now covering the full rotation lifecycle and both protection properties). Host contracts (escrow, reputation, loyalty-token, loyalty-emissions) gain propose/approve/execute_signer_rotation and get_pending_rotation entrypoints plus the mapped error variants. Follow-up from workman-labs#26 / workman-labs#16.
meshackyaro
approved these changes
Jul 24, 2026
meshackyaro
left a comment
Contributor
There was a problem hiding this comment.
This is thorough. Great job @jayteemoney and thanks for contributing to Guildworkman
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #27
Follow-up from #26 / #16.
What this adds
The
governance-guardmulti-sig froze its signer set and threshold atinitialize, with no way to change them short of a full contract upgrade. This adds a timelocked, threshold-gated signer rotation flow to the shared guard and wires it into every host contract that consumes it.New flow:
propose_signer_rotation→approve_signer_rotation→execute_signer_rotation, plus aget_pending_rotationgetter.Design decisions (the part #27 asked to nail down)
Authorization — same threshold as an upgrade. A rotation is approved by the current signer set at the current threshold. Gated no more weakly than an upgrade, and no more strongly either: a strictly-higher threshold is impossible once the threshold is already N-of-N, and the current set can in any case already replace all of a contract's code (governance included) via the upgrade flow. So the same threshold is the honest bound.
Minority protection — a mandatory timelock. Reaching threshold does not apply the rotation; it schedules it
ROTATION_TIMELOCK_LEDGERS(~3 days) in the future. Only after that delay canexecute_signer_rotationswap the set in. A minority about to be removed gets a guaranteed, publicly visible on-chain window — no silent, instant lockout.Majority protection — no unilateral veto, single live slot. Unlike an upgrade (any signer can
cancel_upgrade, because blocking a change is safe), a rotation cannot be cancelled or displaced by one signer — for rotation the status quo itself may be the threat (a lost/compromised key). Only one rotation may be in flight, so a lone signer can neither reset a gathering rotation's approvals nor displace a scheduled one, i.e. a compromised signer cannot block its own removal. Aborting/amending requires a fresh threshold agreement (a superseding proposal once the current one expires).Cross-effect — executing a rotation clears any pending upgrade, whose approvals were gathered under the old signer set and must not carry into the new one.
Changes
PendingRotationstorage +GovernanceDataKey::PendingRotation;propose/approve/execute_signer_rotation+get_pending_rotation;RotationProposed/RotationApproved/RotationScheduled/RotationExecutedevents (via#[contractevent]); error variantsNoPendingRotation,RotationMismatch,RotationNotReady,RotationTimelockActive,RotationExpired,RotationInProgress;ROTATION_TIMELOCK_LEDGERSconst; sharedvalidate_signer_sethelper reused by init and rotation.escrow,reputation,loyalty-token,loyalty-emissions): rotation entrypoints +get_pending_rotation, re-exportedPendingRotation, and the mapped error variants in eachFrom<GovernanceError>/Errorenum.Verification
Ran the full CI locally against
soroban-contracts:cargo fmt --check✓cargo clippy --workspace --all-targets -- -D warnings✓cargo test --workspace✓ (178 tests: guard 37, escrow 37, reputation 34, loyalty-emissions 30, dispute-resolution 28, loyalty-token 12)cargo build --workspace --release --target wasm32v1-none✓Notes for the maintainer
Cargo.lockunchanged.GovernanceDataKey::PendingRotationkey is appended last and rotation is opt-in, so existing deployed state and the upgrade flow are unaffected.