This runbook covers the step-by-step procedures for rotating the admin key on either contract, or rotating the oracle address trusted by the escrow contract. Follow these steps exactly; deviating from the order or skipping verification can leave the contract in an unrecoverable state.
- Prerequisites
- Rotate the Oracle Address (Escrow)
- Rotate the Escrow Admin (Two-Step)
- Rotate the Oracle Contract Admin (Direct)
- Verification Steps
- Rollback Procedures
- Event Reference
Before starting any rotation:
- Confirm the current admin keypair is available and operational.
- Confirm the new keypair has been generated, backed up, and tested (e.g., can sign a testnet transaction).
- If using a multi-sig wallet, ensure all required signers are reachable.
- Note current contract state — check that no rotation is already in progress:
# Escrow: confirm current admin and oracle
stellar contract invoke --id $ESCROW_CONTRACT_ID --network <network> -- get_admin
stellar contract invoke --id $ESCROW_CONTRACT_ID --network <network> -- get_oracle
# Oracle: confirm current admin
stellar contract invoke --id $ORACLE_CONTRACT_ID --network <network> -- get_admin- Recommended: pause the relevant contract during rotation to prevent new activity from racing with the key change (see runbook-pause.md).
Use this procedure when the oracle keypair is compromised or the oracle service is being migrated to a new contract address.
Who can execute: Escrow admin only.
stellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--source <ESCROW_ADMIN_KEYPAIR> \
--network <network> \
-- pausestellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--source <ESCROW_ADMIN_KEYPAIR> \
--network <network> \
-- update_oracle \
--new_oracle <NEW_ORACLE_CONTRACT_ADDRESS>new_oracle must be a valid contract address and must not be the escrow contract's own address. The call fails with InvalidAddress if either condition is violated.
stellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--network <network> \
-- get_oracle
# Expected output: <NEW_ORACLE_CONTRACT_ADDRESS>Confirm the admin.oracle_up event was emitted (see Event Reference).
Submit a test result via the new oracle on testnet (or a staging environment) to confirm end-to-end connectivity before unpausing production.
stellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--source <ESCROW_ADMIN_KEYPAIR> \
--network <network> \
-- unpauseThe escrow contract uses a two-step handoff to prevent accidentally setting an admin address nobody controls. The current admin proposes a nominee; the nominee must accept before control transfers.
Who can execute step 1: Current escrow admin.
Who can execute step 2: The nominated new admin.
stellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--source <CURRENT_ESCROW_ADMIN_KEYPAIR> \
--network <network> \
-- propose_admin \
--new_admin <NEW_ESCROW_ADMIN_ADDRESS>Until the nominee calls
accept_admin, the current admin retains full control. The proposal can be superseded by callingpropose_adminagain with a different address if needed.
stellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--source <NEW_ESCROW_ADMIN_KEYPAIR> \
--network <network> \
-- accept_adminControl transfers atomically at this point.
stellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--network <network> \
-- get_admin
# Expected output: <NEW_ESCROW_ADMIN_ADDRESS>Confirm the admin.xfer event was emitted (see Event Reference).
- Retire the old admin keypair from all secrets managers and CI/CD systems.
- Confirm the old admin address can no longer call admin-gated functions (e.g., attempt a
pausewith the old key; it should fail withUnauthorized).
The oracle contract uses a single-step admin rotation. The current admin directly sets the new admin address — there is no nomination/acceptance step. Take extra care to verify the new address before submitting.
Who can execute: Current oracle admin only.
stellar contract invoke \
--id $ORACLE_CONTRACT_ID \
--source <ORACLE_ADMIN_KEYPAIR> \
--network <network> \
-- pausestellar contract invoke \
--id $ORACLE_CONTRACT_ID \
--source <ORACLE_ADMIN_KEYPAIR> \
--network <network> \
-- update_admin \
--new_admin <NEW_ORACLE_ADMIN_ADDRESS>Control transfers immediately.
stellar contract invoke \
--id $ORACLE_CONTRACT_ID \
--network <network> \
-- get_admin
# Expected output: <NEW_ORACLE_ADMIN_ADDRESS>Confirm the admin.admin_rot event was emitted (see Event Reference).
stellar contract invoke \
--id $ORACLE_CONTRACT_ID \
--source <NEW_ORACLE_ADMIN_KEYPAIR> \
--network <network> \
-- unpauseRetire the old oracle admin keypair from all secrets managers and automated services.
After any rotation, run this full verification pass:
# Confirm escrow admin
stellar contract invoke --id $ESCROW_CONTRACT_ID --network <network> -- get_admin
# Confirm oracle address known to escrow
stellar contract invoke --id $ESCROW_CONTRACT_ID --network <network> -- get_oracle
# Confirm oracle contract admin
stellar contract invoke --id $ORACLE_CONTRACT_ID --network <network> -- get_admin
# Confirm contracts are not paused (unless deliberately left paused)
stellar contract invoke --id $ESCROW_CONTRACT_ID --network <network> -- is_paused
stellar contract invoke --id $ORACLE_CONTRACT_ID --network <network> -- is_pausedSpot-check by running a low-stakes match end-to-end on testnet with the new keys, from create_match through submit_result, and verify the payout is processed.
If the new oracle address is wrong or unreachable, call update_oracle again with the old oracle address — provided the escrow admin key is still available:
stellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--source <ESCROW_ADMIN_KEYPAIR> \
--network <network> \
-- update_oracle \
--new_oracle <OLD_ORACLE_CONTRACT_ADDRESS>If the proposal has not yet been accepted, overwrite it with a corrected nominee:
stellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--source <CURRENT_ESCROW_ADMIN_KEYPAIR> \
--network <network> \
-- propose_admin \
--new_admin <CORRECT_NEW_ADMIN_ADDRESS>Once accept_admin has been called, the old admin has no authority. The new admin must use propose_admin + accept_admin to transfer control back:
# New admin proposes the old (or another) admin
stellar contract invoke \
--id $ESCROW_CONTRACT_ID \
--source <NEW_ESCROW_ADMIN_KEYPAIR> \
--network <network> \
-- propose_admin \
--new_admin <PREVIOUS_OR_RECOVERY_ADMIN_ADDRESS>Because update_admin is a direct one-step transfer, rollback requires the new admin to call update_admin again:
stellar contract invoke \
--id $ORACLE_CONTRACT_ID \
--source <NEW_ORACLE_ADMIN_KEYPAIR> \
--network <network> \
-- update_admin \
--new_admin <PREVIOUS_OR_RECOVERY_ADMIN_ADDRESS>If the new admin keypair is lost or compromised, there is no on-chain recovery path. Prevent this by verifying the new keypair can sign transactions before completing any rotation.
Monitor for these events to confirm operations were recorded on-chain:
| Event topic | Contract | Data | Meaning |
|---|---|---|---|
(admin, oracle_up) |
Escrow | (old_oracle, new_oracle) |
Oracle address updated |
(admin, xfer) |
Escrow | (old_admin, new_admin) |
Admin transfer completed |
(admin, admin_rot) |
Oracle | (old_admin, new_admin) |
Oracle admin rotated |
(admin, paused) |
Either | — | Contract paused |
(admin, unpaused) |
Either | — | Contract unpaused |
Use a Stellar event streaming endpoint or stellar events CLI to confirm these events appear in the ledger after each operation.