Skip to content

feat: Governance signer rotation for the upgrade guard (#27) - #32

Merged
meshackyaro merged 1 commit into
workman-labs:developmentfrom
jayteemoney:feat/governance-signer-rotation
Jul 24, 2026
Merged

feat: Governance signer rotation for the upgrade guard (#27)#32
meshackyaro merged 1 commit into
workman-labs:developmentfrom
jayteemoney:feat/governance-signer-rotation

Conversation

@jayteemoney

Copy link
Copy Markdown
Contributor

Closes #27
Follow-up from #26 / #16.

What this adds

The governance-guard multi-sig 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 to the shared guard and wires it into every host contract that consumes it.

New flow: propose_signer_rotationapprove_signer_rotationexecute_signer_rotation, plus a get_pending_rotation getter.

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 can execute_signer_rotation swap 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

  • governance-guard: PendingRotation storage + GovernanceDataKey::PendingRotation; propose/approve/execute_signer_rotation + get_pending_rotation; RotationProposed/RotationApproved/RotationScheduled/RotationExecuted events (via #[contractevent]); error variants NoPendingRotation, RotationMismatch, RotationNotReady, RotationTimelockActive, RotationExpired, RotationInProgress; ROTATION_TIMELOCK_LEDGERS const; shared validate_signer_set helper reused by init and rotation.
  • Host contracts (escrow, reputation, loyalty-token, loyalty-emissions): rotation entrypoints + get_pending_rotation, re-exported PendingRotation, and the mapped error variants in each From<GovernanceError> / Error enum.
  • Tests: 17 new guard tests (37 total) covering the full lifecycle, threshold/timelock/expiry windows, new-set validation, event emission, the pending-upgrade cross-effect, and both the minority- and majority-protection properties.

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

  • No new external dependencies; Cargo.lock unchanged.
  • The GovernanceDataKey::PendingRotation key is appended last and rotation is opt-in, so existing deployed state and the upgrade flow are unaffected.
  • Known, bounded limitation (documented in code): while one rotation occupies the single slot, a fresh proposal must wait for the current one to expire — a determined griefer could re-occupy the slot each TTL, but only by winning a public timing race and never blocking a rotation that has already reached threshold.

…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 meshackyaro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is thorough. Great job @jayteemoney and thanks for contributing to Guildworkman

@meshackyaro
meshackyaro merged commit e9f9b79 into workman-labs:development Jul 24, 2026
1 check passed
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.

Governance signer rotation for the upgrade guard

2 participants