feat(safe): fold parked facet removals into the primary proposal - #2155
Conversation
… signature) The deferred diamond-cleanup drain minted a SECOND Safe proposal for the parked removals, so a rollout that already signed one proposal had to sign two — a full extra sign/schedule/execute that defeated the near-zero-marginal-signing goal. Fold the removals into the primary proposal's timelock scheduleBatch instead: one diamondCut Remove element per parked facet, appended to the primary's inner calls (N parked facets -> primary calls + N removals). One Safe transaction, one signature. Each removal still carries its origin-PR link (parkedTaskRefs on the primary proposal), so the reviewer sees every folded cleanup inline. Restructures the drain into two phases: prepareDrainNetwork (partition + claim + build removal calls, self-reverting on failure) and proposeWithDrain (feeds those calls into _runPropose, then links claimed tasks to the primary's safeTxHash, or reverts them if the primary throws / was a duplicate). Best-effort preserved: any drain-prep failure falls back to proposing the primary alone. Gated to the timelock path (removals need a scheduleBatch to batch into). Reverses the Q4 "one extra proposal per network" decision; docs updated with the accepted tradeoff (folded removals share the upgrade's timelock op, so reject-one = reject-all). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughParked facet removals now flow through ChangesParked removal proposal flow
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
script/deploy/safe/drain-parked-tasks.test.ts (1)
449-458: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider reusing the exported ref type instead of re-declaring its shape.
{ facet: string; prUrl: string }[]duplicatesIParkedTaskReffrom the module under test; importing it keeps the spy in lockstep if the ref shape changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@script/deploy/safe/drain-parked-tasks.test.ts` around lines 449 - 458, Update the IPrimarySpy interface to import and reuse the exported IParkedTaskRef type from the module under test for both the fn parameter and received refs, removing the duplicated inline `{ facet: string; prUrl: string }[]` shape while preserving the existing optionality.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@script/deploy/safe/drain-parked-tasks.ts`:
- Around line 238-248: Update the rollback loops in prepareDrainNetwork’s catch,
proposeWithDrain’s proposePrimary catch, and finalizeClaimed’s duplicate branch
to isolate each deps.revert()/queue.deps.revert() call with per-task error
handling. Continue attempting all claimed tasks when one revert fails, while
preserving and rethrowing the original triggering error; report individual
revert failures through the existing alert/error mechanism without replacing the
primary failure.
---
Nitpick comments:
In `@script/deploy/safe/drain-parked-tasks.test.ts`:
- Around line 449-458: Update the IPrimarySpy interface to import and reuse the
exported IParkedTaskRef type from the module under test for both the fn
parameter and received refs, removing the duplicated inline `{ facet: string;
prUrl: string }[]` shape while preserving the existing optionality.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 27225f2f-d25c-43bc-a277-2b3825e29341
📒 Files selected for processing (5)
docs/DeferredDiamondCleanupQueue.mddocs/FacetRemovalReconciliation.mdscript/deploy/safe/drain-parked-tasks.test.tsscript/deploy/safe/drain-parked-tasks.tsscript/deploy/safe/propose-to-safe.ts
Gate-review findings on the fold-into-primary drain: - HIGH: finalizeClaimed was not wrapped best-effort, so a Mongo error while linking a claimed task after the primary was already stored crashed runPropose (non-zero exit) and stranded the task in `proposed`. Wrap it and make the link/revert loops per-key resilient (a failed link leaves the task `proposed` for reconcile/loupe self-heal — never reverted, since its removal already rode the stored proposal). Added a test. - Update multisig-rollout.md Phase 3.5 / Phase 4: the drain now folds into the facet-cut proposal and adds NO extra proposal — operators must not wait for or count a separate removal proposal (was "expect two or three per network"). - Document the execution-time atomicity (TOCTOU) tradeoff in DeferredDiamondCleanupQueue.md §6: a folded removal reverting during the timelock window reverts the whole batch, incl. the primary cut. - Minor: add @throws to proposeWithDrain/_runPropose, tighten the module header and the reentrancy-guard comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-gate follow-ups: - Wrap queue.close() best-effort so a connection-close failure can't surface as a primary-proposal failure after the proposal is already stored (closes the last gap in the "drain never affects the exit code" invariant). - docs: the timelock-window revert is FunctionDoesNotExist (this repo's LibDiamond error), not the mudgen-reference CannotRemoveFunctionThatDoesNotExist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-review gate summary (auto-fix trial)Ran an in-session adversarial review (rule-adherence, bug + out-of-diff collaborators, git-history/prior-PR-comments, and a falsification pass), plus a re-gate of the fix commits. No critical bugs — the new eligibility/timelock guards and the Auto-fixed (already pushed):
Escalated — needs a governance call (does NOT block, but @goran should weigh in):
This PR also reverses the Q4 "one extra proposal per network" decision that @goran co-signed — docs updated in both |
prepareDrainNetwork's catch still reverted claimed tasks in a plain loop; a single failed revert would strand the rest and mask the original preparation error. Route it through revertQuietly like the other two rollback paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Fixed in aa82001 — routed |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@script/deploy/safe/drain-parked-tasks.ts`:
- Around line 9-10: Clarify the best-effort guarantee: in
script/deploy/safe/drain-parked-tasks.ts lines 9-10, state that drain-only
failures are isolated only before proposal storage, while folded
diamondCut(Remove) operations execute atomically within the primary timelock
scheduleBatch and any on-chain removal failure can block the rollout. Add the
same execution-time atomicity warning beside the best-effort guidance in
.agents/commands/multisig-rollout.md lines 126-127.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a7f00460-2cae-4491-8143-c82a1a761046
📒 Files selected for processing (5)
.agents/commands/multisig-rollout.mddocs/DeferredDiamondCleanupQueue.mdscript/deploy/safe/drain-parked-tasks.test.tsscript/deploy/safe/drain-parked-tasks.tsscript/deploy/safe/propose-to-safe.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- docs/DeferredDiamondCleanupQueue.md
- script/deploy/safe/propose-to-safe.ts
- script/deploy/safe/drain-parked-tasks.test.ts
…CodeRabbit) The best-effort guarantee only holds until the proposal is stored; once folded, a removal that reverts on-chain during the timelock window reverts the primary cut too. Spell that out in the module header and the multisig-rollout operator note, matching the §6 TOCTOU tradeoff. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Done in the latest push — qualified the best-effort guarantee in both the |
…-721) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…design (EXSC-721) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-review gate — residual findings (auto-fix trial, round 2 on 101ebe2/a004ffe74)Adversarial review of the fold-into-primary change (rules, bugs incl. out-of-diff collaborators, git-history, prior-PR comments, comment-vs-code drift, falsification). The fold logic itself was proven executable on real repo data shapes: partition → claim → one Escalated — needs your decisionE1. Non-owner E2. The §4 "Mandatory pre-execute re-validation" contract is structurally unsatisfiable under the fold, and its silent failure mode is undocumented. Lower-confidence — human judgment (60–79)
Auto-fixed in this push
Escalated items need Daniel's decision before this PR is ready for human review. |
…-execute (EXSC-721) E1: _runPropose throws instead of process.exit so proposeWithDrain can revert claimed parked tasks when the signer is not a Safe owner (citty runMain still exits non-zero on the CLI path). E2: wire revalidateRemovalsOnChain into execute-pending-timelock-tx — rebuild the propose-time snapshot from Remove payloads + parked tasks, abort the whole timelock batch (and mark the queue row failed) if any selector is stale during the delay window. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
script/deploy/safe/diamondRemovalDiff.test.ts (1)
556-619: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider covering the "cuts empty, parked non-empty" mismatch branch.
buildRemovalSnapshotFromPayloadsalso returns{ kind: 'mismatch', reason: 'parked tasks present ... but no Remove diamondCut payloads found' }whenremoveCuts.length === 0andparked.length > 0— a distinct mismatch reason from the one already tested (parked > cuts). This branch isn't exercised in this suite.♻️ Suggested additional test
+ it('signals mismatch when parked tasks exist but no Remove cuts are found', () => { + const built = buildRemovalSnapshotFromPayloads( + [], + [{ facetName: 'A', facetAddress: addr(2) }] + ) + expect(built.kind).toBe('mismatch') + })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@script/deploy/safe/diamondRemovalDiff.test.ts` around lines 556 - 619, Add a test in the buildRemovalSnapshotFromPayloads suite covering non-empty parked identities with no Remove diamondCut payloads. Assert the result is kind "mismatch" and validates the branch-specific reason indicating parked tasks exist without any Remove payloads, distinct from the existing parked-greater-than-cuts mismatch test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@script/deploy/safe/diamondRemovalDiff.ts`:
- Around line 753-759: Remove the local DIAMOND_CUT_ABI definition in
diamondRemovalDiff.ts and import and reuse the shared DIAMOND_CUT_ABI exported
by script/deploy/shared/constants.ts. Keep the existing decodeFunctionData usage
unchanged.
---
Nitpick comments:
In `@script/deploy/safe/diamondRemovalDiff.test.ts`:
- Around line 556-619: Add a test in the buildRemovalSnapshotFromPayloads suite
covering non-empty parked identities with no Remove diamondCut payloads. Assert
the result is kind "mismatch" and validates the branch-specific reason
indicating parked tasks exist without any Remove payloads, distinct from the
existing parked-greater-than-cuts mismatch test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: fceced16-db15-4549-b407-78d8605b6d32
📒 Files selected for processing (14)
.agents/commands/multisig-rollout.mddocs/DeferredDiamondCleanupQueue.mddocs/FacetRemovalReconciliation.mdscript/deploy/safe/confirm-safe-tx.tsscript/deploy/safe/diamondRemovalDiff.test.tsscript/deploy/safe/diamondRemovalDiff.tsscript/deploy/safe/drain-parked-tasks.test.tsscript/deploy/safe/drain-parked-tasks.tsscript/deploy/safe/execute-pending-timelock-tx.tsscript/deploy/safe/list-pending-proposals.tsscript/deploy/safe/parked-tasks.test.tsscript/deploy/safe/parked-tasks.tsscript/deploy/safe/propose-to-safe.tsscript/deploy/safe/safe-utils.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- docs/FacetRemovalReconciliation.md
- .agents/commands/multisig-rollout.md
- script/deploy/safe/drain-parked-tasks.ts
- docs/DeferredDiamondCleanupQueue.md
…SC-721) Leave the timelock row queued on transient Mongo/loupe failures (only mark failed for durable stale/mismatch). Gate Slack notifyFailure behind !dryRun, read the loupe through the runner's publicClient so --rpcUrl is honoured, and tidy snapshot helper naming/JSDoc/tests. Co-authored-by: Cursor <cursoragent@cursor.com>
Pre-review gate — residual findings (auto-fix trial, round 3 on 8cfba17 / follow-up)Adversarial review of the E1/E2 fix commits (non-owner throw + pre-execute Escalated — needs your decisionE3. E4. Trailing-N zip order is fail-safe but fragile. Snapshot zip trusts Lower-confidence — human judgment (60–79)
Auto-fixed in this round
Escalated items need your decision before this PR is ready for the |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
script/deploy/safe/execute-pending-timelock-tx.ts (1)
1136-1139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove
FACETS_LOUPE_ABIto the shared loupe ABI export.
diamondRemovalDiff.tsalready defines the samefacets()ABI asFACETS_LOUPE_ABI, but keeps it unexported for local use. Export that shared constant and import it here to avoid maintaining two identicalparseAbi(...)declarations.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@script/deploy/safe/execute-pending-timelock-tx.ts` around lines 1136 - 1139, Export the existing facets() ABI constant from diamondRemovalDiff.ts, then import and reuse that shared symbol in execute-pending-timelock-tx.ts. Remove the local FACETS_LOUPE_ABI parseAbi declaration while preserving the existing pre-execute revalidation behavior.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@script/deploy/safe/execute-pending-timelock-tx.ts`:
- Around line 1136-1139: Export the existing facets() ABI constant from
diamondRemovalDiff.ts, then import and reuse that shared symbol in
execute-pending-timelock-tx.ts. Remove the local FACETS_LOUPE_ABI parseAbi
declaration while preserving the existing pre-execute revalidation behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 0aee76a7-596c-438d-b622-8b61de2f96a8
📒 Files selected for processing (4)
script/deploy/safe/diamondRemovalDiff.test.tsscript/deploy/safe/diamondRemovalDiff.tsscript/deploy/safe/execute-pending-timelock-tx.tsscript/deploy/safe/parked-tasks.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- script/deploy/safe/diamondRemovalDiff.test.ts
…EXSC-721) Abort (and mark the timelock row failed) when a batch has diamondCut Remove payloads but no parked tasks for the Safe tx hash — closes the unlink / legacy-cleanup hole that previously warn-then-proceeded without revalidation. Document the accepted rare false abort from same-ms proposedAt zip ties (E4=b). Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@script/deploy/safe/execute-pending-timelock-tx.ts`:
- Around line 1208-1222: Update the operation validation flow before the early
return around the safeTxHash check: decode and inspect payloads first, and
permit missing safeTxHash only when built.removeCutCount is zero. When a
hashless operation contains Remove cuts, mark it failed via
markTimelockOpFailed, alert the failure, and return 'failed'; preserve normal
processing for operations with a safeTxHash.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a43471a6-89d6-4eb9-ba83-ea67a850a7e5
📒 Files selected for processing (4)
docs/DeferredDiamondCleanupQueue.mdscript/deploy/safe/diamondRemovalDiff.test.tsscript/deploy/safe/diamondRemovalDiff.tsscript/deploy/safe/execute-pending-timelock-tx.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- script/deploy/safe/diamondRemovalDiff.test.ts
- script/deploy/safe/diamondRemovalDiff.ts
- docs/DeferredDiamondCleanupQueue.md
…n (EXSC-721) Export ABI_DIAMOND_CUT from safe-decode-utils and consume it in extractRemoveFacetCuts (positional tuple components), dropping the duplicate parseAbi — addresses the CodeRabbit maintainability nit. Co-authored-by: Cursor <cursoragent@cursor.com>
Pre-review gate — residual findings (round 4 on f39fdaf / 923fec7)E3=a / E4=b applied. Focused gate on the fail-closed unvalidated path: CLEAN. Auto-fixed
No further escalations from this round. Lower-confidence leftovers (orchestration unit tests, Slack |
#2058 landed with the pre-rename deployedWithEvmVersion fields, which broke TargetEvmVersionLabel / EVM_VERSIONS and failed check-secrets, validate-scripts, and run-ts-tests after this branch merged main. Co-authored-by: Cursor <cursoragent@cursor.com>
Which Linear task belongs to this PR?
Fixes EXSC-721
Why did I implement it this way?
The deferred diamond-cleanup drain was minting a second Safe proposal for parked facet removals, so a rollout that already signed one proposal had to sign two — a full extra sign → timelock schedule → execute cycle. That defeated the queue's whole purpose (removals riding along "at ~zero marginal signing cost"): a separate Safe proposal is never zero-cost.
This folds the removals into the primary proposal's timelock
scheduleBatchinstead — onediamondCutRemove element per parked facet, appended to the primary's inner calls (N parked facets → the primary's call(s) + N removal elements). One Safe transaction, one signature. Each removal still carries its origin-PR link (parkedTaskRefson the primary proposal), soconfirm-safe-tx/list-pending-proposals/ Slack show every folded cleanup inline for the reviewer.Mechanics. The drain is restructured into two phases:
prepareDrainNetwork— partition against the live loupe (gone → supersede, pruned-but-routed → keep + alert, protected → cancel + alert, removable → claim) and build one Remove call per claimed facet. Self-reverts every claim it made if preparation throws.proposeWithDrain— feeds those calls into_runPropose(which appends them to thescheduleBatchbefore signing and returns{ safeTxHash, stored }), then links each claimed task to the primary'ssafeTxHash, or reverts them if the primary throws or was a duplicate.Best-effort is preserved: a flag-off / non-timelock / direct-send / prep-failure path proposes the primary alone. The fold is gated to the timelock path because removals need a
scheduleBatchto batch into. The fold happens purely in TypeScript at therunProposelayer (the primary's calldata is already TS there) — no removal logic threads into the Solidity deploy scripts, so the "language boundary" concern that originally motivated a separate proposal never applied to a sharedscheduleBatch.Design reversal — please note. This reverses the Q4 decision ("one extra proposal per network in the same session") that was co-signed by Goran and Daniel.
@Goran— flagging you since you co-owned that call. Docs updated in bothDeferredDiamondCleanupQueue.md§6 andFacetRemovalReconciliation.md§4.Accepted tradeoff. Folding into one timelock op couples the removals to the upgrade: a reviewer who objects to a folded removal must reject the whole proposal (upgrade included), not just the removal. Acceptable because (a) each removal carries its origin-PR link for review, and (b) the
DRAIN_PARKED_TASKS-off default keeps emergency / break-glass proposals a single clean upgrade with nothing folded in.No Solidity changed; no new env var.
tsc,eslint, and the fullscript/deploy/safeBun suite (348 tests, incl. 27 rewritten drain tests) pass.Checklist before requesting a review
Checklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)