feat(stellar): auction admin rotation via multisig + timelock - #178
Conversation
The premium-name auction admin was fixed at init_auctions with no rotation path, so a lost or compromised operator key had no on-chain remedy short of the WASM upgrade entrypoint, which is not implemented. Reuse the wraith-names governance signer set and the 7-day ROTATION_TIMELOCK_SECS from the signer-rotation flow: propose → approve to quorum → wait out the timelock → execute, under a separate proposal slot so a signer rotation and an admin rotation can be in flight at once. Execution emits AuctionAdminRotated(old_admin, new_admin). Rotation is refused with AuctionInProgress while any auction has a revealed winner and has not settled — its reveal phase and the settle phase that follows — so the operator cannot be swapped mid-auction. A new instance counter tracks those auctions; auctions nobody revealed a bid on have nothing at stake and are not counted. A blocked execution leaves the proposal intact, and settlement is permissionless, so governance can always clear the guard and retry without restarting the timelock. Closes wraith-protocol#165
|
@DSOTec 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! 🚀 |
|
Merged, and this is the best contracts PR of the wave @DSOTec. I told you on #165 that a global auction-liveness check was not implementable in Soroban because there is no key enumeration. You did not take that at face value, and the I have posted a correction on #165 so the wrong version does not stand. Ten rotation tests including the blocked-during-reveal and not-blocked-by-unrevealed cases is exactly the coverage this needed. |
|
It's such a pleasure working on this issue. Looking forward to contributing more @truthixify |
Problem
AuctionConfig.adminis set once atinit_auctionsand there is no rotation path. If the auction operator key is lost or compromised, the only remedy is the WASM upgrade entrypoint, which is not implemented (wraith-names/tests/upgrade_auth.rsstill carries#[ignore]cases). The rest ofwraith-namesalready has the 7-day multisig rotation flow from Wave 7 #5, so auctions should reuse it.Solution
Auction-admin rotation reuses the existing governance signer set, quorum, and
ROTATION_TIMELOCK_SECS(7 days) fromsrc/multisig.rs, under its own proposal slot so a signer rotation and an admin rotation can be in flight at the same time:propose_rotate_auction_admin(caller, new_admin)— auto-approves the callerapprove_rotate_auction_admin(caller)execute_rotate_auction_admin(caller)— after quorum + timelockcancel_rotate_auction_admin(caller)pending_auction_admin_rotation(),auctions_pending_settlement()The issue names a single
rotate_auction_admin(new_admin); a 7-day timelock needs a propose/execute split, so the surface is named to match the existing*_rotate_signersentrypoints.RotationProposalcarries a signer set, so the flow is reused via a mirroredAdminRotationProposal { new_admin, executable_at, approvals }and a sharedrequire_executablequorum+timelock gate thatexecute_rotate_signersnow also uses.Event. Execution emits
AuctionAdminRotatedwith topics("AuctionAdminRotated",)and data(old_admin, new_admin), matching theSignersRotatedconvention.Phase guard. Rotation is rejected with
AuctionInProgresswhile any auction has a revealed winner and has not settled — its reveal phase and the settle phase that follows. A new instance counter (AuctionKey::PendingSettlements) is incremented by the reveal that first gives an auction a winner and decremented at settlement, so the check is O(1) rather than a scan over auctions.Two deliberate choices worth review:
start_auctionis permissionless and free, so anyone could keep opening bidless auctions to indefinitely block an emergency rotation. Counting only auctions with a revealed winner means blocking requires real capital at stake, and it matches "reveal or settle phase" in the sense that matters — value owed to a treasury or a winner.execute.New error codes use the reserved
wraith-namesrange perstellar/ERRORS.md:NamesError::AuctionsNotInitialized = 1600,NamesError::AuctionInProgress = 1601. Consistent withexecute_rotate_signers(which does not require incoming signers to auth), the incoming admin is not required to co-sign.Testing
cargo test -p wraith-names --test auction— 20 passing (12 pre-existing + 8 new):auction_admin_rotation_happy_path— full flow, then asserts theAuctionAdminRotatedcontract id, topics, and(old, new)dataauction_admin_rotation_enforces_quorum_and_timelock—NotSigner,RotationAlreadyPending,QuorumNotMet,AlreadyApprovedRotationauction_admin_rotation_timelock_not_elapsed— rejected atT-1s, succeeds atTauction_admin_rotation_blocked_during_reveal_and_settle— rejected during an active reveal and during the unsettled settle phase, proposal survives both, succeeds aftersettle_auctionauction_admin_rotation_not_blocked_by_unrevealed_auction— bidless auction does not block; the deposit is still refundable in fullauction_admin_rotation_cancel— cancel clears state, fresh proposal restarts the timelockauction_admin_rotation_requires_initialization—MultisigNotInitialized,AuctionsNotInitialized,NoPendingRotationauction_admin_rotation_independent_of_signer_rotation— a pending signer rotation is neither consumed nor blockedAlso verified:
cargo test --workspace(0 failures),cargo fmt --all --check, and the CIERRORS.mdcatalog check run locally against the new variants.Test snapshots: only the six auction snapshots that genuinely gained the new instance entry are updated. Running the suite on a clean
developtree already rewrites ~300 unrelated snapshot files, so that pre-existing drift is left out of this PR.Docs
stellar/MULTISIG.md— full rotation runbook (propose → approve → timelock → phase-guard check → execute → cancel), state inspection, and a compromised-key response note. Marked not yet rehearsed: the futurenet walkthrough and its run link still need a maintainer with deploy access, so that acceptance box stays open.stellar/wraith-names/README.md— rotation surface, event shape, phase guard, and error list.stellar/ERRORS.md— the two new codes.Closes #165