Status: stub # Runbook: Treasury key rotation (devnet mock-USDT) Covers rotating the devnet treasury keypair and/or the mock-USDT mint without double-crediting client ledger balances or double-paying nodes. ## Trust assumptions (read first) Per [ADR-0015](../../../docs/adr/0015-usdt-custodial-settlement.md), a single project-owned wallet custodies all funds; the treasury keypair is loaded only on the operator-designated settlement tracker (ADR-0016 §1). Rotating this key is a trusted-operator action — there is no on-chain multisig or trustless handoff in the alpha design. Devnet uses a self-created mock-USDT SPL mint (6 decimals); real USDT only exists on mainnet, so this procedure is devnet-only until a mainnet cutover ADR supersedes it. ## Prerequisites - Access to `scripts/devnet_setup.py` and its dependencies (`solders`, `meshnet_contracts.solana_adapter.SolanaCustodialTreasury`). - The current treasury keypair path (default `~/.config/solana/meshnet-treasury.json`, or whatever `--treasury-keypair` the running tracker uses) and current `MESHNET_USDT_MINT` / `MESHNET_TREASURY_WALLET` values (see `.env.devnet`, never committed). - Ability to stop/restart the settlement-capable tracker. - Confirm the deposit watcher's dedupe state (transaction signatures already credited) is durable — it must survive the rotation so replayed/rescanned transfers under the *old* wallet don't get re-credited under the *new* one. ## Two rotation scenarios ### A. Rotate the treasury keypair only (same mint, same on-chain wallet funds move) The treasury wallet address changes because it's derived from the keypair, so this requires migrating funds, not just swapping a file. 1. Generate a new keypair (do **not** reuse `_load_or_create_keypair` against the old path — write to a new path so both keys exist during the transition): ``` python scripts/devnet_setup.py --keypair ~/.config/solana/meshnet-treasury-new.json \ --mint --env-out .env.devnet.new ``` This creates the new treasury wallet + token account and reuses the existing mint (no new token, so client balances denominated in that mint are unaffected). 2. Drain the old treasury token account to the new one via a single SPL transfer sized to the *entire current balance* (record the exact amount and the source tx signature before moving anything). 3. **Freeze settlement during the drain**: stop the settlement-capable tracker (or restart it with no `--treasury-keypair` so the settlement loop is inert) before step 2, so no payout is in flight against the old wallet while funds move. 4. Update the tracker's `--treasury-keypair`, `--treasury-wallet`-derived config (i.e. the new `.env.devnet`) and restart the tracker pointed at the new keypair. 5. Verify: `treasury.get_sol_balance()` / mock-USDT balance on the new wallet matches the old wallet's pre-drain balance; old wallet balance is zero. 6. Only after verification, revoke/delete the old keypair file. ### B. Rotate the mock-USDT mint (e.g. compromised or mis-configured mint) This is a bigger change — it invalidates every client's existing off-chain ledger balance denomination reference and any node's pending on-chain payout expectations. Treat as a deliberate migration, not a routine rotation: 1. Settle (pay out) all pending node balances against the *old* mint before cutover — the pending-balance forfeiture/collateral model (ADR-0015) assumes pending balances are payable in a known mint. 2. Create the new mint and treasury token account: ``` python scripts/devnet_setup.py --keypair --env-out .env.devnet ``` (omit `--mint` so a fresh mint is created). 3. Update tracker config (`MESHNET_USDT_MINT`) and restart. 4. Re-mint/airdrop mock USDT to active client wallets under the new mint as needed (`--mint-to`), since off-chain ledger balances are *not* automatically re-denominated — this is a devnet convenience step, not a guarantee that would hold for real USDT. ## Avoiding double-credit The deposit watcher (issue 32) dedupes by on-chain transaction signature. The signature space for the old and new treasury token accounts/mints is disjoint, so: - Do not replay old-wallet deposit history against the new wallet's watcher — it has no record of those signatures and would (correctly) not credit them, but any manual "catch-up crediting" script must not re-process transfers the old watcher already credited. Cross-check the old ledger's credited-tx-sig table before any manual reconciliation entry. - Keep the old watcher's dedupe DB/table around (don't drop it as part of rotation) until you've confirmed no in-flight deposits to the old address remain unconfirmed. ## Rollback - Scenario A: if the new wallet fails verification, restart the tracker with the old `--treasury-keypair` — no client-facing state changed since ledger balances are keyed by API key, not treasury wallet address. - Scenario B: if re-minting under the new mint goes wrong, restart the tracker against the old `MESHNET_USDT_MINT` config; nothing was destroyed on the old mint. ## Secrets handling - Never commit `.env.devnet`, `.env.devnet.new`, or any `*treasury*.json` keypair file. `scripts/devnet_setup.py` writes keypairs with `0o600` permissions — preserve that when copying. - Treat the treasury keypair as the single highest-value secret in this system per ADR-0015/ADR-0016: anyone with it can drain custodial funds.