Skip to content

feat(versioned-proxy)!: Role 1 multi-admin whitelist#12

Merged
Wieedze merged 3 commits into
mainfrom
feat/role1-multi-admin
May 18, 2026
Merged

feat(versioned-proxy)!: Role 1 multi-admin whitelist#12
Wieedze merged 3 commits into
mainfrom
feat/role1-multi-admin

Conversation

@Wieedze
Copy link
Copy Markdown
Owner

@Wieedze Wieedze commented May 18, 2026

Summary

  • Contract: replace single-slot proxyAdmin + 2-step transfer with a proxyAdmins whitelist mapping (same shape as Role 2 fee admins), with a last-admin guard.
  • Surface: instant setProxyAdmin(addr, true/false), new isProxyAdmin(addr) view, new proxyAdminCount() view, new ProxyAdminGranted / ProxyAdminRevoked events.
  • Rationale: the 2-step ceremony was a fat-finger guard for a single-slot role. With multi-admin you can grant the replacement first, then revoke yourself — and the guard collapses into the recommendation to put a Safe in the list (the Safe's internal quorum gives the same protection).
  • Breaking: existing deployed proxies are ABI-incompatible. Factory must be redeployed (bytecode embeds the proxy creation code).

Files of interest

  • packages/contracts/src/IntuitionVersionedFeeProxy.sol — new storage + API
  • packages/contracts/src/IntuitionFeeProxyFactory.sol — passes the full initial admins array
  • packages/webapp/src/components/UpgradeAuthorityPanel.tsx — rewritten multi-admin UX with collapsible Advanced section
  • packages/safe-tx/src/ops/versioned-proxy.ts — new setProxyAdmin AdminOp builder + CLI entry
  • packages/sdk/src/readers.tsreadProxyVersions now returns proxyAdminCount

Test plan

  • bun --filter @intuition-fee-proxy/contracts test — 226 tests pass
  • bun --filter @intuition-fee-proxy/safe-tx test — 72 tests pass
  • bun --filter @intuition-fee-proxy/webapp typecheck — clean
  • bun --filter @intuition-fee-proxy/sdk build — clean
  • bun --filter @intuition-fee-proxy/contracts e2e:local — end-to-end fork validation, including the new step ⑫b (grant → revoke → guards)
  • Factory redeployed on Intuition testnet at 0x48f5829E3F2116F639B6baB1cc810AC9b8ae3e32
  • Manual webapp click-through against a freshly deployed testnet proxy: grant a 2nd Role 1 admin → revoke the 1st → verify last-admin guard renders → expand Advanced section → confirm Role 1 vs Role 2 explainer + Grant-both-roles form behave
  • Safe-propose round-trip deferred to mainnet rollout (Den doesn't index Intuition testnet)

Notes for reviewers

  • The full admin list isn't exposed by an on-chain getter (mirrors the Role 2 pattern). The webapp reconstructs it by replaying ProxyAdminGranted / ProxyAdminRevoked events ordered by (block, logIndex) — see useProxyAdmins.
  • No migration path for existing proxies — by user direction (no integrators yet, all testnet proxies disposable).

🤖 Generated with Claude Code

Wieedze and others added 3 commits May 18, 2026 12:27
BREAKING: replaces the single-slot proxyAdmin + 2-step ownership
transfer with a whitelist of proxyAdmins (mirrors Role 2 fee-admins).
Storage layout, constructor signature, and admin API all change.
Existing deployed proxies are incompatible — fresh deploys required.

Rationale: the user wanted Role 1 to feel symmetric with Role 2 (a
list of admins, instant grant/revoke). The 2-step transfer existed
to prevent fat-fingered transfers of the upgrade authority. That
safety is now delegated to putting a Safe multisig in the
proxyAdmins list — the Safe's internal quorum provides the
ceremony.

Contract changes (src/IntuitionVersionedFeeProxy.sol):
- Storage: drop `address proxyAdmin` + `address pendingProxyAdmin`,
  add `mapping(address=>bool) proxyAdmins` + `uint256 proxyAdminCount`
- Constructor: `address admin` → `address[] initialProxyAdmins`
  (reverts on empty list or any zero entry, dedupes silently)
- Functions: drop `transferProxyAdmin` + `acceptProxyAdmin`, add
  `setProxyAdmin(address, bool)` with:
    * idempotent reject (revert `ProxyAdminAlreadySet` if status
      already matches — keeps the on-chain event log truthful)
    * last-admin guard (revert `LastProxyAdmin` if revoke would
      empty the whitelist — anti-lock-out)
- Modifier `onlyProxyAdmin`: mapping lookup instead of address ==
- Views: drop `proxyAdmin()` + `pendingProxyAdmin()`, add
  `isProxyAdmin(address)` + `proxyAdminCount()`. The full admin list
  is reconstructed from events (same pattern as Role 2)

Interface (src/interfaces/IIntuitionVersionedFeeProxy.sol):
- Events: drop `ProxyAdminTransferStarted/Transferred`, add
  `ProxyAdminGranted(address)` + `ProxyAdminRevoked(address)`
- Function/view changes mirror the contract

Errors (src/libraries/Errors.sol):
- Drop `VersionedFeeProxy_NotPendingProxyAdmin`
- Add `VersionedFeeProxy_ProxyAdminAlreadySet` (idempotent reject)
- Add `VersionedFeeProxy_LastProxyAdmin` (anti-lock-out)

Factory (src/IntuitionFeeProxyFactory.sol):
- `createProxy` signature unchanged
- Now passes the full `initialAdmins[]` to the proxy constructor.
  Both roles seed from the same list — diverge via setters later
  if your team / Safe layout needs it.

Tests:
- All inline `VersionedFactory.deploy(scalar, ...)` updated to
  `deploy([addrs], ...)` across 4 test files
- IntuitionVersionedFeeProxy.test.ts: 2-step transfer block replaced
  by 7-test setProxyAdmin block covering grant, revoke, idempotent
  guards (×2), last-admin guard, grant+revoke roundtrip, non-admin
  reject. supportsInterface selector list updated.
- IntuitionFeeProxyFactory.test.ts: asserts both admins are now in
  Role 1 (proxyAdminCount == initialAdmins.length)

226 tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… webapp

Follow-up to a2c82e5 (contract-level Role 1 → multi-admin whitelist).
Drags every downstream consumer onto the new API so the branch compiles
and the UX matches the contract semantics.

SDK
- readers.readProxyVersions: drop `proxyAdmin` + `pendingProxyAdmin`
  outputs, return `proxyAdminCount: bigint` instead. Both the
  Multicall3 path and the per-field fallback are updated.

safe-tx
- New ops/versioned-proxy.ts exposing `setProxyAdmin(proxy, admin,
  status)` as an AdminOp builder (mirrors v2-admin.setWhitelistedAdmin).
- Registered as `set-proxy-admin` in the CLI op-registry under a new
  `versioned-proxy` category. OP_REGISTRY count test bumped 10 → 11.
- Unit test covers grant + revoke selector + args.

webapp
- useVersionedProxy: replace `useTransferProxyAdmin` +
  `useAcceptProxyAdmin` with `useSetProxyAdmin` + `useIsProxyAdmin`;
  add `useProxyAdmins` that reduces ProxyAdminGranted /
  ProxyAdminRevoked events into the current whitelist (no on-chain
  getter for the full set).
- useProxyRoles: now reads `isProxyAdmin(account)` via wagmi instead
  of comparing against a single `proxyAdmin` address.
- UpgradeAuthorityPanel rewritten as a multi-admin panel (list +
  per-row revoke + grant form + Safe-propose path), with a
  collapsible Advanced section housing the "grant both roles"
  convenience + the Role 1 vs Role 2 explainer.
- AdminsTab + ProxyDetail: drop the obsolete `proxyAdmin` +
  `pendingProxyAdmin` prop plumbing.
- useProxyAdminRotation hook deleted (2-step state machine no longer
  needed); stale AdminRotateStage type removed.
- Docs.tsx: Architecture actor + Primitives + Admin Rotation sections
  rewritten for the whitelist model.
- SafeProposeFeedback component extracted so every Safe-propose
  surface renders feedback identically.

e2e
- e2e-validate.ts step ⑫b: 2-step transfer flow replaced with
  whitelist grant → revoke → non-admin guard → last-admin guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves 5 conflicts triggered by main's Safe-aware refactors landing
in parallel (PRs #10 + #11):

- packages/safe-tx/src/ops/index.ts — keep both `versionedProxy` (ours)
  and `sponsored` (main) namespace exports.

- packages/safe-tx/src/ops/versioned-proxy.ts — main shipped a
  versioned-proxy ops module with the OLD 2-step API
  (transferProxyAdmin + acceptProxyAdmin + registerVersion +
  setDefaultVersion). The 2-step API no longer exists on the contract
  post-a2c82e5. Resolution: keep our `setProxyAdmin` builder, keep
  main's `registerVersion` + `setDefaultVersion` builders (still
  valid — they're separate Role 1 surfaces), drop the 2-step ones.

- packages/safe-tx/test/unit/ops/versioned-proxy.test.ts — same
  reduction: keep setProxyAdmin + registerVersion + setDefaultVersion
  tests, drop 2-step tests.

- packages/webapp/src/components/SafeProposeFeedback.tsx — both
  branches independently extracted this component. Keep main's
  version (already reviewed in #10, slightly cleaner with fragment +
  parent-controlled spacing).

- packages/webapp/src/components/UpgradeAuthorityPanel.tsx — main
  rewrote it as Safe-aware against the old 2-step API. All of that
  is dead. Take our multi-admin whitelist version entirely; the
  Safe-aware integration (useSafePropose + ops.versionedProxy) is
  already preserved in our version.

Plus two test-infra fixes the merge surfaced:

- safe-tx: ANVIL_PORT default 8545 → 8546. Anvil silently fails to
  bind a busy port and the fixture's RPC-ready probe was happily
  talking to a Hardhat node squatting 8545 from another package,
  producing opaque `anvil_impersonateAccount not supported` errors
  in the integration suite. direct-sign.test.ts bumped 8546 → 8547
  to keep parallel runs collision-free.

- safe-tx: broaden the trezor "no device" error regex in
  factory.test.ts to include "trezor getAddress failed — Transport
  is missing", which is what happens when the optional deps are
  installed but no Bridge / device is reachable at getAddress() time.
  Equally actionable to the other three branches the regex already
  accepts.

Verified: 82/82 safe-tx tests pass, webapp typecheck clean, sdk build
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Wieedze Wieedze merged commit d980d00 into main May 18, 2026
1 check passed
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