Skip to content

feat(deployer): support diamond selector removals on upgrade - #592

Open
clawdbot-glitch003 wants to merge 2 commits into
mainfrom
glitch003/deployer-selector-removals
Open

feat(deployer): support diamond selector removals on upgrade#592
clawdbot-glitch003 wants to merge 2 commits into
mainfrom
glitch003/deployer-selector-removals

Conversation

@clawdbot-glitch003

Copy link
Copy Markdown
Collaborator

Follow-up from the #590 code review. The Rust deployer's upgrade path only computed Replace + Add cuts (get_facet_cuts), never Remove — so a function deleted from a facet's source stayed routed to its old facet and remained callable after propose-update/update. That's a latent gap for any selector removal; it surfaced because #590 removes backfillPkpOwners but the normal deploy flow wouldn't actually drop it on-chain.

Design: explicit manifest + safe diff detection

A selector is Removed only if it is both:

  1. listed in a committed manifest (diamond-removals.json), and
  2. detected as orphaned by this upgrade — currently on the old address of a facet we're re-installing, and absent from every new managed facet ABI.

plan_removals() (pure, unit-tested) enforces this. Consequences:

  • Core DiamondCut/Loupe selectors are never candidates — none of their selectors are in the managed set, so their addresses are never classified as "managed", so their selectors can't be orphans. No way to brick the diamond.
  • A partial/incorrect build that accidentally drops a function can't silently strip it: removal requires an explicit manifest entry.
  • An unlisted orphan warns (it'll stay callable — add it to the manifest to drop it).
  • A listed-but-not-orphaned entry is refused (guards against removing a live function).
  • A selector↔signature mismatch in the manifest is rejected (typo guard).

build_diamond_update emits a single Remove FacetCut (facetAddress 0x0) for the approved selectors, so both update and propose-update (Safe flow) get it.

Wiring

  • New --removals <path> arg, default diamond-removals.json; missing file = no removals (backward compatible). Added to the Makefile base/sepolia update + propose recipes.
  • diamond-removals.json declares backfillPkpOwners (0x41275609), so the refactor(contracts): remove completed #575 PKP-owner backfill machinery #590 cleanup upgrade drops it on-chain automatically once the WritesFacet build no longer declares it. (If run before that, it's detected as not-orphaned and skipped with a warning — safe either way.)

Tests

6 unit tests for plan_removals (orphan removed; unlisted orphan warned; non-orphan refused; absent selector skipped; mismatch guard; and a guard asserting selector_of("backfillPkpOwners(address[],uint256[])") == 0x41275609). cargo build, cargo test, cargo clippy clean.

Relationship to the other PRs

  • Facet-source cleanup (removes backfillPkpOwners + owner==0 fallback from the contracts) is a separate PR.
  • This one makes that cleanup's on-chain removal actually happen through the normal deploy flow, and fixes the general "can't remove any selector" gap.

🤖 Generated with Claude Code

The deployer's upgrade path (get_facet_cuts) only computed Replace + Add, so a
function dropped from a facet's source stayed routed to its old facet and
remained callable after propose-update/update (surfaced reviewing the #575
backfillPkpOwners removal). Teach the deployer to Remove selectors, safely.

- plan_removals(): a selector is removed only if it is BOTH listed in a
  committed manifest AND detected as orphaned by the upgrade (on a re-installed
  facet's old address, absent from every new managed facet ABI). Core
  DiamondCut/Loupe selectors are never candidates. Unlisted orphans warn (stay
  callable); listed-but-not-orphaned entries are refused; a missing/mismatched
  selector guard blocks typos. So a partial/bad build can never silently strip
  a live function.
- build_diamond_update() emits a single Remove FacetCut (facetAddress 0x0) for
  the approved selectors; both `update` and `propose-update` benefit.
- New --removals arg (default diamond-removals.json; missing file = no-op,
  backward compatible). Wired into the Makefile base/sepolia recipes.
- diamond-removals.json declares backfillPkpOwners (0x41275609) so the #575
  cleanup upgrade actually drops it on-chain.
- 6 unit tests for plan_removals + a guard that the backfill selector is
  0x41275609.

Pairs with the facet-source cleanup PR (removes backfillPkpOwners from
WritesFacet); the removal fires exactly when the deployed ABI omits it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clawdbot-glitch003
clawdbot-glitch003 requested a review from a team July 21, 2026 23:37
@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Differential Security Review — automated

Base: 2c972ff3Head: 3e5dd085

Severity Count
CRITICAL 0
HIGH 0
MEDIUM 0
LOW 0
INFO 3

Overall risk: LOW — Recommendation: APPROVE

This PR adds a manifest-gated selector-removal feature to the diamond-cut deployer (diamond.rs, contract_deployer.rs, diamond-removals.json, Makefile). Despite the sensitive domain (production diamond-proxy upgrades for a PKP/wallet-custody contract), the removal logic is safe by construction: a selector is only ever removed if it is both explicitly listed in the manifest and algorithmically detected as orphaned by the freshly-built new facet ABIs — a condition the manifest cannot force. Every parse/validation error path fails toward zero removals. Ran adversarial modeling given the domain risk; no exploitable bypass was found, and cargo test confirms the 6 new unit tests pass.

Top findings (all INFO, non-blocking):

  • F1: A selector-hash collision in the manifest's optional selector field could mislead a human reviewer, but cannot bypass the orphan safety gate — no privilege escalation beyond existing merge access.
  • F2: diamond-removals.json is a plain data file that will become as security-relevant as diamond.rs itself once the pending Solidity removal lands; recommend flagging it explicitly in future triage.
  • F3: Facet "managed" classification is keyed by selector value rather than facet identity (pre-existing pattern inherited from get_facet_cuts, not introduced here).

Full report: .security-review/report.md

🤖 Generated by automated differential-review skill

CI cargo fmt --check failed on the plan_removals test asserts. No logic change.

Co-Authored-By: Claude Fable 5 <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.

2 participants