Skip to content

docs(ops): add fee-recipient rotation runbook#572

Merged
san-npm merged 1 commit into
mainfrom
docs/fee-recipient-rotation-runbook
Jun 12, 2026
Merged

docs(ops): add fee-recipient rotation runbook#572
san-npm merged 1 commit into
mainfrom
docs/fee-recipient-rotation-runbook

Conversation

@san-npm

@san-npm san-npm commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

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

  • Rotation is a config + redeploy, not a contract upgrade. The recipient is FE-injected into each order's 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.
  • The complete rotation surface, including the two production references the Partner-fee cross-workspace invariant does 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.
  • The EIP-55 checksum step (the 2026-05-17 strict-EIP-55 init-crash class).
  • Step-by-step: update all mirrors + invariant literal → local invariant check → typecheck/test → PR (Codex + CI gate) → merge → FE redeploy → republish @ophis/sdk → restart rebate-indexer → verify a fresh order's partnerFee.recipient.

Placement

Committed docs/operations/ runbook (alongside disaster-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

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +62 to +64
grep -rln '0x858f0F5eE954846D47155F5203c04aF1819eCeF8' \
--include='*.ts' --include='*.tsx' --include='*.sh' . \
| grep -v node_modules | grep -vE '/(dist|build)/'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +108 to +109
5. Merge. The **Deploy to Cloudflare Pages** workflow rebuilds swap.ophis.fi;
from that point new orders inject the new recipient into appData.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +22 to +24
- **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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +38 to +39
1. Stand up the **new** 2-of-3 Safe (or new EOA, though a Safe is the standard)
and record its address.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@san-npm san-npm merged commit fb83ce3 into main Jun 12, 2026
16 checks passed
@san-npm san-npm deleted the docs/fee-recipient-rotation-runbook branch June 12, 2026 12:49
san-npm added a commit that referenced this pull request Jun 12, 2026
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>
san-npm added a commit that referenced this pull request Jun 12, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant