Crownridge Incident Response
Status: pre-mainnet procedure document (spec §87, §165). Crownridge is not deployed to mainnet and has not received an external audit. This document must be finalized — with real deployed addresses from
deployments/<chainId>-latest.json, confirmed multisig signer sets, and a completed incident drill — before any mainnet launch (spec §88, §164). Nothing here is a guarantee of loss prevention; it is the operating procedure for limiting and disclosing damage.
This document covers: who acts, required response time, the exact contract and function to call, the multisig/timelock procedure, the communication process, and the postmortem process — for every material incident scenario in V1.
1. Actors and authorities
| Actor | On-chain identity | Powers (exact) |
|---|---|---|
| Guardian (security multisig) | SECURITY_MULTISIG |
PAUSER_ROLE on CrownridgeGenesis, CrownridgeBuyback, CrownridgeMintController (pause() / unpause()); GUARDIAN_ROLE on CrownridgeTreasury (setFounderWithdrawPaused(bool)); CANCELLER_ROLE on the TimelockController (cancel(bytes32 id)). All immediate — no delay. |
Timelock (OZ TimelockController, 48h min delay — TIMELOCK_MIN_DELAY) |
deployed per manifest | DEFAULT_ADMIN_ROLE on every protocol contract (role grants/revocations), PARAMETER_ADMIN_ROLE (Genesis/Buyback params, Buyback.setEnabled), RESERVE_MANAGER_ROLE on Treasury (writeDownRecognized, sweepExcess, rescueToken) and LiquidityManager (windDown). Everything it does is visible on-chain for 48h before execution. |
| Protocol multisig | PROTOCOL_MULTISIG |
Sole PROPOSER_ROLE on the Timelock — the only account that can queue timelocked operations. Has no direct power over any protocol contract. |
| Buyback executor | BUYBACK_EXECUTOR (ops address) |
BUYBACK_EXECUTOR_ROLE: may call Buyback.executeBuyback only; every safety check binds on-chain regardless of this caller. |
| Founder | immutable founder in FounderTreasuryController |
May call FounderTreasuryController.withdraw(recipient, amount), which calls Treasury.founderWithdraw. The only human-directed reserve outflow. Disclosed trust assumption (see §7 and docs/RISK.md). |
| Anyone | — | The Timelock's executor role is open (address(0)): once a queued operation's 48h delay elapses and it has not been cancelled, anyone may call execute. Useful in an emergency — execution cannot be bottlenecked on one key. |
Verified in contracts/script/Deploy.s.sol (_wire, _handOff, _postAssert): the deployer retains
no role anywhere post-deploy; the Timelock is self-administered; the guardian holds CANCELLER.
The two speeds — the core principle
Pauses are immediate. Everything else is timelocked (48h).
The guardian can stop any subsystem now; it cannot move funds, change parameters, or grant/revoke roles. Role revocation, write-downs, parameter changes, and unwinding all route through the 48h Timelock proposed by the protocol multisig. In every scenario below, the pause is the immediate lever and the timelocked action is the durable follow-up. Plan on that 48h gap: what is the state of the system while the follow-up is queued, and what do we tell the public during it?
IMMEDIATE (T+0) DELAYED (T+48h)
┌────────────────────────┐ ┌─────────────────────────┐
Guardian ─────►│ Genesis.pause() │ │ revokeRole(...) │
(security │ Buyback.pause() │ │ writeDownRecognized(...)│
multisig) │ MintController.pause() │ │ setRiskParams / caps │
│ Treasury.setFounder- │ │ Buyback.setEnabled(f) │
│ WithdrawPaused(true) │ │ LiquidityMgr.windDown() │
│ Timelock.cancel(id) │ └─────────▲───────────────┘
└────────────────────────┘ │ schedule → 48h → execute
│
Protocol multisig (sole proposer) ───────────────────────────┘
Guardian CANCELLER can veto any queued operation at any point before execution.
Note the deliberate asymmetry: a pause is reversible and moves no funds, so it is safe to execute on suspicion. A wrong pause costs hours of downtime; a slow pause can cost the reserve. When in doubt, pause. Investigate second.
2. Detection and response-time targets
Detection sources (spec §86; implemented as indexer alert rules): FounderTreasuryWithdrawal,
BuybackFunded / BuybackExecuted, ReserveWrittenDown, Issued (unexpected mint), Paused /
Unpaused, RoleGranted / RoleRevoked, Timelock CallScheduled, supply-vs-issuance mismatch,
oracle deviation, conservation check (Treasury.conservationHolds() polled), large transfers.
| Milestone | Target | Owner |
|---|---|---|
| Alert acknowledged | ≤ 15 minutes, 24/7 | On-call (rotating; both multisig teams) |
| Incident confirmed or dismissed | ≤ 30 minutes from alert | On-call + one guardian signer |
| Guardian pause executed | ≤ 1 hour from confirmation (severity-1: as fast as quorum allows) | Guardian |
| Initial public notice | ≤ 4 hours from confirmation | Comms owner (see §8) |
| Timelocked follow-up queued | ≤ 24 hours from confirmation | Protocol multisig |
| Full postmortem published | ≤ 7 days from resolution | Incident commander |
Guardian signers must hold a standing pre-agreement: any signer may initiate, and all signers sign a pause transaction on a confirmed severity-1 alert without debate — pausing is non-destructive and reversible. Signers should be geographically distributed with tested hardware-wallet access. These targets are commitments of process, not guarantees of outcome; a single-transaction exploit can complete before any human response (see §7 for the worst case).
Severity levels
- SEV-1 — active or imminent loss of reserve, unauthorized mint, founder-key compromise, malicious queued timelock op. Pause first, confirm in parallel.
- SEV-2 — vulnerability discovered but not exploited; oracle degradation; reserve-asset stress. Confirm, then pause the affected subsystem.
- SEV-3 — monitoring anomaly, infra outage (indexer/website), no on-chain risk. No pause; investigate and disclose if user-visible.
3. Standing procedures
3.1 Guardian pause (immediate)
One Safe transaction from the security multisig, calling the target directly:
| Lever | Target contract | Call |
|---|---|---|
| Stop Genesis deposits | CrownridgeGenesis |
pause() |
| Stop buybacks | CrownridgeBuyback |
pause() |
| Stop all CRWN issuance | CrownridgeMintController |
pause() |
| Stop founder withdrawals | CrownridgeTreasury |
setFounderWithdrawPaused(true) |
| Veto a queued admin op | TimelockController |
cancel(bytes32 id) |
Effects: Genesis.pause() blocks deposit (whenNotPaused); MintController.pause() blocks
issue — no CRWN can be minted by anyone; Buyback.pause() blocks executeBuyback;
setFounderWithdrawPaused(true) makes Treasury.founderWithdraw revert with
FounderWithdrawIsPaused. CRWN transfers are never pausable (by design — no pause-on-transfer),
and the Uniswap pool is external and cannot be paused by the protocol.
Unpausing uses the same functions/roles and is also immediate, but policy requires a written resolution and sign-off from both multisigs before any unpause.
3.2 Timelocked operation (48h)
- Queue — protocol multisig calls
TimelockController.schedule(target, 0, data, predecessor, salt, delay)(orscheduleBatch) withdelay ≥ 48h. EmitsCallScheduled— monitoring alerts on every schedule, so an unauthorized queue is itself a detected incident. - Wait — 48h minimum. The guardian may
cancel(id)at any time before execution (id = hashOperation(target, 0, data, predecessor, salt)). - Execute — anyone calls
TimelockController.execute(target, 0, data, predecessor, salt).
During incidents, publish the queued operation (target, calldata, id, ETA) in the incident notice
so the public can verify exactly what will change and when.
4. Scenario runbooks
Each runbook: detection → who acts → immediate action (exact call) → timelocked follow-up → communication → recovery → postmortem trigger.
IR-1 · Oracle failure or manipulation (Uniswap v3 TWAP)
The buyback's only oracle is the CRWN/USDG pool TWAP (UniV3TwapLib.consult via pool.observe).
First line of defense is the contract itself — Buyback.executeBuyback fails closed without any
human action: cardinality floor (CardinalityTooLow), pool-liquidity floor
(PoolLiquidityTooLow), zero/invalid price (OracleInvalid), spot-vs-TWAP deviation bound
(DeviationTooHigh), and the post-swap measured-delta accretion check (NotAccretive). A
manipulated pool makes buybacks revert, not misfire.
- Detection: oracle-deviation alert; repeated
executeBuybackreverts; abnormal pool activity;Buyback.preflight()returningok=falsepersistently. - Who acts: Guardian. Response time: ≤ 1 hour (SEV-2; SEV-1 if manipulation is active).
- Immediate:
CrownridgeBuyback.pause()— belt over the contract's own braces, and it stops executor attempts from spending gas into a manipulated market. - Timelocked follow-up (as needed):
Buyback.setEnabled(false)(durable disable,PARAMETER_ADMIN_ROLE); orBuyback.setRiskParams(...)to raisetwapWindow,minCardinality,minPoolLiquidity, or tightenmaxDeviationBpsbefore re-enabling. - Communication: notice that buybacks are suspended pending oracle review; NAV and Genesis are unaffected (the oracle is used only by the buyback path — Genesis pricing is a fixed rate).
- Recovery: verify pool health (cardinality, liquidity, price behavior) over ≥ 48h of
observation, then
unpause()after two-multisig sign-off. - Postmortem: required if paused > 24h or if any executed buyback is suspected non-accretive.
IR-2 · Reserve depeg or freeze (USDG)
NAV is recognizedReserve in USDG, and USDG≈$1 is a stated assumption, not a guarantee
(docs/ECONOMICS.md §1). If USDG depegs, is frozen, or is otherwise impaired, NAV is overstated in
USD terms and issuance pricing is wrong.
- Detection: USDG market-price deviation on major venues; official issuer communications; transfer failures on fork/monitoring probes.
- Who acts: Guardian immediately; protocol multisig for the write-down. Response time: pause ≤ 1 hour of confirmation (SEV-1 if severe).
- Immediate (guardian, one Safe batch):
CrownridgeGenesis.pause()— stop minting against an impaired reserve.CrownridgeMintController.pause()— defense in depth: no issuance path at all.CrownridgeBuyback.pause()— the accretion check is denominated in USDG; with USDG impaired, "accretive in USDG" may be dilutive in real terms. Suspend until re-marked.Treasury.setFounderWithdrawPaused(true)— optional but recommended in a severe depeg, so the impaired-reserve allocation question is resolved deliberately, not by withdrawal order.
- Timelocked follow-up: queue
Treasury.writeDownRecognized(newRecognized, reason)(RESERVE_MANAGER_ROLE= Timelock) marking the reserve down to its impaired value. The function can only decreaserecognizedReserve(CannotIncrease) — it can never flatter NAV. Note the consequence of the 48h delay: dashboards show pre-write-down NAV until execution. The incident notice must state the queued write-down amount and ETA so no one trades on a stale NAV. If the depeg later fully reverses before execution, the guardian cancancel(id); a write-down already executed is not reversed by governance fiat — recovery value re-enters recognition only via the auditedsweepExcesspath with public explanation. - Communication: immediate notice: what is paused, current
recognizedReserve, the queued write-down, explicit reminder that CRWN has no redemption mechanism and no guaranteed floor. - Recovery: unpause Genesis only if USDG is re-verified sound; if permanently impaired, Genesis stays paused and V2 reserve migration is designed in the open (there is no in-place reserve-swap in V1).
- Postmortem: always.
IR-3 · Buyback exploit
Suspected: buybacks executing at manipulated prices, draining more than intended, or any
BuybackExecuted where burn/spend accounting looks wrong. On-chain caps bound the blast radius:
maxSpendPerTx, maxSpendPerDay, maxTreasuryBps (≤ 2% of recognized reserve per rolling day at
proposed params), cooldown, plus the measured-delta accretion revert.
- Detection:
BuybackExecutedanomaly alerts;spentInWindowvs expectations;totalUsdgSpentgrowth rate; accretion monitoring (navPerToken must not fall on a buyback). - Who acts: Guardian. Response time: SEV-1 — pause as fast as quorum allows.
- Immediate:
CrownridgeBuyback.pause(). This stopsexecuteBuybackoutright. The Buyback holds no standing USDG balance or allowance (funds are pulled just-in-time and leftovers returned), so pausing it leaves no stranded value beyond dust recoverable viasweepStrayUsdg. - Timelocked follow-up:
Buyback.setEnabled(false); if the executor key is implicated,Buyback.revokeRole(BUYBACK_EXECUTOR_ROLE, <executor>)(DEFAULT_ADMIN via Timelock). Remember: the executor can only trigger a fully-checked buyback — a compromised executor alone cannot bypass price, cap, or accretion checks; treat executor compromise as SEV-2 unless checks failed. - Communication: notice with the affected transaction hashes, USDG spent, CRWN burned, and the measured NAV impact. If checks were bypassed, this is also IR-5 (vulnerability) — apply its disclosure rules.
- Recovery: root-cause the bypass, fix/re-review, re-run the economic attack suite
(
contracts/test/fork + invariant tests) before any re-enable. - Postmortem: always.
IR-4 · Unauthorized admin action (compromised proposer / malicious queued op)
The protocol multisig is the sole Timelock proposer. A compromised proposer cannot act instantly —
every operation it queues is public for 48h. This is exactly what the guardian's CANCELLER_ROLE
exists for (P2 review finding C1).
- Detection:
CallScheduledalert for any operation not matching the published change calendar. Every queued op is assumed hostile until matched to an announced intent. - Who acts: Guardian. Response time: cancellation any time inside the 48h window; target ≤ 4 hours from detection (SEV-1).
- Immediate:
TimelockController.cancel(id)for each hostile operation. If the queued op touches funds or issuance, also pause the targeted subsystem (§3.1) while investigating. - Follow-up: depends on what was compromised:
- Some protocol-multisig signers: rotate signers inside the Safe itself (a Safe owner change needs only the multisig's own threshold — no timelock involved).
- The protocol multisig wholly compromised: honest statement of the V1 design — the timelock is self-administered, so replacing the proposer requires a timelocked op that only the compromised proposer can queue. The system enters a stalemate, not a drain: the guardian cancels everything the attacker queues (including any attempt to revoke the guardian's own CANCELLER role) indefinitely, while a recovery plan (signer rotation, negotiated re-proposal, or as a last resort a publicly coordinated V2 migration) is executed. Funds cannot move during the stalemate; governance is frozen. Document this trade-off; do not overclaim a unilateral recovery path.
- A granular role on a protocol contract (e.g. executor): queue
revokeRole(<ROLE>, <account>)on the affected contract via the Timelock; pause that subsystem for the 48h in between.
- Communication: publish the cancelled operation's
id, decoded calldata, and why it was hostile; state what is paused and the recovery plan. - Postmortem: always, including how the key/quorum was compromised.
IR-5 · Contract vulnerability (reported or discovered)
- Detection: security report (per
SECURITY.md— verify a disclosure channel is live before mainnet), internal discovery, anomaly alerts. - Who acts: Guardian for containment; incident commander for coordination. Response time: triage ≤ 24h for reports; if exploitable-now, treat as SEV-1 and pause ≤ 1 hour.
- Immediate: pause the affected subsystem only — the levers are per-subsystem by design (spec
§149):
Genesis.pause(),Buyback.pause(),MintController.pause(),Treasury.setFounderWithdrawPaused(true). Not everything has a pause: the Token (transfers are never pausable), the Treasury's holdings themselves, the LiquidityManager (noPausable; its mutating paths are timelockedRESERVE_MANAGER_ROLEcalls plus permissionlesscollectFees, which only moves value into the protocol), and the external Uniswap pool. If the vulnerable surface is unpausable, containment = revoking the roles that reach it (timelocked) plus, for LP exposure, queueingLiquidityManager.windDown(...). - Coordinated disclosure: do not publish exploit details before mitigation. Acknowledge publicly that a report is being investigated once user action could be needed; publish full details only after fix/containment. Core is immutable — there are no upgrades; a code-level fix means deploying a replacement module and re-wiring roles through the 48h Timelock, in public.
- Communication: initial notice (what is paused, what users should/should not do) without exploit mechanics; full technical disclosure with the postmortem.
- Postmortem: always; include whether the 73+ test suite / invariants should have caught it, and add the regression test.
IR-6 · FOUNDER-KEY COMPROMISE — critical (spec §165)
The FounderTreasuryController intentionally has direct withdrawal authority over the recognized
reserve (up to its configured mode). Compromise of the founder key is therefore the single most
dangerous key event in the protocol, and this trade-off is disclosed, not hidden (spec §165:
"the unavoidable trade-off of direct founder withdrawal authority").
Exposure by mode (set immutably at deploy, FOUNDER_MODE):
- CAPPED: drain rate bounded on-chain by
maxPerWithdrawal,rollingLimit/rollingWindow,cooldown, andminTreasuryFloor(FounderTreasuryController.withdrawchecks). The response window is real: worst-case loss before the pause lands ≈ onemaxPerWithdrawalpercooldown. - UNCAPPED:
withdrawableNow()= the entirerecognizedReserve. A single transaction can empty the recognized reserve before any human can respond. The pause helps only with whatever has not yet been withdrawn. State this plainly in all disclosures.
Runbook:
- Detect — any
FounderTreasuryWithdrawalevent pages the guardian immediately (every founder withdrawal is treated as an incident trigger until the founder confirms it out-of-band via a pre-established authenticated channel). Also: founder self-reports key loss; anomalousFounderWithdrawExecutedpatterns. - T+0 — Guardian, one Safe batch (SEV-1, sign-first-ask-later policy):
CrownridgeTreasury.setFounderWithdrawPaused(true)— the immediate lever. Every furtherfounderWithdrawreverts. This is why the flag exists (P2 review finding C2).CrownridgeGenesis.pause()— stop new deposits flowing into a treasury under attack (spec §165.4 "Stop Genesis").CrownridgeMintController.pause()— precautionary: freeze all issuance.CrownridgeBuyback.pause()— precautionary: no reserve leaves for any reason during triage.
- ≤ 24h — Protocol multisig queues the durable revocation:
Treasury.revokeRole(FOUNDER_CONTROLLER_ROLE, <FounderTreasuryController>)viaTimelockController.schedule. Note the role is held by the controller contract, not the founder EOA — revoking it severs the entire path no matter who holds the founder key. Executes at T+48h (anyone can callexecute). Role revocation is timelocked; the pause is what protects the reserve at T+0. The guardian keeps the pause set for the full window and cancels any hostile counter-proposals. - "Move remaining reserve to a safe address" (spec §165.3) — the architecture does not permit
it, deliberately.
CrownridgeTreasuryhas nowithdrawAll, no arbitrarytransfer/callpath, andrescueTokenexplicitly refuses USDG and CRWN. Any function able to move the reserve to a "safe address" would itself be the drain an attacker wants. The equivalent protection is pause (step 2) + revocation (step 3): once both land, no path exists by which any key — founder, guardian, or multisig — can move the reserve out, other than the buyback's capped, accretion-checked market purchases. Say exactly this in the public notice; do not imply a rescue capability that does not exist. - Publish (≤ 4h) — incident notice: withdrawals observed (tx hashes, amounts from
FounderTreasuryWithdrawalevents), currentrecognizedReserve,cumulativeFounderWithdrawn, the pause state, the queued revocationidand ETA. - Reconcile — verify
conservationHolds(); sum allFounderTreasuryWithdrawalevents againstcumulativeFounderWithdrawn; recompute NAV and navPerToken (they already reflect the loss —founderWithdrawdecrementsrecognizedReserveat withdrawal time, so no write-down is needed for stolen-via-withdrawal funds;writeDownRecognizedapplies only if some additional impairment is discovered). Publish the reconciliation. - Re-establish founder authority (if and only if desired) — the founder address is immutable
in the controller, so rotation = deploy a new
FounderTreasuryControllerwith the new founder address (and mode/caps re-decided — a compromise is a strong argument for CAPPED with tighter limits), then grantFOUNDER_CONTROLLER_ROLEto it via the 48h Timelock, in public, with the parameters disclosed before execution. Only thensetFounderWithdrawPaused(false). Leaving founder authority permanently revoked is also a legitimate outcome and must be considered in the postmortem. - Postmortem (≤ 7 days) — mandatory, public: key-management failure analysis, exact timeline (detection → pause → revocation), total loss, mode/parameter re-evaluation (CAPPED limits, whether UNCAPPED remains defensible), and monitoring gaps.
Out-of-scope for V1
Spec §87 also lists strategy exploit and bridge exploit: V1 has no yield strategies and no bridge/cross-chain issuance, so there is nothing to freeze. These runbooks must be written before any V2 feature ships (spec §79–§85).
5. Emergency lever quick reference
| Scenario | Immediate (guardian, T+0) | Timelocked follow-up (T+48h) |
|---|---|---|
| Oracle failure | Buyback.pause() |
Buyback.setEnabled(false) / setRiskParams |
| USDG depeg/freeze | Genesis.pause() + MintController.pause() + Buyback.pause() (+ founder-pause) |
Treasury.writeDownRecognized(newAmount, reason) |
| Buyback exploit | Buyback.pause() |
setEnabled(false), revoke BUYBACK_EXECUTOR_ROLE |
| Unauthorized admin op | TimelockController.cancel(id) (+ subsystem pause) |
revokeRole(...) / Safe signer rotation |
| Contract vulnerability | pause the affected subsystem | role revocation, LiquidityManager.windDown, module replacement |
| Founder-key compromise | Treasury.setFounderWithdrawPaused(true) + Genesis.pause() + MintController.pause() + Buyback.pause() |
Treasury.revokeRole(FOUNDER_CONTROLLER_ROLE, controller); new controller if rotating |
6. Communication process
- Channels: the protocol website status page and the official X account (handles pending — founder action item; wire real channels here before mainnet). On-chain events are themselves the primary record — notices always cite tx hashes and event names so anyone can verify.
- Initial notice (≤ 4h from confirmation): what happened (facts only), what is paused, what
users should or should not do, what is queued in the timelock (with
idand ETA), when the next update comes. Never speculate on cause, never promise recovery of funds, never state amounts not yet verified on-chain. - Update cadence: at least every 24h while any pause is active, even if the update is "no change."
- Honesty rules bind under pressure: no "funds are safe" unless provably true at that moment from on-chain state; NAV/backing/market-price terms used precisely; no implication that CRWN is redeemable (it is not, in V1); "Built on Robinhood Chain" phrasing only.
7. Standing disclosure
The founder withdrawal authority is a real trust assumption in every incident model above. The guardian pause and the 48h-timelocked revocation are mitigations, not eliminations of that trust — in UNCAPPED mode in particular, response time bounds nothing about a first-strike withdrawal. Users must weigh this before depositing. See docs/RISK.md and the FounderController disclosure on the public site.
8. Postmortem process
Every SEV-1, every SEV-2 that triggered a pause, and every founder-withdrawal false alarm gets a postmortem, published within 7 days of resolution:
- Timeline — detection, confirmation, each action with tx hash, resolution (UTC).
- Impact — reserve delta,
recognizedReservebefore/after, NAV/navPerToken effect, affected users, downtime per subsystem. - Root cause — technical and procedural; no blameless-ness theater but no scapegoating.
- Detection review — which alert fired (or should have); update indexer alert rules.
- Action items — each with an owner and a deadline; parameter changes go through the public
timelock; test-suite additions land as regression tests in
contracts/test/. - Drill feedback — pre-mainnet, each runbook above must be executed at least once as a drill on testnet/fork (spec: "incident drills succeed"), and this document updated with what the drill taught.
Incident commander: the guardian signer who acknowledged the alert, unless explicitly handed off. One commander per incident; the commander owns the timeline, the comms, and the postmortem.