docs(ops): add fee-recipient rotation runbook#572
Conversation
Documents how to rotate the protocol fee-recipient Safe if it is ever compromised, keeping the recipient an immutable, auditable hardcoded constant (the chosen design over an env-configurable recipient). Key points captured: - Rotation is a config + redeploy, not a contract upgrade: the recipient is FE-injected via appData and CoW's Settlement pays whatever appData says. Settled orders already paid the old Safe (no clawback); only orders signed after the redeploy use the new recipient. - The full rotation surface, including the two production references the cross-workspace invariant does NOT cover (rebate-indexer OPHIS_SAFE_ADDRESS + test fixtures), so rotations are driven from a repo-wide grep. - EIP-55 checksum step (the 2026-05-17 strict-EIP-55 crash class). - Verify + the rationale for keeping the constant hardcoded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed647c12b1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| grep -rln '0x858f0F5eE954846D47155F5203c04aF1819eCeF8' \ | ||
| --include='*.ts' --include='*.tsx' --include='*.sh' . \ | ||
| | grep -v node_modules | grep -vE '/(dist|build)/' |
There was a problem hiding this comment.
Broaden the rotation grep to include all live address users
The documented discovery command only searches *.ts, *.tsx, and *.sh, but a repo-wide rg shows live operational references it will miss, including infra/shared/cron/safe-drift-check.sh.tmpl (the committed template rendered into the weekly Safe drift monitor per infra/shared/cron/README.md) and contracts/script/SweepSettlementBuffer.s.sol (DEFAULT_SAFE for settlement-buffer sweeps). If operators follow this runbook during a compromised-Safe rotation, those defaults can remain pointed at the old Safe, so monitoring/sweeps may continue using the compromised recipient even though the app and SDK were updated.
Useful? React with 👍 / 👎.
| 5. Merge. The **Deploy to Cloudflare Pages** workflow rebuilds swap.ophis.fi; | ||
| from that point new orders inject the new recipient into appData. |
There was a problem hiding this comment.
Redeploy the backend allowlist before shipping new appData
On the Ophis backend path, changing only the frontend/SDK makes fresh orders carry a recipient that apps/backend/crates/app-data/src/app_data.rs still rejects via PARTNER_FEE_RECIPIENT_ALLOWLIST, and autopilot also filters non-allowlisted recipients in apps/backend/crates/autopilot/src/domain/fee/mod.rs. For a real rotation to a new Safe, the runbook needs to include updating that Rust allowlist and redeploying the backend before or with the frontend; otherwise new swap.ophis.fi orders can be rejected or have their partner-fee policy dropped instead of simply paying the new Safe.
Useful? React with 👍 / 👎.
| - **Settled orders already paid the old Safe.** There is no clawback. If the old | ||
| Safe is compromised, moving its existing balance to safety is a separate Safe | ||
| transaction, handled by the signers, out of scope of this code rotation. |
There was a problem hiding this comment.
Recover unswept settlement balances during rotation
For the Ophis OP fork, settled CIP-75 fees do not necessarily already sit in the old Safe: contracts/script/SweepSettlementBuffer.s.sol and infra/optimism-mainnet/scripts/sweep-to-safe.sh both document that fees accumulate in the Settlement contract until a sweep transfers them to the configured Safe. During a compromised-Safe rotation, treating all settled fees as unrecoverable Safe balance can leave unswept Settlement buffers routed with stale defaults or written off, even though they should be swept to the replacement recipient as part of the rotation.
Useful? React with 👍 / 👎.
| 1. Stand up the **new** 2-of-3 Safe (or new EOA, though a Safe is the standard) | ||
| and record its address. |
There was a problem hiding this comment.
Require cross-chain control of the replacement Safe
This step only says to stand up a new Safe and record its address, but the app/SDK use one hardcoded recipient for every served chain, and the existing constant relies on a CREATE2-deterministic Safe address resolving on all CoW chains. If operators create an ordinary single-chain Safe and reuse that one address globally, fees on other chains can be sent to an address where no Safe is deployed or controlled; the pre-flight should require either an EOA or a replacement Safe deployment plan that proves the same address is controllable on every fee chain before updating the constant.
Useful? React with 👍 / 👎.
Post-merge Codex review of #572 found the first draft missed the OP fork's server-side enforcement and custody model. Rewritten to cover: - P1: the OP backend ALLOWLIST. PARTNER_FEE_RECIPIENT_ALLOWLIST (apps/backend/crates/app-data/src/app_data.rs, enforced there + in autopilot fee/mod.rs) REJECTS any non-allowlisted recipient. Rotating only the FE/SDK makes the backend reject every new order. New ordered step: add the new recipient to the allowlist (raw-byte Address form) and redeploy the OP backend BEFORE/with the frontend; keep the old entry until in-flight orders drain, then remove it. - P2 sweep: OP fees accrue in the Settlement contract until swept, so "already in the old Safe" was wrong. Added a sweep-first step and the sweep config files (SweepSettlementBuffer.s.sol DEFAULT_SAFE, sweep/check scripts). - P2 cross-chain: the constant is one address on every chain; pre-flight now requires the replacement be controllable at the SAME address on OP+Gnosis+ETH (CREATE2 plan or EOA), not a single-chain Safe. - P2 grep: broadened discovery to a repo-wide search and enumerated the full surface (backend allowlist, sweep contract, cron drift monitor, infra scripts, live .env, rebate, docs); dated historical records left intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…574) Post-merge Codex review of #572 found the first draft missed the OP fork's server-side enforcement and custody model. Rewritten to cover: - P1: the OP backend ALLOWLIST. PARTNER_FEE_RECIPIENT_ALLOWLIST (apps/backend/crates/app-data/src/app_data.rs, enforced there + in autopilot fee/mod.rs) REJECTS any non-allowlisted recipient. Rotating only the FE/SDK makes the backend reject every new order. New ordered step: add the new recipient to the allowlist (raw-byte Address form) and redeploy the OP backend BEFORE/with the frontend; keep the old entry until in-flight orders drain, then remove it. - P2 sweep: OP fees accrue in the Settlement contract until swept, so "already in the old Safe" was wrong. Added a sweep-first step and the sweep config files (SweepSettlementBuffer.s.sol DEFAULT_SAFE, sweep/check scripts). - P2 cross-chain: the constant is one address on every chain; pre-flight now requires the replacement be controllable at the SAME address on OP+Gnosis+ETH (CREATE2 plan or EOA), not a single-chain Safe. - P2 grep: broadened discovery to a repo-wide search and enumerated the full surface (backend allowlist, sweep contract, cron drift monitor, infra scripts, live .env, rebate, docs); dated historical records left intact. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Per the decision to keep the protocol fee-recipient an immutable, auditable hardcoded constant (rather than env-configurable), this documents how to rotate it if the 2-of-3 Safe is ever compromised.
Why a runbook (and not env config)
A hardcoded constant + the cross-workspace CI invariant means the fee destination is auditable from source and cannot drift silently or be redirected by whoever controls the deploy env. The trade is that rotation needs a procedure, which is this doc.
What it captures
appData; CoW's Settlement is immutable and pays whatever appData says. Settled orders already paid the old Safe (no clawback); only orders signed after the redeploy use the new recipient.Partner-fee cross-workspace invariantdoes not cover —apps/rebate-indexer/src/safe/addresses.ts(OPHIS_SAFE_ADDRESS) and the test fixtures — so the procedure is driven by a repo-wide grep, not the invariant's 3-file list.@ophis/sdk→ restart rebate-indexer → verify a fresh order'spartnerFee.recipient.Placement
Committed
docs/operations/runbook (alongsidedisaster-recovery-runbook.md,allowlist-governance-runbook.md, etc.). Contains no secrets — the Safe address is already in committed source and the on-chain trust model is public — and being committed makes it resilient and auditable.🤖 Generated with Claude Code