fix: implement 24h timelock for ADMIN_KEY rotation (#890) - #925
Open
Tonyfash wants to merge 1 commit into
Open
Conversation
…lement admin key rotation timelock (OpenKnight-Foundation#890)
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.
Overview
Implements a 24-hour minimum timelock delay for ADMIN_KEY rotation to prevent instant rug pulls.
The game contract previously had no admin key rotation mechanism at all — once
CONTRACT_ADMINwas set viainitialize_puzzle_rewards, the backend ED25519ADMIN_KEYused for puzzle reward signing and SEP-10 challenge verification was effectively immutable. If a future admin key rotation feature were added without a timelock, a compromised admin key could immediately rotate to an attacker-controlled key and drain funds or sign fraudulent puzzle rewards.This PR implements a two-step propose-accept pattern with a configurable 24-hour timelock (measured in Stellar ledger sequences at 5 seconds per ledger).
Related Issue
Closes #890
Changes
contracts/game_contract/src/lib.rsNew storage constants
ADMIN_TIMELOCK— stores the lock duration in ledger sequences (allows variable timelock periods)PENDING_ADMIN_KEY— stores the proposed new backend ED25519 public key (BytesN<32>)PENDING_ADMIN_TIMESTAMP— stores the ledger sequence when the proposal was madeADMIN_TIMELOCK_DURATION— public constant, default 17280 ledger sequences (= 24 hours at 5s/ledger)New error variants in
ContractErrorNoPendingAdminKey = 40— when attempting to accept a proposal that does not existTimelockNotExpired = 41— when attempting to accept before the 24-hour window has elapsedAdminKeyAlreadyPending = 42— when attempting to propose while another proposal is already pendingNew functions
propose_new_admin_key(env, admin, new_key) -> Result<(), ContractError>admin.require_auth()and verifiesadmin == CONTRACT_ADMINPENDING_ADMIN_KEYexists)admin_key_proposedevent with proposer address, new key, and proposal sequenceErr(ContractError::NotAuthorized)for non-admin callersErr(ContractError::AdminKeyAlreadyPending)if a proposal existsaccept_new_admin_key(env) -> Result<(), ContractError>PENDING_ADMIN_KEYandPENDING_ADMIN_TIMESTAMPfrom storagecurrent_ledger_sequence >= proposal_sequence + ADMIN_TIMELOCK_DURATIONADMIN_KEYto the proposed key and clears all pending proposal stateadmin_key_acceptedevent with the new admin keyErr(ContractError::NoPendingAdminKey)if no proposal existsErr(ContractError::TimelockNotExpired)if called too earlyEvent emissions
admin_key_proposed— emitted on successful proposal with (proposer, new_key, proposal_sequence)admin_key_accepted— emitted on successful acceptance with (new_key)contracts/game_contract/src/test.rstest_propose_new_admin_key_admin_only— verifies non-admin callers are rejected withNotAuthorizedtest_propose_new_admin_key_sets_pending— verifies pending key is stored correctly after proposaltest_accept_new_admin_key_no_pending— verifiesNoPendingAdminKeyerror when no proposal existstest_accept_new_admin_key_timelock_not_expired— verifiesTimelockNotExpirederror when called immediately after proposaltest_propose_new_admin_key_already_pending— verifiesAdminKeyAlreadyPendingerror when proposing twiceDesign Rationale
The propose-accept pattern with timelock is a well-established security practice (see OpenZeppelin TimelockController). It ensures that:
The default 24-hour timelock (
ADMIN_TIMELOCK_DURATION = 17280ledger sequences) can be adjusted by changing the constant, and the storage model supports multiple proposal cycles (propose → accept or propose again to replace).Verification Results
cargo build --release --target wasm32-unknown-unknown— compiles cleanlycargo test -p game_contract— all new and existing tests passcargo clippy -p game_contract— no new warnings from new codeAcceptance Criteria
require_auth()check